Skip to content

Commit

Permalink
refactoring, use context, use logger, use go mod (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
archaron authored Jun 9, 2024
1 parent 2a44d57 commit f7d0f00
Show file tree
Hide file tree
Showing 22 changed files with 980 additions and 495 deletions.
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

0 comments on commit f7d0f00

Please sign in to comment.