Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactoring, use context, use logger, use go mod #24

Merged
merged 1 commit into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Look in the examples directory to learn how to use this library:
API documentation is available at [godoc.org](https://godoc.org/github.com/go-routeros/routeros).
Page on the [Mikrotik Wiki](http://wiki.mikrotik.com/wiki/API_in_Go).

Released versions:
Usage of `gopkg.in` was removed in favor of Go modules. Please, update you import paths to
`github.com/go-routeros/routeros/v3`.

Old released versions:
[**v2**](https://github.com/go-routeros/routeros/tree/v2)
[**v1**](https://github.com/go-routeros/routeros/tree/v1)

To install it, run:
`go get github.com/go-routeros/routeros/v3`
44 changes: 38 additions & 6 deletions async.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package routeros

import "github.com/go-routeros/routeros/proto"
import (
"context"

"github.com/go-routeros/routeros/v3/proto"
)

type sentenceProcessor interface {
processSentence(sen *proto.Sentence) (bool, error)
Expand All @@ -12,6 +16,11 @@ type replyCloser interface {

// Async starts asynchronous mode and returns immediately.
func (c *Client) Async() <-chan error {
return c.AsyncContext(context.Background())
}

// AsyncContext starts asynchronous mode with context and returns immediately.
func (c *Client) AsyncContext(ctx context.Context) <-chan error {
c.mu.Lock()
defer c.mu.Unlock()

Expand All @@ -23,16 +32,16 @@ func (c *Client) Async() <-chan error {
}
c.async = true
c.tags = make(map[string]sentenceProcessor)
go c.asyncLoopChan(errC)
go c.asyncLoopChan(ctx, errC)
return errC
}

func (c *Client) asyncLoopChan(errC chan<- error) {
func (c *Client) asyncLoopChan(ctx context.Context, errC chan<- error) {
defer close(errC)

// If c.Close() has been called, c.closing will be true, and
// err will be “use of closed network connection”. Ignore that error.
err := c.asyncLoop()
if err != nil {
if err := c.asyncLoop(ctx); err != nil {
c.mu.Lock()
closing := c.closing
c.mu.Unlock()
Expand All @@ -42,9 +51,17 @@ func (c *Client) asyncLoopChan(errC chan<- error) {
}
}

func (c *Client) asyncLoop() error {
// asyncLoop - main goroutine for async mode. Read and process sentences, handle context done.
func (c *Client) asyncLoop(ctx context.Context) error {
go func() {
<-ctx.Done()

c.r.Cancel()
}()

for {
sen, err := c.r.ReadSentence()

if err != nil {
c.closeTags(err)
return err
Expand All @@ -53,6 +70,8 @@ func (c *Client) asyncLoop() error {
c.mu.Lock()
r, ok := c.tags[sen.Tag]
c.mu.Unlock()

// cannot find tag for this sentence, ignore
if !ok {
continue
}
Expand All @@ -71,9 +90,22 @@ func (c *Client) closeTags(err error) {
c.mu.Lock()
defer c.mu.Unlock()

// If c.Close() has been called, c.closing will be true, and
// err will be “use of closed network connection”. Ignore that error.
if c.closing {
for _, r := range c.tags {
closeReply(r, nil)
}

c.tags = nil

return
}

for _, r := range c.tags {
closeReply(r, err)
}

c.tags = nil
}

Expand Down
4 changes: 3 additions & 1 deletion chan_reply.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package routeros

import "github.com/go-routeros/routeros/proto"
import (
"github.com/go-routeros/routeros/v3/proto"
)

// chanReply is shared between ListenReply and AsyncReply.
type chanReply struct {
Expand Down
151 changes: 116 additions & 35 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,88 +4,171 @@ Package routeros is a pure Go client library for accessing Mikrotik devices usin
package routeros

import (
"crypto/md5"
"context"
"crypto/md5" //nolint:gosec
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"io"
"log/slog"
"net"
"os"
"sync"
"sync/atomic"
"time"

"github.com/go-routeros/routeros/proto"
"github.com/go-routeros/routeros/v3/proto"
)

// Client is a RouterOS API client.
type Client struct {
Queue int

log *slog.Logger
logMutex sync.Mutex

rwc io.ReadWriteCloser
r proto.Reader
w proto.Writer
closing bool
async bool
nextTag int64
tags map[string]sentenceProcessor
mu sync.Mutex
mw sync.Mutex

r proto.Reader
w proto.Writer
}

var (
ErrNoChallengeReceived = errors.New("no ret (challenge) received")
ErrInvalidChallengeReceived = errors.New("invalid ret (challenge) hex string received")
)

var defaultHandler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
})

// NewClient returns a new Client over rwc. Login must be called.
func NewClient(rwc io.ReadWriteCloser) (*Client, error) {
return &Client{
rwc: rwc,
r: proto.NewReader(rwc),
w: proto.NewWriter(rwc),
log: slog.New(defaultHandler),

r: proto.NewReader(rwc),
w: proto.NewWriter(rwc),
}, nil
}

// incrementTag atomically increments tag number and returns result
func (c *Client) incrementTag() int64 {
return atomic.AddInt64(&c.nextTag, 1)
}

// IsAsync return true if client run in async mode.
func (c *Client) IsAsync() bool {
c.mu.Lock()
defer c.mu.Unlock()

return c.async
}

// Dial connects and logs in to a RouterOS device.
func Dial(address, username, password string) (*Client, error) {
conn, err := net.Dial("tcp", address)
return DialContext(context.Background(), address, username, password)
}

// DialTimeout connects and logs in to a RouterOS device with timeout.
func DialTimeout(address, username, password string, timeout time.Duration) (*Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

return DialContext(ctx, address, username, password)
}

// DialContext connects and logs in to a RouterOS device using context.
func DialContext(ctx context.Context, address, username, password string) (*Client, error) {
conn, err := new(net.Dialer).DialContext(ctx, "tcp", address)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not connect to router os: %w", err)
}
return newClientAndLogin(conn, username, password)
return newClientAndLogin(ctx, conn, username, password)
}

// DialTLS connects and logs in to a RouterOS device using TLS.
func DialTLS(address, username, password string, tlsConfig *tls.Config) (*Client, error) {
conn, err := tls.Dial("tcp", address, tlsConfig)
return DialTLSContext(context.Background(), address, username, password, tlsConfig)
}

// DialTLSTimeout connects and logs in to a RouterOS device using TLS with timeout.
func DialTLSTimeout(address, username, password string, tlsConfig *tls.Config, timeout time.Duration) (*Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

return DialTLSContext(ctx, address, username, password, tlsConfig)
}

// DialTLSContext connects and logs in to a RouterOS device using TLS and context.
func DialTLSContext(ctx context.Context, address, username, password string, tlsConfig *tls.Config) (*Client, error) {
conn, err := (&tls.Dialer{Config: tlsConfig}).DialContext(ctx, "tcp", address)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not connect to router os: %w", err)
}
return newClientAndLogin(conn, username, password)
return newClientAndLogin(ctx, conn, username, password)
}

func newClientAndLogin(rwc io.ReadWriteCloser, username, password string) (*Client, error) {
// newClientAndLogin - creates a new client with context over specified rwc, then logs in to the RouterOS, returns new client.
func newClientAndLogin(ctx context.Context, rwc io.ReadWriteCloser, username, password string) (*Client, error) {
c, err := NewClient(rwc)
if err != nil {
rwc.Close()
return nil, err
return nil, fmt.Errorf("could not connect to router os: %w; close: %w", err, rwc.Close())
}
err = c.Login(username, password)
err = c.LoginContext(ctx, username, password)
if err != nil {
c.Close()
return nil, err
return nil, fmt.Errorf("could not login: %w; close %w", err, c.Close())
}
return c, nil
}

func (c *Client) SetLogHandler(handler LogHandler) {
c.logMutex.Lock()
c.log = slog.New(handler)
c.logMutex.Unlock()
}

func (c *Client) logger() *slog.Logger {
c.logMutex.Lock()
defer c.logMutex.Unlock()

return c.log
}

// Close closes the connection to the RouterOS device.
func (c *Client) Close() {
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()

c.r.Close()
c.w.Close()

if c.closing {
c.mu.Unlock()
return
return nil
}

c.closing = true
c.mu.Unlock()
c.rwc.Close()

return c.rwc.Close()
}

// Login runs the /login command. Dial and DialTLS call this automatically.
func (c *Client) Login(username, password string) error {
r, err := c.Run("/login", "=name="+username, "=password="+password)
return c.LoginContext(context.Background(), username, password)
}

// LoginContext runs the /login command. DialContext and DialTLSContext call this automatically.
func (c *Client) LoginContext(ctx context.Context, username, password string) error {
r, err := c.RunContext(ctx, "/login", "=name="+username, "=password="+password)
if err != nil {
return err
}
Expand All @@ -95,27 +178,25 @@ func (c *Client) Login(username, password string) error {
if r.Done != nil {
return nil
}
return errors.New("RouterOS: /login: no ret (challenge) received")
return fmt.Errorf("RouterOS: /login: %w", ErrNoChallengeReceived)
}

// Login method pre-6.43 two stages, challenge
b, err := hex.DecodeString(ret)
if err != nil {
return fmt.Errorf("RouterOS: /login: invalid ret (challenge) hex string received: %s", err)
var dec []byte
if dec, err = hex.DecodeString(ret); err != nil {
return fmt.Errorf("RouterOS: /login: %w: %w", ErrInvalidChallengeReceived, err)
}

r, err = c.Run("/login", "=name="+username, "=response="+c.challengeResponse(b, password))
if err != nil {
return err
}
_, err = c.RunContext(ctx, "/login", "=name="+username, "=response="+c.challengeResponse(dec, password))

return nil
return err
}

// challengeResponse - prepare MD5 hash for auth challenge response
func (c *Client) challengeResponse(cha []byte, password string) string {
h := md5.New()
h := md5.New() //nolint:gosec
h.Write([]byte{0})
io.WriteString(h, password)
h.Write([]byte(password))
h.Write(cha)
return fmt.Sprintf("00%x", h.Sum(nil))
}
Loading