package tcp import ( "context" "crypto/tls" "errors" "fmt" "io" "net" "time" "github.com/plgd-dev/go-coap/v2/message" "github.com/plgd-dev/go-coap/v2/message/codes" coapNet "github.com/plgd-dev/go-coap/v2/net" "github.com/plgd-dev/go-coap/v2/net/blockwise" "github.com/plgd-dev/go-coap/v2/net/monitor/inactivity" "github.com/plgd-dev/go-coap/v2/pkg/runner/periodic" "github.com/plgd-dev/go-coap/v2/tcp/message/pool" kitSync "github.com/plgd-dev/kit/v2/sync" ) var defaultDialOptions = func() dialOptions { opts := dialOptions{ ctx: context.Background(), maxMessageSize: 64 * 1024, errors: func(err error) { fmt.Println(err) }, goPool: func(f func()) error { go func() { f() }() return nil }, dialer: &net.Dialer{Timeout: time.Second * 3}, net: "tcp", blockwiseSZX: blockwise.SZX1024, blockwiseEnable: true, blockwiseTransferTimeout: time.Second * 3, createInactivityMonitor: func() inactivity.Monitor { return inactivity.NewNilMonitor() }, periodicRunner: func(f func(now time.Time) bool) { go func() { for f(time.Now()) { time.Sleep(4 * time.Second) } }() }, connectionCacheSize: 2048, messagePool: pool.New(1024, 2048), } opts.handler = func(w *ResponseWriter, r *pool.Message) { switch r.Code() { case codes.POST, codes.PUT, codes.GET, codes.DELETE: if err := w.SetResponse(codes.NotFound, message.TextPlain, nil); err != nil { opts.errors(fmt.Errorf("client handler: cannot set response: %w", err)) } } } return opts }() type dialOptions struct { ctx context.Context net string blockwiseTransferTimeout time.Duration messagePool *pool.Pool goPool GoPoolFunc dialer *net.Dialer tlsCfg *tls.Config periodicRunner periodic.Func createInactivityMonitor func() inactivity.Monitor handler HandlerFunc errors ErrorFunc maxMessageSize uint32 connectionCacheSize uint16 disablePeerTCPSignalMessageCSMs bool closeSocket bool blockwiseEnable bool blockwiseSZX blockwise.SZX disableTCPSignalMessageCSM bool } // A DialOption sets options such as credentials, keepalive parameters, etc. type DialOption interface { applyDial(*dialOptions) } type Notifier interface { Notify() } // ClientConn represents a virtual connection to a conceptual endpoint, to perform COAPs commands. type ClientConn struct { noCopy session *Session observationTokenHandler *HandlerContainer observationRequests *kitSync.Map } // Dial creates a client connection to the given target. func Dial(target string, opts ...DialOption) (*ClientConn, error) { cfg := defaultDialOptions for _, o := range opts { o.applyDial(&cfg) } var conn net.Conn var err error if cfg.tlsCfg != nil { conn, err = tls.DialWithDialer(cfg.dialer, cfg.net, target, cfg.tlsCfg) } else { conn, err = cfg.dialer.DialContext(cfg.ctx, cfg.net, target) } if err != nil { return nil, err } opts = append(opts, WithCloseSocket()) return Client(conn, opts...), nil } func bwCreateAcquireMessage(messagePool *pool.Pool) func(ctx context.Context) blockwise.Message { return func(ctx context.Context) blockwise.Message { return messagePool.AcquireMessage(ctx) } } func bwCreateReleaseMessage(messagePool *pool.Pool) func(m blockwise.Message) { return func(m blockwise.Message) { messagePool.ReleaseMessage(m.(*pool.Message)) } } func bwCreateHandlerFunc(messagePool *pool.Pool, observationRequests *kitSync.Map) func(token message.Token) (blockwise.Message, bool) { return func(token message.Token) (blockwise.Message, bool) { msg, ok := observationRequests.LoadWithFunc(token.Hash(), func(v interface{}) interface{} { r := v.(message.Message) d := messagePool.AcquireMessage(r.Context) d.ResetOptionsTo(r.Options) d.SetCode(r.Code) d.SetToken(r.Token) return d }) if !ok { return nil, ok } bwMessage := msg.(blockwise.Message) return bwMessage, ok } } // Client creates client over tcp/tcp-tls connection. func Client(conn net.Conn, opts ...DialOption) *ClientConn { cfg := defaultDialOptions for _, o := range opts { o.applyDial(&cfg) } if cfg.errors == nil { cfg.errors = func(error) { // default no-op } } if cfg.createInactivityMonitor == nil { cfg.createInactivityMonitor = func() inactivity.Monitor { return inactivity.NewNilMonitor() } } if cfg.messagePool == nil { cfg.messagePool = pool.New(0, 0) } errorsFunc := cfg.errors cfg.errors = func(err error) { if coapNet.IsCancelOrCloseError(err) { // this error was produced by cancellation context or closing connection. return } errorsFunc(fmt.Errorf("tcp: %w", err)) } observationRequests := kitSync.NewMap() var blockWise *blockwise.BlockWise if cfg.blockwiseEnable { blockWise = blockwise.NewBlockWise( bwCreateAcquireMessage(cfg.messagePool), bwCreateReleaseMessage(cfg.messagePool), cfg.blockwiseTransferTimeout, cfg.errors, false, bwCreateHandlerFunc(cfg.messagePool, observationRequests), ) } l := coapNet.NewConn(conn) monitor := cfg.createInactivityMonitor() observationTokenHandler := NewHandlerContainer() session := NewSession(cfg.ctx, l, NewObservationHandler(observationTokenHandler, cfg.handler), cfg.maxMessageSize, cfg.goPool, cfg.errors, cfg.blockwiseSZX, blockWise, cfg.disablePeerTCPSignalMessageCSMs, cfg.disableTCPSignalMessageCSM, cfg.closeSocket, monitor, cfg.connectionCacheSize, cfg.messagePool, ) cc := NewClientConn(session, observationTokenHandler, observationRequests) cfg.periodicRunner(func(now time.Time) bool { cc.CheckExpirations(now) return cc.Context().Err() == nil }) go func() { err := cc.Run() if err != nil { cfg.errors(fmt.Errorf("%v: %w", cc.RemoteAddr(), err)) } }() return cc } // NewClientConn creates connection over session and observation. func NewClientConn(session *Session, observationTokenHandler *HandlerContainer, observationRequests *kitSync.Map) *ClientConn { return &ClientConn{ session: session, observationTokenHandler: observationTokenHandler, observationRequests: observationRequests, } } func (cc *ClientConn) Session() *Session { return cc.session } // Close closes connection without wait of ends Run function. func (cc *ClientConn) Close() error { err := cc.session.Close() if errors.Is(err, net.ErrClosed) { return nil } return err } func (cc *ClientConn) do(req *pool.Message) (*pool.Message, error) { token := req.Token() if token == nil { return nil, fmt.Errorf("invalid token") } respChan := make(chan *pool.Message, 1) if err := cc.session.TokenHandler().Insert(token, func(w *ResponseWriter, r *pool.Message) { r.Hijack() select { case respChan <- r: default: } }); err != nil { return nil, fmt.Errorf("cannot add token handler: %w", err) } defer func() { if _, err := cc.session.TokenHandler().Pop(token); err != nil && !errors.Is(err, ErrKeyNotExists) { cc.session.errors(fmt.Errorf("cannot remove token handler: %w", err)) } }() if err := cc.session.WriteMessage(req); err != nil { return nil, fmt.Errorf("cannot write request: %w", err) } select { case <-req.Context().Done(): return nil, req.Context().Err() case <-cc.session.Context().Done(): return nil, fmt.Errorf("connection was closed: %w", cc.Context().Err()) case resp := <-respChan: return resp, nil } } // Do sends an coap message and returns an coap response. // // An error is returned if by failure to speak COAP (such as a network connectivity problem). // Any status code doesn't cause an error. // // Caller is responsible to release request and response. func (cc *ClientConn) Do(req *pool.Message) (*pool.Message, error) { if !cc.session.PeerBlockWiseTransferEnabled() || cc.session.blockWise == nil { return cc.do(req) } bwresp, err := cc.session.blockWise.Do(req, cc.session.blockwiseSZX, cc.session.maxMessageSize, func(bwreq blockwise.Message) (blockwise.Message, error) { return cc.do(bwreq.(*pool.Message)) }) if err != nil { return nil, err } return bwresp.(*pool.Message), nil } func (cc *ClientConn) writeMessage(req *pool.Message) error { return cc.session.WriteMessage(req) } // WriteMessage sends an coap message. func (cc *ClientConn) WriteMessage(req *pool.Message) error { if !cc.session.PeerBlockWiseTransferEnabled() || cc.session.blockWise == nil { return cc.writeMessage(req) } return cc.session.blockWise.WriteMessage(cc.RemoteAddr(), req, cc.session.blockwiseSZX, cc.session.maxMessageSize, func(bwreq blockwise.Message) error { return cc.writeMessage(bwreq.(*pool.Message)) }) } func newCommonRequest(ctx context.Context, messagePool *pool.Pool, code codes.Code, path string, opts ...message.Option) (*pool.Message, error) { token, err := message.GetToken() if err != nil { return nil, fmt.Errorf("cannot get token: %w", err) } req := messagePool.AcquireMessage(ctx) req.SetCode(code) req.SetToken(token) req.ResetOptionsTo(opts) if err := req.SetPath(path); err != nil { messagePool.ReleaseMessage(req) return nil, err } return req, nil } // NewGetRequest creates get request. // // Use ctx to set timeout. func NewGetRequest(ctx context.Context, messagePool *pool.Pool, path string, opts ...message.Option) (*pool.Message, error) { return newCommonRequest(ctx, messagePool, codes.GET, path, opts...) } // Get issues a GET to the specified path. // // Use ctx to set timeout. // // An error is returned if by failure to speak COAP (such as a network connectivity problem). // Any status code doesn't cause an error. func (cc *ClientConn) Get(ctx context.Context, path string, opts ...message.Option) (*pool.Message, error) { req, err := NewGetRequest(ctx, cc.session.messagePool, path, opts...) if err != nil { return nil, fmt.Errorf("cannot create get request: %w", err) } defer cc.session.messagePool.ReleaseMessage(req) return cc.Do(req) } // NewPostRequest creates post request. // // Use ctx to set timeout. // // An error is returned if by failure to speak COAP (such as a network connectivity problem). // Any status code doesn't cause an error. // // If payload is nil then content format is not used. func NewPostRequest(ctx context.Context, messagePool *pool.Pool, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) { req, err := newCommonRequest(ctx, messagePool, codes.POST, path, opts...) if err != nil { return nil, err } if payload != nil { req.SetContentFormat(contentFormat) req.SetBody(payload) } return req, nil } // Post issues a POST to the specified path. // // Use ctx to set timeout. // // An error is returned if by failure to speak COAP (such as a network connectivity problem). // Any status code doesn't cause an error. // // If payload is nil then content format is not used. func (cc *ClientConn) Post(ctx context.Context, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) { req, err := NewPostRequest(ctx, cc.session.messagePool, path, contentFormat, payload, opts...) if err != nil { return nil, fmt.Errorf("cannot create post request: %w", err) } defer cc.session.messagePool.ReleaseMessage(req) return cc.Do(req) } // NewPutRequest creates put request. // // Use ctx to set timeout. // // If payload is nil then content format is not used. func NewPutRequest(ctx context.Context, messagePool *pool.Pool, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) { req, err := newCommonRequest(ctx, messagePool, codes.PUT, path, opts...) if err != nil { return nil, err } if payload != nil { req.SetContentFormat(contentFormat) req.SetBody(payload) } return req, nil } // Put issues a PUT to the specified path. // // Use ctx to set timeout. // // An error is returned if by failure to speak COAP (such as a network connectivity problem). // Any status code doesn't cause an error. // // If payload is nil then content format is not used. func (cc *ClientConn) Put(ctx context.Context, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) { req, err := NewPutRequest(ctx, cc.session.messagePool, path, contentFormat, payload, opts...) if err != nil { return nil, fmt.Errorf("cannot create put request: %w", err) } defer cc.session.messagePool.ReleaseMessage(req) return cc.Do(req) } // NewDeleteRequest creates delete request. // // Use ctx to set timeout. func NewDeleteRequest(ctx context.Context, messagePool *pool.Pool, path string, opts ...message.Option) (*pool.Message, error) { return newCommonRequest(ctx, messagePool, codes.DELETE, path, opts...) } // Delete deletes the resource identified by the request path. // // Use ctx to set timeout. func (cc *ClientConn) Delete(ctx context.Context, path string, opts ...message.Option) (*pool.Message, error) { req, err := NewDeleteRequest(ctx, cc.session.messagePool, path, opts...) if err != nil { return nil, fmt.Errorf("cannot create delete request: %w", err) } defer cc.session.messagePool.ReleaseMessage(req) return cc.Do(req) } // Context returns the client's context. // // If connections was closed context is cancelled. func (cc *ClientConn) Context() context.Context { return cc.session.Context() } // Ping issues a PING to the client and waits for PONG response. // // Use ctx to set timeout. func (cc *ClientConn) Ping(ctx context.Context) error { resp := make(chan bool, 1) receivedPong := func() { select { case resp <- true: default: } } cancel, err := cc.AsyncPing(receivedPong) if err != nil { return err } defer cancel() select { case <-resp: return nil case <-ctx.Done(): return ctx.Err() } } // AsyncPing sends ping and receivedPong will be called when pong arrives. It returns cancellation of ping operation. func (cc *ClientConn) AsyncPing(receivedPong func()) (func(), error) { token, err := message.GetToken() if err != nil { return nil, fmt.Errorf("cannot get token: %w", err) } req := cc.session.messagePool.AcquireMessage(cc.Context()) req.SetToken(token) req.SetCode(codes.Ping) defer cc.session.messagePool.ReleaseMessage(req) err = cc.session.TokenHandler().Insert(token, func(w *ResponseWriter, r *pool.Message) { if r.Code() == codes.Pong { receivedPong() } }) if err != nil { return nil, fmt.Errorf("cannot add token handler: %w", err) } removeTokenHandler := func() { if _, errT := cc.session.TokenHandler().Pop(token); errT != nil && !errors.Is(errT, ErrKeyNotExists) { cc.session.errors(fmt.Errorf("cannot remove token handler: %w", errT)) } } err = cc.session.WriteMessage(req) if err != nil { removeTokenHandler() return nil, fmt.Errorf("cannot write request: %w", err) } return removeTokenHandler, nil } // Run reads and process requests from a connection, until the connection is not closed. func (cc *ClientConn) Run() (err error) { return cc.session.Run(cc) } // AddOnClose calls function on close connection event. func (cc *ClientConn) AddOnClose(f EventFunc) { cc.session.AddOnClose(f) } // RemoteAddr gets remote address. func (cc *ClientConn) RemoteAddr() net.Addr { return cc.session.connection.RemoteAddr() } func (cc *ClientConn) LocalAddr() net.Addr { return cc.session.connection.LocalAddr() } // Client get instance which implements mux.Client. func (cc *ClientConn) Client() *ClientTCP { return NewClientTCP(cc) } // Sequence acquires sequence number. func (cc *ClientConn) Sequence() uint64 { return cc.session.Sequence() } // SetContextValue stores the value associated with key to context of connection. func (cc *ClientConn) SetContextValue(key interface{}, val interface{}) { cc.session.SetContextValue(key, val) } // Done signalizes that connection is not more processed. func (cc *ClientConn) Done() <-chan struct{} { return cc.session.Done() } // CheckExpirations checks and remove expired items from caches. func (cc *ClientConn) CheckExpirations(now time.Time) { cc.session.CheckExpirations(now, cc) } func (cc *ClientConn) AcquireMessage(ctx context.Context) *pool.Message { return cc.session.messagePool.AcquireMessage(ctx) } func (cc *ClientConn) ReleaseMessage(m *pool.Message) { cc.session.messagePool.ReleaseMessage(m) }