├── .codecov.yml ├── .github ├── FUNDING.yml └── workflows │ ├── cifuzz.yml │ ├── golangci-lint.yml │ ├── staticAnalysis.yml │ ├── test-for-fork.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── dtls ├── client.go ├── client_test.go ├── example_test.go ├── server.go ├── server │ ├── config.go │ ├── server.go │ └── session.go └── server_test.go ├── examples ├── dtls │ ├── cid │ │ ├── client │ │ │ └── main.go │ │ └── server │ │ │ └── main.go │ ├── pki │ │ ├── cert_gen.go │ │ ├── cert_gen_test.go │ │ ├── cert_generation.md │ │ ├── cert_util.go │ │ ├── client │ │ │ └── main.go │ │ └── server │ │ │ └── main.go │ └── psk │ │ ├── client │ │ └── main.go │ │ └── server │ │ └── main.go ├── mcast │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── observe │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── options │ └── server │ │ └── main.go └── simple │ ├── client │ └── main.go │ └── server │ └── main.go ├── go.mod ├── go.sum ├── message ├── bench_test.go ├── codes │ ├── code_string.go │ ├── codes.go │ └── codes_test.go ├── encodeDecodeUint32.go ├── encodeDecodeUint32_test.go ├── error.go ├── getETag.go ├── getETag_test.go ├── getToken.go ├── getToken_test.go ├── getmid.go ├── message.go ├── message_internal_test.go ├── noresponse │ ├── error.go │ ├── noresponse.go │ └── noresponse_test.go ├── option.go ├── option_test.go ├── options.go ├── options_test.go ├── pool │ ├── message.go │ ├── message_test.go │ └── pool.go ├── status │ ├── status.go │ └── status_test.go ├── tcpOptions.go └── type.go ├── mux ├── client.go ├── example_logging_middleware_test.go ├── message.go ├── middleware.go ├── muxResponseWriter.go ├── regexp.go ├── router.go └── router_test.go ├── net ├── blockwise │ ├── blockwise.go │ ├── blockwise_test.go │ └── error.go ├── client │ ├── client.go │ ├── limitParallelRequests │ │ ├── limitParallelRequests.go │ │ └── limitParallelRequests_test.go │ └── receivedMessageReader.go ├── conn.go ├── connUDP.go ├── connUDP_internal_test.go ├── conn_test.go ├── dtlslistener.go ├── error.go ├── error_plan9.go ├── error_unix.go ├── error_windows.go ├── monitor │ └── inactivity │ │ ├── keepalive.go │ │ └── monitor.go ├── observation │ ├── handler.go │ ├── observation.go │ └── observation_test.go ├── options.go ├── responsewriter │ └── responseWriter.go ├── supportsOverrideRemoteAddr.go ├── tcplistener.go ├── tlslistener.go └── tlslistener_test.go ├── options ├── commonOptions.go ├── commonOptions_test.go ├── config │ └── common.go ├── tcpOptions.go ├── tcpOptions_test.go ├── udpOptions.go └── udpOptions_test.go ├── pkg ├── cache │ ├── cache.go │ └── cache_test.go ├── connections │ └── connections.go ├── errors │ └── error.go ├── fn │ ├── funcList.go │ └── funcList_test.go ├── math │ ├── cast.go │ └── cast_test.go ├── rand │ ├── rand.go │ └── rand_test.go ├── runner │ └── periodic │ │ └── periodic.go └── sync │ ├── map.go │ └── map_test.go ├── renovate.json ├── server.go ├── sonar-project.properties ├── tcp ├── client.go ├── client │ ├── config.go │ ├── conn.go │ └── session.go ├── client_test.go ├── clientobserve_test.go ├── coder │ ├── bench_test.go │ ├── coder.go │ ├── coder_test.go │ └── error.go ├── example_test.go ├── server.go ├── server │ ├── config.go │ └── server.go └── server_test.go ├── test └── net │ └── uri.go ├── udp ├── client.go ├── client │ ├── bench_test.go │ ├── config.go │ ├── conn.go │ ├── conn_test.go │ ├── mutexmap.go │ ├── mutexmap_test.go │ └── observe_test.go ├── client_test.go ├── coder │ ├── bench_test.go │ ├── coder.go │ ├── coder_test.go │ └── error.go ├── example_test.go ├── isRunninigInterface_1.18_test.go ├── isRunninigInterface_1.20_test.go ├── server.go ├── server │ ├── config.go │ ├── discover.go │ ├── server.go │ └── session.go └── server_test.go └── v3 /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "examples" 3 | - "**/*_test.go" 4 | - "v3" 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: go-coap 2 | -------------------------------------------------------------------------------- /.github/workflows/cifuzz.yml: -------------------------------------------------------------------------------- 1 | name: CIFuzz 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | permissions: {} 6 | jobs: 7 | Fuzzing: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | security-events: write 11 | steps: 12 | - name: Build Fuzzers 13 | id: build 14 | uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master 15 | with: 16 | oss-fuzz-project-name: 'go-coap' 17 | language: go 18 | - name: Run Fuzzers 19 | uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master 20 | with: 21 | oss-fuzz-project-name: 'go-coap' 22 | language: go 23 | fuzz-seconds: 300 24 | output-sarif: true 25 | - name: Upload Crash 26 | uses: actions/upload-artifact@v4 27 | if: failure() && steps.build.outcome == 'success' 28 | with: 29 | name: artifacts 30 | path: ./out/artifacts 31 | - name: Upload Sarif 32 | if: always() && steps.build.outcome == 'success' 33 | uses: github/codeql-action/upload-sarif@v3 34 | with: 35 | # Path to SARIF file relative to the root of the repository 36 | sarif_file: cifuzz-sarif/results.sarif 37 | checkout_path: cifuzz-sarif 38 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: Golangci-lint 2 | 3 | # golangci-lint is a fast Go linters runner. It runs linters in parallel, 4 | # uses caching, supports yaml config, has integrations with all major IDE and 5 | # has dozens of linters included. 6 | # see: https://github.com/golangci/golangci-lint-action 7 | 8 | on: 9 | pull_request: 10 | workflow_dispatch: 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 1.20+ 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: "^1.20" # The Go version to download (if necessary) and use. 23 | check-latest: true 24 | 25 | - run: go version 26 | 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v6 29 | with: 30 | version: latest 31 | args: --timeout=5m 32 | -------------------------------------------------------------------------------- /.github/workflows/staticAnalysis.yml: -------------------------------------------------------------------------------- 1 | # Run static analysis checks 2 | name: Static Analysis 3 | 4 | on: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | analysis: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Go 1.20+ 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "^1.20" # The Go version to download (if necessary) and use. 20 | check-latest: true 21 | 22 | - run: go version 23 | 24 | - name: Run go vet 25 | run: go vet ./... 26 | 27 | - name: Install and run gocyclo 28 | run: | 29 | export PATH=${PATH}:`go env GOPATH`/bin 30 | go install github.com/fzipp/gocyclo/cmd/gocyclo@latest 31 | gocyclo -over 15 -ignore ".pb(.gw)?.go$|_test.go$|wsproxy" . || echo "gocyclo detected too complex functions" 32 | -------------------------------------------------------------------------------- /.github/workflows/test-for-fork.yml: -------------------------------------------------------------------------------- 1 | # Workflow to run tests for PRs from forked repository 2 | name: Test for forked repository 3 | 4 | # Run on pull requests events from forked repository 5 | on: [pull_request] 6 | 7 | jobs: 8 | test-for-fork: 9 | # Run only for forked repository 10 | if: github.event.pull_request.head.repo.full_name != github.repository 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macOS-latest] 15 | 16 | steps: 17 | - name: Set up Go 1.20+ 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: "^1.20" 21 | check-latest: true 22 | 23 | - run: go version 24 | 25 | # Checks-out repository under $GITHUB_WORKSPACE 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | # Build everything 30 | - name: Run a build 31 | run: go build ./... 32 | 33 | # Runs a single command using the runners shell, -p1 for `race: limit on 8128 simultaneously alive goroutines is exceeded, dying` at macos 34 | - name: Run a test 35 | run: go test -v -race ./... -coverpkg=./... -covermode=atomic -coverprofile=./coverage.txt 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Workflow to run tests, publish coverage to codecov and run SonarCloud scan 2 | name: Test 3 | 4 | # Run for events in main repository (for forked repository look in test-for-fork.yml) 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | # don't run for forks 15 | if: github.event_name == 'push' || 16 | (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || 17 | github.event_name == 'workflow_dispatch' 18 | 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: [ubuntu-latest, windows-latest, macOS-latest] 24 | 25 | steps: 26 | - name: Set up Go 1.20+ 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: "^1.20" 30 | check-latest: true 31 | 32 | - run: go version 33 | 34 | # Checks-out repository under $GITHUB_WORKSPACE with tags and history (needed by "SonarCloud Scan" step) 35 | - name: Full checkout 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 # Full clone for SonarCloud 39 | 40 | # Build everything 41 | - name: Run a build 42 | run: go build ./... 43 | shell: bash 44 | 45 | # Runs a single command using the runners shell, -p1 for `race: limit on 8128 simultaneously alive goroutines is exceeded, dying` at macos 46 | - name: Run a test 47 | run: go test -v -race ./... -coverpkg=./... -covermode=atomic -coverprofile=./coverage.txt -json > ./report.json 48 | shell: bash 49 | 50 | - name: Dump test report 51 | if: always() 52 | run: cat ./report.json 53 | shell: bash 54 | 55 | - name: Prepare upload files 56 | run: | 57 | mkdir -p ./outputs 58 | cp ./coverage.txt ./outputs/${{ matrix.os }}.coverage.txt 59 | cp ./report.json ./outputs/${{ matrix.os }}.report.json 60 | shell: bash 61 | 62 | - name: Upload coverage and report files 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: ${{ hashFiles('./outputs') || 'none' }} 66 | path: ./outputs 67 | retention-days: 1 68 | if-no-files-found: warn 69 | 70 | coverage-sonar-cloud-scan: 71 | needs: test 72 | # The type of runner that the job will run on 73 | runs-on: ubuntu-latest 74 | steps: 75 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 76 | - uses: actions/checkout@v4 77 | with: 78 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 79 | 80 | - name: Download artifacts 81 | uses: actions/download-artifact@v4 82 | with: 83 | path: ./outputs 84 | 85 | - name: Prepare coverage and report files 86 | run: | 87 | mkdir -p .tmp/coverage 88 | mkdir -p .tmp/report 89 | find ./outputs -name "*.coverage.txt" -exec sh -c 'cp $1 .tmp/coverage/$(echo $1 | sed "s/[\/.]/-/g" ).coverage.txt' _ {} \; 90 | find ./outputs -name "*.report.json" -exec sh -c 'cp $1 .tmp/report/$(echo $1 | sed "s/[\/.]/-/g" ).report.json' _ {} \; 91 | 92 | - name: Code coverage 93 | uses: codecov/codecov-action@v4.0.0-beta.3 94 | with: 95 | token: ${{ secrets.CODECOV_TOKEN }} 96 | directory: .tmp/ 97 | 98 | - name: SonarCloud Scan 99 | uses: SonarSource/sonarcloud-github-action@master 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 103 | 104 | # test on oldest supported major Go version 105 | test1_20: 106 | # don't run for forks 107 | if: github.event_name == 'push' || 108 | (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || 109 | github.event_name == 'workflow_dispatch' 110 | 111 | runs-on: ubuntu-latest 112 | 113 | steps: 114 | - name: Set up Go 1.20 115 | uses: actions/setup-go@v5 116 | with: 117 | go-version: "~1.20" 118 | 119 | - run: go version 120 | 121 | - name: Checkout 122 | uses: actions/checkout@v4 123 | 124 | - name: Run a build 125 | run: go build ./... 126 | 127 | - name: Run a test 128 | run: go test -v -race ./... -coverpkg=./... -covermode=atomic -coverprofile=./coverage.txt 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | debug 8 | server 9 | !server/ 10 | client 11 | !client/ 12 | vendor/ 13 | v3/ 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.testFlags": [ 3 | "-race", 4 | "-v", 5 | "-cover", 6 | ], 7 | "go.testEnvVars": { 8 | // "GOFLAGS":"-mod=vendor", 9 | }, 10 | "go.testTimeout": "40s", 11 | "files.watcherExclude": { 12 | "**/go-coap/v3/**": true 13 | } 14 | } -------------------------------------------------------------------------------- /dtls/client.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/pion/dtls/v3" 8 | dtlsnet "github.com/pion/dtls/v3/pkg/net" 9 | "github.com/plgd-dev/go-coap/v3/dtls/server" 10 | "github.com/plgd-dev/go-coap/v3/message" 11 | "github.com/plgd-dev/go-coap/v3/message/codes" 12 | "github.com/plgd-dev/go-coap/v3/message/pool" 13 | coapNet "github.com/plgd-dev/go-coap/v3/net" 14 | "github.com/plgd-dev/go-coap/v3/net/blockwise" 15 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 16 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 17 | "github.com/plgd-dev/go-coap/v3/options" 18 | "github.com/plgd-dev/go-coap/v3/udp" 19 | udpClient "github.com/plgd-dev/go-coap/v3/udp/client" 20 | ) 21 | 22 | var DefaultConfig = func() udpClient.Config { 23 | cfg := udpClient.DefaultConfig 24 | cfg.Handler = func(w *responsewriter.ResponseWriter[*udpClient.Conn], r *pool.Message) { 25 | switch r.Code() { 26 | case codes.POST, codes.PUT, codes.GET, codes.DELETE: 27 | if err := w.SetResponse(codes.NotFound, message.TextPlain, nil); err != nil { 28 | cfg.Errors(fmt.Errorf("dtls client: cannot set response: %w", err)) 29 | } 30 | } 31 | } 32 | return cfg 33 | }() 34 | 35 | // Dial creates a client connection to the given target. 36 | func Dial(target string, dtlsCfg *dtls.Config, opts ...udp.Option) (*udpClient.Conn, error) { 37 | cfg := DefaultConfig 38 | for _, o := range opts { 39 | o.UDPClientApply(&cfg) 40 | } 41 | 42 | c, err := cfg.Dialer.DialContext(cfg.Ctx, cfg.Net, target) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | conn, err := dtls.Client(dtlsnet.PacketConnFromConn(c), c.RemoteAddr(), dtlsCfg) 48 | if err != nil { 49 | return nil, err 50 | } 51 | opts = append(opts, options.WithCloseSocket()) 52 | return Client(conn, opts...), nil 53 | } 54 | 55 | // Client creates client over dtls connection. 56 | func Client(conn *dtls.Conn, opts ...udp.Option) *udpClient.Conn { 57 | cfg := DefaultConfig 58 | for _, o := range opts { 59 | o.UDPClientApply(&cfg) 60 | } 61 | if cfg.Errors == nil { 62 | cfg.Errors = func(error) { 63 | // default no-op 64 | } 65 | } 66 | if cfg.CreateInactivityMonitor == nil { 67 | cfg.CreateInactivityMonitor = func() udpClient.InactivityMonitor { 68 | return inactivity.NewNilMonitor[*udpClient.Conn]() 69 | } 70 | } 71 | if cfg.MessagePool == nil { 72 | cfg.MessagePool = pool.New(0, 0) 73 | } 74 | errorsFunc := cfg.Errors 75 | cfg.Errors = func(err error) { 76 | if coapNet.IsCancelOrCloseError(err) { 77 | // this error was produced by cancellation context or closing connection. 78 | return 79 | } 80 | errorsFunc(fmt.Errorf("dtls: %v: %w", conn.RemoteAddr(), err)) 81 | } 82 | 83 | createBlockWise := func(*udpClient.Conn) *blockwise.BlockWise[*udpClient.Conn] { 84 | return nil 85 | } 86 | if cfg.BlockwiseEnable { 87 | createBlockWise = func(cc *udpClient.Conn) *blockwise.BlockWise[*udpClient.Conn] { 88 | v := cc 89 | return blockwise.New( 90 | v, 91 | cfg.BlockwiseTransferTimeout, 92 | cfg.Errors, 93 | func(token message.Token) (*pool.Message, bool) { 94 | return v.GetObservationRequest(token) 95 | }, 96 | ) 97 | } 98 | } 99 | 100 | monitor := cfg.CreateInactivityMonitor() 101 | l := coapNet.NewConn(conn) 102 | session := server.NewSession(cfg.Ctx, 103 | l, 104 | cfg.MaxMessageSize, 105 | cfg.MTU, 106 | cfg.CloseSocket, 107 | ) 108 | cc := udpClient.NewConnWithOpts(session, 109 | &cfg, 110 | udpClient.WithBlockWise(createBlockWise), 111 | udpClient.WithInactivityMonitor(monitor), 112 | udpClient.WithRequestMonitor(cfg.RequestMonitor), 113 | ) 114 | 115 | cfg.PeriodicRunner(func(now time.Time) bool { 116 | cc.CheckExpirations(now) 117 | return cc.Context().Err() == nil 118 | }) 119 | 120 | go func() { 121 | err := cc.Run() 122 | if err != nil { 123 | cfg.Errors(err) 124 | } 125 | }() 126 | 127 | return cc 128 | } 129 | -------------------------------------------------------------------------------- /dtls/example_test.go: -------------------------------------------------------------------------------- 1 | package dtls_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "time" 9 | 10 | piondtls "github.com/pion/dtls/v3" 11 | "github.com/plgd-dev/go-coap/v3/dtls" 12 | "github.com/plgd-dev/go-coap/v3/net" 13 | ) 14 | 15 | func ExampleConn_Get() { 16 | dtlsCfg := &piondtls.Config{ 17 | PSK: func(hint []byte) ([]byte, error) { 18 | fmt.Printf("Hint: %s \n", hint) 19 | return []byte{0xAB, 0xC1, 0x23}, nil 20 | }, 21 | PSKIdentityHint: []byte("Pion DTLS Server"), 22 | CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_PSK_WITH_AES_128_CCM_8}, 23 | } 24 | conn, err := dtls.Dial("pluggedin.cloud:5684", dtlsCfg) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | defer conn.Close() 29 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 30 | defer cancel() 31 | res, err := conn.Get(ctx, "/oic/res") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | data, err := io.ReadAll(res.Body()) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | fmt.Printf("%v", data) 40 | } 41 | 42 | func ExampleServer() { 43 | dtlsCfg := &piondtls.Config{ 44 | PSK: func(hint []byte) ([]byte, error) { 45 | fmt.Printf("Hint: %s \n", hint) 46 | return []byte{0xAB, 0xC1, 0x23}, nil 47 | }, 48 | PSKIdentityHint: []byte("Pion DTLS Server"), 49 | CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_PSK_WITH_AES_128_CCM_8}, 50 | } 51 | l, err := net.NewDTLSListener("udp", "0.0.0.0:5683", dtlsCfg) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | defer l.Close() 56 | s := dtls.NewServer() 57 | defer s.Stop() 58 | log.Fatal(s.Serve(l)) 59 | } 60 | -------------------------------------------------------------------------------- /dtls/server.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import "github.com/plgd-dev/go-coap/v3/dtls/server" 4 | 5 | func NewServer(opt ...server.Option) *server.Server { 6 | return server.New(opt...) 7 | } 8 | -------------------------------------------------------------------------------- /dtls/server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/plgd-dev/go-coap/v3/message" 8 | "github.com/plgd-dev/go-coap/v3/message/codes" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 11 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 12 | "github.com/plgd-dev/go-coap/v3/options/config" 13 | udpClient "github.com/plgd-dev/go-coap/v3/udp/client" 14 | ) 15 | 16 | // The HandlerFunc type is an adapter to allow the use of 17 | // ordinary functions as COAP handlers. 18 | type HandlerFunc = func(*responsewriter.ResponseWriter[*udpClient.Conn], *pool.Message) 19 | 20 | type ErrorFunc = func(error) 21 | 22 | // OnNewConnFunc is the callback for new connections. 23 | type OnNewConnFunc = func(*udpClient.Conn) 24 | 25 | type GetMIDFunc = func() int32 26 | 27 | var DefaultConfig = func() Config { 28 | opts := Config{ 29 | Common: config.NewCommon[*udpClient.Conn](), 30 | CreateInactivityMonitor: func() udpClient.InactivityMonitor { 31 | timeout := time.Second * 16 32 | onInactive := func(cc *udpClient.Conn) { 33 | _ = cc.Close() 34 | } 35 | return inactivity.New(timeout, onInactive) 36 | }, 37 | RequestMonitor: func(*udpClient.Conn, *pool.Message) (bool, error) { 38 | return false, nil 39 | }, 40 | OnNewConn: func(*udpClient.Conn) { 41 | // do nothing by default 42 | }, 43 | TransmissionNStart: 1, 44 | TransmissionAcknowledgeTimeout: time.Second * 2, 45 | TransmissionMaxRetransmit: 4, 46 | GetMID: message.GetMID, 47 | MTU: udpClient.DefaultMTU, 48 | } 49 | opts.Handler = func(w *responsewriter.ResponseWriter[*udpClient.Conn], _ *pool.Message) { 50 | if err := w.SetResponse(codes.NotFound, message.TextPlain, nil); err != nil { 51 | opts.Errors(fmt.Errorf("dtls server: cannot set response: %w", err)) 52 | } 53 | } 54 | return opts 55 | }() 56 | 57 | type Config struct { 58 | config.Common[*udpClient.Conn] 59 | CreateInactivityMonitor func() udpClient.InactivityMonitor 60 | GetMID GetMIDFunc 61 | Handler HandlerFunc 62 | OnNewConn OnNewConnFunc 63 | RequestMonitor udpClient.RequestMonitorFunc 64 | TransmissionNStart uint32 65 | TransmissionAcknowledgeTimeout time.Duration 66 | TransmissionMaxRetransmit uint32 67 | MTU uint16 68 | } 69 | -------------------------------------------------------------------------------- /dtls/server/session.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "sync" 9 | "sync/atomic" 10 | 11 | "github.com/plgd-dev/go-coap/v3/message/pool" 12 | coapNet "github.com/plgd-dev/go-coap/v3/net" 13 | "github.com/plgd-dev/go-coap/v3/udp/client" 14 | "github.com/plgd-dev/go-coap/v3/udp/coder" 15 | ) 16 | 17 | type EventFunc = func() 18 | 19 | type Session struct { 20 | onClose []EventFunc 21 | 22 | ctx atomic.Pointer[context.Context] 23 | 24 | cancel context.CancelFunc 25 | connection *coapNet.Conn 26 | 27 | done chan struct{} 28 | 29 | mutex sync.Mutex 30 | 31 | maxMessageSize uint32 32 | 33 | mtu uint16 34 | 35 | closeSocket bool 36 | } 37 | 38 | func NewSession( 39 | ctx context.Context, 40 | connection *coapNet.Conn, 41 | maxMessageSize uint32, 42 | mtu uint16, 43 | closeSocket bool, 44 | ) *Session { 45 | ctx, cancel := context.WithCancel(ctx) 46 | s := &Session{ 47 | cancel: cancel, 48 | connection: connection, 49 | maxMessageSize: maxMessageSize, 50 | closeSocket: closeSocket, 51 | mtu: mtu, 52 | done: make(chan struct{}), 53 | } 54 | s.ctx.Store(&ctx) 55 | return s 56 | } 57 | 58 | // Done signalizes that connection is not more processed. 59 | func (s *Session) Done() <-chan struct{} { 60 | return s.done 61 | } 62 | 63 | func (s *Session) AddOnClose(f EventFunc) { 64 | s.mutex.Lock() 65 | defer s.mutex.Unlock() 66 | s.onClose = append(s.onClose, f) 67 | } 68 | 69 | func (s *Session) popOnClose() []EventFunc { 70 | s.mutex.Lock() 71 | defer s.mutex.Unlock() 72 | tmp := s.onClose 73 | s.onClose = nil 74 | return tmp 75 | } 76 | 77 | func (s *Session) shutdown() { 78 | defer close(s.done) 79 | for _, f := range s.popOnClose() { 80 | f() 81 | } 82 | } 83 | 84 | func (s *Session) Close() error { 85 | s.cancel() 86 | if s.closeSocket { 87 | return s.connection.Close() 88 | } 89 | return nil 90 | } 91 | 92 | func (s *Session) Context() context.Context { 93 | return *s.ctx.Load() 94 | } 95 | 96 | // SetContextValue stores the value associated with key to context of connection. 97 | func (s *Session) SetContextValue(key interface{}, val interface{}) { 98 | ctx := context.WithValue(s.Context(), key, val) 99 | s.ctx.Store(&ctx) 100 | } 101 | 102 | func (s *Session) WriteMessage(req *pool.Message) error { 103 | data, err := req.MarshalWithEncoder(coder.DefaultCoder) 104 | if err != nil { 105 | return fmt.Errorf("cannot marshal: %w", err) 106 | } 107 | err = s.connection.WriteWithContext(req.Context(), data) 108 | if err != nil { 109 | return fmt.Errorf("cannot write to connection: %w", err) 110 | } 111 | return err 112 | } 113 | 114 | // WriteMulticastMessage sends multicast to the remote multicast address. 115 | // Currently it is not implemented - is just satisfy golang udp/client/Session interface. 116 | func (s *Session) WriteMulticastMessage(*pool.Message, *net.UDPAddr, ...coapNet.MulticastOption) error { 117 | return errors.New("multicast messages not implemented for DTLS") 118 | } 119 | 120 | func (s *Session) MaxMessageSize() uint32 { 121 | return s.maxMessageSize 122 | } 123 | 124 | func (s *Session) RemoteAddr() net.Addr { 125 | return s.connection.RemoteAddr() 126 | } 127 | 128 | func (s *Session) LocalAddr() net.Addr { 129 | return s.connection.LocalAddr() 130 | } 131 | 132 | // Run reads and processes requests from a connection, until the connection is closed. 133 | func (s *Session) Run(cc *client.Conn) (err error) { 134 | defer func() { 135 | err1 := s.Close() 136 | if err == nil { 137 | err = err1 138 | } 139 | s.shutdown() 140 | }() 141 | m := make([]byte, s.mtu) 142 | for { 143 | readBuf := m 144 | readLen, err := s.connection.ReadWithContext(s.Context(), readBuf) 145 | if err != nil { 146 | return fmt.Errorf("cannot read from connection: %w", err) 147 | } 148 | readBuf = readBuf[:readLen] 149 | err = cc.Process(nil, readBuf) 150 | if err != nil { 151 | return err 152 | } 153 | } 154 | } 155 | 156 | // NetConn returns the underlying connection that is wrapped by s. The Conn returned is shared by all invocations of NetConn, so do not modify it. 157 | func (s *Session) NetConn() net.Conn { 158 | return s.connection.NetConn() 159 | } 160 | -------------------------------------------------------------------------------- /examples/dtls/cid/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | piondtls "github.com/pion/dtls/v3" 10 | "github.com/plgd-dev/go-coap/v3/dtls" 11 | ) 12 | 13 | func main() { 14 | conf := &piondtls.Config{ 15 | PSK: func(hint []byte) ([]byte, error) { 16 | fmt.Printf("Server's hint: %s \n", hint) 17 | return []byte{0xAB, 0xC1, 0x23}, nil 18 | }, 19 | PSKIdentityHint: []byte("Pion DTLS Client"), 20 | CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_PSK_WITH_AES_128_CCM_8}, 21 | ConnectionIDGenerator: piondtls.OnlySendCIDGenerator(), 22 | } 23 | raddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:5688") 24 | if err != nil { 25 | log.Fatalf("Error resolving UDP address: %v", err) 26 | } 27 | 28 | // Setup first UDP listener. 29 | udpconn, err := net.ListenUDP("udp", nil) 30 | if err != nil { 31 | log.Fatalf("Error establishing UDP listener: %v", err) 32 | } 33 | 34 | // Create DTLS client on UDP listener. 35 | client, err := piondtls.Client(udpconn, raddr, conf) 36 | if err != nil { 37 | log.Fatalf("Error establishing DTLS client: %v", err) 38 | } 39 | co := dtls.Client(client) 40 | resp, err := co.Get(context.Background(), "/a") 41 | if err != nil { 42 | log.Fatalf("Error performing request: %v", err) 43 | } 44 | log.Printf("Response payload: %+v", resp) 45 | resp, err = co.Get(context.Background(), "/b") 46 | if err != nil { 47 | log.Fatalf("Error performing request: %v", err) 48 | } 49 | log.Printf("Response payload: %+v", resp) 50 | 51 | // Export state to resume connection from another address. 52 | state, ok := client.ConnectionState() 53 | if !ok { 54 | log.Fatalf("Error exporting DTLS state") 55 | } 56 | 57 | // Setup second UDP listener on a different address. 58 | udpconn, err = net.ListenUDP("udp", nil) 59 | if err != nil { 60 | log.Fatalf("Error establishing UDP listener: %v", err) 61 | } 62 | 63 | // Resume connection on new address with previous state. 64 | client, err = piondtls.Resume(&state, udpconn, raddr, conf) 65 | if err != nil { 66 | log.Fatalf("Error resuming DTLS connection: %v", err) 67 | } 68 | co = dtls.Client(client) 69 | // Requests can be performed without performing a second handshake. 70 | resp, err = co.Get(context.Background(), "/a") 71 | if err != nil { 72 | log.Fatalf("Error performing request: %v", err) 73 | } 74 | log.Printf("Response payload: %+v", resp) 75 | resp, err = co.Get(context.Background(), "/b") 76 | if err != nil { 77 | log.Fatalf("Error performing request: %v", err) 78 | } 79 | log.Printf("Response payload: %+v", resp) 80 | } 81 | -------------------------------------------------------------------------------- /examples/dtls/cid/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "log" 8 | "net" 9 | "time" 10 | 11 | piondtls "github.com/pion/dtls/v3" 12 | "github.com/plgd-dev/go-coap/v3/dtls/server" 13 | "github.com/plgd-dev/go-coap/v3/message" 14 | "github.com/plgd-dev/go-coap/v3/message/codes" 15 | "github.com/plgd-dev/go-coap/v3/mux" 16 | "github.com/plgd-dev/go-coap/v3/options" 17 | udpClient "github.com/plgd-dev/go-coap/v3/udp/client" 18 | "go.uber.org/atomic" 19 | ) 20 | 21 | func handleA(w mux.ResponseWriter, r *mux.Message) { 22 | log.Printf("got message in handleA: %+v from %v\n", r, w.Conn().RemoteAddr()) 23 | err := w.SetResponse(codes.GET, message.TextPlain, bytes.NewReader([]byte("A hello world"))) 24 | if err != nil { 25 | log.Printf("cannot set response: %v", err) 26 | } 27 | } 28 | 29 | func handleB(w mux.ResponseWriter, r *mux.Message) { 30 | log.Printf("got message in handleB: %+v from %v\n", r, w.Conn().RemoteAddr()) 31 | customResp := w.Conn().AcquireMessage(r.Context()) 32 | defer w.Conn().ReleaseMessage(customResp) 33 | customResp.SetCode(codes.Content) 34 | customResp.SetToken(r.Token()) 35 | customResp.SetContentFormat(message.TextPlain) 36 | customResp.SetBody(bytes.NewReader([]byte("B hello world"))) 37 | err := w.Conn().WriteMessage(customResp) 38 | if err != nil { 39 | log.Printf("cannot set response: %v", err) 40 | } 41 | } 42 | 43 | // wrappedListener wraps a net.Listener and implements a go-coap DTLS 44 | // server.Listener. 45 | // NOTE: this utility is for example purposes only. Context should be handled 46 | // properly in meaningful scenarios. 47 | type wrappedListener struct { 48 | l net.Listener 49 | closed atomic.Bool 50 | } 51 | 52 | // AcceptWithContext disregards the passed context and calls the underlying 53 | // net.Listener Accept(). 54 | func (w *wrappedListener) AcceptWithContext(_ context.Context) (net.Conn, error) { 55 | return w.l.Accept() 56 | } 57 | 58 | // Close calls the underlying net.Listener Close(). 59 | func (w *wrappedListener) Close() error { 60 | return w.l.Close() 61 | } 62 | 63 | // wrapListener wraps a net.Listener and returns a DTLS server.Listener. 64 | func wrapListener(l net.Listener) server.Listener { 65 | return &wrappedListener{ 66 | l: l, 67 | } 68 | } 69 | 70 | func main() { 71 | m := mux.NewRouter() 72 | m.Handle("/a", mux.HandlerFunc(handleA)) 73 | m.Handle("/b", mux.HandlerFunc(handleB)) 74 | laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:5688") 75 | if err != nil { 76 | log.Fatalf("Error dialing: %v", err) 77 | } 78 | l, err := piondtls.Listen("udp", laddr, &piondtls.Config{ 79 | PSK: func(hint []byte) ([]byte, error) { 80 | fmt.Printf("Client's hint: %s \n", hint) 81 | return []byte{0xAB, 0xC1, 0x23}, nil 82 | }, 83 | PSKIdentityHint: []byte("Pion DTLS Server"), 84 | CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_PSK_WITH_AES_128_CCM_8}, 85 | ConnectionIDGenerator: piondtls.RandomCIDGenerator(8), 86 | }) 87 | if err != nil { 88 | log.Fatalf("Error establishing DTLS listener: %v", err) 89 | } 90 | s := server.New(options.WithMux(m), options.WithInactivityMonitor(10*time.Second, func(cc *udpClient.Conn) {})) 91 | s.Serve(wrapListener(l)) 92 | } 93 | -------------------------------------------------------------------------------- /examples/dtls/pki/cert_gen.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "io" 12 | "math/big" 13 | "net" 14 | "time" 15 | ) 16 | 17 | var ( 18 | algo = elliptic.P256() 19 | notBefore = time.Now() 20 | notAfter = notBefore.Add(time.Hour) 21 | subject = pkix.Name{ 22 | Country: []string{"BR"}, 23 | Province: []string{"Parana"}, 24 | Locality: []string{"Curitiba"}, 25 | Organization: []string{"Test"}, 26 | CommonName: "test.com", 27 | } 28 | ) 29 | 30 | func sequentialBytes(n int) io.Reader { 31 | sequence := make([]byte, n) 32 | for i := 0; i < n; i++ { 33 | sequence[i] = byte(i) 34 | } 35 | return bytes.NewReader(sequence) 36 | } 37 | 38 | // GenerateCA creates a deterministic certificate authority (for test purposes only) 39 | func GenerateCA() (ca *x509.Certificate, cert, key []byte, priv *ecdsa.PrivateKey, err error) { 40 | priv, err = ecdsa.GenerateKey(algo, sequentialBytes(64)) 41 | if err != nil { 42 | return 43 | } 44 | 45 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 46 | serialNumber, err := rand.Int(sequentialBytes(128), serialNumberLimit) 47 | if err != nil { 48 | return 49 | } 50 | 51 | ca = &x509.Certificate{ 52 | NotBefore: notBefore, 53 | NotAfter: notAfter, 54 | SerialNumber: serialNumber, 55 | 56 | Subject: subject, 57 | EmailAddresses: []string{"ca@test.com"}, 58 | IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, 59 | 60 | IsCA: true, 61 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 62 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 63 | BasicConstraintsValid: true, 64 | } 65 | 66 | derBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv) 67 | if err != nil { 68 | return 69 | } 70 | cert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 71 | 72 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 73 | if err != nil { 74 | return 75 | } 76 | key = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}) 77 | 78 | return 79 | } 80 | 81 | // GenerateCertificate creates a certificate 82 | func GenerateCertificate(ca *x509.Certificate, caPriv *ecdsa.PrivateKey, email string) (cert, key []byte, err error) { 83 | priv, err := ecdsa.GenerateKey(algo, rand.Reader) 84 | if err != nil { 85 | return 86 | } 87 | 88 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 89 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 90 | if err != nil { 91 | return 92 | } 93 | 94 | template := x509.Certificate{ 95 | NotBefore: notBefore, 96 | NotAfter: notAfter, 97 | SerialNumber: serialNumber, 98 | 99 | Subject: subject, 100 | EmailAddresses: []string{email}, 101 | IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, 102 | 103 | SubjectKeyId: []byte{1, 2, 3, 4, 6}, 104 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 105 | KeyUsage: x509.KeyUsageDigitalSignature, 106 | } 107 | 108 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca, &priv.PublicKey, caPriv) 109 | if err != nil { 110 | return 111 | } 112 | 113 | cert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 114 | 115 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 116 | if err != nil { 117 | return 118 | } 119 | key = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}) 120 | 121 | return 122 | } 123 | -------------------------------------------------------------------------------- /examples/dtls/pki/cert_gen_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestGenerateCA(t *testing.T) { 10 | ca, cert, key, caPriv, err := GenerateCA() 11 | require.NoError(t, err) 12 | require.Contains(t, string(cert), "-----BEGIN CERTIFICATE-----") 13 | require.Contains(t, string(key), "-----BEGIN EC PRIVATE KEY-----") 14 | 15 | cert, key, err = GenerateCertificate(ca, caPriv, "cert@test.com") 16 | require.NoError(t, err) 17 | require.Contains(t, string(cert), "-----BEGIN CERTIFICATE-----") 18 | require.Contains(t, string(key), "-----BEGIN EC PRIVATE KEY-----") 19 | } 20 | -------------------------------------------------------------------------------- /examples/dtls/pki/cert_generation.md: -------------------------------------------------------------------------------- 1 | # Server And Client PKI generation 2 | The certificates used in the pki examples are generated using golang crypto library. 3 | You can also generate them using openssl, as seen below. 4 | 5 | ## Generate self signed CA 6 | ```sh 7 | CERT_SUBJ="/C=BR/ST=Parana/L=Curitiba/O=Dis/CN=example.com" 8 | openssl ecparam -name secp224r1 -genkey -noout -out root_ca_key.pem 9 | openssl ec -in root_ca_key.pem -pubout -out root_ca_pubkey.pem 10 | openssl req -new -key root_ca_key.pem -x509 -nodes -days 365 -out root_ca_cert.pem -subj $CERT_SUBJ 11 | ``` 12 | 13 | ## Generate server 14 | ```sh 15 | openssl ecparam -name secp224r1 -genkey -noout -out server_key.pem 16 | openssl req -new -sha256 -key server_key.pem -subj $CERT_SUBJ -out server.csr 17 | openssl x509 -req -in server.csr -CA root_ca_cert.pem -CAkey root_ca_key.pem -CAcreateserial -out server_cert.pem -days 500 -sha256 18 | ``` 19 | 20 | ## Generate client 21 | ```sh 22 | CERT_SUBJ="/C=BR/ST=Parana/L=Curitiba/O=Dis/CN=example.com/emailAddress=client1@example.com" 23 | openssl ecparam -name secp224r1 -genkey -noout -out client_key.pem 24 | openssl req -new -sha256 -key client_key.pem -subj $CERT_SUBJ -out client.csr 25 | openssl x509 -req -in client.csr -CA root_ca_cert.pem -CAkey root_ca_key.pem -CAcreateserial -out client_cert.pem -days 500 -sha256 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/dtls/pki/cert_util.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "errors" 11 | "strings" 12 | ) 13 | 14 | // LoadCertificate loads cert from bytes 15 | func LoadCertificate(certBytes []byte) (*tls.Certificate, error) { 16 | var certificate tls.Certificate 17 | 18 | for { 19 | block, rest := pem.Decode(certBytes) 20 | if block == nil { 21 | break 22 | } 23 | 24 | if block.Type != "CERTIFICATE" { 25 | return nil, errors.New("block is not a certificate, unable to load certificates") 26 | } 27 | 28 | certificate.Certificate = append(certificate.Certificate, block.Bytes) 29 | certBytes = rest 30 | } 31 | 32 | if len(certificate.Certificate) == 0 { 33 | return nil, errors.New("no certificate found, unable to load certificates") 34 | } 35 | 36 | return &certificate, nil 37 | } 38 | 39 | // LoadKey loads key from bytes 40 | func LoadKey(keyBytes []byte) (crypto.PrivateKey, error) { 41 | block, _ := pem.Decode(keyBytes) 42 | if block == nil || !strings.HasSuffix(block.Type, "PRIVATE KEY") { 43 | return nil, errors.New("block is not a private key, unable to load key") 44 | } 45 | 46 | if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { 47 | return key, nil 48 | } 49 | 50 | if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { 51 | switch key := key.(type) { 52 | case *rsa.PrivateKey, *ecdsa.PrivateKey: 53 | return key, nil 54 | default: 55 | return nil, errors.New("unknown key time in PKCS#8 wrapping, unable to load key") 56 | } 57 | } 58 | 59 | if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil { 60 | return key, nil 61 | } 62 | 63 | return nil, errors.New("no private key found, unable to load key") 64 | } 65 | 66 | // LoadKeyAndCertificate loads client certificate 67 | func LoadKeyAndCertificate(keyBytes []byte, certBytes []byte) (*tls.Certificate, error) { 68 | certificate, err := LoadCertificate(certBytes) 69 | if err != nil { 70 | return nil, err 71 | } 72 | key, err := LoadKey(keyBytes) 73 | if err != nil { 74 | return nil, err 75 | } 76 | certificate.PrivateKey = key 77 | return certificate, nil 78 | } 79 | 80 | // LoadCertPool loads cert pool from ca certificate 81 | func LoadCertPool(caBytes []byte) (*x509.CertPool, error) { 82 | rootCertificate, err := LoadCertificate(caBytes) 83 | if err != nil { 84 | return nil, err 85 | } 86 | certPool := x509.NewCertPool() 87 | for _, certBytes := range rootCertificate.Certificate { 88 | cert, err := x509.ParseCertificate(certBytes) 89 | if err != nil { 90 | certPool = nil 91 | return nil, err 92 | } 93 | certPool.AddCert(cert) 94 | } 95 | 96 | return certPool, nil 97 | } 98 | -------------------------------------------------------------------------------- /examples/dtls/pki/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | piondtls "github.com/pion/dtls/v3" 11 | "github.com/plgd-dev/go-coap/v3/dtls" 12 | "github.com/plgd-dev/go-coap/v3/examples/dtls/pki" 13 | ) 14 | 15 | func main() { 16 | config, err := createClientConfig() 17 | if err != nil { 18 | log.Fatalln(err) 19 | return 20 | } 21 | co, err := dtls.Dial("localhost:5688", config) 22 | if err != nil { 23 | log.Fatalf("Error dialing: %v", err) 24 | } 25 | path := "/a" 26 | if len(os.Args) > 1 { 27 | path = os.Args[1] 28 | } 29 | 30 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 31 | defer cancel() 32 | resp, err := co.Get(ctx, path) 33 | if err != nil { 34 | log.Fatalf("Error sending request: %v", err) 35 | } 36 | log.Printf("Response payload: %+v", resp) 37 | } 38 | 39 | func createClientConfig() (*piondtls.Config, error) { 40 | // root cert 41 | ca, rootBytes, _, caPriv, err := pki.GenerateCA() 42 | if err != nil { 43 | return nil, err 44 | } 45 | // client cert 46 | certBytes, keyBytes, err := pki.GenerateCertificate(ca, caPriv, "client@test.com") 47 | if err != nil { 48 | return nil, err 49 | } 50 | certificate, err := pki.LoadKeyAndCertificate(keyBytes, certBytes) 51 | if err != nil { 52 | return nil, err 53 | } 54 | // cert pool 55 | certPool, err := pki.LoadCertPool(rootBytes) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &piondtls.Config{ 61 | Certificates: []tls.Certificate{*certificate}, 62 | ExtendedMasterSecret: piondtls.RequireExtendedMasterSecret, 63 | RootCAs: certPool, 64 | InsecureSkipVerify: true, 65 | }, nil 66 | } 67 | -------------------------------------------------------------------------------- /examples/dtls/pki/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "fmt" 9 | "log" 10 | "math/big" 11 | "time" 12 | 13 | piondtls "github.com/pion/dtls/v3" 14 | "github.com/plgd-dev/go-coap/v3/dtls" 15 | "github.com/plgd-dev/go-coap/v3/examples/dtls/pki" 16 | "github.com/plgd-dev/go-coap/v3/message" 17 | "github.com/plgd-dev/go-coap/v3/message/codes" 18 | "github.com/plgd-dev/go-coap/v3/mux" 19 | "github.com/plgd-dev/go-coap/v3/net" 20 | "github.com/plgd-dev/go-coap/v3/options" 21 | "github.com/plgd-dev/go-coap/v3/udp/client" 22 | ) 23 | 24 | func onNewConn(cc *client.Conn) { 25 | dtlsConn, ok := cc.NetConn().(*piondtls.Conn) 26 | if !ok { 27 | log.Fatalf("invalid type %T", cc.NetConn()) 28 | } 29 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 30 | defer cancel() 31 | // force handshake otherwhise ConnectionState is not available 32 | err := dtlsConn.HandshakeContext(ctx) 33 | if err != nil { 34 | log.Fatalf("handshake failed: %v", err) 35 | } 36 | state, ok := dtlsConn.ConnectionState() 37 | if !ok { 38 | log.Fatalf("cannot get connection state") 39 | } 40 | clientCert, err := x509.ParseCertificate(state.PeerCertificates[0]) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | cc.SetContextValue("client-cert", clientCert) 45 | cc.AddOnClose(func() { 46 | log.Println("closed connection") 47 | }) 48 | } 49 | 50 | func toHexInt(n *big.Int) string { 51 | return fmt.Sprintf("%x", n) // or %X or upper case 52 | } 53 | 54 | func handleA(w mux.ResponseWriter, r *mux.Message) { 55 | clientCert := r.Context().Value("client-cert").(*x509.Certificate) 56 | log.Println("Serial number:", toHexInt(clientCert.SerialNumber)) 57 | log.Println("Subject:", clientCert.Subject) 58 | log.Println("Email:", clientCert.EmailAddresses) 59 | 60 | log.Printf("got message in handleA: %+v from %v\n", r, w.Conn().RemoteAddr()) 61 | err := w.SetResponse(codes.GET, message.TextPlain, bytes.NewReader([]byte("A hello world"))) 62 | if err != nil { 63 | log.Printf("cannot set response: %v", err) 64 | } 65 | } 66 | 67 | func main() { 68 | m := mux.NewRouter() 69 | m.Handle("/a", mux.HandlerFunc(handleA)) 70 | 71 | config, err := createServerConfig() 72 | if err != nil { 73 | log.Fatalln(err) 74 | return 75 | } 76 | 77 | log.Fatal(listenAndServeDTLS("udp", ":5688", config, m)) 78 | } 79 | 80 | func listenAndServeDTLS(network string, addr string, config *piondtls.Config, handler mux.Handler) error { 81 | l, err := net.NewDTLSListener(network, addr, config) 82 | if err != nil { 83 | return err 84 | } 85 | defer l.Close() 86 | s := dtls.NewServer(options.WithMux(handler), options.WithOnNewConn(onNewConn)) 87 | return s.Serve(l) 88 | } 89 | 90 | func createServerConfig() (*piondtls.Config, error) { 91 | // root cert 92 | ca, rootBytes, _, caPriv, err := pki.GenerateCA() 93 | if err != nil { 94 | return nil, err 95 | } 96 | // server cert 97 | certBytes, keyBytes, err := pki.GenerateCertificate(ca, caPriv, "server@test.com") 98 | if err != nil { 99 | return nil, err 100 | } 101 | certificate, err := pki.LoadKeyAndCertificate(keyBytes, certBytes) 102 | if err != nil { 103 | return nil, err 104 | } 105 | // cert pool 106 | certPool, err := pki.LoadCertPool(rootBytes) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return &piondtls.Config{ 112 | Certificates: []tls.Certificate{*certificate}, 113 | ExtendedMasterSecret: piondtls.RequireExtendedMasterSecret, 114 | ClientCAs: certPool, 115 | ClientAuth: piondtls.RequireAndVerifyClientCert, 116 | }, nil 117 | } 118 | -------------------------------------------------------------------------------- /examples/dtls/psk/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | piondtls "github.com/pion/dtls/v3" 11 | "github.com/plgd-dev/go-coap/v3/dtls" 12 | ) 13 | 14 | func main() { 15 | co, err := dtls.Dial("localhost:5688", &piondtls.Config{ 16 | PSK: func(hint []byte) ([]byte, error) { 17 | fmt.Printf("Server's hint: %s \n", hint) 18 | return []byte{0xAB, 0xC1, 0x23}, nil 19 | }, 20 | PSKIdentityHint: []byte("Pion DTLS Client"), 21 | CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_PSK_WITH_AES_128_CCM_8}, 22 | }) 23 | if err != nil { 24 | log.Fatalf("Error dialing: %v", err) 25 | } 26 | path := "/a" 27 | if len(os.Args) > 1 { 28 | path = os.Args[1] 29 | } 30 | 31 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 32 | defer cancel() 33 | resp, err := co.Get(ctx, path) 34 | if err != nil { 35 | log.Fatalf("Error sending request: %v", err) 36 | } 37 | log.Printf("Response payload: %+v", resp) 38 | } 39 | -------------------------------------------------------------------------------- /examples/dtls/psk/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | 8 | piondtls "github.com/pion/dtls/v3" 9 | coap "github.com/plgd-dev/go-coap/v3" 10 | "github.com/plgd-dev/go-coap/v3/message" 11 | "github.com/plgd-dev/go-coap/v3/message/codes" 12 | "github.com/plgd-dev/go-coap/v3/mux" 13 | ) 14 | 15 | func handleA(w mux.ResponseWriter, r *mux.Message) { 16 | log.Printf("got message in handleA: %+v from %v\n", r, w.Conn().RemoteAddr()) 17 | err := w.SetResponse(codes.GET, message.TextPlain, bytes.NewReader([]byte("A hello world"))) 18 | if err != nil { 19 | log.Printf("cannot set response: %v", err) 20 | } 21 | } 22 | 23 | func handleB(w mux.ResponseWriter, r *mux.Message) { 24 | log.Printf("got message in handleB: %+v from %v\n", r, w.Conn().RemoteAddr()) 25 | customResp := w.Conn().AcquireMessage(r.Context()) 26 | defer w.Conn().ReleaseMessage(customResp) 27 | customResp.SetCode(codes.Content) 28 | customResp.SetToken(r.Token()) 29 | customResp.SetContentFormat(message.TextPlain) 30 | customResp.SetBody(bytes.NewReader([]byte("B hello world"))) 31 | err := w.Conn().WriteMessage(customResp) 32 | if err != nil { 33 | log.Printf("cannot set response: %v", err) 34 | } 35 | } 36 | 37 | func main() { 38 | m := mux.NewRouter() 39 | m.Handle("/a", mux.HandlerFunc(handleA)) 40 | m.Handle("/b", mux.HandlerFunc(handleB)) 41 | 42 | log.Fatal(coap.ListenAndServeDTLS("udp", ":5688", &piondtls.Config{ 43 | PSK: func(hint []byte) ([]byte, error) { 44 | fmt.Printf("Client's hint: %s \n", hint) 45 | return []byte{0xAB, 0xC1, 0x23}, nil 46 | }, 47 | PSKIdentityHint: []byte("Pion DTLS Client"), 48 | CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_PSK_WITH_AES_128_CCM_8}, 49 | }, m)) 50 | } 51 | -------------------------------------------------------------------------------- /examples/mcast/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | gonet "net" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message" 9 | "github.com/plgd-dev/go-coap/v3/message/codes" 10 | "github.com/plgd-dev/go-coap/v3/mux" 11 | "github.com/plgd-dev/go-coap/v3/net" 12 | "github.com/plgd-dev/go-coap/v3/options" 13 | "github.com/plgd-dev/go-coap/v3/udp" 14 | ) 15 | 16 | func handleMcast(w mux.ResponseWriter, r *mux.Message) { 17 | path, err := r.Options().Path() 18 | if err != nil { 19 | log.Printf("cannot get path: %v", err) 20 | return 21 | } 22 | 23 | log.Printf("Got mcast message: path=%q: from %v", path, w.Conn().RemoteAddr()) 24 | w.SetResponse(codes.Content, message.TextPlain, bytes.NewReader([]byte("mcast response"))) 25 | } 26 | 27 | func main() { 28 | m := mux.NewRouter() 29 | m.Handle("/oic/res", mux.HandlerFunc(handleMcast)) 30 | multicastAddr := "224.0.1.187:5683" 31 | 32 | l, err := net.NewListenUDP("udp4", multicastAddr) 33 | if err != nil { 34 | log.Println(err) 35 | return 36 | } 37 | 38 | ifaces, err := gonet.Interfaces() 39 | if err != nil { 40 | log.Println(err) 41 | return 42 | } 43 | 44 | a, err := gonet.ResolveUDPAddr("udp", multicastAddr) 45 | if err != nil { 46 | log.Println(err) 47 | return 48 | } 49 | 50 | for i := range ifaces { 51 | iface := ifaces[i] 52 | err := l.JoinGroup(&iface, a) 53 | if err != nil { 54 | log.Printf("cannot JoinGroup(%v, %v): %v", iface, a, err) 55 | } 56 | } 57 | err = l.SetMulticastLoopback(true) 58 | if err != nil { 59 | log.Println(err) 60 | return 61 | } 62 | 63 | defer l.Close() 64 | s := udp.NewServer(options.WithMux(m)) 65 | defer s.Stop() 66 | log.Fatal(s.Serve(l)) 67 | } 68 | -------------------------------------------------------------------------------- /examples/observe/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message/pool" 9 | "github.com/plgd-dev/go-coap/v3/udp" 10 | ) 11 | 12 | func main() { 13 | sync := make(chan bool) 14 | co, err := udp.Dial("localhost:5688") 15 | if err != nil { 16 | log.Fatalf("Error dialing: %v", err) 17 | } 18 | num := 0 19 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 20 | defer cancel() 21 | obs, err := co.Observe(ctx, "/some/path", func(req *pool.Message) { 22 | log.Printf("Got %+v\n", req) 23 | num++ 24 | if num >= 10 { 25 | sync <- true 26 | } 27 | }) 28 | if err != nil { 29 | log.Fatalf("Unexpected error '%v'", err) 30 | } 31 | <-sync 32 | ctx, cancel = context.WithTimeout(context.Background(), time.Second) 33 | defer cancel() 34 | obs.Cancel(ctx) 35 | } 36 | -------------------------------------------------------------------------------- /examples/observe/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | coap "github.com/plgd-dev/go-coap/v3" 10 | "github.com/plgd-dev/go-coap/v3/message" 11 | "github.com/plgd-dev/go-coap/v3/message/codes" 12 | "github.com/plgd-dev/go-coap/v3/mux" 13 | ) 14 | 15 | func getPath(opts message.Options) string { 16 | path, err := opts.Path() 17 | if err != nil { 18 | log.Printf("cannot get path: %v", err) 19 | return "" 20 | } 21 | return path 22 | } 23 | 24 | func sendResponse(cc mux.Conn, token []byte, subded time.Time, obs int64) error { 25 | m := cc.AcquireMessage(cc.Context()) 26 | defer cc.ReleaseMessage(m) 27 | m.SetCode(codes.Content) 28 | m.SetToken(token) 29 | m.SetBody(bytes.NewReader([]byte(fmt.Sprintf("Been running for %v", time.Since(subded))))) 30 | m.SetContentFormat(message.TextPlain) 31 | if obs >= 0 { 32 | m.SetObserve(uint32(obs)) 33 | } 34 | return cc.WriteMessage(m) 35 | } 36 | 37 | func periodicTransmitter(cc mux.Conn, token []byte) { 38 | subded := time.Now() 39 | 40 | for obs := int64(2); ; obs++ { 41 | err := sendResponse(cc, token, subded, obs) 42 | if err != nil { 43 | log.Printf("Error on transmitter, stopping: %v", err) 44 | return 45 | } 46 | time.Sleep(time.Second) 47 | } 48 | } 49 | 50 | func main() { 51 | log.Fatal(coap.ListenAndServe("udp", ":5688", 52 | mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { 53 | log.Printf("Got message path=%v: %+v from %v", getPath(r.Options()), r, w.Conn().RemoteAddr()) 54 | obs, err := r.Options().Observe() 55 | switch { 56 | case r.Code() == codes.GET && err == nil && obs == 0: 57 | go periodicTransmitter(w.Conn(), r.Token()) 58 | case r.Code() == codes.GET: 59 | err := sendResponse(w.Conn(), r.Token(), time.Now(), -1) 60 | if err != nil { 61 | log.Printf("Error on transmitter: %v", err) 62 | } 63 | } 64 | }))) 65 | } 66 | -------------------------------------------------------------------------------- /examples/options/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "fmt" 8 | "log" 9 | 10 | piondtls "github.com/pion/dtls/v3" 11 | coap "github.com/plgd-dev/go-coap/v3" 12 | "github.com/plgd-dev/go-coap/v3/message" 13 | "github.com/plgd-dev/go-coap/v3/message/codes" 14 | "github.com/plgd-dev/go-coap/v3/mux" 15 | "github.com/plgd-dev/go-coap/v3/options" 16 | 17 | dtlsServer "github.com/plgd-dev/go-coap/v3/dtls/server" 18 | tcpServer "github.com/plgd-dev/go-coap/v3/tcp/server" 19 | udpClient "github.com/plgd-dev/go-coap/v3/udp/client" 20 | ) 21 | 22 | func handleA(w mux.ResponseWriter, r *mux.Message) { 23 | log.Printf("got message in handleA: %+v from %v\n", r, w.Conn().RemoteAddr()) 24 | err := w.SetResponse(codes.GET, message.TextPlain, bytes.NewReader([]byte("A hello world"))) 25 | if err != nil { 26 | log.Printf("cannot set response: %v", err) 27 | } 28 | } 29 | 30 | func handleB(w mux.ResponseWriter, r *mux.Message) { 31 | log.Printf("got message in handleB: %+v from %v\n", r, w.Conn().RemoteAddr()) 32 | customResp := w.Conn().AcquireMessage(r.Context()) 33 | defer w.Conn().ReleaseMessage(customResp) 34 | customResp.SetCode(codes.Content) 35 | customResp.SetToken(r.Token()) 36 | customResp.SetContentFormat(message.TextPlain) 37 | customResp.SetBody(bytes.NewReader([]byte("B hello world"))) 38 | err := w.Conn().WriteMessage(customResp) 39 | if err != nil { 40 | log.Printf("cannot set response: %v", err) 41 | } 42 | } 43 | 44 | func handleOnNewConn(cc *udpClient.Conn) { 45 | dtlsConn, ok := cc.NetConn().(*piondtls.Conn) 46 | if !ok { 47 | log.Fatalf("invalid type %T", cc.NetConn()) 48 | } 49 | state, ok := dtlsConn.ConnectionState() 50 | if !ok { 51 | log.Fatalf("cannot get connection state") 52 | } 53 | clientId := state.IdentityHint 54 | cc.SetContextValue("clientId", clientId) 55 | cc.AddOnClose(func() { 56 | clientId := state.IdentityHint 57 | log.Printf("closed connection clientId: %s", clientId) 58 | }) 59 | } 60 | 61 | func main() { 62 | m := mux.NewRouter() 63 | m.Handle("/a", mux.HandlerFunc(handleA)) 64 | m.Handle("/b", mux.HandlerFunc(handleB)) 65 | 66 | tcpOpts := []tcpServer.Option{} 67 | tcpOpts = append(tcpOpts, 68 | options.WithMux(m), 69 | options.WithContext(context.Background())) 70 | 71 | dtlsOpts := []dtlsServer.Option{} 72 | dtlsOpts = append(dtlsOpts, 73 | options.WithMux(m), 74 | options.WithContext(context.Background()), 75 | options.WithOnNewConn(handleOnNewConn), 76 | ) 77 | 78 | go func() { 79 | // serve a tcp server on :5686 80 | log.Fatal(coap.ListenAndServeWithOptions("tcp", ":5686", tcpOpts)) 81 | }() 82 | 83 | go func() { 84 | // serve a tls tcp server on :5687 85 | log.Fatal(coap.ListenAndServeTCPTLSWithOptions("tcp", "5687", &tls.Config{}, tcpOpts...)) 86 | }() 87 | 88 | go func() { 89 | // serve a udp dtls server on :5688 90 | log.Fatal(coap.ListenAndServeDTLSWithOptions("udp", ":5688", &piondtls.Config{ 91 | PSK: func(hint []byte) ([]byte, error) { 92 | fmt.Printf("Client's hint: %s \n", hint) 93 | return []byte{0xAB, 0xC1, 0x23}, nil 94 | }, 95 | PSKIdentityHint: []byte("Pion DTLS Client"), 96 | CipherSuites: []piondtls.CipherSuiteID{piondtls.TLS_PSK_WITH_AES_128_CCM_8}, 97 | }, dtlsOpts...)) 98 | }() 99 | } 100 | -------------------------------------------------------------------------------- /examples/simple/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/plgd-dev/go-coap/v3/udp" 10 | ) 11 | 12 | func main() { 13 | co, err := udp.Dial("localhost:5688") 14 | if err != nil { 15 | log.Fatalf("Error dialing: %v", err) 16 | } 17 | path := "/a" 18 | if len(os.Args) > 1 { 19 | path = os.Args[1] 20 | } 21 | 22 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 23 | defer cancel() 24 | resp, err := co.Get(ctx, path) 25 | if err != nil { 26 | log.Fatalf("Error sending request: %v", err) 27 | } 28 | log.Printf("Response payload: %v", resp.String()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/simple/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | 7 | coap "github.com/plgd-dev/go-coap/v3" 8 | "github.com/plgd-dev/go-coap/v3/message" 9 | "github.com/plgd-dev/go-coap/v3/message/codes" 10 | "github.com/plgd-dev/go-coap/v3/mux" 11 | ) 12 | 13 | func loggingMiddleware(next mux.Handler) mux.Handler { 14 | return mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { 15 | log.Printf("ClientAddress %v, %v\n", w.Conn().RemoteAddr(), r.String()) 16 | next.ServeCOAP(w, r) 17 | }) 18 | } 19 | 20 | func handleA(w mux.ResponseWriter, r *mux.Message) { 21 | err := w.SetResponse(codes.Content, message.TextPlain, bytes.NewReader([]byte("hello world"))) 22 | if err != nil { 23 | log.Printf("cannot set response: %v", err) 24 | } 25 | } 26 | 27 | func handleB(w mux.ResponseWriter, r *mux.Message) { 28 | customResp := w.Conn().AcquireMessage(r.Context()) 29 | defer w.Conn().ReleaseMessage(customResp) 30 | customResp.SetCode(codes.Content) 31 | customResp.SetToken(r.Token()) 32 | customResp.SetContentFormat(message.TextPlain) 33 | customResp.SetBody(bytes.NewReader([]byte("B hello world"))) 34 | err := w.Conn().WriteMessage(customResp) 35 | if err != nil { 36 | log.Printf("cannot set response: %v", err) 37 | } 38 | } 39 | 40 | func main() { 41 | r := mux.NewRouter() 42 | r.Use(loggingMiddleware) 43 | r.Handle("/a", mux.HandlerFunc(handleA)) 44 | r.Handle("/b", mux.HandlerFunc(handleB)) 45 | 46 | log.Fatal(coap.ListenAndServe("udp", ":5688", r)) 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plgd-dev/go-coap/v3 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dsnet/golib/memfile v1.0.0 7 | github.com/pion/dtls/v3 v3.0.2 8 | github.com/stretchr/testify v1.9.0 9 | go.uber.org/atomic v1.11.0 10 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 11 | golang.org/x/net v0.28.0 12 | golang.org/x/sync v0.8.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/pion/logging v0.2.2 // indirect 18 | github.com/pion/transport/v3 v3.0.7 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | golang.org/x/crypto v0.26.0 // indirect 21 | golang.org/x/sys v0.24.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dsnet/golib/memfile v1.0.0 h1:J9pUspY2bDCbF9o+YGwcf3uG6MdyITfh/Fk3/CaEiFs= 4 | github.com/dsnet/golib/memfile v1.0.0/go.mod h1:tXGNW9q3RwvWt1VV2qrRKlSSz0npnh12yftCSCy2T64= 5 | github.com/pion/dtls/v3 v3.0.2 h1:425DEeJ/jfuTTghhUDW0GtYZYIwwMtnKKJNMcWccTX0= 6 | github.com/pion/dtls/v3 v3.0.2/go.mod h1:dfIXcFkKoujDQ+jtd8M6RgqKK3DuaUilm3YatAbGp5k= 7 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 8 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 9 | github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= 10 | github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 14 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 16 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 17 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 18 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 19 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= 20 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 21 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 22 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 23 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 24 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 25 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 26 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 30 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /message/bench_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "testing" 4 | 5 | func BenchmarkPathOption(b *testing.B) { 6 | buf := make([]byte, 256) 7 | b.ResetTimer() 8 | for i := uint32(0); i < uint32(b.N); i++ { 9 | options := make(Options, 0, 10) 10 | path := "a/b/c" 11 | 12 | options, bufLen, err := options.SetPath(buf, path) 13 | if err != nil { 14 | b.Fatalf("unexpected error %d", err) 15 | } 16 | if bufLen != 3 { 17 | b.Fatalf("unexpected length %d", bufLen) 18 | } 19 | 20 | v := make([]string, 3) 21 | n, err := options.GetStrings(URIPath, v) 22 | if n != 3 { 23 | b.Fatalf("bad length") 24 | } 25 | if err != nil { 26 | b.Fatalf("unexpected code") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /message/codes/code_string.go: -------------------------------------------------------------------------------- 1 | package codes 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | ) 7 | 8 | var codeToString = map[Code]string{ 9 | Empty: "Empty", 10 | GET: "GET", 11 | POST: "POST", 12 | PUT: "PUT", 13 | DELETE: "DELETE", 14 | Created: "Created", 15 | Deleted: "Deleted", 16 | Valid: "Valid", 17 | Changed: "Changed", 18 | Content: "Content", 19 | BadRequest: "BadRequest", 20 | Unauthorized: "Unauthorized", 21 | BadOption: "BadOption", 22 | Forbidden: "Forbidden", 23 | NotFound: "NotFound", 24 | MethodNotAllowed: "MethodNotAllowed", 25 | NotAcceptable: "NotAcceptable", 26 | PreconditionFailed: "PreconditionFailed", 27 | RequestEntityTooLarge: "RequestEntityTooLarge", 28 | UnsupportedMediaType: "UnsupportedMediaType", 29 | TooManyRequests: "TooManyRequests", 30 | InternalServerError: "InternalServerError", 31 | NotImplemented: "NotImplemented", 32 | BadGateway: "BadGateway", 33 | ServiceUnavailable: "ServiceUnavailable", 34 | GatewayTimeout: "GatewayTimeout", 35 | ProxyingNotSupported: "ProxyingNotSupported", 36 | CSM: "Capabilities and Settings Messages", 37 | Ping: "Ping", 38 | Pong: "Pong", 39 | Release: "Release", 40 | Abort: "Abort", 41 | } 42 | 43 | func (c Code) String() string { 44 | val, ok := codeToString[c] 45 | if ok { 46 | return val 47 | } 48 | return "Code(" + strconv.FormatInt(int64(c), 10) + ")" 49 | } 50 | 51 | func ToCode(v string) (Code, error) { 52 | for key, val := range codeToString { 53 | if v == val { 54 | return key, nil 55 | } 56 | } 57 | return 0, errors.New("not found") 58 | } 59 | -------------------------------------------------------------------------------- /message/codes/codes.go: -------------------------------------------------------------------------------- 1 | package codes 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/plgd-dev/go-coap/v3/pkg/math" 9 | ) 10 | 11 | // A Code is an unsigned 16-bit coap code as defined in the coap spec. 12 | type Code uint16 13 | 14 | // Request Codes 15 | const ( 16 | GET Code = 1 17 | POST Code = 2 18 | PUT Code = 3 19 | DELETE Code = 4 20 | ) 21 | 22 | // Response Codes 23 | const ( 24 | Empty Code = 0 25 | Created Code = 65 26 | Deleted Code = 66 27 | Valid Code = 67 28 | Changed Code = 68 29 | Content Code = 69 30 | Continue Code = 95 31 | BadRequest Code = 128 32 | Unauthorized Code = 129 33 | BadOption Code = 130 34 | Forbidden Code = 131 35 | NotFound Code = 132 36 | MethodNotAllowed Code = 133 37 | NotAcceptable Code = 134 38 | RequestEntityIncomplete Code = 136 39 | PreconditionFailed Code = 140 40 | RequestEntityTooLarge Code = 141 41 | UnsupportedMediaType Code = 143 42 | TooManyRequests Code = 157 43 | InternalServerError Code = 160 44 | NotImplemented Code = 161 45 | BadGateway Code = 162 46 | ServiceUnavailable Code = 163 47 | GatewayTimeout Code = 164 48 | ProxyingNotSupported Code = 165 49 | ) 50 | 51 | // Signaling Codes for TCP 52 | const ( 53 | CSM Code = 225 54 | Ping Code = 226 55 | Pong Code = 227 56 | Release Code = 228 57 | Abort Code = 229 58 | ) 59 | 60 | const _maxCode = 255 61 | 62 | var _maxCodeLen int 63 | 64 | var strToCode = map[string]Code{ 65 | `"GET"`: GET, 66 | `"POST"`: POST, 67 | `"PUT"`: PUT, 68 | `"DELETE"`: DELETE, 69 | `"Created"`: Created, 70 | `"Deleted"`: Deleted, 71 | `"Valid"`: Valid, 72 | `"Changed"`: Changed, 73 | `"Content"`: Content, 74 | `"BadRequest"`: BadRequest, 75 | `"Unauthorized"`: Unauthorized, 76 | `"BadOption"`: BadOption, 77 | `"Forbidden"`: Forbidden, 78 | `"NotFound"`: NotFound, 79 | `"MethodNotAllowed"`: MethodNotAllowed, 80 | `"NotAcceptable"`: NotAcceptable, 81 | `"PreconditionFailed"`: PreconditionFailed, 82 | `"RequestEntityTooLarge"`: RequestEntityTooLarge, 83 | `"UnsupportedMediaType"`: UnsupportedMediaType, 84 | `"TooManyRequests"`: TooManyRequests, 85 | `"InternalServerError"`: InternalServerError, 86 | `"NotImplemented"`: NotImplemented, 87 | `"BadGateway"`: BadGateway, 88 | `"ServiceUnavailable"`: ServiceUnavailable, 89 | `"GatewayTimeout"`: GatewayTimeout, 90 | `"ProxyingNotSupported"`: ProxyingNotSupported, 91 | `"Capabilities and Settings Messages"`: CSM, 92 | `"Ping"`: Ping, 93 | `"Pong"`: Pong, 94 | `"Release"`: Release, 95 | `"Abort"`: Abort, 96 | } 97 | 98 | func getMaxCodeLen() int { 99 | // max uint32 as string binary representation: "0b" + 32 digits 100 | maxLen := 34 101 | for k := range strToCode { 102 | kLen := len(k) 103 | if kLen > maxLen { 104 | maxLen = kLen 105 | } 106 | } 107 | return maxLen 108 | } 109 | 110 | func init() { 111 | _maxCodeLen = getMaxCodeLen() 112 | } 113 | 114 | // UnmarshalJSON unmarshals b into the Code. 115 | func (c *Code) UnmarshalJSON(b []byte) error { 116 | // From json.Unmarshaler: By convention, to approximate the behavior of 117 | // Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as 118 | // a no-op. 119 | if string(b) == "null" { 120 | return nil 121 | } 122 | if c == nil { 123 | return errors.New("nil receiver passed to UnmarshalJSON") 124 | } 125 | 126 | if len(b) > _maxCodeLen { 127 | return fmt.Errorf("invalid code: input too large(length=%d)", len(b)) 128 | } 129 | 130 | if ci, err := strconv.ParseUint(string(b), 10, 32); err == nil { 131 | if ci >= _maxCode { 132 | return fmt.Errorf("invalid code: %q", ci) 133 | } 134 | *c = math.CastTo[Code](ci) 135 | return nil 136 | } 137 | 138 | if jc, ok := strToCode[string(b)]; ok { 139 | *c = jc 140 | return nil 141 | } 142 | return fmt.Errorf("invalid code: %v", b) 143 | } 144 | -------------------------------------------------------------------------------- /message/codes/codes_test.go: -------------------------------------------------------------------------------- 1 | package codes 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestJSONUnmarshal(t *testing.T) { 12 | var got []Code 13 | want := []Code{GET, NotFound, InternalServerError, Abort} 14 | in := `["GET", "NotFound", "InternalServerError", "Abort"]` 15 | err := json.Unmarshal([]byte(in), &got) 16 | require.NoError(t, err) 17 | require.Equal(t, want, got) 18 | 19 | inNumeric := "[" 20 | for i, c := range want { 21 | if i > 0 { 22 | inNumeric += "," 23 | } 24 | inNumeric += strconv.FormatUint(uint64(c), 10) 25 | } 26 | inNumeric += "]" 27 | err = json.Unmarshal([]byte(inNumeric), &got) 28 | require.NoError(t, err) 29 | require.Equal(t, want, got) 30 | } 31 | 32 | func TestUnmarshalJSONNoop(t *testing.T) { 33 | var got Code 34 | err := got.UnmarshalJSON([]byte("null")) 35 | require.NoError(t, err) 36 | } 37 | 38 | func TestUnmarshalJSONNilReceiver(t *testing.T) { 39 | var got *Code 40 | in := GET.String() 41 | err := got.UnmarshalJSON([]byte(in)) 42 | require.Error(t, err) 43 | } 44 | 45 | func TestUnmarshalJSONUnknownInput(t *testing.T) { 46 | inputs := [][]byte{nil, []byte(""), []byte("xxx"), []byte("Code(17)"), []byte("255")} 47 | for _, in := range inputs { 48 | var got Code 49 | err := got.UnmarshalJSON(in) 50 | require.Error(t, err) 51 | } 52 | 53 | var got Code 54 | longStr := "This is a very long string that is longer than the max code length" 55 | require.Greater(t, len(longStr), getMaxCodeLen()) 56 | err := got.UnmarshalJSON([]byte(longStr)) 57 | require.Error(t, err) 58 | } 59 | 60 | func TestUnmarshalJSONMarshalUnmarshal(t *testing.T) { 61 | for i := 0; i < _maxCode; i++ { 62 | var cUnMarshaled Code 63 | c := Code(i) 64 | 65 | cJSON, err := json.Marshal(c) 66 | require.NoError(t, err) 67 | 68 | err = json.Unmarshal(cJSON, &cUnMarshaled) 69 | require.NoError(t, err) 70 | 71 | require.Equal(t, c, cUnMarshaled) 72 | } 73 | } 74 | 75 | func TestCodeToString(t *testing.T) { 76 | strCodes := make([]string, 0, len(codeToString)) 77 | for _, val := range codeToString { 78 | strCodes = append(strCodes, val) 79 | } 80 | 81 | for _, str := range strCodes { 82 | _, err := ToCode(str) 83 | require.NoError(t, err) 84 | } 85 | } 86 | 87 | func FuzzUnmarshalJSON(f *testing.F) { 88 | f.Add([]byte("null")) 89 | f.Add([]byte("xxx")) 90 | f.Add([]byte("Code(17)")) 91 | f.Add([]byte("0b101010")) 92 | f.Add([]byte("0o52")) 93 | f.Add([]byte("0x2a")) 94 | f.Add([]byte("42")) 95 | 96 | f.Fuzz(func(_ *testing.T, input_data []byte) { 97 | var got Code 98 | _ = got.UnmarshalJSON(input_data) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /message/encodeDecodeUint32.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/plgd-dev/go-coap/v3/pkg/math" 7 | ) 8 | 9 | func EncodeUint32(buf []byte, value uint32) (int, error) { 10 | switch { 11 | case value == 0: 12 | return 0, nil 13 | case value <= max1ByteNumber: 14 | if len(buf) < 1 { 15 | return 1, ErrTooSmall 16 | } 17 | buf[0] = byte(value) 18 | return 1, nil 19 | case value <= max2ByteNumber: 20 | if len(buf) < 2 { 21 | return 2, ErrTooSmall 22 | } 23 | binary.BigEndian.PutUint16(buf, math.CastTo[uint16](value)) 24 | return 2, nil 25 | case value <= max3ByteNumber: 26 | if len(buf) < 3 { 27 | return 3, ErrTooSmall 28 | } 29 | rv := make([]byte, 4) 30 | binary.BigEndian.PutUint32(rv, value) 31 | copy(buf, rv[1:]) 32 | return 3, nil 33 | default: 34 | if len(buf) < 4 { 35 | return 4, ErrTooSmall 36 | } 37 | binary.BigEndian.PutUint32(buf, value) 38 | return 4, nil 39 | } 40 | } 41 | 42 | func DecodeUint32(buf []byte) (uint32, int, error) { 43 | if len(buf) > 4 { 44 | buf = buf[:4] 45 | } 46 | tmp := []byte{0, 0, 0, 0} 47 | copy(tmp[4-len(buf):], buf) 48 | value := binary.BigEndian.Uint32(tmp) 49 | return value, len(buf), nil 50 | } 51 | -------------------------------------------------------------------------------- /message/encodeDecodeUint32_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestEncodeUint32(t *testing.T) { 10 | type args struct { 11 | value uint32 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want int 17 | wantErr bool 18 | }{ 19 | { 20 | name: "0", 21 | args: args{0}, 22 | }, 23 | { 24 | name: "256", 25 | args: args{256}, 26 | want: 2, 27 | }, 28 | { 29 | name: "16384", 30 | args: args{16384}, 31 | want: 2, 32 | }, 33 | { 34 | name: "5000000", 35 | args: args{5000000}, 36 | want: 3, 37 | }, 38 | { 39 | name: "20000000", 40 | args: args{20000000}, 41 | want: 4, 42 | }, 43 | } 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | buf := make([]byte, 4) 47 | got, err := EncodeUint32(buf, tt.args.value) 48 | if tt.wantErr { 49 | require.Error(t, err) 50 | return 51 | } 52 | require.NoError(t, err) 53 | require.Equal(t, tt.want, got) 54 | buf = buf[:got] 55 | val, n, err := DecodeUint32(buf) 56 | require.NoError(t, err) 57 | require.Equal(t, len(buf), n) 58 | require.Equal(t, tt.args.value, val) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /message/error.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrTooSmall = errors.New("too small bytes buffer") 7 | ErrInvalidOptionHeaderExt = errors.New("invalid option header ext") 8 | ErrInvalidTokenLen = errors.New("invalid token length") 9 | ErrInvalidValueLength = errors.New("invalid value length") 10 | ErrShortRead = errors.New("invalid short read") 11 | ErrOptionTruncated = errors.New("option truncated") 12 | ErrOptionUnexpectedExtendMarker = errors.New("option unexpected extend marker") 13 | ErrOptionsTooSmall = errors.New("too small options buffer") 14 | ErrInvalidEncoding = errors.New("invalid encoding") 15 | ErrOptionNotFound = errors.New("option not found") 16 | ErrOptionDuplicate = errors.New("duplicated option") 17 | ) 18 | -------------------------------------------------------------------------------- /message/getETag.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "hash/crc64" 7 | "io" 8 | ) 9 | 10 | // GetETag calculates ETag from payload via CRC64 11 | func GetETag(r io.ReadSeeker) ([]byte, error) { 12 | if r == nil { 13 | return make([]byte, 8), nil 14 | } 15 | c64 := crc64.New(crc64.MakeTable(crc64.ISO)) 16 | orig, err := r.Seek(0, io.SeekCurrent) 17 | if err != nil { 18 | return nil, err 19 | } 20 | _, err = r.Seek(0, io.SeekStart) 21 | if err != nil { 22 | return nil, err 23 | } 24 | buf := make([]byte, 4096) 25 | for { 26 | bufR := buf 27 | n, errR := r.Read(bufR) 28 | if errors.Is(errR, io.EOF) { 29 | break 30 | } 31 | if errR != nil { 32 | return nil, errR 33 | } 34 | bufR = bufR[:n] 35 | c64.Write(bufR) 36 | } 37 | _, err = r.Seek(orig, io.SeekStart) 38 | if err != nil { 39 | return nil, err 40 | } 41 | b := make([]byte, 8) 42 | binary.BigEndian.PutUint64(b, c64.Sum64()) 43 | return b, nil 44 | } 45 | -------------------------------------------------------------------------------- /message/getETag_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetETag(t *testing.T) { 11 | got, err := GetETag(bytes.NewReader([]byte("vfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGhvfbslVuq3Ql5XUGh"))) 12 | require.NoError(t, err) 13 | require.Equal(t, []byte{0x24, 0x17, 0x23, 0x1c, 0x79, 0x28, 0x4e, 0x54}, got) 14 | } 15 | -------------------------------------------------------------------------------- /message/getToken.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "hash/crc64" 7 | ) 8 | 9 | type Token []byte 10 | 11 | func (t Token) String() string { 12 | return hex.EncodeToString(t) 13 | } 14 | 15 | func (t Token) Hash() uint64 { 16 | return crc64.Checksum(t, crc64.MakeTable(crc64.ISO)) 17 | } 18 | 19 | // GetToken generates a random token by a given length 20 | func GetToken() (Token, error) { 21 | b := make(Token, 8) 22 | _, err := rand.Read(b) 23 | // Note that err == nil only if we read len(b) bytes. 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return b, nil 29 | } 30 | -------------------------------------------------------------------------------- /message/getToken_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestGetToken(t *testing.T) { 10 | token, err := GetToken() 11 | require.NoError(t, err) 12 | require.NotEmpty(t, token) 13 | require.NotEqual(t, 0, token.Hash()) 14 | } 15 | -------------------------------------------------------------------------------- /message/getmid.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "math" 7 | "sync/atomic" 8 | "time" 9 | 10 | pkgMath "github.com/plgd-dev/go-coap/v3/pkg/math" 11 | pkgRand "github.com/plgd-dev/go-coap/v3/pkg/rand" 12 | ) 13 | 14 | var weakRng = pkgRand.NewRand(time.Now().UnixNano()) 15 | 16 | var msgID = uint32(RandMID()) 17 | 18 | // GetMID generates a message id for UDP. (0 <= mid <= 65535) 19 | func GetMID() int32 { 20 | return int32(pkgMath.CastTo[uint16](atomic.AddUint32(&msgID, 1))) 21 | } 22 | 23 | func RandMID() int32 { 24 | b := make([]byte, 4) 25 | _, err := rand.Read(b) 26 | if err != nil { 27 | // fallback to cryptographically insecure pseudo-random generator 28 | return int32(pkgMath.CastTo[uint16](weakRng.Uint32() >> 16)) 29 | } 30 | return int32(pkgMath.CastTo[uint16](binary.BigEndian.Uint32(b))) 31 | } 32 | 33 | // ValidateMID validates a message id for UDP. (0 <= mid <= 65535) 34 | func ValidateMID(mid int32) bool { 35 | return mid >= 0 && mid <= math.MaxUint16 36 | } 37 | -------------------------------------------------------------------------------- /message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message/codes" 7 | ) 8 | 9 | // MaxTokenSize maximum of token size that can be used in message 10 | const MaxTokenSize = 8 11 | 12 | type Message struct { 13 | Token Token 14 | Options Options 15 | Code codes.Code 16 | Payload []byte 17 | 18 | // For DTLS and UDP messages 19 | MessageID int32 // uint16 is valid, all other values are invalid, -1 is used for unset 20 | Type Type // uint8 is valid, all other values are invalid, -1 is used for unset 21 | } 22 | 23 | func (r *Message) String() string { 24 | if r == nil { 25 | return "nil" 26 | } 27 | buf := fmt.Sprintf("Code: %v, Token: %v", r.Code, r.Token) 28 | path, err := r.Options.Path() 29 | if err == nil { 30 | buf = fmt.Sprintf("%s, Path: %v", buf, path) 31 | } 32 | cf, err := r.Options.ContentFormat() 33 | if err == nil { 34 | buf = fmt.Sprintf("%s, ContentFormat: %v", buf, cf) 35 | } 36 | queries, err := r.Options.Queries() 37 | if err == nil { 38 | buf = fmt.Sprintf("%s, Queries: %+v", buf, queries) 39 | } 40 | if ValidateType(r.Type) { 41 | buf = fmt.Sprintf("%s, Type: %v", buf, r.Type) 42 | } 43 | if ValidateMID(r.MessageID) { 44 | buf = fmt.Sprintf("%s, MessageID: %v", buf, r.MessageID) 45 | } 46 | if len(r.Payload) > 0 { 47 | buf = fmt.Sprintf("%s, PayloadLen: %v", buf, len(r.Payload)) 48 | } 49 | return buf 50 | } 51 | 52 | // IsPing returns true if the message is a ping. 53 | func (r *Message) IsPing(isTCP bool) bool { 54 | if isTCP { 55 | return r.Code == codes.Ping 56 | } 57 | return r.Code == codes.Empty && r.Type == Confirmable && len(r.Token) == 0 && len(r.Options) == 0 && len(r.Payload) == 0 58 | } 59 | -------------------------------------------------------------------------------- /message/message_internal_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message/codes" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMessageIsPing(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | message *Message 14 | isTCP bool 15 | want bool 16 | }{ 17 | { 18 | name: "Ping message (TCP)", 19 | message: &Message{ 20 | Code: codes.Ping, 21 | Type: Confirmable, 22 | Token: nil, 23 | Options: nil, 24 | Payload: nil, 25 | }, 26 | isTCP: true, 27 | want: true, 28 | }, 29 | { 30 | name: "Ping message (UDP)", 31 | message: &Message{ 32 | Code: codes.Empty, 33 | Type: Confirmable, 34 | Token: nil, 35 | Options: nil, 36 | Payload: nil, 37 | }, 38 | isTCP: false, 39 | want: true, 40 | }, 41 | { 42 | name: "Non-ping message (TCP)", 43 | message: &Message{ 44 | Code: codes.GET, 45 | Type: Confirmable, 46 | Token: []byte{1, 2, 3}, 47 | Options: []Option{{ID: 1, Value: []byte{4, 5, 6}}}, 48 | Payload: []byte{7, 8, 9}, 49 | }, 50 | isTCP: true, 51 | want: false, 52 | }, 53 | { 54 | name: "Non-ping message (UDP)", 55 | message: &Message{ 56 | Code: codes.GET, 57 | Type: Confirmable, 58 | Token: []byte{1, 2, 3}, 59 | Options: []Option{{ID: 1, Value: []byte{4, 5, 6}}}, 60 | Payload: []byte{7, 8, 9}, 61 | }, 62 | isTCP: false, 63 | want: false, 64 | }, 65 | } 66 | 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | got := tt.message.IsPing(tt.isTCP) 70 | require.Equal(t, tt.want, got) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /message/noresponse/error.go: -------------------------------------------------------------------------------- 1 | package noresponse 2 | 3 | import "errors" 4 | 5 | // ErrMessageNotInterested message is not of interest to the client 6 | var ErrMessageNotInterested = errors.New("message not to be sent due to disinterest") 7 | -------------------------------------------------------------------------------- /message/noresponse/noresponse.go: -------------------------------------------------------------------------------- 1 | package noresponse 2 | 3 | import ( 4 | "github.com/plgd-dev/go-coap/v3/message/codes" 5 | ) 6 | 7 | var ( 8 | resp2XXCodes = []codes.Code{codes.Created, codes.Deleted, codes.Valid, codes.Changed, codes.Content} 9 | resp4XXCodes = []codes.Code{codes.BadRequest, codes.Unauthorized, codes.BadOption, codes.Forbidden, codes.NotFound, codes.MethodNotAllowed, codes.NotAcceptable, codes.PreconditionFailed, codes.RequestEntityTooLarge, codes.UnsupportedMediaType} 10 | resp5XXCodes = []codes.Code{codes.InternalServerError, codes.NotImplemented, codes.BadGateway, codes.ServiceUnavailable, codes.GatewayTimeout, codes.ProxyingNotSupported} 11 | noResponseValueMap = map[uint32][]codes.Code{ 12 | 2: resp2XXCodes, 13 | 8: resp4XXCodes, 14 | 16: resp5XXCodes, 15 | } 16 | ) 17 | 18 | func isSet(n uint32, pos uint32) bool { 19 | val := n & (1 << pos) 20 | return (val > 0) 21 | } 22 | 23 | func decodeNoResponseOption(v uint32) []codes.Code { 24 | var codes []codes.Code 25 | if v == 0 { 26 | // No suppresed code 27 | return codes 28 | } 29 | 30 | var i uint32 31 | // Max bit value:4; ref:table_2_rfc7967 32 | for i = 0; i <= 4; i++ { 33 | if isSet(v, i) { 34 | index := uint32(1 << i) 35 | codes = append(codes, noResponseValueMap[index]...) 36 | } 37 | } 38 | return codes 39 | } 40 | 41 | // IsNoResponseCode validates response code against NoResponse option from request. 42 | // https://www.rfc-editor.org/rfc/rfc7967.txt 43 | func IsNoResponseCode(code codes.Code, noRespValue uint32) error { 44 | suppressedCodes := decodeNoResponseOption(noRespValue) 45 | 46 | for _, suppressedCode := range suppressedCodes { 47 | if suppressedCode == code { 48 | return ErrMessageNotInterested 49 | } 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /message/noresponse/noresponse_test.go: -------------------------------------------------------------------------------- 1 | package noresponse 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message/codes" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNoResponse2XXCodes(t *testing.T) { 11 | codes := decodeNoResponseOption(2) 12 | exp := resp2XXCodes 13 | require.Equal(t, exp, codes) 14 | } 15 | 16 | func TestNoResponse4XXCodes(t *testing.T) { 17 | codes := decodeNoResponseOption(8) 18 | exp := resp4XXCodes 19 | require.Equal(t, exp, codes) 20 | } 21 | 22 | func TestNoResponse5XXCodes(t *testing.T) { 23 | codes := decodeNoResponseOption(16) 24 | exp := resp5XXCodes 25 | require.Equal(t, exp, codes) 26 | } 27 | 28 | func TestNoResponseCombinationXXCodes(t *testing.T) { 29 | codes := decodeNoResponseOption(18) 30 | exp := resp2XXCodes 31 | exp = append(exp, resp5XXCodes...) 32 | require.Equal(t, exp, codes) 33 | } 34 | 35 | func TestNoResponseAllCodes(t *testing.T) { 36 | allCodes := decodeNoResponseOption(0) 37 | exp := []codes.Code(nil) 38 | require.Equal(t, exp, allCodes) 39 | } 40 | 41 | func TestNoResponseBehaviour(t *testing.T) { 42 | err := IsNoResponseCode(codes.Content, 2) 43 | require.Error(t, err) 44 | } 45 | -------------------------------------------------------------------------------- /message/option_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestMediaTypeString(t *testing.T) { 10 | for i := 0; i < 12000; i++ { 11 | func(mt int, s string) { 12 | if v, err := ToMediaType(s); err == nil { 13 | require.Equal(t, MediaType(mt), v) 14 | } 15 | }(i, MediaType(i).String()) 16 | } 17 | } 18 | 19 | func TestOptionIDString(t *testing.T) { 20 | for i := 0; i < 12000; i++ { 21 | func(oid int, s string) { 22 | if v, err := ToOptionID(s); err == nil { 23 | require.Equal(t, OptionID(oid), v) 24 | } 25 | }(i, OptionID(i).String()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /message/pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "go.uber.org/atomic" 9 | ) 10 | 11 | type Pool struct { 12 | // This field needs to be the first in the struct to ensure proper word alignment on 32-bit platforms. 13 | // See: https://golang.org/pkg/sync/atomic/#pkg-note-BUG 14 | currentMessagesInPool atomic.Int64 15 | messagePool sync.Pool 16 | maxNumMessages uint32 17 | maxMessageBufferSize uint16 18 | } 19 | 20 | func New(maxNumMessages uint32, maxMessageBufferSize uint16) *Pool { 21 | return &Pool{ 22 | maxNumMessages: maxNumMessages, 23 | maxMessageBufferSize: maxMessageBufferSize, 24 | } 25 | } 26 | 27 | // AcquireMessage returns an empty Message instance from Message pool. 28 | // 29 | // The returned Message instance may be passed to ReleaseMessage when it is 30 | // no longer needed. This allows Message recycling, reduces GC pressure 31 | // and usually improves performance. 32 | func (p *Pool) AcquireMessage(ctx context.Context) *Message { 33 | v := p.messagePool.Get() 34 | if v == nil { 35 | return NewMessage(ctx) 36 | } 37 | r, ok := v.(*Message) 38 | if !ok { 39 | panic(fmt.Errorf("invalid message type(%T) for pool", v)) 40 | } 41 | p.currentMessagesInPool.Dec() 42 | r.ctx = ctx 43 | return r 44 | } 45 | 46 | // ReleaseMessage returns req acquired via AcquireMessage to Message pool. 47 | // 48 | // It is forbidden accessing req and/or its' members after returning 49 | // it to Message pool. 50 | func (p *Pool) ReleaseMessage(req *Message) { 51 | for { 52 | v := p.currentMessagesInPool.Load() 53 | if v >= int64(p.maxNumMessages) { 54 | return 55 | } 56 | next := v + 1 57 | if p.currentMessagesInPool.CompareAndSwap(v, next) { 58 | break 59 | } 60 | } 61 | req.Reset() 62 | req.ctx = nil 63 | p.messagePool.Put(req) 64 | } 65 | -------------------------------------------------------------------------------- /message/status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message/codes" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | ) 11 | 12 | const ( 13 | OK codes.Code = 10000 14 | Timeout codes.Code = 10001 15 | Canceled codes.Code = 10002 16 | Unknown codes.Code = 10003 17 | ) 18 | 19 | // Status holds error of coap 20 | type Status struct { //nolint:errname 21 | err error 22 | msg *pool.Message 23 | code codes.Code 24 | } 25 | 26 | func CodeToString(c codes.Code) string { 27 | switch c { 28 | case OK: 29 | return "OK" 30 | case Timeout: 31 | return "Timeout" 32 | case Canceled: 33 | return "Canceled" 34 | case Unknown: 35 | return "Unknown" 36 | } 37 | return c.String() 38 | } 39 | 40 | func (s Status) Error() string { 41 | return fmt.Sprintf("coap error: code = %s desc = %v", CodeToString(s.Code()), s.err) 42 | } 43 | 44 | // Code returns the status code contained in s. 45 | func (s Status) Code() codes.Code { 46 | if s.msg != nil { 47 | return s.msg.Code() 48 | } 49 | return s.code 50 | } 51 | 52 | // Message returns a coap message. 53 | func (s Status) Message() *pool.Message { 54 | return s.msg 55 | } 56 | 57 | // COAPError just for check interface 58 | func (s Status) COAPError() Status { 59 | return s 60 | } 61 | 62 | // Error returns an error representing c and msg. If c is OK, returns nil. 63 | func Error(msg *pool.Message, err error) Status { 64 | return Status{ 65 | msg: msg, 66 | err: err, 67 | } 68 | } 69 | 70 | // Errorf returns Error(c, fmt.Sprintf(format, a...)). 71 | func Errorf(msg *pool.Message, format string, a ...interface{}) Status { 72 | return Error(msg, fmt.Errorf(format, a...)) 73 | } 74 | 75 | // FromError returns a Status representing err if it was produced from this 76 | // package or has a method `COAPError() Status`. Otherwise, ok is false and a 77 | // Status is returned with codes.Unknown and the original error message. 78 | func FromError(err error) (s Status, ok bool) { 79 | if err == nil { 80 | return Status{ 81 | code: OK, 82 | }, true 83 | } 84 | var coapError interface { 85 | COAPError() Status 86 | } 87 | if errors.As(err, &coapError) { 88 | return coapError.COAPError(), true 89 | } 90 | return Status{ 91 | code: Unknown, 92 | err: err, 93 | }, false 94 | } 95 | 96 | // Convert is a convenience function which removes the need to handle the 97 | // boolean return value from FromError. 98 | func Convert(err error) Status { 99 | s, _ := FromError(err) 100 | return s 101 | } 102 | 103 | // Code returns the Code of the error if it is a Status error, codes.OK if err 104 | // is nil, or codes.Unknown otherwise. 105 | func Code(err error) codes.Code { 106 | // Don't use FromError to avoid allocation of OK status. 107 | if err == nil { 108 | return OK 109 | } 110 | var coapError interface { 111 | COAPError() Status 112 | } 113 | if errors.As(err, &coapError) { 114 | return coapError.COAPError().Code() 115 | } 116 | return Unknown 117 | } 118 | 119 | // FromContextError converts a context error into a Status. It returns a 120 | // Status with codes.OK if err is nil, or a Status with codes.Unknown if err is 121 | // non-nil and not a context error. 122 | func FromContextError(err error) Status { 123 | switch { 124 | case err == nil: 125 | return Status{ 126 | code: OK, 127 | } 128 | case errors.Is(err, context.DeadlineExceeded): 129 | return Status{ 130 | code: Timeout, 131 | err: err, 132 | } 133 | case errors.Is(err, context.Canceled): 134 | return Status{ 135 | code: Canceled, 136 | err: err, 137 | } 138 | default: 139 | return Status{ 140 | code: Unknown, 141 | err: err, 142 | } 143 | } 144 | } 145 | 146 | // Unwrap unpacks wrapped errors. 147 | func (s Status) Unwrap() error { 148 | return s.err 149 | } 150 | -------------------------------------------------------------------------------- /message/status/status_test.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message/codes" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestStatus(t *testing.T) { 14 | s, ok := FromError(nil) 15 | require.True(t, ok) 16 | require.Equal(t, OK, s.Code()) 17 | 18 | _, ok = FromError(errors.New("test")) 19 | require.False(t, ok) 20 | 21 | msg := pool.NewMessage(context.TODO()) 22 | msg.SetCode(codes.NotFound) 23 | err := Errorf(msg, "test %w", context.Canceled) 24 | s, ok = FromError(err) 25 | require.True(t, ok) 26 | require.Equal(t, codes.NotFound, s.Code()) 27 | require.ErrorIs(t, err, context.Canceled) 28 | 29 | s = Convert(err) 30 | require.Equal(t, codes.NotFound, s.Code()) 31 | require.Equal(t, codes.NotFound, Code(err)) 32 | 33 | require.Equal(t, OK, Code(nil)) 34 | 35 | err = FromContextError(context.Canceled) 36 | require.ErrorIs(t, err, context.Canceled) 37 | require.Equal(t, Canceled, Code(err)) 38 | 39 | err = FromContextError(nil) 40 | require.Equal(t, OK, Code(err)) 41 | 42 | err = FromContextError(context.DeadlineExceeded) 43 | require.Equal(t, Timeout, Code(err)) 44 | 45 | err = FromContextError(errors.New("test")) 46 | require.Equal(t, Unknown, Code(err)) 47 | require.Equal(t, Unknown, Code(errors.New("test"))) 48 | } 49 | -------------------------------------------------------------------------------- /message/tcpOptions.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | // Signal CSM Option IDs 4 | /* 5 | +-----+---+---+-------------------+--------+--------+---------+ 6 | | No. | C | R | Name | Format | Length | Default | 7 | +-----+---+---+-------------------+--------+--------+---------+ 8 | | 2 | | | MaxMessageSize | uint | 0-4 | 1152 | 9 | | 4 | | | BlockWiseTransfer | empty | 0 | (none) | 10 | +-----+---+---+-------------------+--------+--------+---------+ 11 | C=Critical, R=Repeatable 12 | */ 13 | 14 | const ( 15 | TCPMaxMessageSize OptionID = 2 16 | TCPBlockWiseTransfer OptionID = 4 17 | ) 18 | 19 | // Signal Ping/Pong Option IDs 20 | /* 21 | +-----+---+---+-------------------+--------+--------+---------+ 22 | | No. | C | R | Name | Format | Length | Default | 23 | +-----+---+---+-------------------+--------+--------+---------+ 24 | | 2 | | | Custody | empty | 0 | (none) | 25 | +-----+---+---+-------------------+--------+--------+---------+ 26 | C=Critical, R=Repeatable 27 | */ 28 | 29 | const ( 30 | TCPCustody OptionID = 2 31 | ) 32 | 33 | // Signal Release Option IDs 34 | /* 35 | +-----+---+---+---------------------+--------+--------+---------+ 36 | | No. | C | R | Name | Format | Length | Default | 37 | +-----+---+---+---------------------+--------+--------+---------+ 38 | | 2 | | x | Alternative-Address | string | 1-255 | (none) | 39 | | 4 | | | Hold-Off | uint3 | 0-3 | (none) | 40 | +-----+---+---+---------------------+--------+--------+---------+ 41 | C=Critical, R=Repeatable 42 | */ 43 | 44 | const ( 45 | TCPAlternativeAddress OptionID = 2 46 | TCPHoldOff OptionID = 4 47 | ) 48 | 49 | // Signal Abort Option IDs 50 | /* 51 | +-----+---+---+---------------------+--------+--------+---------+ 52 | | No. | C | R | Name | Format | Length | Default | 53 | +-----+---+---+---------------------+--------+--------+---------+ 54 | | 2 | | | Bad-CSM-Option | uint | 0-2 | (none) | 55 | +-----+---+---+---------------------+--------+--------+---------+ 56 | C=Critical, R=Repeatable 57 | */ 58 | const ( 59 | TCPBadCSMOption OptionID = 2 60 | ) 61 | 62 | var TCPSignalCSMOptionDefs = map[OptionID]OptionDef{ 63 | TCPMaxMessageSize: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 4}, 64 | TCPBlockWiseTransfer: {ValueFormat: ValueEmpty, MinLen: 0, MaxLen: 0}, 65 | } 66 | 67 | var TCPSignalPingPongOptionDefs = map[OptionID]OptionDef{ 68 | TCPCustody: {ValueFormat: ValueEmpty, MinLen: 0, MaxLen: 0}, 69 | } 70 | 71 | var TCPSignalReleaseOptionDefs = map[OptionID]OptionDef{ 72 | TCPAlternativeAddress: {ValueFormat: ValueString, MinLen: 1, MaxLen: 255}, 73 | TCPHoldOff: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 3}, 74 | } 75 | 76 | var TCPSignalAbortOptionDefs = map[OptionID]OptionDef{ 77 | TCPBadCSMOption: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 2}, 78 | } 79 | -------------------------------------------------------------------------------- /message/type.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | // Type represents the message type. 9 | // It's only part of CoAP UDP messages. 10 | // Reliable transports like TCP do not have a type. 11 | type Type int16 12 | 13 | const ( 14 | // Used for unset 15 | Unset Type = -1 16 | // Confirmable messages require acknowledgements. 17 | Confirmable Type = 0 18 | // NonConfirmable messages do not require acknowledgements. 19 | NonConfirmable Type = 1 20 | // Acknowledgement is a message indicating a response to confirmable message. 21 | Acknowledgement Type = 2 22 | // Reset indicates a permanent negative acknowledgement. 23 | Reset Type = 3 24 | ) 25 | 26 | var typeToString = map[Type]string{ 27 | Unset: "Unset", 28 | Confirmable: "Confirmable", 29 | NonConfirmable: "NonConfirmable", 30 | Acknowledgement: "Acknowledgement", 31 | Reset: "Reset", 32 | } 33 | 34 | func (t Type) String() string { 35 | val, ok := typeToString[t] 36 | if ok { 37 | return val 38 | } 39 | return "Type(" + strconv.FormatInt(int64(t), 10) + ")" 40 | } 41 | 42 | // ValidateType validates the type for UDP. (0 <= typ <= 255) 43 | func ValidateType(typ Type) bool { 44 | return typ >= 0 && typ <= math.MaxUint8 45 | } 46 | -------------------------------------------------------------------------------- /mux/client.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | ) 11 | 12 | type Observation = interface { 13 | Cancel(ctx context.Context, opts ...message.Option) error 14 | Canceled() bool 15 | } 16 | 17 | type Conn interface { 18 | // create message from pool 19 | AcquireMessage(ctx context.Context) *pool.Message 20 | // return back the message to the pool for next use 21 | ReleaseMessage(m *pool.Message) 22 | 23 | Ping(ctx context.Context) error 24 | Get(ctx context.Context, path string, opts ...message.Option) (*pool.Message, error) 25 | Delete(ctx context.Context, path string, opts ...message.Option) (*pool.Message, error) 26 | Post(ctx context.Context, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) 27 | Put(ctx context.Context, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) 28 | Observe(ctx context.Context, path string, observeFunc func(notification *pool.Message), opts ...message.Option) (Observation, error) 29 | 30 | RemoteAddr() net.Addr 31 | // NetConn returns the underlying connection that is wrapped by client. The Conn returned is shared by all invocations of NetConn, so do not modify it. 32 | NetConn() net.Conn 33 | Context() context.Context 34 | SetContextValue(key interface{}, val interface{}) 35 | WriteMessage(req *pool.Message) error 36 | // used for GET,PUT,POST,DELETE 37 | Do(req *pool.Message) (*pool.Message, error) 38 | // used for observation (GET with observe 0) 39 | DoObserve(req *pool.Message, observeFunc func(req *pool.Message)) (Observation, error) 40 | Close() error 41 | Sequence() uint64 42 | // Done signalizes that connection is not more processed. 43 | Done() <-chan struct{} 44 | AddOnClose(func()) 45 | 46 | NewGetRequest(ctx context.Context, path string, opts ...message.Option) (*pool.Message, error) 47 | NewObserveRequest(ctx context.Context, path string, opts ...message.Option) (*pool.Message, error) 48 | NewPutRequest(ctx context.Context, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) 49 | NewPostRequest(ctx context.Context, path string, contentFormat message.MediaType, payload io.ReadSeeker, opts ...message.Option) (*pool.Message, error) 50 | NewDeleteRequest(ctx context.Context, path string, opts ...message.Option) (*pool.Message, error) 51 | } 52 | -------------------------------------------------------------------------------- /mux/example_logging_middleware_test.go: -------------------------------------------------------------------------------- 1 | package mux_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/plgd-dev/go-coap/v3/mux" 7 | ) 8 | 9 | // Middleware function, which will be called for each request 10 | func loggingMiddleware(next mux.Handler) mux.Handler { 11 | return mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { 12 | log.Printf("ClientAddress %v, %v\n", w.Conn().RemoteAddr(), r.String()) 13 | next.ServeCOAP(w, r) 14 | }) 15 | } 16 | 17 | func Example_authenticationMiddleware() { 18 | r := mux.NewRouter() 19 | r.HandleFunc("/", func(mux.ResponseWriter, *mux.Message) { 20 | // Do something here 21 | }) 22 | r.Use(loggingMiddleware) 23 | } 24 | -------------------------------------------------------------------------------- /mux/message.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import "github.com/plgd-dev/go-coap/v3/message/pool" 4 | 5 | // RouteParams contains all the information related to a route 6 | type RouteParams struct { 7 | Path string 8 | Vars map[string]string 9 | PathTemplate string 10 | } 11 | 12 | // Message contains message with sequence number. 13 | type Message struct { 14 | *pool.Message 15 | RouteParams *RouteParams 16 | } 17 | -------------------------------------------------------------------------------- /mux/middleware.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | // MiddlewareFunc is a function which receives an Handler and returns another Handler. 4 | // Typically, the returned handler is a closure which does something with the ResponseWriter and Message passed 5 | // to it, and then calls the handler passed as parameter to the MiddlewareFunc. 6 | type MiddlewareFunc func(Handler) Handler 7 | 8 | // Middleware allows MiddlewareFunc to implement the middleware interface. 9 | func (mw MiddlewareFunc) Middleware(handler Handler) Handler { 10 | return mw(handler) 11 | } 12 | 13 | // Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. 14 | func (r *Router) Use(mwf ...MiddlewareFunc) { 15 | r.middlewares = append(r.middlewares, mwf...) 16 | } 17 | -------------------------------------------------------------------------------- /mux/muxResponseWriter.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message" 7 | "github.com/plgd-dev/go-coap/v3/message/codes" 8 | "github.com/plgd-dev/go-coap/v3/message/pool" 9 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 10 | ) 11 | 12 | // ToHandler converts mux handler to udp/dtls/tcp handler. 13 | func ToHandler[C Conn](m Handler) func(w *responsewriter.ResponseWriter[C], r *pool.Message) { 14 | return func(w *responsewriter.ResponseWriter[C], r *pool.Message) { 15 | muxw := &muxResponseWriter[C]{ 16 | w: w, 17 | } 18 | m.ServeCOAP(muxw, &Message{ 19 | Message: r, 20 | RouteParams: new(RouteParams), 21 | }) 22 | } 23 | } 24 | 25 | type muxResponseWriter[C Conn] struct { 26 | w *responsewriter.ResponseWriter[C] 27 | } 28 | 29 | // SetResponse simplifies the setup of the response for the request. ETags must be set via options. For advanced setup, use Message(). 30 | func (w *muxResponseWriter[C]) SetResponse(code codes.Code, contentFormat message.MediaType, d io.ReadSeeker, opts ...message.Option) error { 31 | return w.w.SetResponse(code, contentFormat, d, opts...) 32 | } 33 | 34 | // Conn peer connection. 35 | func (w *muxResponseWriter[C]) Conn() Conn { 36 | return w.w.Conn() 37 | } 38 | 39 | // Message direct access to the response. 40 | func (w *muxResponseWriter[C]) Message() *pool.Message { 41 | return w.w.Message() 42 | } 43 | 44 | // SetMessage replaces the response message. Ensure that Token, MessageID(udp), and Type(udp) messages are paired correctly. 45 | func (w *muxResponseWriter[C]) SetMessage(msg *pool.Message) { 46 | w.w.SetMessage(msg) 47 | } 48 | -------------------------------------------------------------------------------- /net/blockwise/error.go: -------------------------------------------------------------------------------- 1 | package blockwise 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrBlockNumberExceedLimit block number exceeded the limit 1,048,576 7 | ErrBlockNumberExceedLimit = errors.New("block number exceeded the limit 1,048,576") 8 | 9 | // ErrBlockInvalidSize block has invalid size 10 | ErrBlockInvalidSize = errors.New("block has invalid size") 11 | 12 | // ErrInvalidOptionBlock2 message has invalid value of Block2 13 | ErrInvalidOptionBlock2 = errors.New("message has invalid value of Block2") 14 | 15 | // ErrInvalidOptionBlock1 message has invalid value of Block1 16 | ErrInvalidOptionBlock1 = errors.New("message has invalid value of Block1") 17 | 18 | // ErrInvalidResponseCode response code has invalid value 19 | ErrInvalidResponseCode = errors.New("response code has invalid value") 20 | 21 | // ErrInvalidPayloadSize invalid payload size 22 | ErrInvalidPayloadSize = errors.New("invalid payload size") 23 | 24 | // ErrInvalidSZX invalid block-wise transfer szx 25 | ErrInvalidSZX = errors.New("invalid block-wise transfer szx") 26 | ) 27 | -------------------------------------------------------------------------------- /net/client/limitParallelRequests/limitParallelRequests.go: -------------------------------------------------------------------------------- 1 | package limitparallelrequests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "hash/crc64" 7 | "math" 8 | 9 | "github.com/plgd-dev/go-coap/v3/message" 10 | "github.com/plgd-dev/go-coap/v3/message/pool" 11 | coapSync "github.com/plgd-dev/go-coap/v3/pkg/sync" 12 | "golang.org/x/sync/semaphore" 13 | ) 14 | 15 | type ( 16 | DoFunc = func(req *pool.Message) (*pool.Message, error) 17 | DoObserveFunc = func(req *pool.Message, observeFunc func(req *pool.Message)) (Observation, error) 18 | ) 19 | 20 | type Observation = interface { 21 | Cancel(ctx context.Context, opts ...message.Option) error 22 | Canceled() bool 23 | } 24 | 25 | type endpointQueue struct { 26 | processedCounter int64 27 | orderedRequest []chan struct{} 28 | } 29 | 30 | type LimitParallelRequests struct { 31 | endpointLimit int64 32 | limit *semaphore.Weighted 33 | do DoFunc 34 | doObserve DoObserveFunc 35 | // only one request can be processed by one endpoint 36 | endpointQueues *coapSync.Map[uint64, *endpointQueue] 37 | } 38 | 39 | // New creates new LimitParallelRequests. When limit, endpointLimit == 0, then limit is not used. 40 | func New(limit, endpointLimit int64, do DoFunc, doObserve DoObserveFunc) *LimitParallelRequests { 41 | if limit <= 0 { 42 | limit = math.MaxInt64 43 | } 44 | if endpointLimit <= 0 { 45 | endpointLimit = math.MaxInt64 46 | } 47 | return &LimitParallelRequests{ 48 | limit: semaphore.NewWeighted(limit), 49 | endpointLimit: endpointLimit, 50 | do: do, 51 | doObserve: doObserve, 52 | endpointQueues: coapSync.NewMap[uint64, *endpointQueue](), 53 | } 54 | } 55 | 56 | func hash(opts message.Options) uint64 { 57 | h := crc64.New(crc64.MakeTable(crc64.ISO)) 58 | for _, opt := range opts { 59 | if opt.ID == message.URIPath { 60 | _, _ = h.Write(opt.Value) // hash never returns an error 61 | } 62 | } 63 | return h.Sum64() 64 | } 65 | 66 | func (c *LimitParallelRequests) acquireEndpoint(ctx context.Context, endpointLimitKey uint64) error { 67 | reqChan := make(chan struct{}) // channel is closed when request can be processed by releaseEndpoint 68 | _, _ = c.endpointQueues.LoadOrStoreWithFunc(endpointLimitKey, func(value *endpointQueue) *endpointQueue { 69 | if value.processedCounter < c.endpointLimit { 70 | close(reqChan) 71 | value.processedCounter++ 72 | return value 73 | } 74 | value.orderedRequest = append(value.orderedRequest, reqChan) 75 | return value 76 | }, func() *endpointQueue { 77 | close(reqChan) 78 | return &endpointQueue{ 79 | processedCounter: 1, 80 | } 81 | }) 82 | select { 83 | case <-ctx.Done(): 84 | c.releaseEndpoint(endpointLimitKey) 85 | return ctx.Err() 86 | case <-reqChan: 87 | return nil 88 | } 89 | } 90 | 91 | func (c *LimitParallelRequests) releaseEndpoint(endpointLimitKey uint64) { 92 | _, _ = c.endpointQueues.ReplaceWithFunc(endpointLimitKey, func(oldValue *endpointQueue, oldLoaded bool) (newValue *endpointQueue, doDelete bool) { 93 | if oldLoaded { 94 | if len(oldValue.orderedRequest) > 0 { 95 | reqChan := oldValue.orderedRequest[0] 96 | oldValue.orderedRequest = oldValue.orderedRequest[1:] 97 | close(reqChan) 98 | } else { 99 | oldValue.processedCounter-- 100 | if oldValue.processedCounter == 0 { 101 | return nil, true 102 | } 103 | } 104 | return oldValue, false 105 | } 106 | return nil, true 107 | }) 108 | } 109 | 110 | func (c *LimitParallelRequests) Do(req *pool.Message) (*pool.Message, error) { 111 | endpointLimitKey := hash(req.Options()) 112 | if err := c.acquireEndpoint(req.Context(), endpointLimitKey); err != nil { 113 | return nil, fmt.Errorf("cannot process request %v for client endpoint limit: %w", req, err) 114 | } 115 | defer c.releaseEndpoint(endpointLimitKey) 116 | if err := c.limit.Acquire(req.Context(), 1); err != nil { 117 | return nil, fmt.Errorf("cannot process request %v for client limit: %w", req, err) 118 | } 119 | defer c.limit.Release(1) 120 | return c.do(req) 121 | } 122 | 123 | func (c *LimitParallelRequests) DoObserve(req *pool.Message, observeFunc func(req *pool.Message)) (Observation, error) { 124 | endpointLimitKey := hash(req.Options()) 125 | if err := c.acquireEndpoint(req.Context(), endpointLimitKey); err != nil { 126 | return nil, fmt.Errorf("cannot process observe request %v for client endpoint limit: %w", req, err) 127 | } 128 | defer c.releaseEndpoint(endpointLimitKey) 129 | err := c.limit.Acquire(req.Context(), 1) 130 | if err != nil { 131 | return nil, fmt.Errorf("cannot process observe request %v for client limit: %w", req, err) 132 | } 133 | defer c.limit.Release(1) 134 | return c.doObserve(req, observeFunc) 135 | } 136 | -------------------------------------------------------------------------------- /net/client/receivedMessageReader.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message/pool" 7 | "go.uber.org/atomic" 8 | ) 9 | 10 | type ReceivedMessageReaderClient interface { 11 | Done() <-chan struct{} 12 | ProcessReceivedMessage(req *pool.Message) 13 | } 14 | 15 | type ReceivedMessageReader[C ReceivedMessageReaderClient] struct { 16 | queue chan *pool.Message 17 | cc C 18 | 19 | private struct { 20 | mutex sync.Mutex 21 | loopDone chan struct{} 22 | readingMessages *atomic.Bool 23 | } 24 | } 25 | 26 | // NewReceivedMessageReader creates a new ReceivedMessageReader[C] instance. 27 | func NewReceivedMessageReader[C ReceivedMessageReaderClient](cc C, queueSize int) *ReceivedMessageReader[C] { 28 | r := ReceivedMessageReader[C]{ 29 | queue: make(chan *pool.Message, queueSize), 30 | cc: cc, 31 | private: struct { 32 | mutex sync.Mutex 33 | loopDone chan struct{} 34 | readingMessages *atomic.Bool 35 | }{ 36 | loopDone: make(chan struct{}), 37 | readingMessages: atomic.NewBool(true), 38 | }, 39 | } 40 | 41 | go r.loop(r.private.loopDone, r.private.readingMessages) 42 | return &r 43 | } 44 | 45 | // C returns the channel to push received messages to. 46 | func (r *ReceivedMessageReader[C]) C() chan<- *pool.Message { 47 | return r.queue 48 | } 49 | 50 | // The loop function continuously listens to messages. IT can be replaced with a new one by calling the TryToReplaceLoop function, 51 | // ensuring that only one loop is reading from the queue at a time. 52 | // The loopDone channel is used to signal when the loop should be closed. 53 | // The readingMessages variable is used to indicate if the loop is currently reading from the queue. 54 | // When the loop is not reading from the queue, it sets readingMessages to false, and when it starts reading again, it sets it to true. 55 | // If the client is closed, the loop also closes. 56 | func (r *ReceivedMessageReader[C]) loop(loopDone chan struct{}, readingMessages *atomic.Bool) { 57 | for { 58 | select { 59 | // if the loop is replaced, the old loop will be closed 60 | case <-loopDone: 61 | return 62 | // process received message until the queue is empty 63 | case req := <-r.queue: 64 | // This signalizes that the loop is not reading messages. 65 | readingMessages.Store(false) 66 | r.cc.ProcessReceivedMessage(req) 67 | // This signalizes that the loop is reading messages. We call mutex because we want to ensure that TryToReplaceLoop has ended and 68 | // loopDone is closed if it was replaced. 69 | r.private.mutex.Lock() 70 | readingMessages.Store(true) 71 | r.private.mutex.Unlock() 72 | // if the client is closed, the loop will be closed 73 | case <-r.cc.Done(): 74 | return 75 | } 76 | } 77 | } 78 | 79 | // TryToReplaceLoop function attempts to replace the loop with a new one, 80 | // but only if the loop is not currently reading messages. If the loop is reading messages, 81 | // the function returns immediately. If the loop is not reading messages, the current loop is closed, 82 | // and new loopDone and readingMessages channels and variables are created. 83 | func (r *ReceivedMessageReader[C]) TryToReplaceLoop() { 84 | r.private.mutex.Lock() 85 | if r.private.readingMessages.Load() { 86 | r.private.mutex.Unlock() 87 | return 88 | } 89 | defer r.private.mutex.Unlock() 90 | close(r.private.loopDone) 91 | loopDone := make(chan struct{}) 92 | readingMessages := atomic.NewBool(true) 93 | r.private.loopDone = loopDone 94 | r.private.readingMessages = readingMessages 95 | go r.loop(loopDone, readingMessages) 96 | } 97 | -------------------------------------------------------------------------------- /net/conn.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | 9 | "go.uber.org/atomic" 10 | ) 11 | 12 | // Conn is a generic stream-oriented network connection that provides Read/Write with context. 13 | // 14 | // Multiple goroutines may invoke methods on a Conn simultaneously. 15 | type Conn struct { 16 | connection net.Conn 17 | closed atomic.Bool 18 | handshakeContext func(ctx context.Context) error 19 | lock sync.Mutex 20 | } 21 | 22 | // NewConn creates connection over net.Conn. 23 | func NewConn(c net.Conn) *Conn { 24 | connection := Conn{ 25 | connection: c, 26 | } 27 | 28 | if v, ok := c.(interface { 29 | HandshakeContext(ctx context.Context) error 30 | }); ok { 31 | connection.handshakeContext = v.HandshakeContext 32 | } 33 | 34 | return &connection 35 | } 36 | 37 | // LocalAddr returns the local network address. The Addr returned is shared by all invocations of LocalAddr, so do not modify it. 38 | func (c *Conn) LocalAddr() net.Addr { 39 | return c.connection.LocalAddr() 40 | } 41 | 42 | // NetConn returns the underlying connection that is wrapped by c. The Conn returned is shared by all invocations of Connection, so do not modify it. 43 | func (c *Conn) NetConn() net.Conn { 44 | return c.connection 45 | } 46 | 47 | // RemoteAddr returns the remote network address. The Addr returned is shared by all invocations of RemoteAddr, so do not modify it. 48 | func (c *Conn) RemoteAddr() net.Addr { 49 | return c.connection.RemoteAddr() 50 | } 51 | 52 | // Close closes the connection. 53 | func (c *Conn) Close() error { 54 | if !c.closed.CompareAndSwap(false, true) { 55 | return nil 56 | } 57 | return c.connection.Close() 58 | } 59 | 60 | func (c *Conn) handshake(ctx context.Context) error { 61 | if c.handshakeContext != nil { 62 | err := c.handshakeContext(ctx) 63 | if err == nil { 64 | return nil 65 | } 66 | errC := c.Close() 67 | if errC == nil { 68 | return err 69 | } 70 | return fmt.Errorf("%v", []error{err, errC}) 71 | } 72 | return nil 73 | } 74 | 75 | // WriteWithContext writes data with context. 76 | func (c *Conn) WriteWithContext(ctx context.Context, data []byte) error { 77 | if err := c.handshake(ctx); err != nil { 78 | return err 79 | } 80 | written := 0 81 | c.lock.Lock() 82 | defer c.lock.Unlock() 83 | for written < len(data) { 84 | select { 85 | case <-ctx.Done(): 86 | return ctx.Err() 87 | default: 88 | } 89 | if c.closed.Load() { 90 | return ErrConnectionIsClosed 91 | } 92 | n, err := c.connection.Write(data[written:]) 93 | if err != nil { 94 | return err 95 | } 96 | written += n 97 | } 98 | return nil 99 | } 100 | 101 | // ReadFullWithContext reads stream with context until whole buffer is satisfied. 102 | func (c *Conn) ReadFullWithContext(ctx context.Context, buffer []byte) error { 103 | offset := 0 104 | for offset < len(buffer) { 105 | n, err := c.ReadWithContext(ctx, buffer[offset:]) 106 | if err != nil { 107 | return fmt.Errorf("cannot read full from connection: %w", err) 108 | } 109 | offset += n 110 | } 111 | return nil 112 | } 113 | 114 | // ReadWithContext reads stream with context. 115 | func (c *Conn) ReadWithContext(ctx context.Context, buffer []byte) (int, error) { 116 | select { 117 | case <-ctx.Done(): 118 | return -1, ctx.Err() 119 | default: 120 | } 121 | if c.closed.Load() { 122 | return -1, ErrConnectionIsClosed 123 | } 124 | if err := c.handshake(ctx); err != nil { 125 | return -1, err 126 | } 127 | return c.connection.Read(buffer) 128 | } 129 | -------------------------------------------------------------------------------- /net/conn_test.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestConnWriteWithContext(t *testing.T) { 13 | ctxCanceled, ctxCancel := context.WithCancel(context.Background()) 14 | ctxCancel() 15 | helloWorld := make([]byte, 1024*1024*256) 16 | 17 | type args struct { 18 | ctx context.Context 19 | data []byte 20 | } 21 | tests := []struct { 22 | name string 23 | args args 24 | wantErr bool 25 | }{ 26 | { 27 | name: "valid", 28 | args: args{ 29 | ctx: context.Background(), 30 | data: helloWorld, 31 | }, 32 | }, 33 | { 34 | name: "cancelled", 35 | args: args{ 36 | ctx: ctxCanceled, 37 | data: helloWorld, 38 | }, 39 | wantErr: true, 40 | }, 41 | } 42 | 43 | listener, err := NewTCPListener("tcp", "127.0.0.1:") 44 | require.NoError(t, err) 45 | defer func() { 46 | err := listener.Close() 47 | require.NoError(t, err) 48 | }() 49 | ctx, cancel := context.WithCancel(context.Background()) 50 | defer cancel() 51 | 52 | go func() { 53 | for { 54 | conn, err := listener.AcceptWithContext(ctx) 55 | if err != nil { 56 | return 57 | } 58 | c := NewConn(conn) 59 | b := make([]byte, len(helloWorld)) 60 | _ = c.ReadFullWithContext(ctx, b) 61 | err = c.Close() 62 | assert.NoError(t, err) 63 | } 64 | }() 65 | 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | tcpConn, err := net.Dial("tcp", listener.Addr().String()) 69 | require.NoError(t, err) 70 | c := NewConn(tcpConn) 71 | defer func() { 72 | errC := c.Close() 73 | require.NoError(t, errC) 74 | }() 75 | 76 | c.LocalAddr() 77 | c.RemoteAddr() 78 | 79 | err = c.WriteWithContext(tt.args.ctx, tt.args.data) 80 | if tt.wantErr { 81 | require.Error(t, err) 82 | return 83 | } 84 | require.NoError(t, err) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /net/dtlslistener.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | dtls "github.com/pion/dtls/v3" 9 | "go.uber.org/atomic" 10 | ) 11 | 12 | // DTLSListener is a DTLS listener that provides accept with context. 13 | type DTLSListener struct { 14 | listener net.Listener 15 | closed atomic.Bool 16 | } 17 | 18 | func newNetDTLSListener(network string, addr string, dtlsCfg *dtls.Config) (net.Listener, error) { 19 | a, err := net.ResolveUDPAddr(network, addr) 20 | if err != nil { 21 | return nil, fmt.Errorf("cannot resolve address: %w", err) 22 | } 23 | dtls, err := dtls.Listen(network, a, dtlsCfg) 24 | if err != nil { 25 | return nil, fmt.Errorf("cannot create new net dtls listener: %w", err) 26 | } 27 | return dtls, nil 28 | } 29 | 30 | // NewDTLSListener creates dtls listener. 31 | // Known networks are "udp", "udp4" (IPv4-only), "udp6" (IPv6-only). 32 | func NewDTLSListener(network string, addr string, dtlsCfg *dtls.Config) (*DTLSListener, error) { 33 | dtls, err := newNetDTLSListener(network, addr, dtlsCfg) 34 | if err != nil { 35 | return nil, fmt.Errorf("cannot create new dtls listener: %w", err) 36 | } 37 | return &DTLSListener{listener: dtls}, nil 38 | } 39 | 40 | // AcceptWithContext waits with context for a generic Conn. 41 | func (l *DTLSListener) AcceptWithContext(ctx context.Context) (net.Conn, error) { 42 | select { 43 | case <-ctx.Done(): 44 | return nil, ctx.Err() 45 | default: 46 | } 47 | if l.closed.Load() { 48 | return nil, ErrListenerIsClosed 49 | } 50 | return l.listener.Accept() 51 | } 52 | 53 | // Accept waits for a generic Conn. 54 | func (l *DTLSListener) Accept() (net.Conn, error) { 55 | return l.AcceptWithContext(context.Background()) 56 | } 57 | 58 | // Close closes the connection. 59 | func (l *DTLSListener) Close() error { 60 | if !l.closed.CompareAndSwap(false, true) { 61 | return nil 62 | } 63 | return l.listener.Close() 64 | } 65 | 66 | // Addr represents a network end point address. 67 | func (l *DTLSListener) Addr() net.Addr { 68 | return l.listener.Addr() 69 | } 70 | -------------------------------------------------------------------------------- /net/error.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net" 8 | ) 9 | 10 | var ( 11 | ErrListenerIsClosed = io.EOF 12 | ErrConnectionIsClosed = io.EOF 13 | ErrWriteInterrupted = errors.New("only part data was written to socket") 14 | ) 15 | 16 | func IsCancelOrCloseError(err error) bool { 17 | if err == nil { 18 | return false 19 | } 20 | if errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { 21 | // this error was produced by cancellation context or closing connection. 22 | return true 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /net/error_plan9.go: -------------------------------------------------------------------------------- 1 | //go:build plan9 2 | // +build plan9 3 | 4 | package net 5 | 6 | // Check if error returned by operation on a socket failed because 7 | // the other side has closed the connection. 8 | func IsConnectionBrokenError(err error) bool { 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /net/error_unix.go: -------------------------------------------------------------------------------- 1 | //go:build aix || darwin || dragonfly || freebsd || js || linux || netbsd || openbsd || solaris 2 | // +build aix darwin dragonfly freebsd js linux netbsd openbsd solaris 3 | 4 | package net 5 | 6 | import ( 7 | "errors" 8 | "syscall" 9 | ) 10 | 11 | // Check if error returned by operation on a socket failed because 12 | // the other side has closed the connection. 13 | func IsConnectionBrokenError(err error) bool { 14 | return errors.Is(err, syscall.EPIPE) || 15 | errors.Is(err, syscall.ECONNRESET) 16 | } 17 | -------------------------------------------------------------------------------- /net/error_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package net 5 | 6 | import ( 7 | "errors" 8 | "syscall" 9 | ) 10 | 11 | func IsConnectionBrokenError(err error) bool { 12 | return errors.Is(err, syscall.WSAECONNRESET) || 13 | errors.Is(err, syscall.WSAECONNABORTED) 14 | } 15 | -------------------------------------------------------------------------------- /net/monitor/inactivity/keepalive.go: -------------------------------------------------------------------------------- 1 | package inactivity 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "go.uber.org/atomic" 7 | ) 8 | 9 | type cancelPingFunc func() 10 | 11 | type KeepAlive[C Conn] struct { 12 | pongToken atomic.Uint64 13 | onInactive OnInactiveFunc[C] 14 | 15 | sendPing func(cc C, receivePong func()) (func(), error) 16 | cancelPing atomic.UnsafePointer 17 | numFails atomic.Uint32 18 | 19 | maxRetries uint32 20 | } 21 | 22 | func NewKeepAlive[C Conn](maxRetries uint32, onInactive OnInactiveFunc[C], sendPing func(cc C, receivePong func()) (func(), error)) *KeepAlive[C] { 23 | return &KeepAlive[C]{ 24 | maxRetries: maxRetries, 25 | sendPing: sendPing, 26 | onInactive: onInactive, 27 | } 28 | } 29 | 30 | func (m *KeepAlive[C]) checkCancelPing() { 31 | cancelPingPtr := m.cancelPing.Swap(nil) 32 | if cancelPingPtr != nil { 33 | cancelPing := *(*cancelPingFunc)(cancelPingPtr) 34 | cancelPing() 35 | } 36 | } 37 | 38 | func (m *KeepAlive[C]) OnInactive(cc C) { 39 | v := m.incrementFails() 40 | m.checkCancelPing() 41 | if v > m.maxRetries { 42 | m.onInactive(cc) 43 | return 44 | } 45 | pongToken := m.pongToken.Add(1) 46 | cancel, err := m.sendPing(cc, func() { 47 | if m.pongToken.Load() == pongToken { 48 | m.resetFails() 49 | } 50 | }) 51 | if err != nil { 52 | return 53 | } 54 | m.cancelPing.Store(unsafe.Pointer(&cancel)) 55 | } 56 | 57 | func (m *KeepAlive[C]) incrementFails() uint32 { 58 | return m.numFails.Add(1) 59 | } 60 | 61 | func (m *KeepAlive[C]) resetFails() { 62 | m.numFails.Store(0) 63 | } 64 | -------------------------------------------------------------------------------- /net/monitor/inactivity/monitor.go: -------------------------------------------------------------------------------- 1 | package inactivity 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | type OnInactiveFunc[C Conn] func(cc C) 10 | 11 | type Conn = interface { 12 | Context() context.Context 13 | Close() error 14 | } 15 | 16 | type Monitor[C Conn] struct { 17 | lastActivity atomic.Value 18 | duration time.Duration 19 | onInactive OnInactiveFunc[C] 20 | } 21 | 22 | func (m *Monitor[C]) Notify() { 23 | m.lastActivity.Store(time.Now()) 24 | } 25 | 26 | func (m *Monitor[C]) LastActivity() time.Time { 27 | if t, ok := m.lastActivity.Load().(time.Time); ok { 28 | return t 29 | } 30 | return time.Time{} 31 | } 32 | 33 | func CloseConn(cc Conn) { 34 | // call cc.Close() directly to check and handle error if necessary 35 | _ = cc.Close() 36 | } 37 | 38 | func New[C Conn](duration time.Duration, onInactive OnInactiveFunc[C]) *Monitor[C] { 39 | m := &Monitor[C]{ 40 | duration: duration, 41 | onInactive: onInactive, 42 | } 43 | m.Notify() 44 | return m 45 | } 46 | 47 | func (m *Monitor[C]) CheckInactivity(now time.Time, cc C) { 48 | if m.onInactive == nil || m.duration == time.Duration(0) { 49 | return 50 | } 51 | if now.After(m.LastActivity().Add(m.duration)) { 52 | m.onInactive(cc) 53 | } 54 | } 55 | 56 | type NilMonitor[C Conn] struct{} 57 | 58 | func (m *NilMonitor[C]) CheckInactivity(time.Time, C) { 59 | // do nothing 60 | } 61 | 62 | func (m *NilMonitor[C]) Notify() { 63 | // do nothing 64 | } 65 | 66 | func NewNilMonitor[C Conn]() *NilMonitor[C] { 67 | return &NilMonitor[C]{} 68 | } 69 | -------------------------------------------------------------------------------- /net/observation/observation.go: -------------------------------------------------------------------------------- 1 | package observation 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // ObservationSequenceTimeout defines how long is sequence number is valid. https://tools.ietf.org/html/rfc7641#section-3.4 8 | const ObservationSequenceTimeout = 128 * time.Second 9 | 10 | // ValidSequenceNumber implements conditions in https://tools.ietf.org/html/rfc7641#section-3.4 11 | func ValidSequenceNumber(oldValue, newValue uint32, lastEventOccurs time.Time, now time.Time) bool { 12 | if oldValue < newValue && (newValue-oldValue) < (1<<23) { 13 | return true 14 | } 15 | if oldValue > newValue && (oldValue-newValue) > (1<<23) { 16 | return true 17 | } 18 | if now.Sub(lastEventOccurs) > ObservationSequenceTimeout { 19 | return true 20 | } 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /net/observation/observation_test.go: -------------------------------------------------------------------------------- 1 | package observation 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestValidSequenceNumber(t *testing.T) { 11 | type args struct { 12 | old uint32 13 | new uint32 14 | lastEventOccurs time.Time 15 | now time.Time 16 | } 17 | tests := []struct { 18 | name string 19 | args args 20 | want bool 21 | }{ 22 | { 23 | name: "(1 << 25)-1, 0, now-1s, now", 24 | args: args{ 25 | old: (1 << 25) - 1, 26 | new: 0, 27 | lastEventOccurs: time.Now().Add(-time.Second), 28 | now: time.Now(), 29 | }, 30 | want: true, 31 | }, 32 | { 33 | name: "0, 1, 0, now", 34 | args: args{ 35 | new: 1, 36 | now: time.Now(), 37 | }, 38 | want: true, 39 | }, 40 | { 41 | name: "1582, 1583, now-1s, now", 42 | args: args{ 43 | old: 1582, 44 | new: 1583, 45 | lastEventOccurs: time.Now().Add(-time.Second), 46 | now: time.Now(), 47 | }, 48 | want: true, 49 | }, 50 | { 51 | name: "1582, 1, now-129s, now", 52 | args: args{ 53 | old: 1582, 54 | new: 1, 55 | lastEventOccurs: time.Now().Add(-time.Second * 129), 56 | now: time.Now(), 57 | }, 58 | want: true, 59 | }, 60 | { 61 | name: "1582, 1, now-125s, now", 62 | args: args{ 63 | old: 1582, 64 | new: 1, 65 | lastEventOccurs: time.Now().Add(-time.Second * 125), 66 | now: time.Now(), 67 | }, 68 | want: false, 69 | }, 70 | { 71 | name: "1 << 23, 0, now-1s, now", 72 | args: args{ 73 | old: 1 << 23, 74 | new: 0, 75 | lastEventOccurs: time.Now().Add(-time.Second), 76 | now: time.Now(), 77 | }, 78 | want: false, 79 | }, 80 | { 81 | name: "0, 1 << 23+1, now-1s, now", 82 | args: args{ 83 | old: 0, 84 | new: 1<<23 + 1, 85 | lastEventOccurs: time.Now().Add(-time.Second), 86 | now: time.Now(), 87 | }, 88 | want: false, 89 | }, 90 | { 91 | name: "1582, 1582, now-1s, now", 92 | args: args{ 93 | old: 1582, 94 | new: 1582, 95 | lastEventOccurs: time.Now().Add(-time.Second), 96 | now: time.Now(), 97 | }, 98 | want: false, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | got := ValidSequenceNumber(tt.args.old, tt.args.new, tt.args.lastEventOccurs, tt.args.now) 104 | assert.Equal(t, tt.want, got) 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /net/options.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import "net" 4 | 5 | // A UDPOption sets options such as errors parameters, etc. 6 | type UDPOption interface { 7 | ApplyUDP(*UDPConnConfig) 8 | } 9 | 10 | type ErrorsOpt struct { 11 | errors func(err error) 12 | } 13 | 14 | func (h ErrorsOpt) ApplyUDP(o *UDPConnConfig) { 15 | o.Errors = h.errors 16 | } 17 | 18 | func WithErrors(v func(err error)) ErrorsOpt { 19 | return ErrorsOpt{ 20 | errors: v, 21 | } 22 | } 23 | 24 | func DefaultMulticastOptions() MulticastOptions { 25 | return MulticastOptions{ 26 | IFaceMode: MulticastAllInterface, 27 | HopLimit: 1, 28 | } 29 | } 30 | 31 | type MulticastInterfaceMode int 32 | 33 | const ( 34 | MulticastAllInterface MulticastInterfaceMode = 0 35 | MulticastAnyInterface MulticastInterfaceMode = 1 36 | MulticastSpecificInterface MulticastInterfaceMode = 2 37 | ) 38 | 39 | type InterfaceError = func(iface *net.Interface, err error) 40 | 41 | type MulticastOptions struct { 42 | IFaceMode MulticastInterfaceMode 43 | Iface *net.Interface 44 | Source *net.IP 45 | HopLimit int 46 | InterfaceError InterfaceError 47 | } 48 | 49 | func (m *MulticastOptions) Apply(o MulticastOption) { 50 | o.applyMC(m) 51 | } 52 | 53 | // A MulticastOption sets options such as hop limit, etc. 54 | type MulticastOption interface { 55 | applyMC(*MulticastOptions) 56 | } 57 | 58 | type MulticastInterfaceModeOpt struct { 59 | mode MulticastInterfaceMode 60 | } 61 | 62 | func (m MulticastInterfaceModeOpt) applyMC(o *MulticastOptions) { 63 | o.IFaceMode = m.mode 64 | } 65 | 66 | func WithAnyMulticastInterface() MulticastOption { 67 | return MulticastInterfaceModeOpt{mode: MulticastAnyInterface} 68 | } 69 | 70 | func WithAllMulticastInterface() MulticastOption { 71 | return MulticastInterfaceModeOpt{mode: MulticastAllInterface} 72 | } 73 | 74 | type MulticastInterfaceOpt struct { 75 | iface net.Interface 76 | } 77 | 78 | func (m MulticastInterfaceOpt) applyMC(o *MulticastOptions) { 79 | o.Iface = &m.iface 80 | o.IFaceMode = MulticastSpecificInterface 81 | } 82 | 83 | func WithMulticastInterface(iface net.Interface) MulticastOption { 84 | return &MulticastInterfaceOpt{iface: iface} 85 | } 86 | 87 | type MulticastHoplimitOpt struct { 88 | hoplimit int 89 | } 90 | 91 | func (m MulticastHoplimitOpt) applyMC(o *MulticastOptions) { 92 | o.HopLimit = m.hoplimit 93 | } 94 | 95 | func WithMulticastHoplimit(hoplimit int) MulticastOption { 96 | return &MulticastHoplimitOpt{hoplimit: hoplimit} 97 | } 98 | 99 | type MulticastSourceOpt struct { 100 | source net.IP 101 | } 102 | 103 | func (m MulticastSourceOpt) applyMC(o *MulticastOptions) { 104 | o.Source = &m.source 105 | } 106 | 107 | func WithMulticastSource(source net.IP) MulticastOption { 108 | return &MulticastSourceOpt{source: source} 109 | } 110 | 111 | type MulticastInterfaceErrorOpt struct { 112 | interfaceError InterfaceError 113 | } 114 | 115 | func (m MulticastInterfaceErrorOpt) applyMC(o *MulticastOptions) { 116 | o.InterfaceError = m.interfaceError 117 | } 118 | 119 | // WithMulticastInterfaceError sets the callback for interface errors. If it is set error is not propagated as result of WriteMulticast. 120 | func WithMulticastInterfaceError(interfaceError InterfaceError) MulticastOption { 121 | return &MulticastInterfaceErrorOpt{interfaceError: interfaceError} 122 | } 123 | -------------------------------------------------------------------------------- /net/responsewriter/responseWriter.go: -------------------------------------------------------------------------------- 1 | package responsewriter 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message" 7 | "github.com/plgd-dev/go-coap/v3/message/codes" 8 | "github.com/plgd-dev/go-coap/v3/message/noresponse" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | ) 11 | 12 | type Client interface { 13 | ReleaseMessage(msg *pool.Message) 14 | } 15 | 16 | // A ResponseWriter is used by an COAP handler to construct an COAP response. 17 | type ResponseWriter[C Client] struct { 18 | noResponseValue *uint32 19 | response *pool.Message 20 | cc C 21 | } 22 | 23 | func New[C Client](response *pool.Message, cc C, requestOptions ...message.Option) *ResponseWriter[C] { 24 | var noResponseValue *uint32 25 | if len(requestOptions) > 0 { 26 | reqOpts := message.Options(requestOptions) 27 | v, err := reqOpts.GetUint32(message.NoResponse) 28 | if err == nil { 29 | noResponseValue = &v 30 | } 31 | } 32 | 33 | return &ResponseWriter[C]{ 34 | response: response, 35 | cc: cc, 36 | noResponseValue: noResponseValue, 37 | } 38 | } 39 | 40 | // SetResponse simplifies the setup of the response for the request. ETags must be set via options. For advanced setup, use Message(). 41 | func (r *ResponseWriter[C]) SetResponse(code codes.Code, contentFormat message.MediaType, d io.ReadSeeker, opts ...message.Option) error { 42 | if r.noResponseValue != nil { 43 | err := noresponse.IsNoResponseCode(code, *r.noResponseValue) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | r.response.SetCode(code) 50 | r.response.ResetOptionsTo(opts) 51 | if d != nil { 52 | r.response.SetContentFormat(contentFormat) 53 | r.response.SetBody(d) 54 | } 55 | return nil 56 | } 57 | 58 | // SetMessage replaces the response message. The original message was released to the message pool, so don't use it any more. Ensure that Token, MessageID(udp), and Type(udp) messages are paired correctly. 59 | func (r *ResponseWriter[C]) SetMessage(m *pool.Message) { 60 | r.cc.ReleaseMessage(r.response) 61 | r.response = m 62 | } 63 | 64 | // Message direct access to the response. 65 | func (r *ResponseWriter[C]) Message() *pool.Message { 66 | return r.response 67 | } 68 | 69 | // Swap message in response without releasing. 70 | func (r *ResponseWriter[C]) Swap(m *pool.Message) *pool.Message { 71 | tmp := r.response 72 | r.response = m 73 | return tmp 74 | } 75 | 76 | // CConn peer connection. 77 | func (r *ResponseWriter[C]) Conn() C { 78 | return r.cc 79 | } 80 | -------------------------------------------------------------------------------- /net/supportsOverrideRemoteAddr.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import "net" 4 | 5 | func supportsOverrideRemoteAddr(c *net.UDPConn) bool { 6 | return c.RemoteAddr() == nil 7 | } 8 | -------------------------------------------------------------------------------- /net/tcplistener.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "go.uber.org/atomic" 9 | ) 10 | 11 | // TCPListener is a TCP network listener that provides accept with context. 12 | type TCPListener struct { 13 | listener *net.TCPListener 14 | closed atomic.Bool 15 | } 16 | 17 | func newNetTCPListener(network string, addr string) (*net.TCPListener, error) { 18 | a, err := net.ResolveTCPAddr(network, addr) 19 | if err != nil { 20 | return nil, fmt.Errorf("cannot resolve address: %w", err) 21 | } 22 | tcp, err := net.ListenTCP(network, a) 23 | if err != nil { 24 | return nil, fmt.Errorf("cannot create new net tcp listener: %w", err) 25 | } 26 | return tcp, nil 27 | } 28 | 29 | // NewTCPListener creates tcp listener. 30 | // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). 31 | func NewTCPListener(network string, addr string) (*TCPListener, error) { 32 | tcp, err := newNetTCPListener(network, addr) 33 | if err != nil { 34 | return nil, fmt.Errorf("cannot create new tcp listener: %w", err) 35 | } 36 | return &TCPListener{listener: tcp}, nil 37 | } 38 | 39 | // AcceptWithContext waits with context for a generic Conn. 40 | func (l *TCPListener) AcceptWithContext(ctx context.Context) (net.Conn, error) { 41 | select { 42 | case <-ctx.Done(): 43 | return nil, ctx.Err() 44 | default: 45 | } 46 | if l.closed.Load() { 47 | return nil, ErrListenerIsClosed 48 | } 49 | return l.listener.Accept() 50 | } 51 | 52 | // Accept waits for a generic Conn. 53 | func (l *TCPListener) Accept() (net.Conn, error) { 54 | return l.AcceptWithContext(context.Background()) 55 | } 56 | 57 | // Close closes the connection. 58 | func (l *TCPListener) Close() error { 59 | if !l.closed.CompareAndSwap(false, true) { 60 | return nil 61 | } 62 | return l.listener.Close() 63 | } 64 | 65 | // Addr represents a network end point address. 66 | func (l *TCPListener) Addr() net.Addr { 67 | return l.listener.Addr() 68 | } 69 | -------------------------------------------------------------------------------- /net/tlslistener.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | 9 | "go.uber.org/atomic" 10 | ) 11 | 12 | // TLSListener is a TLS listener that provides accept with context. 13 | type TLSListener struct { 14 | listener net.Listener 15 | tcp *net.TCPListener 16 | closed atomic.Bool 17 | } 18 | 19 | // NewTLSListener creates tcp listener. 20 | // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). 21 | func NewTLSListener(network string, addr string, tlsCfg *tls.Config) (*TLSListener, error) { 22 | tcp, err := newNetTCPListener(network, addr) 23 | if err != nil { 24 | return nil, fmt.Errorf("cannot create new tls listener: %w", err) 25 | } 26 | tls := tls.NewListener(tcp, tlsCfg) 27 | return &TLSListener{ 28 | tcp: tcp, 29 | listener: tls, 30 | }, nil 31 | } 32 | 33 | // AcceptWithContext waits with context for a generic Conn. 34 | func (l *TLSListener) AcceptWithContext(ctx context.Context) (net.Conn, error) { 35 | select { 36 | case <-ctx.Done(): 37 | return nil, ctx.Err() 38 | default: 39 | } 40 | if l.closed.Load() { 41 | return nil, ErrListenerIsClosed 42 | } 43 | return l.listener.Accept() 44 | } 45 | 46 | // Accept waits for a generic Conn. 47 | func (l *TLSListener) Accept() (net.Conn, error) { 48 | return l.AcceptWithContext(context.Background()) 49 | } 50 | 51 | // Close closes the connection. 52 | func (l *TLSListener) Close() error { 53 | if !l.closed.CompareAndSwap(false, true) { 54 | return nil 55 | } 56 | return l.listener.Close() 57 | } 58 | 59 | // Addr represents a network end point address. 60 | func (l *TLSListener) Addr() net.Addr { 61 | return l.listener.Addr() 62 | } 63 | -------------------------------------------------------------------------------- /options/config/common.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | "github.com/plgd-dev/go-coap/v3/net/blockwise" 11 | "github.com/plgd-dev/go-coap/v3/net/client" 12 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 13 | "github.com/plgd-dev/go-coap/v3/pkg/runner/periodic" 14 | ) 15 | 16 | type ( 17 | ErrorFunc = func(error) 18 | HandlerFunc[C responsewriter.Client] func(w *responsewriter.ResponseWriter[C], r *pool.Message) 19 | ProcessReceivedMessageFunc[C responsewriter.Client] func(req *pool.Message, cc C, handler HandlerFunc[C]) 20 | ) 21 | 22 | type Common[C responsewriter.Client] struct { 23 | LimitClientParallelRequests int64 24 | LimitClientEndpointParallelRequests int64 25 | Ctx context.Context 26 | Errors ErrorFunc 27 | PeriodicRunner periodic.Func 28 | MessagePool *pool.Pool 29 | GetToken client.GetTokenFunc 30 | MaxMessageSize uint32 31 | BlockwiseTransferTimeout time.Duration 32 | BlockwiseSZX blockwise.SZX 33 | BlockwiseEnable bool 34 | ProcessReceivedMessage ProcessReceivedMessageFunc[C] 35 | ReceivedMessageQueueSize int 36 | } 37 | 38 | func NewCommon[C responsewriter.Client]() Common[C] { 39 | return Common[C]{ 40 | Ctx: context.Background(), 41 | MaxMessageSize: 64 * 1024, 42 | Errors: func(err error) { 43 | fmt.Println(err) 44 | }, 45 | BlockwiseSZX: blockwise.SZX1024, 46 | BlockwiseEnable: true, 47 | BlockwiseTransferTimeout: time.Second * 3, 48 | PeriodicRunner: func(f func(now time.Time) bool) { 49 | go func() { 50 | for f(time.Now()) { 51 | time.Sleep(4 * time.Second) 52 | } 53 | }() 54 | }, 55 | MessagePool: pool.New(1024, 2048), 56 | GetToken: message.GetToken, 57 | LimitClientParallelRequests: 1, 58 | LimitClientEndpointParallelRequests: 1, 59 | ReceivedMessageQueueSize: 16, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /options/tcpOptions.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | tcpClient "github.com/plgd-dev/go-coap/v3/tcp/client" 7 | tcpServer "github.com/plgd-dev/go-coap/v3/tcp/server" 8 | ) 9 | 10 | // DisablePeerTCPSignalMessageCSMsOpt coap-tcp csm option. 11 | type DisablePeerTCPSignalMessageCSMsOpt struct{} 12 | 13 | func (o DisablePeerTCPSignalMessageCSMsOpt) TCPServerApply(cfg *tcpServer.Config) { 14 | cfg.DisablePeerTCPSignalMessageCSMs = true 15 | } 16 | 17 | func (o DisablePeerTCPSignalMessageCSMsOpt) TCPClientApply(cfg *tcpClient.Config) { 18 | cfg.DisablePeerTCPSignalMessageCSMs = true 19 | } 20 | 21 | // WithDisablePeerTCPSignalMessageCSMs ignor peer's CSM message. 22 | func WithDisablePeerTCPSignalMessageCSMs() DisablePeerTCPSignalMessageCSMsOpt { 23 | return DisablePeerTCPSignalMessageCSMsOpt{} 24 | } 25 | 26 | // DisableTCPSignalMessageCSMOpt coap-tcp csm option. 27 | type DisableTCPSignalMessageCSMOpt struct{} 28 | 29 | func (o DisableTCPSignalMessageCSMOpt) TCPServerApply(cfg *tcpServer.Config) { 30 | cfg.DisableTCPSignalMessageCSM = true 31 | } 32 | 33 | func (o DisableTCPSignalMessageCSMOpt) TCPClientApply(cfg *tcpClient.Config) { 34 | cfg.DisableTCPSignalMessageCSM = true 35 | } 36 | 37 | // WithDisableTCPSignalMessageCSM don't send CSM when client conn is created. 38 | func WithDisableTCPSignalMessageCSM() DisableTCPSignalMessageCSMOpt { 39 | return DisableTCPSignalMessageCSMOpt{} 40 | } 41 | 42 | // TLSOpt tls configuration option. 43 | type TLSOpt struct { 44 | tlsCfg *tls.Config 45 | } 46 | 47 | func (o TLSOpt) TCPClientApply(cfg *tcpClient.Config) { 48 | cfg.TLSCfg = o.tlsCfg 49 | } 50 | 51 | // WithTLS creates tls connection. 52 | func WithTLS(cfg *tls.Config) TLSOpt { 53 | return TLSOpt{ 54 | tlsCfg: cfg, 55 | } 56 | } 57 | 58 | // ConnectionCacheOpt network option. 59 | type ConnectionCacheSizeOpt struct { 60 | connectionCacheSize uint16 61 | } 62 | 63 | func (o ConnectionCacheSizeOpt) TCPServerApply(cfg *tcpServer.Config) { 64 | cfg.ConnectionCacheSize = o.connectionCacheSize 65 | } 66 | 67 | func (o ConnectionCacheSizeOpt) TCPClientApply(cfg *tcpClient.Config) { 68 | cfg.ConnectionCacheSize = o.connectionCacheSize 69 | } 70 | 71 | // WithConnectionCacheSize configure's maximum size of cache of read buffer. 72 | func WithConnectionCacheSize(connectionCacheSize uint16) ConnectionCacheSizeOpt { 73 | return ConnectionCacheSizeOpt{ 74 | connectionCacheSize: connectionCacheSize, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /options/tcpOptions_test.go: -------------------------------------------------------------------------------- 1 | package options_test 2 | 3 | import ( 4 | "crypto/tls" 5 | "testing" 6 | 7 | "github.com/plgd-dev/go-coap/v3/options" 8 | "github.com/plgd-dev/go-coap/v3/tcp" 9 | "github.com/plgd-dev/go-coap/v3/tcp/client" 10 | "github.com/plgd-dev/go-coap/v3/tcp/server" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestTCPClientApply(t *testing.T) { 15 | cfg := client.Config{} 16 | tlsCfg := &tls.Config{} 17 | opt := []tcp.Option{ 18 | options.WithDisablePeerTCPSignalMessageCSMs(), 19 | options.WithDisableTCPSignalMessageCSM(), 20 | options.WithTLS(tlsCfg), 21 | options.WithConnectionCacheSize(100), 22 | } 23 | for _, o := range opt { 24 | o.TCPClientApply(&cfg) 25 | } 26 | require.True(t, cfg.DisablePeerTCPSignalMessageCSMs) 27 | require.True(t, cfg.DisableTCPSignalMessageCSM) 28 | require.Equal(t, tlsCfg, cfg.TLSCfg) 29 | require.Equal(t, uint16(100), cfg.ConnectionCacheSize) 30 | } 31 | 32 | func TestTCPServerApply(t *testing.T) { 33 | cfg := server.Config{} 34 | opt := []server.Option{ 35 | options.WithDisablePeerTCPSignalMessageCSMs(), 36 | options.WithDisableTCPSignalMessageCSM(), 37 | options.WithConnectionCacheSize(100), 38 | } 39 | for _, o := range opt { 40 | o.TCPServerApply(&cfg) 41 | } 42 | require.True(t, cfg.DisablePeerTCPSignalMessageCSMs) 43 | require.True(t, cfg.DisableTCPSignalMessageCSM) 44 | require.Equal(t, uint16(100), cfg.ConnectionCacheSize) 45 | } 46 | -------------------------------------------------------------------------------- /options/udpOptions.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "time" 5 | 6 | dtlsServer "github.com/plgd-dev/go-coap/v3/dtls/server" 7 | udpClient "github.com/plgd-dev/go-coap/v3/udp/client" 8 | udpServer "github.com/plgd-dev/go-coap/v3/udp/server" 9 | ) 10 | 11 | // TransmissionOpt transmission options. 12 | type TransmissionOpt struct { 13 | transmissionNStart uint32 14 | transmissionAcknowledgeTimeout time.Duration 15 | transmissionMaxRetransmit uint32 16 | } 17 | 18 | func (o TransmissionOpt) UDPServerApply(cfg *udpServer.Config) { 19 | cfg.TransmissionNStart = o.transmissionNStart 20 | cfg.TransmissionAcknowledgeTimeout = o.transmissionAcknowledgeTimeout 21 | cfg.TransmissionMaxRetransmit = o.transmissionMaxRetransmit 22 | } 23 | 24 | func (o TransmissionOpt) DTLSServerApply(cfg *dtlsServer.Config) { 25 | cfg.TransmissionNStart = o.transmissionNStart 26 | cfg.TransmissionAcknowledgeTimeout = o.transmissionAcknowledgeTimeout 27 | cfg.TransmissionMaxRetransmit = o.transmissionMaxRetransmit 28 | } 29 | 30 | func (o TransmissionOpt) UDPClientApply(cfg *udpClient.Config) { 31 | cfg.TransmissionNStart = o.transmissionNStart 32 | cfg.TransmissionAcknowledgeTimeout = o.transmissionAcknowledgeTimeout 33 | cfg.TransmissionMaxRetransmit = o.transmissionMaxRetransmit 34 | } 35 | 36 | // WithTransmission set options for (re)transmission for Confirmable message-s. 37 | func WithTransmission(transmissionNStart uint32, 38 | transmissionAcknowledgeTimeout time.Duration, 39 | transmissionMaxRetransmit uint32, 40 | ) TransmissionOpt { 41 | return TransmissionOpt{ 42 | transmissionNStart: transmissionNStart, 43 | transmissionAcknowledgeTimeout: transmissionAcknowledgeTimeout, 44 | transmissionMaxRetransmit: transmissionMaxRetransmit, 45 | } 46 | } 47 | 48 | // MTUOpt transmission options. 49 | type MTUOpt struct { 50 | mtu uint16 51 | } 52 | 53 | func (o MTUOpt) UDPServerApply(cfg *udpServer.Config) { 54 | cfg.MTU = o.mtu 55 | } 56 | 57 | func (o MTUOpt) DTLSServerApply(cfg *dtlsServer.Config) { 58 | cfg.MTU = o.mtu 59 | } 60 | 61 | func (o MTUOpt) UDPClientApply(cfg *udpClient.Config) { 62 | cfg.MTU = o.mtu 63 | } 64 | 65 | // Setup MTU unit 66 | func WithMTU(mtu uint16) MTUOpt { 67 | return MTUOpt{ 68 | mtu: mtu, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /options/udpOptions_test.go: -------------------------------------------------------------------------------- 1 | package options_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | dtlsServer "github.com/plgd-dev/go-coap/v3/dtls/server" 8 | "github.com/plgd-dev/go-coap/v3/options" 9 | "github.com/plgd-dev/go-coap/v3/udp" 10 | "github.com/plgd-dev/go-coap/v3/udp/client" 11 | udpServer "github.com/plgd-dev/go-coap/v3/udp/server" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestUDPServerApply(t *testing.T) { 16 | cfg := udpServer.Config{} 17 | opt := []udpServer.Option{ 18 | options.WithTransmission(10, time.Second, 5), 19 | options.WithMTU(1500), 20 | } 21 | for _, o := range opt { 22 | o.UDPServerApply(&cfg) 23 | } 24 | // WithTransmission 25 | require.Equal(t, uint32(10), cfg.TransmissionNStart) 26 | require.Equal(t, time.Second, cfg.TransmissionAcknowledgeTimeout) 27 | require.Equal(t, uint32(5), cfg.TransmissionMaxRetransmit) 28 | // WithMTU 29 | require.Equal(t, uint16(1500), cfg.MTU) 30 | } 31 | 32 | func TestDTLSServerApply(t *testing.T) { 33 | cfg := dtlsServer.Config{} 34 | opt := []dtlsServer.Option{ 35 | options.WithTransmission(10, time.Second, 5), 36 | options.WithMTU(1500), 37 | } 38 | for _, o := range opt { 39 | o.DTLSServerApply(&cfg) 40 | } 41 | // WithTransmission 42 | require.Equal(t, uint32(10), cfg.TransmissionNStart) 43 | require.Equal(t, time.Second, cfg.TransmissionAcknowledgeTimeout) 44 | require.Equal(t, uint32(5), cfg.TransmissionMaxRetransmit) 45 | // WithMTU 46 | require.Equal(t, uint16(1500), cfg.MTU) 47 | } 48 | 49 | func TestUDPClientApply(t *testing.T) { 50 | cfg := client.Config{} 51 | opt := []udp.Option{ 52 | options.WithTransmission(10, time.Second, 5), 53 | options.WithMTU(1500), 54 | } 55 | for _, o := range opt { 56 | o.UDPClientApply(&cfg) 57 | } 58 | // WithTransmission 59 | require.Equal(t, uint32(10), cfg.TransmissionNStart) 60 | require.Equal(t, time.Second, cfg.TransmissionAcknowledgeTimeout) 61 | require.Equal(t, uint32(5), cfg.TransmissionMaxRetransmit) 62 | // WithMTU 63 | require.Equal(t, uint16(1500), cfg.MTU) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/plgd-dev/go-coap/v3/pkg/sync" 7 | "go.uber.org/atomic" 8 | ) 9 | 10 | func DefaultOnExpire[D any](D) { 11 | // for nothing on expire 12 | } 13 | 14 | type Element[D any] struct { 15 | ValidUntil atomic.Time 16 | data D 17 | onExpire func(d D) 18 | } 19 | 20 | func (e *Element[D]) IsExpired(now time.Time) bool { 21 | value := e.ValidUntil.Load() 22 | if value.IsZero() { 23 | return false 24 | } 25 | return now.After(value) 26 | } 27 | 28 | func (e *Element[D]) Data() D { 29 | return e.data 30 | } 31 | 32 | func NewElement[D any](data D, validUntil time.Time, onExpire func(d D)) *Element[D] { 33 | if onExpire == nil { 34 | onExpire = DefaultOnExpire[D] 35 | } 36 | e := &Element[D]{data: data, onExpire: onExpire} 37 | e.ValidUntil.Store(validUntil) 38 | return e 39 | } 40 | 41 | type Cache[K comparable, D any] struct { 42 | *sync.Map[K, *Element[D]] 43 | } 44 | 45 | func NewCache[K comparable, D any]() *Cache[K, D] { 46 | return &Cache[K, D]{ 47 | Map: sync.NewMap[K, *Element[D]](), 48 | } 49 | } 50 | 51 | func (c *Cache[K, D]) LoadOrStore(key K, e *Element[D]) (actual *Element[D], loaded bool) { 52 | now := time.Now() 53 | c.Map.ReplaceWithFunc(key, func(oldValue *Element[D], oldLoaded bool) (newValue *Element[D], deleteValue bool) { 54 | if oldLoaded { 55 | if !oldValue.IsExpired(now) { 56 | actual = oldValue 57 | return oldValue, false 58 | } 59 | } 60 | actual = e 61 | return e, false 62 | }) 63 | return actual, actual != e 64 | } 65 | 66 | func (c *Cache[K, D]) Load(key K) (actual *Element[D]) { 67 | actual, loaded := c.Map.Load(key) 68 | if !loaded { 69 | return nil 70 | } 71 | if actual.IsExpired(time.Now()) { 72 | return nil 73 | } 74 | return actual 75 | } 76 | 77 | func (c *Cache[K, D]) CheckExpirations(now time.Time) { 78 | c.Range(func(key K, value *Element[D]) bool { 79 | if value.IsExpired(now) { 80 | c.Map.Delete(key) 81 | value.onExpire(value.Data()) 82 | } 83 | return true 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAddElement(t *testing.T) { 11 | cache := NewCache[string, string]() 12 | 13 | elementToCache := string("elem") 14 | elem := NewElement(elementToCache, time.Now().Add(1*time.Minute), nil) 15 | loadedElem, loaded := cache.LoadOrStore("abcd", elem) 16 | 17 | require.False(t, loaded) 18 | require.Equal(t, elementToCache, loadedElem.data) 19 | 20 | elementToCache2 := string("elem2") 21 | elem2 := NewElement(elementToCache2, time.Now().Add(1*time.Minute), nil) 22 | 23 | loadedElem2, loaded2 := cache.LoadOrStore("abcdefg", elem2) 24 | require.False(t, loaded2) 25 | require.Equal(t, elementToCache2, loadedElem2.data) 26 | 27 | elementToCache3 := string("elem") 28 | elem3 := NewElement(elementToCache3, time.Now().Add(1*time.Minute), nil) 29 | 30 | loadedElem, loaded = cache.LoadOrStore("abcd", elem3) 31 | require.True(t, loaded) 32 | require.Equal(t, elementToCache3, loadedElem.data) 33 | } 34 | 35 | func TestLoadElement(t *testing.T) { 36 | cache := NewCache[string, string]() 37 | 38 | loadedElem := cache.Load("abcd") 39 | require.Nil(t, loadedElem) 40 | 41 | elementToCache := string("elem") 42 | elem := NewElement(elementToCache, time.Now().Add(1*time.Minute), nil) 43 | loadedElem, loaded := cache.LoadOrStore("abcd", elem) 44 | 45 | require.False(t, loaded) 46 | require.Equal(t, elementToCache, loadedElem.data) 47 | } 48 | 49 | func TestDeleteElement(t *testing.T) { 50 | cache := NewCache[string, string]() 51 | 52 | loadedElem := cache.Load("abcd") 53 | require.Nil(t, loadedElem) 54 | 55 | elementToCache := string("elem") 56 | elem := NewElement(elementToCache, time.Now().Add(1*time.Minute), nil) 57 | loadedElem, loaded := cache.LoadOrStore("abcd", elem) 58 | 59 | require.False(t, loaded) 60 | require.Equal(t, elementToCache, loadedElem.data) 61 | 62 | loadedElem = cache.Load("abcd") 63 | require.Equal(t, elementToCache, loadedElem.data) 64 | 65 | cache.Delete("abcd") 66 | loadedElem = cache.Load("abcd") 67 | require.Nil(t, loadedElem) 68 | } 69 | 70 | func TestElementExpiration(t *testing.T) { 71 | expirationInvoked := false 72 | cache := NewCache[string, string]() 73 | 74 | elementToCache := string("elem") 75 | elem := NewElement(elementToCache, time.Now().Add(1*time.Second), func(string) { 76 | expirationInvoked = true 77 | }) 78 | loadedElem, _ := cache.LoadOrStore("abcd", elem) 79 | 80 | elementToCache = string("elem") 81 | elem = NewElement(elementToCache, time.Time{}, nil) 82 | cache.LoadOrStore("abcdef", elem) 83 | 84 | require.False(t, expirationInvoked) 85 | require.False(t, loadedElem.IsExpired(time.Now())) 86 | require.True(t, loadedElem.IsExpired(time.Now().Add(2*time.Second))) 87 | require.False(t, expirationInvoked) 88 | cache.CheckExpirations(time.Now().Add(2 * time.Second)) 89 | require.True(t, expirationInvoked) 90 | 91 | require.False(t, elem.IsExpired(time.Now())) 92 | require.False(t, elem.IsExpired(time.Now().Add(time.Hour))) 93 | } 94 | 95 | func TestRangeFunction(t *testing.T) { 96 | cache := NewCache[string, string]() 97 | 98 | loadedElem := cache.Load("abcd") 99 | require.Nil(t, loadedElem) 100 | 101 | elementToCache := string("elem") 102 | elem := NewElement(elementToCache, time.Now().Add(1*time.Minute), nil) 103 | cache.LoadOrStore("abcd", elem) 104 | 105 | elementToCache = string("elem2") 106 | elem = NewElement(elementToCache, time.Now().Add(1*time.Minute), nil) 107 | cache.LoadOrStore("abcdef", elem) 108 | 109 | actualMap := make(map[string]string) 110 | expectedMap := make(map[string]string) 111 | expectedMap["abcd"] = "elem" 112 | expectedMap["abcdef"] = "elem2" 113 | foundElements := 0 114 | 115 | cache.Range(func(key string, value *Element[string]) bool { 116 | actualMap[key] = value.Data() 117 | foundElements++ 118 | return true 119 | }) 120 | 121 | require.Equal(t, 2, foundElements) 122 | 123 | for k := range actualMap { 124 | _, contains := actualMap[k] 125 | require.True(t, contains) 126 | require.Equal(t, expectedMap[k], actualMap[k]) 127 | } 128 | } 129 | 130 | func TestPullOutAllFunction(t *testing.T) { 131 | cache := NewCache[string, string]() 132 | 133 | elementToCache := string("elem") 134 | elem := NewElement(elementToCache, time.Now().Add(1*time.Minute), nil) 135 | cache.LoadOrStore("abcd", elem) 136 | 137 | elementToCache = string("elem2") 138 | elem = NewElement(elementToCache, time.Now().Add(1*time.Minute), nil) 139 | cache.LoadOrStore("abcdef", elem) 140 | 141 | cache.LoadAndDeleteAll() 142 | 143 | foundElements := 0 144 | cache.Range(func(string, *Element[string]) bool { 145 | foundElements++ 146 | return true 147 | }) 148 | require.Equal(t, 0, foundElements) 149 | } 150 | -------------------------------------------------------------------------------- /pkg/connections/connections.go: -------------------------------------------------------------------------------- 1 | package connections 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type Connections struct { 12 | data *sync.Map 13 | } 14 | 15 | func New() *Connections { 16 | return &Connections{ 17 | data: &sync.Map{}, 18 | } 19 | } 20 | 21 | type Connection interface { 22 | Context() context.Context 23 | CheckExpirations(now time.Time) 24 | Close() error 25 | RemoteAddr() net.Addr 26 | } 27 | 28 | func (c *Connections) Store(conn Connection) { 29 | c.data.Store(conn.RemoteAddr().String(), conn) 30 | } 31 | 32 | func (c *Connections) length() int { 33 | var l int 34 | c.data.Range(func(_, _ interface{}) bool { 35 | l++ 36 | return true 37 | }) 38 | return l 39 | } 40 | 41 | func (c *Connections) copyConnections() []Connection { 42 | m := make([]Connection, 0, c.length()) 43 | c.data.Range(func(_, value interface{}) bool { 44 | con, ok := value.(Connection) 45 | if !ok { 46 | panic(fmt.Errorf("invalid type %T in connections map", con)) 47 | } 48 | m = append(m, con) 49 | return true 50 | }) 51 | return m 52 | } 53 | 54 | func (c *Connections) CheckExpirations(now time.Time) { 55 | for _, cc := range c.copyConnections() { 56 | select { 57 | case <-cc.Context().Done(): 58 | continue 59 | default: 60 | cc.CheckExpirations(now) 61 | } 62 | } 63 | } 64 | 65 | func (c *Connections) Close() { 66 | for _, cc := range c.copyConnections() { 67 | _ = cc.Close() 68 | } 69 | } 70 | 71 | func (c *Connections) Delete(conn Connection) { 72 | c.data.Delete(conn.RemoteAddr().String()) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | var ErrKeyAlreadyExists = errors.New("key already exists") 6 | -------------------------------------------------------------------------------- /pkg/fn/funcList.go: -------------------------------------------------------------------------------- 1 | package fn 2 | 3 | type FuncList []func() 4 | 5 | // Return a function that executions all added functions 6 | // 7 | // Functions are executed in reverse order they were added. 8 | func (c FuncList) ToFunction() func() { 9 | return func() { 10 | for i := range c { 11 | c[len(c)-1-i]() 12 | } 13 | } 14 | } 15 | 16 | // Execute all added functions 17 | func (c FuncList) Execute() { 18 | c.ToFunction()() 19 | } 20 | -------------------------------------------------------------------------------- /pkg/fn/funcList_test.go: -------------------------------------------------------------------------------- 1 | package fn_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/plgd-dev/go-coap/v3/pkg/fn" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFuncList(t *testing.T) { 11 | fns := make(fn.FuncList, 0, 2) 12 | 13 | counter := 0 14 | // functions should execute in reverse order they were added in 15 | second := 0 16 | fns = append(fns, func() { 17 | second = counter 18 | counter++ 19 | }) 20 | first := 0 21 | fns = append(fns, func() { 22 | first = counter 23 | counter++ 24 | }) 25 | 26 | fns.Execute() 27 | require.Equal(t, 0, first) 28 | require.Equal(t, 1, second) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/math/cast.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "reflect" 7 | "unsafe" 8 | 9 | "golang.org/x/exp/constraints" 10 | ) 11 | 12 | // Max returns maximal value for given integer type 13 | func Max[T constraints.Integer]() T { 14 | size := unsafe.Sizeof(T(0)) 15 | switch reflect.TypeOf((*T)(nil)).Elem().Kind() { 16 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 17 | return T(1<<(size*8-1) - 1) // 2^(n-1) - 1 for signed integers 18 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 19 | return T(1<<(size*8) - 1) // 2^n - 1 for unsigned integers 20 | default: 21 | panic("unsupported type") 22 | } 23 | } 24 | 25 | // Min returns minimal value for given integer type 26 | func Min[T constraints.Integer]() T { 27 | size := unsafe.Sizeof(T(0)) 28 | switch reflect.TypeOf((*T)(nil)).Elem().Kind() { 29 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 30 | return T(int64(-1) << (size*8 - 1)) // -2^(n-1) 31 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 32 | return T(0) 33 | default: 34 | panic("unsupported type") 35 | } 36 | } 37 | 38 | // CastTo casts one integer type to another with bounds checking and returns error in case of overflow 39 | func SafeCastTo[T, F constraints.Integer](from F) (T, error) { 40 | if from > 0 && uint64(Max[T]()) < uint64(from) { 41 | return T(0), fmt.Errorf("value(%v) exceeds the maximum value for type(%v)", from, Max[T]()) 42 | } 43 | if from < 0 && int64(Min[T]()) > int64(from) { 44 | return T(0), fmt.Errorf("value(%v) exceeds the minimum value for type(%v)", from, Min[T]()) 45 | } 46 | return T(from), nil 47 | } 48 | 49 | // CastTo casts one integer type to another without bounds checking 50 | func CastTo[T, F constraints.Integer](from F) T { 51 | return T(from) 52 | } 53 | 54 | // MustSafeCastTo casts one integer type to another with bounds checking and panics in case of overflow 55 | func MustSafeCastTo[T, F constraints.Integer](from F) T { 56 | to, err := SafeCastTo[T](from) 57 | if err != nil { 58 | log.Panicf("value (%v) out of bounds for type %T", from, T(0)) 59 | } 60 | return to 61 | } 62 | -------------------------------------------------------------------------------- /pkg/math/cast_test.go: -------------------------------------------------------------------------------- 1 | package math_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | pkgMath "github.com/plgd-dev/go-coap/v3/pkg/math" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCastToUint8(t *testing.T) { 12 | // uint8 13 | _, err := pkgMath.SafeCastTo[uint8](uint8(0)) 14 | require.NoError(t, err) 15 | _, err = pkgMath.SafeCastTo[uint8](math.MaxUint8) 16 | require.NoError(t, err) 17 | // int8 18 | _, err = pkgMath.SafeCastTo[uint8](math.MinInt8) 19 | require.Error(t, err) 20 | _, err = pkgMath.SafeCastTo[uint8](int8(0)) 21 | require.NoError(t, err) 22 | _, err = pkgMath.SafeCastTo[uint8](math.MaxInt8) 23 | require.NoError(t, err) 24 | // uint64 25 | _, err = pkgMath.SafeCastTo[uint8](uint64(0)) 26 | require.NoError(t, err) 27 | _, err = pkgMath.SafeCastTo[uint8](uint64(math.MaxUint8)) 28 | require.NoError(t, err) 29 | _, err = pkgMath.SafeCastTo[uint8](uint64(math.MaxUint64)) 30 | require.Error(t, err) 31 | // int64 32 | _, err = pkgMath.SafeCastTo[uint8](math.MaxInt64) 33 | require.Error(t, err) 34 | _, err = pkgMath.SafeCastTo[uint8](int64(0)) 35 | require.NoError(t, err) 36 | _, err = pkgMath.SafeCastTo[uint8](math.MaxInt64) 37 | require.Error(t, err) 38 | _, err = pkgMath.SafeCastTo[uint8](int64(math.MaxUint8)) 39 | require.NoError(t, err) 40 | } 41 | 42 | func TestCastToInt8(t *testing.T) { 43 | // uint8 44 | _, err := pkgMath.SafeCastTo[int8](uint8(0)) 45 | require.NoError(t, err) 46 | _, err = pkgMath.SafeCastTo[int8](math.MaxUint8) 47 | require.Error(t, err) 48 | // int8 49 | _, err = pkgMath.SafeCastTo[int8](math.MinInt8) 50 | require.NoError(t, err) 51 | _, err = pkgMath.SafeCastTo[int8](math.MaxInt8) 52 | require.NoError(t, err) 53 | // uint64 54 | _, err = pkgMath.SafeCastTo[int8](uint64(0)) 55 | require.NoError(t, err) 56 | _, err = pkgMath.SafeCastTo[int8](uint64(math.MaxInt8)) 57 | require.NoError(t, err) 58 | _, err = pkgMath.SafeCastTo[int8](uint64(math.MaxUint64)) 59 | require.Error(t, err) 60 | // int64 61 | _, err = pkgMath.SafeCastTo[int8](math.MaxInt64) 62 | require.Error(t, err) 63 | _, err = pkgMath.SafeCastTo[int8](int64(0)) 64 | require.NoError(t, err) 65 | _, err = pkgMath.SafeCastTo[int8](math.MaxInt64) 66 | require.Error(t, err) 67 | _, err = pkgMath.SafeCastTo[int8](int64(math.MaxInt8)) 68 | require.NoError(t, err) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/rand/rand.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | ) 7 | 8 | type Rand struct { 9 | src *rand.Rand 10 | lock sync.Mutex 11 | } 12 | 13 | func NewRand(seed int64) *Rand { 14 | return &Rand{ 15 | src: rand.New(rand.NewSource(seed)), //nolint:gosec 16 | } 17 | } 18 | 19 | func (l *Rand) Int63() int64 { 20 | l.lock.Lock() 21 | val := l.src.Int63() 22 | l.lock.Unlock() 23 | return val 24 | } 25 | 26 | func (l *Rand) Uint32() uint32 { 27 | l.lock.Lock() 28 | val := l.src.Uint32() 29 | l.lock.Unlock() 30 | return val 31 | } 32 | -------------------------------------------------------------------------------- /pkg/rand/rand_test.go: -------------------------------------------------------------------------------- 1 | package rand_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/plgd-dev/go-coap/v3/pkg/rand" 8 | ) 9 | 10 | func TestRand(*testing.T) { 11 | r := rand.NewRand(0) 12 | _ = r.Int63() 13 | _ = r.Uint32() 14 | } 15 | 16 | func TestMultiThreadedRand(*testing.T) { 17 | r := rand.NewRand(0) 18 | var done sync.WaitGroup 19 | for i := 0; i < 100; i++ { 20 | done.Add(1) 21 | go func(index int) { 22 | if index%2 == 0 { 23 | _ = r.Int63() 24 | } else { 25 | _ = r.Uint32() 26 | } 27 | done.Done() 28 | }(i) 29 | } 30 | done.Wait() 31 | } 32 | -------------------------------------------------------------------------------- /pkg/runner/periodic/periodic.go: -------------------------------------------------------------------------------- 1 | package periodic 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | type Func = func(f func(now time.Time) bool) 10 | 11 | func New(stop <-chan struct{}, tick time.Duration) Func { 12 | var m sync.Map 13 | var idx uint64 14 | go func() { 15 | t := time.NewTicker(tick) 16 | defer t.Stop() 17 | for { 18 | var now time.Time 19 | select { 20 | case now = <-t.C: 21 | case <-stop: 22 | return 23 | } 24 | v := make(map[uint64]func(time.Time) bool) 25 | m.Range(func(key, value interface{}) bool { 26 | v[key.(uint64)] = value.(func(time.Time) bool) //nolint:forcetypeassert 27 | return true 28 | }) 29 | for k, f := range v { 30 | if ok := f(now); !ok { 31 | m.Delete(k) 32 | } 33 | } 34 | } 35 | }() 36 | return func(f func(time.Time) bool) { 37 | if f == nil { 38 | return 39 | } 40 | v := atomic.AddUint64(&idx, 1) 41 | m.Store(v, f) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/sync/map_test.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestLength(t *testing.T) { 10 | m := NewMap[int, string]() 11 | m.Store(1, "1") 12 | require.Equal(t, 1, m.Length()) 13 | m.Store(1, "2") 14 | require.Equal(t, 1, m.Length()) 15 | m.Store(2, "2") 16 | require.Equal(t, 2, m.Length()) 17 | } 18 | 19 | func getTestMapContent() map[int]string { 20 | return map[int]string{ 21 | 1: "one", 22 | 2: "two", 23 | 3: "three", 24 | 4: "four", 25 | 5: "five", 26 | } 27 | } 28 | 29 | func TestCopyData(t *testing.T) { 30 | src := getTestMapContent() 31 | 32 | m := NewMap[int, string]() 33 | for k, v := range src { 34 | m.StoreWithFunc(k, func() string { 35 | return v 36 | }) 37 | } 38 | for k, v := range src { 39 | value, ok := m.Load(k) 40 | require.True(t, ok) 41 | require.Equal(t, v, value) 42 | } 43 | 44 | c := m.CopyData() 45 | require.Equal(t, src, c) 46 | } 47 | 48 | func TestLoad(t *testing.T) { 49 | src := getTestMapContent() 50 | 51 | m := NewMap[int, string]() 52 | for k, v := range src { 53 | m.Store(k, v) 54 | } 55 | for k, v := range src { 56 | value, ok := m.Load(k) 57 | require.True(t, ok) 58 | require.Equal(t, v, value) 59 | } 60 | 61 | value, ok := m.LoadWithFunc(1, func(v string) string { 62 | return "prefix" + v 63 | }) 64 | require.True(t, ok) 65 | require.Equal(t, "prefix"+src[1], value) 66 | } 67 | 68 | func TestLoadOrStore(t *testing.T) { 69 | src := getTestMapContent() 70 | 71 | m := NewMap[int, string]() 72 | for k, v := range src { 73 | value, loaded := m.LoadOrStore(k, v) 74 | require.False(t, loaded) 75 | require.Equal(t, v, value) 76 | } 77 | 78 | newV := "forty two" 79 | v1, l1 := m.LoadOrStoreWithFunc(42, func(string) string { 80 | require.FailNow(t, "unexpected load call") 81 | return "" 82 | }, func() string { 83 | return newV 84 | }) 85 | require.False(t, l1) 86 | require.Equal(t, newV, v1) 87 | 88 | for k, v := range src { 89 | vr, lr := m.LoadOrStoreWithFunc(k, func(value string) string { 90 | return "prefix-" + value 91 | }, func() string { 92 | require.FailNow(t, "unexpected create call") 93 | return v 94 | }) 95 | require.True(t, lr) 96 | require.Equal(t, "prefix-"+v, vr) 97 | } 98 | 99 | value, loaded := m.LoadOrStore(1, "first") 100 | require.True(t, loaded) 101 | require.NotEqual(t, "first", value) 102 | } 103 | 104 | func testRange(t *testing.T, m *Map[int, string], rangeFn func(f func(key int, value string) bool)) { 105 | src := getTestMapContent() 106 | for k, v := range src { 107 | m.Store(k, v) 108 | } 109 | 110 | count := 0 111 | rangeFn(func(int, string) bool { 112 | count++ 113 | return false 114 | }) 115 | require.Equal(t, 1, count) 116 | 117 | rangeFn(func(key int, value string) bool { 118 | require.Equal(t, src[key], value) 119 | return true 120 | }) 121 | } 122 | 123 | func TestRange(t *testing.T) { 124 | m := NewMap[int, string]() 125 | testRange(t, m, m.Range) 126 | } 127 | 128 | func TestRange2(t *testing.T) { 129 | m := NewMap[int, string]() 130 | testRange(t, m, m.Range2) 131 | } 132 | 133 | func TestReplace(t *testing.T) { 134 | src := getTestMapContent() 135 | m := NewMap[int, string]() 136 | for k, v := range src { 137 | m.Store(k, v) 138 | } 139 | 140 | newV := "first" 141 | oldV, ok := m.Replace(1, newV) 142 | require.True(t, ok) 143 | require.Equal(t, src[1], oldV) 144 | 145 | oldV, ok = m.ReplaceWithFunc(1, func(string, bool) (string, bool) { 146 | return "", true 147 | }) 148 | require.True(t, ok) 149 | require.Equal(t, newV, oldV) 150 | require.Equal(t, len(src)-1, m.Length()) 151 | 152 | newV = "forty two" 153 | _, ok = m.ReplaceWithFunc(42, func(string, bool) (string, bool) { 154 | return newV, false 155 | }) 156 | require.False(t, ok) 157 | require.Equal(t, len(src), m.Length()) 158 | v, ok := m.Load(42) 159 | require.True(t, ok) 160 | require.Equal(t, newV, v) 161 | } 162 | 163 | func TestDelete(t *testing.T) { 164 | src := getTestMapContent() 165 | m := NewMap[int, string]() 166 | for k, v := range src { 167 | m.Store(k, v) 168 | } 169 | 170 | m.Delete(42) 171 | require.Equal(t, len(src), m.Length()) 172 | 173 | m.Delete(1) 174 | require.Equal(t, len(src)-1, m.Length()) 175 | _, ok := m.Load(1) 176 | require.False(t, ok) 177 | 178 | m.DeleteWithFunc(1, func(string) { 179 | require.FailNow(t, "unexpected deleted value") 180 | }) 181 | 182 | m.DeleteWithFunc(3, func(value string) { 183 | require.Equal(t, src[3], value) 184 | }) 185 | require.Equal(t, len(src)-2, m.Length()) 186 | } 187 | 188 | func TestLoadAndDelete(t *testing.T) { 189 | src := getTestMapContent() 190 | m := NewMap[int, string]() 191 | for k, v := range src { 192 | m.Store(k, v) 193 | } 194 | 195 | _, ok := m.LoadAndDelete(42) 196 | require.False(t, ok) 197 | 198 | v, ok := m.LoadAndDelete(2) 199 | require.True(t, ok) 200 | require.Equal(t, src[2], v) 201 | delete(src, 2) 202 | 203 | _, ok = m.LoadAndDeleteWithFunc(2, func(value string) string { 204 | require.FailNow(t, "unexpected pulled value") 205 | return value 206 | }) 207 | require.False(t, ok) 208 | 209 | v, ok = m.LoadAndDeleteWithFunc(1, func(value string) string { 210 | return value + "-suffix" 211 | }) 212 | require.True(t, ok) 213 | require.Equal(t, src[1]+"-suffix", v) 214 | delete(src, 1) 215 | 216 | data := m.LoadAndDeleteAll() 217 | require.Equal(t, 0, m.Length()) 218 | require.Equal(t, src, data) 219 | } 220 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "postUpdateOptions": [ 6 | "gomodTidy" 7 | ], 8 | "commitBody": "Generated by renovateBot", 9 | "packageRules": [ 10 | { 11 | "matchPackagePatterns": [ 12 | "*" 13 | ], 14 | "schedule": [ 15 | "on the first day of the month" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Package coap provides a CoAP client and server. 2 | package coap 3 | 4 | import ( 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | 9 | piondtls "github.com/pion/dtls/v3" 10 | "github.com/plgd-dev/go-coap/v3/dtls" 11 | dtlsServer "github.com/plgd-dev/go-coap/v3/dtls/server" 12 | "github.com/plgd-dev/go-coap/v3/mux" 13 | "github.com/plgd-dev/go-coap/v3/net" 14 | "github.com/plgd-dev/go-coap/v3/options" 15 | "github.com/plgd-dev/go-coap/v3/tcp" 16 | tcpServer "github.com/plgd-dev/go-coap/v3/tcp/server" 17 | "github.com/plgd-dev/go-coap/v3/udp" 18 | udpServer "github.com/plgd-dev/go-coap/v3/udp/server" 19 | ) 20 | 21 | // ListenAndServe Starts a server on address and network specified Invoke handler 22 | // for incoming queries. 23 | func ListenAndServe(network string, addr string, handler mux.Handler) (err error) { 24 | switch network { 25 | case "udp", "udp4", "udp6", "": 26 | l, err := net.NewListenUDP(network, addr) 27 | if err != nil { 28 | return err 29 | } 30 | defer func() { 31 | if errC := l.Close(); errC != nil && err == nil { 32 | err = errC 33 | } 34 | }() 35 | s := udp.NewServer(options.WithMux(handler)) 36 | return s.Serve(l) 37 | case "tcp", "tcp4", "tcp6": 38 | l, err := net.NewTCPListener(network, addr) 39 | if err != nil { 40 | return err 41 | } 42 | defer func() { 43 | if errC := l.Close(); errC != nil && err == nil { 44 | err = errC 45 | } 46 | }() 47 | s := tcp.NewServer(options.WithMux(handler)) 48 | return s.Serve(l) 49 | default: 50 | return fmt.Errorf("invalid network (%v)", network) 51 | } 52 | } 53 | 54 | // ListenAndServeTCPTLS Starts a server on address and network over TLS specified Invoke handler 55 | // for incoming queries. 56 | func ListenAndServeTCPTLS(network, addr string, config *tls.Config, handler mux.Handler) (err error) { 57 | l, err := net.NewTLSListener(network, addr, config) 58 | if err != nil { 59 | return err 60 | } 61 | defer func() { 62 | if errC := l.Close(); errC != nil && err == nil { 63 | err = errC 64 | } 65 | }() 66 | s := tcp.NewServer(options.WithMux(handler)) 67 | return s.Serve(l) 68 | } 69 | 70 | // ListenAndServeDTLS Starts a server on address and network over DTLS specified Invoke handler 71 | // for incoming queries. 72 | func ListenAndServeDTLS(network string, addr string, config *piondtls.Config, handler mux.Handler) (err error) { 73 | l, err := net.NewDTLSListener(network, addr, config) 74 | if err != nil { 75 | return err 76 | } 77 | defer func() { 78 | if errC := l.Close(); errC != nil && err == nil { 79 | err = errC 80 | } 81 | }() 82 | s := dtls.NewServer(options.WithMux(handler)) 83 | return s.Serve(l) 84 | } 85 | 86 | // ListenAndServeWithOption Starts a server on address and network specified Invoke options 87 | // for incoming queries. The options is only support tcpServer.Option and udpServer.Option 88 | func ListenAndServeWithOptions(network, addr string, opts ...any) (err error) { 89 | tcpOptions := []tcpServer.Option{} 90 | udpOptions := []udpServer.Option{} 91 | for _, opt := range opts { 92 | switch o := opt.(type) { 93 | case tcpServer.Option: 94 | tcpOptions = append(tcpOptions, o) 95 | 96 | // Duplicate the option for UDP if needed. 97 | if udpOpt, ok := o.(udpServer.Option); ok { 98 | udpOptions = append(udpOptions, udpOpt) 99 | } 100 | case udpServer.Option: 101 | udpOptions = append(udpOptions, o) 102 | default: 103 | return errors.New("only support tcpServer.Option and udpServer.Option") 104 | } 105 | } 106 | 107 | switch network { 108 | case "udp", "udp4", "udp6", "": 109 | l, err := net.NewListenUDP(network, addr) 110 | if err != nil { 111 | return err 112 | } 113 | defer func() { 114 | if errC := l.Close(); errC != nil && err == nil { 115 | err = errC 116 | } 117 | }() 118 | s := udp.NewServer(udpOptions...) 119 | return s.Serve(l) 120 | case "tcp", "tcp4", "tcp6": 121 | l, err := net.NewTCPListener(network, addr) 122 | if err != nil { 123 | return err 124 | } 125 | defer func() { 126 | if errC := l.Close(); errC != nil && err == nil { 127 | err = errC 128 | } 129 | }() 130 | s := tcp.NewServer(tcpOptions...) 131 | return s.Serve(l) 132 | default: 133 | return fmt.Errorf("invalid network (%v)", network) 134 | } 135 | } 136 | 137 | // ListenAndServeTCPTLSWithOptions Starts a server on address and network over TLS specified Invoke options 138 | // for incoming queries. 139 | func ListenAndServeTCPTLSWithOptions(network, addr string, config *tls.Config, opts ...tcpServer.Option) (err error) { 140 | l, err := net.NewTLSListener(network, addr, config) 141 | if err != nil { 142 | return err 143 | } 144 | defer func() { 145 | if errC := l.Close(); errC != nil && err == nil { 146 | err = errC 147 | } 148 | }() 149 | s := tcp.NewServer(opts...) 150 | return s.Serve(l) 151 | } 152 | 153 | // ListenAndServeDTLSWithOptions Starts a server on address and network over DTLS specified Invoke options 154 | // for incoming queries. 155 | func ListenAndServeDTLSWithOptions(network string, addr string, config *piondtls.Config, opts ...dtlsServer.Option) (err error) { 156 | l, err := net.NewDTLSListener(network, addr, config) 157 | if err != nil { 158 | return err 159 | } 160 | defer func() { 161 | if errC := l.Close(); errC != nil && err == nil { 162 | err = errC 163 | } 164 | }() 165 | s := dtls.NewServer(opts...) 166 | return s.Serve(l) 167 | } 168 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=plgd-dev_go-coap 2 | sonar.organization=plgd-dev 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=hub 6 | #sonar.projectVersion=1.0 7 | 8 | #sonar.log.level=DEBUG 9 | #sonar.verbose=true 10 | 11 | sonar.python.version=3.8 12 | 13 | sonar.sources=. 14 | sonar.exclusions=**/*_test.go,**/*.pb.go,**/*.pb.gw.go,**/options.go,**/main.go,v3/** 15 | 16 | sonar.tests=. 17 | sonar.test.inclusions=**/*_test.go 18 | sonar.test.exclusions= 19 | 20 | #wildcard do not work for tests.reportPaths 21 | #sonar.go.tests.reportPaths=.tmp/report/certificate-authority.report.json,.tmp/report/cloud2cloud-connector.report.json,.tmp/report/cloud2cloud-gateway.report.json,.tmp/report/coap-gateway.report.json,.tmp/report/grpc-gateway.report.json,.tmp/report/http-gateway.report.json,.tmp/report/identity-store.report.json,.tmp/report/resource-aggregate.report.json,.tmp/report/resource-directory.report.json 22 | 23 | sonar.go.coverage.reportPaths=.tmp/coverage/*.coverage.txt 24 | sonar.coverage.exclusions=examples/**,**/main.go,**/*.pb.go,**/*.pb.gw.go,**/*.js,**/*.py,**/*_test.go 25 | -------------------------------------------------------------------------------- /tcp/client.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/plgd-dev/go-coap/v3/message" 10 | "github.com/plgd-dev/go-coap/v3/message/pool" 11 | coapNet "github.com/plgd-dev/go-coap/v3/net" 12 | "github.com/plgd-dev/go-coap/v3/net/blockwise" 13 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 14 | "github.com/plgd-dev/go-coap/v3/options" 15 | client "github.com/plgd-dev/go-coap/v3/tcp/client" 16 | ) 17 | 18 | // A Option sets options such as credentials, keepalive parameters, etc. 19 | type Option interface { 20 | TCPClientApply(cfg *client.Config) 21 | } 22 | 23 | // Dial creates a client connection to the given target. 24 | func Dial(target string, opts ...Option) (*client.Conn, error) { 25 | cfg := client.DefaultConfig 26 | for _, o := range opts { 27 | o.TCPClientApply(&cfg) 28 | } 29 | 30 | var conn net.Conn 31 | var err error 32 | if cfg.TLSCfg != nil { 33 | conn, err = tls.DialWithDialer(cfg.Dialer, cfg.Net, target, cfg.TLSCfg) 34 | } else { 35 | conn, err = cfg.Dialer.DialContext(cfg.Ctx, cfg.Net, target) 36 | } 37 | if err != nil { 38 | return nil, err 39 | } 40 | opts = append(opts, options.WithCloseSocket()) 41 | return Client(conn, opts...), nil 42 | } 43 | 44 | // Client creates client over tcp/tcp-tls connection. 45 | func Client(conn net.Conn, opts ...Option) *client.Conn { 46 | cfg := client.DefaultConfig 47 | for _, o := range opts { 48 | o.TCPClientApply(&cfg) 49 | } 50 | if cfg.Errors == nil { 51 | cfg.Errors = func(error) { 52 | // default no-op 53 | } 54 | } 55 | if cfg.CreateInactivityMonitor == nil { 56 | cfg.CreateInactivityMonitor = func() client.InactivityMonitor { 57 | return inactivity.NewNilMonitor[*client.Conn]() 58 | } 59 | } 60 | if cfg.MessagePool == nil { 61 | cfg.MessagePool = pool.New(0, 0) 62 | } 63 | errorsFunc := cfg.Errors 64 | cfg.Errors = func(err error) { 65 | if coapNet.IsCancelOrCloseError(err) { 66 | // this error was produced by cancellation context or closing connection. 67 | return 68 | } 69 | errorsFunc(fmt.Errorf("tcp: %w", err)) 70 | } 71 | 72 | createBlockWise := func(*client.Conn) *blockwise.BlockWise[*client.Conn] { 73 | return nil 74 | } 75 | if cfg.BlockwiseEnable { 76 | createBlockWise = func(cc *client.Conn) *blockwise.BlockWise[*client.Conn] { 77 | v := cc 78 | return blockwise.New( 79 | v, 80 | cfg.BlockwiseTransferTimeout, 81 | cfg.Errors, 82 | func(token message.Token) (*pool.Message, bool) { 83 | return v.GetObservationRequest(token) 84 | }, 85 | ) 86 | } 87 | } 88 | 89 | l := coapNet.NewConn(conn) 90 | monitor := cfg.CreateInactivityMonitor() 91 | cc := client.NewConnWithOpts(l, 92 | &cfg, 93 | client.WithBlockWise(createBlockWise), 94 | client.WithInactivityMonitor(monitor), 95 | client.WithRequestMonitor(cfg.RequestMonitor), 96 | ) 97 | 98 | cfg.PeriodicRunner(func(now time.Time) bool { 99 | cc.CheckExpirations(now) 100 | return cc.Context().Err() == nil 101 | }) 102 | 103 | go func() { 104 | err := cc.Run() 105 | if err != nil { 106 | cfg.Errors(fmt.Errorf("%v: %w", cc.RemoteAddr(), err)) 107 | } 108 | }() 109 | 110 | return cc 111 | } 112 | -------------------------------------------------------------------------------- /tcp/client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/plgd-dev/go-coap/v3/message" 10 | "github.com/plgd-dev/go-coap/v3/message/codes" 11 | "github.com/plgd-dev/go-coap/v3/message/pool" 12 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 13 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 14 | "github.com/plgd-dev/go-coap/v3/options/config" 15 | ) 16 | 17 | var DefaultConfig = func() Config { 18 | opts := Config{ 19 | Common: config.NewCommon[*Conn](), 20 | CreateInactivityMonitor: func() InactivityMonitor { 21 | return inactivity.NewNilMonitor[*Conn]() 22 | }, 23 | RequestMonitor: func(*Conn, *pool.Message) (bool, error) { 24 | return false, nil 25 | }, 26 | Dialer: &net.Dialer{Timeout: time.Second * 3}, 27 | Net: "tcp", 28 | ConnectionCacheSize: 2048, 29 | } 30 | opts.Handler = func(w *responsewriter.ResponseWriter[*Conn], r *pool.Message) { 31 | switch r.Code() { 32 | case codes.POST, codes.PUT, codes.GET, codes.DELETE: 33 | if err := w.SetResponse(codes.NotFound, message.TextPlain, nil); err != nil { 34 | opts.Errors(fmt.Errorf("client handler: cannot set response: %w", err)) 35 | } 36 | } 37 | } 38 | return opts 39 | }() 40 | 41 | type Config struct { 42 | config.Common[*Conn] 43 | CreateInactivityMonitor CreateInactivityMonitorFunc 44 | RequestMonitor RequestMonitorFunc 45 | Net string 46 | Dialer *net.Dialer 47 | TLSCfg *tls.Config 48 | Handler HandlerFunc 49 | ConnectionCacheSize uint16 50 | DisablePeerTCPSignalMessageCSMs bool 51 | CloseSocket bool 52 | DisableTCPSignalMessageCSM bool 53 | } 54 | -------------------------------------------------------------------------------- /tcp/coder/bench_test.go: -------------------------------------------------------------------------------- 1 | package coder 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message" 7 | "github.com/plgd-dev/go-coap/v3/message/codes" 8 | ) 9 | 10 | func BenchmarkMarshalMessage(b *testing.B) { 11 | options := make(message.Options, 0, 32) 12 | bufOptions := make([]byte, 1024) 13 | bufOptionsUsed := bufOptions 14 | 15 | var enc int 16 | options, enc, _ = options.SetPath(bufOptionsUsed, "/a/b/c/d/e") 17 | bufOptionsUsed = bufOptionsUsed[enc:] 18 | 19 | options, _, _ = options.SetContentFormat(bufOptionsUsed, message.TextPlain) 20 | msg := message.Message{ 21 | Code: codes.GET, 22 | Payload: []byte{0x1}, 23 | Token: []byte{0x1, 0x2, 0x3}, 24 | Options: options, 25 | } 26 | buffer := make([]byte, 1024) 27 | 28 | b.ResetTimer() 29 | for i := uint32(0); i < uint32(b.N); i++ { 30 | _, err := DefaultCoder.Encode(msg, buffer) 31 | if err != nil { 32 | b.Fatalf("cannot marshal") 33 | } 34 | } 35 | } 36 | 37 | func BenchmarkUnmarshalMessage(b *testing.B) { 38 | buffer := []byte{211, 0, 1, 1, 2, 3, 177, 97, 1, 98, 1, 99, 1, 100, 1, 101, 16, 255, 1} 39 | options := make(message.Options, 0, 32) 40 | msg := message.Message{ 41 | Options: options, 42 | } 43 | 44 | b.ResetTimer() 45 | for i := uint32(0); i < uint32(b.N); i++ { 46 | msg.Options = options 47 | _, err := DefaultCoder.Decode(buffer, &msg) 48 | if err != nil { 49 | b.Fatalf("cannot unmarshal: %v", err) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tcp/coder/coder_test.go: -------------------------------------------------------------------------------- 1 | package coder 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message" 7 | "github.com/plgd-dev/go-coap/v3/message/codes" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func testMarshalMessage(t *testing.T, msg message.Message, buf []byte, expectedOut []byte) { 12 | length, err := DefaultCoder.Encode(msg, buf) 13 | require.NoError(t, err) 14 | buf = buf[:length] 15 | require.Equal(t, expectedOut, buf) 16 | } 17 | 18 | func testUnmarshalMessage(t *testing.T, msg message.Message, buf []byte, expectedOut message.Message) { 19 | _, err := DefaultCoder.Decode(buf, &msg) 20 | require.NoError(t, err) 21 | require.Equal(t, expectedOut, msg) 22 | } 23 | 24 | func TestMarshalMessage(t *testing.T) { 25 | buf := make([]byte, 1024) 26 | testMarshalMessage(t, message.Message{}, buf, []byte{0, 0}) 27 | testMarshalMessage(t, message.Message{Code: codes.GET}, buf, []byte{0, byte(codes.GET)}) 28 | testMarshalMessage(t, message.Message{Code: codes.GET, Payload: []byte{0x1}}, buf, []byte{32, byte(codes.GET), 0xff, 0x1}) 29 | testMarshalMessage(t, message.Message{Code: codes.GET, Payload: []byte{0x1}, Token: []byte{0x1, 0x2, 0x3}}, buf, []byte{35, byte(codes.GET), 0x1, 0x2, 0x3, 0xff, 0x1}) 30 | bufOptions := make([]byte, 1024) 31 | bufOptionsUsed := bufOptions 32 | options := make(message.Options, 0, 32) 33 | 34 | options, enc, err := options.SetPath(bufOptionsUsed, "/a/b/c/d/e") 35 | if err != nil { 36 | t.Fatalf("Cannot set uri") 37 | } 38 | bufOptionsUsed = bufOptionsUsed[enc:] 39 | options, _, err = options.SetContentFormat(bufOptionsUsed, message.TextPlain) 40 | if err != nil { 41 | t.Fatalf("Cannot set content format") 42 | } 43 | 44 | testMarshalMessage(t, message.Message{ 45 | Code: codes.GET, 46 | Payload: []byte{0x1}, 47 | Token: []byte{0x1, 0x2, 0x3}, 48 | Options: options, 49 | }, buf, []byte{211, 0, 1, 1, 2, 3, 177, 97, 1, 98, 1, 99, 1, 100, 1, 101, 16, 255, 1}) 50 | } 51 | 52 | func TestUnmarshalMessage(t *testing.T) { 53 | testUnmarshalMessage(t, message.Message{}, []byte{0, 0}, message.Message{}) 54 | testUnmarshalMessage(t, message.Message{}, []byte{0, byte(codes.GET)}, message.Message{Code: codes.GET}) 55 | testUnmarshalMessage(t, message.Message{}, []byte{32, byte(codes.GET), 0xff, 0x1}, message.Message{Code: codes.GET, Payload: []byte{0x1}}) 56 | testUnmarshalMessage(t, message.Message{}, []byte{35, byte(codes.GET), 0x1, 0x2, 0x3, 0xff, 0x1}, message.Message{Code: codes.GET, Payload: []byte{0x1}, Token: []byte{0x1, 0x2, 0x3}}) 57 | testUnmarshalMessage(t, message.Message{Options: make(message.Options, 0, 32)}, []byte{211, 0, 1, 1, 2, 3, 177, 97, 1, 98, 1, 99, 1, 100, 1, 101, 16, 255, 1}, message.Message{ 58 | Code: codes.GET, 59 | Payload: []byte{0x1}, 60 | Token: []byte{0x1, 0x2, 0x3}, 61 | Options: []message.Option{{ID: 11, Value: []byte{97}}, {ID: 11, Value: []byte{98}}, {ID: 11, Value: []byte{99}}, {ID: 11, Value: []byte{100}}, {ID: 11, Value: []byte{101}}, {ID: 12, Value: []byte{}}}, 62 | }) 63 | } 64 | 65 | func FuzzDecode(f *testing.F) { 66 | f.Add([]byte{211, 0, 1, 1, 2, 3, 177, 97, 1, 98, 1, 99, 1, 100, 1, 101, 16, 255, 1}) 67 | 68 | f.Fuzz(func(_ *testing.T, input_data []byte) { 69 | msg := message.Message{Options: make(message.Options, 0, 32)} 70 | _, _ = DefaultCoder.Decode(input_data, &msg) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /tcp/coder/error.go: -------------------------------------------------------------------------------- 1 | package coder 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrMessageTruncated = errors.New("message is truncated") 7 | ErrMessageInvalidVersion = errors.New("message has invalid version") 8 | ) 9 | -------------------------------------------------------------------------------- /tcp/example_test.go: -------------------------------------------------------------------------------- 1 | package tcp_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "time" 9 | 10 | "github.com/plgd-dev/go-coap/v3/net" 11 | "github.com/plgd-dev/go-coap/v3/tcp" 12 | ) 13 | 14 | func ExampleConn_Get() { 15 | conn, err := tcp.Dial("try.plgd.cloud:5683") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer conn.Close() 20 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 21 | defer cancel() 22 | res, err := conn.Get(ctx, "/oic/res") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | data, err := io.ReadAll(res.Body()) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | fmt.Printf("%v", data) 31 | } 32 | 33 | func ExampleServer() { 34 | l, err := net.NewTCPListener("tcp", "0.0.0.0:5683") 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | defer l.Close() 39 | s := tcp.NewServer() 40 | defer s.Stop() 41 | log.Fatal(s.Serve(l)) 42 | } 43 | -------------------------------------------------------------------------------- /tcp/server.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "github.com/plgd-dev/go-coap/v3/tcp/server" 5 | ) 6 | 7 | func NewServer(opt ...server.Option) *server.Server { 8 | return server.New(opt...) 9 | } 10 | -------------------------------------------------------------------------------- /tcp/server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/plgd-dev/go-coap/v3/message" 8 | "github.com/plgd-dev/go-coap/v3/message/codes" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 11 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 12 | "github.com/plgd-dev/go-coap/v3/options/config" 13 | "github.com/plgd-dev/go-coap/v3/tcp/client" 14 | ) 15 | 16 | // The HandlerFunc type is an adapter to allow the use of 17 | // ordinary functions as COAP handlers. 18 | type HandlerFunc = func(*responsewriter.ResponseWriter[*client.Conn], *pool.Message) 19 | 20 | type ErrorFunc = func(error) 21 | 22 | type GoPoolFunc = func(func()) error 23 | 24 | // OnNewConnFunc is the callback for new connections. 25 | type OnNewConnFunc = func(*client.Conn) 26 | 27 | var DefaultConfig = func() Config { 28 | opts := Config{ 29 | Common: config.NewCommon[*client.Conn](), 30 | CreateInactivityMonitor: func() client.InactivityMonitor { 31 | maxRetries := uint32(2) 32 | timeout := time.Second * 16 33 | onInactive := func(cc *client.Conn) { 34 | _ = cc.Close() 35 | } 36 | keepalive := inactivity.NewKeepAlive(maxRetries, onInactive, func(cc *client.Conn, receivePong func()) (func(), error) { 37 | return cc.AsyncPing(receivePong) 38 | }) 39 | return inactivity.New(timeout/time.Duration(maxRetries+1), keepalive.OnInactive) 40 | }, 41 | OnNewConn: func(*client.Conn) { 42 | // do nothing by default 43 | }, 44 | RequestMonitor: func(*client.Conn, *pool.Message) (bool, error) { 45 | return false, nil 46 | }, 47 | ConnectionCacheSize: 2 * 1024, 48 | } 49 | opts.Handler = func(w *responsewriter.ResponseWriter[*client.Conn], _ *pool.Message) { 50 | if err := w.SetResponse(codes.NotFound, message.TextPlain, nil); err != nil { 51 | opts.Errors(fmt.Errorf("server handler: cannot set response: %w", err)) 52 | } 53 | } 54 | return opts 55 | }() 56 | 57 | type Config struct { 58 | config.Common[*client.Conn] 59 | CreateInactivityMonitor client.CreateInactivityMonitorFunc 60 | Handler HandlerFunc 61 | OnNewConn OnNewConnFunc 62 | RequestMonitor client.RequestMonitorFunc 63 | ConnectionCacheSize uint16 64 | DisablePeerTCPSignalMessageCSMs bool 65 | DisableTCPSignalMessageCSM bool 66 | } 67 | -------------------------------------------------------------------------------- /test/net/uri.go: -------------------------------------------------------------------------------- 1 | // Helper package for tests, must not be used in production code. 2 | package net 3 | 4 | import ( 5 | "regexp" 6 | "strings" 7 | "time" 8 | 9 | pkgRand "github.com/plgd-dev/go-coap/v3/pkg/rand" 10 | ) 11 | 12 | var weakRng = pkgRand.NewRand(time.Now().UnixNano()) 13 | 14 | const ( 15 | // 71 allowed letters in URL path segment 16 | urlLetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~!$&'()*+,;=:@" 17 | 18 | urlLetterIdxBits = 7 // we need 7 bits to represent a letter index (0..70) 19 | urlLetterIdxMask = 1< 0 { 26 | b[0] = '/' 27 | } 28 | for i := 1; i < n; { 29 | if idx := int(weakRng.Int63() & urlLetterIdxMask); idx < len(urlLetterBytes) { 30 | b[i] = urlLetterBytes[idx] 31 | i++ 32 | } 33 | } 34 | return string(b) 35 | } 36 | 37 | // RandomValidURLString generate URL path of length n where the delimiter '/' occurs 38 | // at least every maxSegmentLen characters. 39 | func RandomValidURLString(n, maxSegmentLen int) string { 40 | b := make([]byte, n) 41 | if n > 0 { 42 | b[0] = '/' 43 | } 44 | for i := 1; i < n; { 45 | if idx := int(weakRng.Int63() & urlLetterIdxMask); idx < len(urlLetterBytes) { 46 | b[i] = urlLetterBytes[idx] 47 | i++ 48 | } 49 | } 50 | 51 | // ensure that at least every maxSegmentLen-th character is '/', otherwise 52 | // SetPath will fail with invalid path error 53 | index := 0 54 | for { 55 | remainder := n - index 56 | if remainder < maxSegmentLen { 57 | break 58 | } 59 | shift := uint8(weakRng.Int63() >> 55) 60 | index = index + int(shift) + 1 61 | if index >= n { 62 | index = n - 1 63 | } 64 | b[index] = '/' 65 | } 66 | return string(b) 67 | } 68 | 69 | // NormalizeURLPath replace repeated '/' characters with a single '/' character 70 | // and remove ending '/'. 71 | func NormalizeURLPath(s string) string { 72 | if len(s) == 0 { 73 | return s 74 | } 75 | space := regexp.MustCompile("/+") 76 | return strings.TrimSuffix(space.ReplaceAllString(s, "/"), "/") 77 | } 78 | -------------------------------------------------------------------------------- /udp/client.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/plgd-dev/go-coap/v3/message" 10 | "github.com/plgd-dev/go-coap/v3/message/pool" 11 | coapNet "github.com/plgd-dev/go-coap/v3/net" 12 | "github.com/plgd-dev/go-coap/v3/net/blockwise" 13 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 14 | "github.com/plgd-dev/go-coap/v3/options" 15 | "github.com/plgd-dev/go-coap/v3/udp/client" 16 | "github.com/plgd-dev/go-coap/v3/udp/server" 17 | ) 18 | 19 | // A Option sets options such as credentials, keepalive parameters, etc. 20 | type Option interface { 21 | UDPClientApply(cfg *client.Config) 22 | } 23 | 24 | // Dial creates a client connection to the given target. 25 | func Dial(target string, opts ...Option) (*client.Conn, error) { 26 | cfg := client.DefaultConfig 27 | for _, o := range opts { 28 | o.UDPClientApply(&cfg) 29 | } 30 | c, err := cfg.Dialer.DialContext(cfg.Ctx, cfg.Net, target) 31 | if err != nil { 32 | return nil, err 33 | } 34 | conn, ok := c.(*net.UDPConn) 35 | if !ok { 36 | return nil, fmt.Errorf("unsupported connection type: %T", c) 37 | } 38 | opts = append(opts, options.WithCloseSocket()) 39 | return Client(conn, opts...), nil 40 | } 41 | 42 | // Client creates client over udp connection. 43 | func Client(conn *net.UDPConn, opts ...Option) *client.Conn { 44 | cfg := client.DefaultConfig 45 | for _, o := range opts { 46 | o.UDPClientApply(&cfg) 47 | } 48 | if cfg.Errors == nil { 49 | cfg.Errors = func(error) { 50 | // default no-op 51 | } 52 | } 53 | if cfg.CreateInactivityMonitor == nil { 54 | cfg.CreateInactivityMonitor = func() client.InactivityMonitor { 55 | return inactivity.NewNilMonitor[*client.Conn]() 56 | } 57 | } 58 | if cfg.MessagePool == nil { 59 | cfg.MessagePool = pool.New(0, 0) 60 | } 61 | 62 | errorsFunc := cfg.Errors 63 | cfg.Errors = func(err error) { 64 | if coapNet.IsCancelOrCloseError(err) { 65 | // this error was produced by cancellation context or closing connection. 66 | return 67 | } 68 | errorsFunc(fmt.Errorf("udp: %v: %w", conn.RemoteAddr(), err)) 69 | } 70 | addr, _ := conn.RemoteAddr().(*net.UDPAddr) 71 | createBlockWise := func(*client.Conn) *blockwise.BlockWise[*client.Conn] { 72 | return nil 73 | } 74 | if cfg.BlockwiseEnable { 75 | createBlockWise = func(cc *client.Conn) *blockwise.BlockWise[*client.Conn] { 76 | v := cc 77 | return blockwise.New( 78 | v, 79 | cfg.BlockwiseTransferTimeout, 80 | cfg.Errors, 81 | func(token message.Token) (*pool.Message, bool) { 82 | return v.GetObservationRequest(token) 83 | }, 84 | ) 85 | } 86 | } 87 | 88 | monitor := cfg.CreateInactivityMonitor() 89 | l := coapNet.NewUDPConn(cfg.Net, conn, coapNet.WithErrors(cfg.Errors)) 90 | session := server.NewSession(cfg.Ctx, 91 | context.Background(), 92 | l, 93 | addr, 94 | cfg.MaxMessageSize, 95 | cfg.MTU, 96 | cfg.CloseSocket, 97 | ) 98 | cc := client.NewConnWithOpts(session, &cfg, 99 | client.WithBlockWise(createBlockWise), 100 | client.WithInactivityMonitor(monitor), 101 | client.WithRequestMonitor(cfg.RequestMonitor), 102 | ) 103 | cfg.PeriodicRunner(func(now time.Time) bool { 104 | cc.CheckExpirations(now) 105 | return cc.Context().Err() == nil 106 | }) 107 | 108 | go func() { 109 | err := cc.Run() 110 | if err != nil { 111 | cfg.Errors(err) 112 | } 113 | }() 114 | 115 | return cc 116 | } 117 | -------------------------------------------------------------------------------- /udp/client/bench_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "testing" 4 | 5 | func BenchmarkM(b *testing.B) { 6 | m := NewMutexMap() 7 | b.ResetTimer() 8 | for i := 0; i < b.N; i++ { 9 | // run uncontended lock/unlock - should be quite fast 10 | m.Lock(i).Unlock() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /udp/client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message" 9 | "github.com/plgd-dev/go-coap/v3/message/codes" 10 | "github.com/plgd-dev/go-coap/v3/message/pool" 11 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 12 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 13 | "github.com/plgd-dev/go-coap/v3/options/config" 14 | ) 15 | 16 | const DefaultMTU = 1472 17 | 18 | var DefaultConfig = func() Config { 19 | opts := Config{ 20 | Common: config.NewCommon[*Conn](), 21 | CreateInactivityMonitor: func() InactivityMonitor { 22 | return inactivity.NewNilMonitor[*Conn]() 23 | }, 24 | RequestMonitor: func(*Conn, *pool.Message) (bool, error) { 25 | return false, nil 26 | }, 27 | Dialer: &net.Dialer{Timeout: time.Second * 3}, 28 | Net: "udp", 29 | TransmissionNStart: 1, 30 | TransmissionAcknowledgeTimeout: time.Second * 2, 31 | TransmissionMaxRetransmit: 4, 32 | GetMID: message.GetMID, 33 | MTU: DefaultMTU, 34 | } 35 | opts.Handler = func(w *responsewriter.ResponseWriter[*Conn], r *pool.Message) { 36 | switch r.Code() { 37 | case codes.POST, codes.PUT, codes.GET, codes.DELETE: 38 | if err := w.SetResponse(codes.NotFound, message.TextPlain, nil); err != nil { 39 | opts.Errors(fmt.Errorf("udp client: cannot set response: %w", err)) 40 | } 41 | } 42 | } 43 | return opts 44 | }() 45 | 46 | type Config struct { 47 | config.Common[*Conn] 48 | CreateInactivityMonitor CreateInactivityMonitorFunc 49 | RequestMonitor RequestMonitorFunc 50 | Net string 51 | GetMID GetMIDFunc 52 | Handler HandlerFunc 53 | Dialer *net.Dialer 54 | TransmissionNStart uint32 55 | TransmissionAcknowledgeTimeout time.Duration 56 | TransmissionMaxRetransmit uint32 57 | CloseSocket bool 58 | MTU uint16 59 | } 60 | -------------------------------------------------------------------------------- /udp/client/mutexmap.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // MutexMap wraps a map of mutexes. Each key locks separately. 9 | type MutexMap struct { 10 | ma map[interface{}]*mutexMapEntry // entry map 11 | ml sync.Mutex // lock for entry map 12 | } 13 | 14 | type mutexMapEntry struct { 15 | key interface{} // key in ma 16 | m *MutexMap // point back to MutexMap, so we can synchronize removing this mutexMapEntry when cnt==0 17 | el sync.Mutex // entry-specific lock 18 | cnt uint16 // reference count 19 | } 20 | 21 | // Unlocker provides an Unlock method to release the lock. 22 | type Unlocker interface { 23 | Unlock() 24 | } 25 | 26 | // NewMutexMap returns an initialized MutexMap. 27 | func NewMutexMap() *MutexMap { 28 | return &MutexMap{ma: make(map[interface{}]*mutexMapEntry)} 29 | } 30 | 31 | // Lock acquires a lock corresponding to this key. 32 | // This method will never return nil and Unlock() must be called 33 | // to release the lock when done. 34 | func (m *MutexMap) Lock(key interface{}) Unlocker { 35 | // read or create entry for this key atomically 36 | m.ml.Lock() 37 | e, ok := m.ma[key] 38 | if !ok { 39 | e = &mutexMapEntry{m: m, key: key} 40 | m.ma[key] = e 41 | } 42 | e.cnt++ // ref count 43 | m.ml.Unlock() 44 | 45 | // acquire lock, will block here until e.cnt==1 46 | e.el.Lock() 47 | 48 | return e 49 | } 50 | 51 | // Unlock releases the lock for this entry. 52 | func (entry *mutexMapEntry) Unlock() { 53 | m := entry.m 54 | 55 | // decrement and if needed remove entry atomically 56 | m.ml.Lock() 57 | e, ok := m.ma[entry.key] 58 | if !ok { // entry must exist 59 | m.ml.Unlock() 60 | panic(fmt.Errorf("unlock requested for key=%v but no entry found", entry.key)) 61 | } 62 | e.cnt-- // ref count 63 | if e.cnt < 1 { // if it hits zero then we own it and remove from map 64 | delete(m.ma, entry.key) 65 | } 66 | m.ml.Unlock() 67 | 68 | // now that map stuff is handled, we unlock and let 69 | // anything else waiting on this key through 70 | e.el.Unlock() 71 | } 72 | -------------------------------------------------------------------------------- /udp/client/mutexmap_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "strings" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestMutexMap(t *testing.T) { 13 | r := rand.New(rand.NewSource(42)) 14 | 15 | m := NewMutexMap() 16 | _ = m 17 | 18 | keyCount := 20 19 | // currently for the race detector the max number of simultaneous goroutines is 8128 20 | // so the iCount value must be <= 8128 21 | iCount := 5000 22 | out := make(chan string, iCount*2) 23 | 24 | // run a bunch of concurrent requests for various keys, 25 | // the idea is to have a lot of lock contention 26 | var wg sync.WaitGroup 27 | wg.Add(iCount) 28 | for i := 0; i < iCount; i++ { 29 | go func(rn int) { 30 | defer wg.Done() 31 | key := strconv.Itoa(rn) 32 | 33 | // you can prove the test works by commenting the locking out and seeing it fail 34 | l := m.Lock(key) 35 | defer l.Unlock() 36 | 37 | out <- key + " A" 38 | time.Sleep(time.Microsecond) // make 'em wait a mo' 39 | out <- key + " B" 40 | }(r.Intn(keyCount)) 41 | } 42 | wg.Wait() 43 | close(out) 44 | 45 | // verify the map is empty now 46 | if l := len(m.ma); l != 0 { 47 | t.Errorf("unexpected map length at test end: %v", l) 48 | } 49 | 50 | // confirm that the output always produced the correct sequence 51 | outLists := make([][]string, keyCount) 52 | for s := range out { 53 | sParts := strings.Fields(s) 54 | kn, err := strconv.Atoi(sParts[0]) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | outLists[kn] = append(outLists[kn], sParts[1]) 59 | } 60 | for kn := 0; kn < keyCount; kn++ { 61 | l := outLists[kn] // list of output for this particular key 62 | for i := 0; i < len(l); i += 2 { 63 | if l[i] != "A" || l[i+1] != "B" { 64 | t.Errorf("For key=%v and i=%v got unexpected values %v and %v", kn, i, l[i], l[i+1]) 65 | break 66 | } 67 | } 68 | } 69 | if t.Failed() { 70 | t.Logf("Failed, outLists: %#v", outLists) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /udp/coder/bench_test.go: -------------------------------------------------------------------------------- 1 | package coder 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/plgd-dev/go-coap/v3/message" 7 | "github.com/plgd-dev/go-coap/v3/message/codes" 8 | ) 9 | 10 | func BenchmarkMarshalMessage(b *testing.B) { 11 | options := make(message.Options, 0, 32) 12 | bufOptions := make([]byte, 1024) 13 | bufOptionsUsed := bufOptions 14 | 15 | var enc int 16 | options, enc, _ = options.SetPath(bufOptionsUsed, "/a/b/c/d/e") 17 | bufOptionsUsed = bufOptionsUsed[enc:] 18 | 19 | options, _, _ = options.SetContentFormat(bufOptionsUsed, message.TextPlain) 20 | msg := message.Message{ 21 | Code: codes.GET, 22 | Payload: []byte{0x1}, 23 | Token: []byte{0x1, 0x2, 0x3}, 24 | Options: options, 25 | } 26 | buffer := make([]byte, 1024) 27 | 28 | b.ResetTimer() 29 | for i := uint32(0); i < uint32(b.N); i++ { 30 | _, err := DefaultCoder.Encode(msg, buffer) 31 | if err != nil { 32 | b.Fatalf("cannot marshal") 33 | } 34 | } 35 | } 36 | 37 | func BenchmarkUnmarshalMessage(b *testing.B) { 38 | buffer := []byte{ 39 | 0x40, 0x1, 0x30, 0x39, 0x46, 0x77, 40 | 0x65, 0x65, 0x74, 0x61, 0x67, 0xa1, 0x3, 41 | 0xff, 'h', 'i', 42 | } 43 | options := make(message.Options, 0, 32) 44 | msg := message.Message{ 45 | Options: options, 46 | } 47 | 48 | b.ResetTimer() 49 | for i := uint32(0); i < uint32(b.N); i++ { 50 | msg.Options = options 51 | _, err := DefaultCoder.Decode(buffer, &msg) 52 | if err != nil { 53 | b.Fatalf("cannot unmarshal: %v", err) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /udp/coder/coder.go: -------------------------------------------------------------------------------- 1 | package coder 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/plgd-dev/go-coap/v3/message" 9 | "github.com/plgd-dev/go-coap/v3/message/codes" 10 | "github.com/plgd-dev/go-coap/v3/pkg/math" 11 | ) 12 | 13 | var DefaultCoder = new(Coder) 14 | 15 | type Coder struct{} 16 | 17 | func (c *Coder) Size(m message.Message) (int, error) { 18 | if len(m.Token) > message.MaxTokenSize { 19 | return -1, message.ErrInvalidTokenLen 20 | } 21 | size := 4 + len(m.Token) 22 | payloadLen := len(m.Payload) 23 | optionsLen, err := m.Options.Marshal(nil) 24 | if !errors.Is(err, message.ErrTooSmall) { 25 | return -1, err 26 | } 27 | if payloadLen > 0 { 28 | // for separator 0xff 29 | payloadLen++ 30 | } 31 | size += payloadLen + optionsLen 32 | return size, nil 33 | } 34 | 35 | func (c *Coder) Encode(m message.Message, buf []byte) (int, error) { 36 | /* 37 | 0 1 2 3 38 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 39 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 40 | |Ver| T | TKL | Code | Message ID | 41 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 42 | | Token (if any, TKL bytes) ... 43 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 44 | | Options (if any) ... 45 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 46 | |1 1 1 1 1 1 1 1| Payload (if any) ... 47 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 48 | */ 49 | if !message.ValidateMID(m.MessageID) { 50 | return -1, fmt.Errorf("invalid MessageID(%v)", m.MessageID) 51 | } 52 | if !message.ValidateType(m.Type) { 53 | return -1, fmt.Errorf("invalid Type(%v)", m.Type) 54 | } 55 | size, err := c.Size(m) 56 | if err != nil { 57 | return -1, err 58 | } 59 | if len(buf) < size { 60 | return size, message.ErrTooSmall 61 | } 62 | 63 | tmpbuf := []byte{0, 0} 64 | // safe: checked by message.ValidateMID above 65 | binary.BigEndian.PutUint16(tmpbuf, math.CastTo[uint16](m.MessageID)) 66 | 67 | buf[0] = (1 << 6) | byte(m.Type)<<4 | byte(0xf&len(m.Token)) 68 | buf[1] = byte(m.Code) 69 | buf[2] = tmpbuf[0] 70 | buf[3] = tmpbuf[1] 71 | buf = buf[4:] 72 | 73 | if len(m.Token) > message.MaxTokenSize { 74 | return -1, message.ErrInvalidTokenLen 75 | } 76 | copy(buf, m.Token) 77 | buf = buf[len(m.Token):] 78 | 79 | optionsLen, err := m.Options.Marshal(buf) 80 | switch { 81 | case err == nil: 82 | case errors.Is(err, message.ErrTooSmall): 83 | return size, err 84 | default: 85 | return -1, err 86 | } 87 | buf = buf[optionsLen:] 88 | 89 | if len(m.Payload) > 0 { 90 | buf[0] = 0xff 91 | buf = buf[1:] 92 | } 93 | copy(buf, m.Payload) 94 | return size, nil 95 | } 96 | 97 | func (c *Coder) Decode(data []byte, m *message.Message) (int, error) { 98 | size := len(data) 99 | if size < 4 { 100 | return -1, ErrMessageTruncated 101 | } 102 | 103 | if data[0]>>6 != 1 { 104 | return -1, ErrMessageInvalidVersion 105 | } 106 | 107 | typ := message.Type((data[0] >> 4) & 0x3) 108 | tokenLen := int(data[0] & 0xf) 109 | if tokenLen > 8 { 110 | return -1, message.ErrInvalidTokenLen 111 | } 112 | 113 | code := codes.Code(data[1]) 114 | messageID := binary.BigEndian.Uint16(data[2:4]) 115 | data = data[4:] 116 | if len(data) < tokenLen { 117 | return -1, ErrMessageTruncated 118 | } 119 | token := data[:tokenLen] 120 | if len(token) == 0 { 121 | token = nil 122 | } 123 | data = data[tokenLen:] 124 | 125 | optionDefs := message.CoapOptionDefs 126 | proc, err := m.Options.Unmarshal(data, optionDefs) 127 | if err != nil { 128 | return -1, err 129 | } 130 | data = data[proc:] 131 | if len(data) == 0 { 132 | data = nil 133 | } 134 | 135 | m.Payload = data 136 | m.Code = code 137 | m.Token = token 138 | m.Type = typ 139 | m.MessageID = int32(messageID) 140 | 141 | return size, nil 142 | } 143 | -------------------------------------------------------------------------------- /udp/coder/error.go: -------------------------------------------------------------------------------- 1 | package coder 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrMessageTruncated = errors.New("message is truncated") 7 | ErrMessageInvalidVersion = errors.New("message has invalid version") 8 | ) 9 | -------------------------------------------------------------------------------- /udp/example_test.go: -------------------------------------------------------------------------------- 1 | package udp_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "sync" 9 | "time" 10 | 11 | "github.com/plgd-dev/go-coap/v3/message/pool" 12 | "github.com/plgd-dev/go-coap/v3/net" 13 | "github.com/plgd-dev/go-coap/v3/udp" 14 | "github.com/plgd-dev/go-coap/v3/udp/client" 15 | ) 16 | 17 | func ExampleConn_Get() { 18 | conn, err := udp.Dial("pluggedin.cloud:5683") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | defer conn.Close() 23 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 24 | defer cancel() 25 | res, err := conn.Get(ctx, "/oic/res") 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | data, err := io.ReadAll(res.Body()) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | fmt.Printf("%v", data) 34 | } 35 | 36 | func ExampleServer_Serve() { 37 | l, err := net.NewListenUDP("udp", "0.0.0.0:5683") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | defer l.Close() 42 | s := udp.NewServer() 43 | defer s.Stop() 44 | log.Fatal(s.Serve(l)) 45 | } 46 | 47 | func ExampleServer_Discover() { 48 | l, err := net.NewListenUDP("udp", "") 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | defer l.Close() 53 | var wg sync.WaitGroup 54 | defer wg.Wait() 55 | 56 | s := udp.NewServer() 57 | defer s.Stop() 58 | wg.Add(1) 59 | go func() { 60 | defer wg.Done() 61 | errS := s.Serve(l) 62 | if errS != nil { 63 | log.Println(errS) 64 | } 65 | }() 66 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 67 | defer cancel() 68 | err = s.Discover(ctx, "224.0.1.187:5683", "/oic/res", func(_ *client.Conn, res *pool.Message) { 69 | data, errR := io.ReadAll(res.Body()) 70 | if errR != nil { 71 | log.Fatal(errR) 72 | } 73 | fmt.Printf("%v", data) 74 | }) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /udp/isRunninigInterface_1.18_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 || go1.19 2 | 3 | package udp_test 4 | 5 | import "net" 6 | 7 | func isRunningInterface(net.Interface) bool { 8 | return true 9 | } 10 | -------------------------------------------------------------------------------- /udp/isRunninigInterface_1.20_test.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.18 && !go1.19 2 | 3 | package udp_test 4 | 5 | import "net" 6 | 7 | func isRunningInterface(i net.Interface) bool { 8 | return i.Flags&net.FlagRunning == net.FlagRunning 9 | } 10 | -------------------------------------------------------------------------------- /udp/server.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import "github.com/plgd-dev/go-coap/v3/udp/server" 4 | 5 | func NewServer(opt ...server.Option) *server.Server { 6 | return server.New(opt...) 7 | } 8 | -------------------------------------------------------------------------------- /udp/server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/plgd-dev/go-coap/v3/message" 8 | "github.com/plgd-dev/go-coap/v3/message/codes" 9 | "github.com/plgd-dev/go-coap/v3/message/pool" 10 | "github.com/plgd-dev/go-coap/v3/net/monitor/inactivity" 11 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 12 | "github.com/plgd-dev/go-coap/v3/options/config" 13 | udpClient "github.com/plgd-dev/go-coap/v3/udp/client" 14 | ) 15 | 16 | // The HandlerFunc type is an adapter to allow the use of 17 | // ordinary functions as COAP handlers. 18 | type HandlerFunc = func(*responsewriter.ResponseWriter[*udpClient.Conn], *pool.Message) 19 | 20 | type ErrorFunc = func(error) 21 | 22 | // OnNewConnFunc is the callback for new connections. 23 | type OnNewConnFunc = func(cc *udpClient.Conn) 24 | 25 | type GetMIDFunc = func() int32 26 | 27 | var DefaultConfig = func() Config { 28 | opts := Config{ 29 | Common: config.NewCommon[*udpClient.Conn](), 30 | CreateInactivityMonitor: func() udpClient.InactivityMonitor { 31 | timeout := time.Second * 16 32 | onInactive := func(cc *udpClient.Conn) { 33 | _ = cc.Close() 34 | } 35 | return inactivity.New(timeout, onInactive) 36 | }, 37 | OnNewConn: func(*udpClient.Conn) { 38 | // do nothing by default 39 | }, 40 | RequestMonitor: func(*udpClient.Conn, *pool.Message) (bool, error) { 41 | return false, nil 42 | }, 43 | TransmissionNStart: 1, 44 | TransmissionAcknowledgeTimeout: time.Second * 2, 45 | TransmissionMaxRetransmit: 4, 46 | GetMID: message.GetMID, 47 | MTU: udpClient.DefaultMTU, 48 | } 49 | opts.Handler = func(w *responsewriter.ResponseWriter[*udpClient.Conn], _ *pool.Message) { 50 | if err := w.SetResponse(codes.NotFound, message.TextPlain, nil); err != nil { 51 | opts.Errors(fmt.Errorf("udp server: cannot set response: %w", err)) 52 | } 53 | } 54 | return opts 55 | }() 56 | 57 | type Config struct { 58 | config.Common[*udpClient.Conn] 59 | CreateInactivityMonitor udpClient.CreateInactivityMonitorFunc 60 | GetMID GetMIDFunc 61 | Handler HandlerFunc 62 | OnNewConn OnNewConnFunc 63 | RequestMonitor udpClient.RequestMonitorFunc 64 | TransmissionNStart uint32 65 | TransmissionAcknowledgeTimeout time.Duration 66 | TransmissionMaxRetransmit uint32 67 | MTU uint16 68 | } 69 | -------------------------------------------------------------------------------- /udp/server/discover.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | 9 | "github.com/plgd-dev/go-coap/v3/message" 10 | "github.com/plgd-dev/go-coap/v3/message/pool" 11 | coapNet "github.com/plgd-dev/go-coap/v3/net" 12 | "github.com/plgd-dev/go-coap/v3/net/responsewriter" 13 | pkgErrors "github.com/plgd-dev/go-coap/v3/pkg/errors" 14 | "github.com/plgd-dev/go-coap/v3/udp/client" 15 | "github.com/plgd-dev/go-coap/v3/udp/coder" 16 | ) 17 | 18 | // Discover sends GET to multicast or unicast address and waits for responses until context timeouts or server shutdown. 19 | // For unicast there is a difference against the Dial. The Dial is connection-oriented and it means that, if you send a request to an address, the peer must send the response from the same 20 | // address where was request sent. For Discover it allows the client to send a response from another address where was request send. 21 | // By default it is sent over all network interfaces and all compatible source IP addresses with hop limit 1. 22 | // Via opts you can specify the network interface, source IP address, and hop limit. 23 | func (s *Server) Discover(ctx context.Context, address, path string, receiverFunc func(cc *client.Conn, resp *pool.Message), opts ...coapNet.MulticastOption) error { 24 | token, err := s.cfg.GetToken() 25 | if err != nil { 26 | return fmt.Errorf("cannot get token: %w", err) 27 | } 28 | req := s.cfg.MessagePool.AcquireMessage(ctx) 29 | defer s.cfg.MessagePool.ReleaseMessage(req) 30 | err = req.SetupGet(path, token) 31 | if err != nil { 32 | return fmt.Errorf("cannot create discover request: %w", err) 33 | } 34 | req.SetMessageID(s.cfg.GetMID()) 35 | req.SetType(message.NonConfirmable) 36 | return s.DiscoveryRequest(req, address, receiverFunc, opts...) 37 | } 38 | 39 | // DiscoveryRequest sends request to multicast/unicast address and wait for responses until request timeouts or server shutdown. 40 | // For unicast there is a difference against the Dial. The Dial is connection-oriented and it means that, if you send a request to an address, the peer must send the response from the same 41 | // address where was request sent. For Discover it allows the client to send a response from another address where was request send. 42 | // By default it is sent over all network interfaces and all compatible source IP addresses with hop limit 1. 43 | // Via opts you can specify the network interface, source IP address, and hop limit. 44 | func (s *Server) DiscoveryRequest(req *pool.Message, address string, receiverFunc func(cc *client.Conn, resp *pool.Message), opts ...coapNet.MulticastOption) error { 45 | token := req.Token() 46 | if len(token) == 0 { 47 | return errors.New("invalid token") 48 | } 49 | c := s.conn() 50 | if c == nil { 51 | return errors.New("server doesn't serve connection") 52 | } 53 | addr, err := net.ResolveUDPAddr(c.Network(), address) 54 | if err != nil { 55 | return fmt.Errorf("cannot resolve address: %w", err) 56 | } 57 | 58 | data, err := req.MarshalWithEncoder(coder.DefaultCoder) 59 | if err != nil { 60 | return fmt.Errorf("cannot marshal req: %w", err) 61 | } 62 | s.multicastRequests.Store(token.Hash(), req) 63 | defer s.multicastRequests.Delete(token.Hash()) 64 | if _, loaded := s.multicastHandler.LoadOrStore(token.Hash(), func(w *responsewriter.ResponseWriter[*client.Conn], r *pool.Message) { 65 | receiverFunc(w.Conn(), r) 66 | }); loaded { 67 | return pkgErrors.ErrKeyAlreadyExists 68 | } 69 | defer func() { 70 | _, _ = s.multicastHandler.LoadAndDelete(token.Hash()) 71 | }() 72 | 73 | if addr.IP.IsMulticast() { 74 | err = c.WriteMulticast(req.Context(), addr, data, opts...) 75 | if err != nil { 76 | return err 77 | } 78 | } else { 79 | err = c.WriteWithContext(req.Context(), addr, data) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | 85 | select { 86 | case <-req.Context().Done(): 87 | return nil 88 | case <-s.ctx.Done(): 89 | return fmt.Errorf("server was closed: %w", s.ctx.Err()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /udp/server/session.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "sync/atomic" 9 | 10 | "github.com/plgd-dev/go-coap/v3/message/pool" 11 | coapNet "github.com/plgd-dev/go-coap/v3/net" 12 | "github.com/plgd-dev/go-coap/v3/udp/client" 13 | "github.com/plgd-dev/go-coap/v3/udp/coder" 14 | ) 15 | 16 | type EventFunc = func() 17 | 18 | type Session struct { 19 | onClose []EventFunc 20 | 21 | ctx atomic.Pointer[context.Context] 22 | 23 | doneCtx context.Context 24 | connection *coapNet.UDPConn 25 | doneCancel context.CancelFunc 26 | 27 | cancel context.CancelFunc 28 | raddr *net.UDPAddr 29 | 30 | mutex sync.Mutex 31 | maxMessageSize uint32 32 | mtu uint16 33 | 34 | closeSocket bool 35 | } 36 | 37 | func NewSession( 38 | ctx context.Context, 39 | doneCtx context.Context, 40 | connection *coapNet.UDPConn, 41 | raddr *net.UDPAddr, 42 | maxMessageSize uint32, 43 | mtu uint16, 44 | closeSocket bool, 45 | ) *Session { 46 | ctx, cancel := context.WithCancel(ctx) 47 | 48 | doneCtx, doneCancel := context.WithCancel(doneCtx) 49 | s := &Session{ 50 | cancel: cancel, 51 | connection: connection, 52 | raddr: raddr, 53 | maxMessageSize: maxMessageSize, 54 | mtu: mtu, 55 | closeSocket: closeSocket, 56 | doneCtx: doneCtx, 57 | doneCancel: doneCancel, 58 | } 59 | s.ctx.Store(&ctx) 60 | return s 61 | } 62 | 63 | // SetContextValue stores the value associated with key to context of connection. 64 | func (s *Session) SetContextValue(key interface{}, val interface{}) { 65 | ctx := context.WithValue(s.Context(), key, val) 66 | s.ctx.Store(&ctx) 67 | } 68 | 69 | // Done signalizes that connection is not more processed. 70 | func (s *Session) Done() <-chan struct{} { 71 | return s.doneCtx.Done() 72 | } 73 | 74 | func (s *Session) AddOnClose(f EventFunc) { 75 | s.mutex.Lock() 76 | defer s.mutex.Unlock() 77 | s.onClose = append(s.onClose, f) 78 | } 79 | 80 | func (s *Session) popOnClose() []EventFunc { 81 | s.mutex.Lock() 82 | defer s.mutex.Unlock() 83 | tmp := s.onClose 84 | s.onClose = nil 85 | return tmp 86 | } 87 | 88 | func (s *Session) shutdown() { 89 | defer s.doneCancel() 90 | for _, f := range s.popOnClose() { 91 | f() 92 | } 93 | } 94 | 95 | func (s *Session) Close() error { 96 | s.cancel() 97 | if s.closeSocket { 98 | return s.connection.Close() 99 | } 100 | return nil 101 | } 102 | 103 | func (s *Session) Context() context.Context { 104 | return *s.ctx.Load() 105 | } 106 | 107 | func (s *Session) WriteMessage(req *pool.Message) error { 108 | data, err := req.MarshalWithEncoder(coder.DefaultCoder) 109 | if err != nil { 110 | return fmt.Errorf("cannot marshal: %w", err) 111 | } 112 | return s.connection.WriteWithOptions(data, coapNet.WithContext(req.Context()), coapNet.WithRemoteAddr(s.raddr), coapNet.WithControlMessage(req.ControlMessage())) 113 | } 114 | 115 | // WriteMulticastMessage sends multicast to the remote multicast address. 116 | // By default it is sent over all network interfaces and all compatible source IP addresses with hop limit 1. 117 | // Via opts you can specify the network interface, source IP address, and hop limit. 118 | func (s *Session) WriteMulticastMessage(req *pool.Message, address *net.UDPAddr, opts ...coapNet.MulticastOption) error { 119 | data, err := req.MarshalWithEncoder(coder.DefaultCoder) 120 | if err != nil { 121 | return fmt.Errorf("cannot marshal: %w", err) 122 | } 123 | 124 | return s.connection.WriteMulticast(req.Context(), address, data, opts...) 125 | } 126 | 127 | func (s *Session) Run(cc *client.Conn) (err error) { 128 | defer func() { 129 | err1 := s.Close() 130 | if err == nil { 131 | err = err1 132 | } 133 | s.shutdown() 134 | }() 135 | m := make([]byte, s.mtu) 136 | for { 137 | buf := m 138 | var cm *coapNet.ControlMessage 139 | n, err := s.connection.ReadWithOptions(buf, coapNet.WithContext(s.Context()), coapNet.WithGetControlMessage(&cm)) 140 | if err != nil { 141 | return err 142 | } 143 | buf = buf[:n] 144 | err = cc.Process(cm, buf) 145 | if err != nil { 146 | return err 147 | } 148 | } 149 | } 150 | 151 | func (s *Session) MaxMessageSize() uint32 { 152 | return s.maxMessageSize 153 | } 154 | 155 | func (s *Session) RemoteAddr() net.Addr { 156 | return s.raddr 157 | } 158 | 159 | func (s *Session) LocalAddr() net.Addr { 160 | return s.connection.LocalAddr() 161 | } 162 | 163 | // NetConn returns the underlying connection that is wrapped by s. The Conn returned is shared by all invocations of NetConn, so do not modify it. 164 | func (s *Session) NetConn() net.Conn { 165 | return s.connection.NetConn() 166 | } 167 | -------------------------------------------------------------------------------- /v3: -------------------------------------------------------------------------------- 1 | . --------------------------------------------------------------------------------