From b8d3a44d22a0225a5f3812db0e31de18d81b1c36 Mon Sep 17 00:00:00 2001 From: Md Imran Hossain Date: Tue, 15 Sep 2026 17:13:32 +0600 Subject: [PATCH] functionality updates --- Cargo.toml | 1 + README.md | 11 +- __pycache__/coap-inspect.cpython-312.pyc | Bin 0 -> 12746 bytes coap-inspect.py | 200 +++++++++++++++ healthcheck.sh | 31 +++ nodejs/client.js | 32 --- nodejs/package-lock.json | 309 ----------------------- nodejs/package.json | 16 -- nodejs/server.js | 102 -------- src/main.rs | 1 + static/index.html | 168 +++++++----- 11 files changed, 338 insertions(+), 533 deletions(-) create mode 100644 __pycache__/coap-inspect.cpython-312.pyc create mode 100755 coap-inspect.py create mode 100755 healthcheck.sh delete mode 100644 nodejs/client.js delete mode 100644 nodejs/package-lock.json delete mode 100644 nodejs/package.json delete mode 100644 nodejs/server.js diff --git a/Cargo.toml b/Cargo.toml index 9134b97..0a1b178 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "coap-hello-udp" version = "0.1.0" edition = "2024" +default-run = "coap-hello-udp" [dependencies] coap-lite = "0.13.3" diff --git a/README.md b/README.md index b6505a2..d5d010b 100644 --- a/README.md +++ b/README.md @@ -46,20 +46,19 @@ cargo run --release --bin coap-hello-udp cargo run --release --bin bridge ``` -Then open for a test console with a name field and a -button per route. Every response shows both the CoAP code and the HTTP status -it was mapped to; the raw CoAP code is also returned in an `X-Coap-Code` header. +Every response shows the CoAP code mapped onto an HTTP status, with the raw +CoAP code returned verbatim in an `X-Coap-Code` header. These are now ordinary URLs — clickable, curl-able, bookmarkable: | URL | Response | |--------------------------------------------------|------------------------------| -| `http://localhost:8080/` | the test console | +| `http://localhost:8080/` | the CoAP reference page | | `http://localhost:8080/hello` | `Hello, World!` | | `http://localhost:8080/hello?name=Imran` | `Hello, Imran!` | | `http://localhost:8080/health` | `{"status":"ok","uptime":12}`| -POST needs a body, so it is easiest from the console page or curl: +POST needs a body, so use curl: ```bash curl -X POST -d "Imran" http://localhost:8080/hello @@ -104,7 +103,7 @@ decoding. | `src/lib.rs` | Shared CoAP client used by the CLI and the bridge. | | `src/bin/client.rs` | CLI client for smoke tests. | | `src/bin/bridge.rs` | HTTP -> CoAP proxy so a browser can reach the server. | -| `static/index.html` | The browser test console the bridge serves at `/`. | +| `static/index.html` | CoAP reference page: routes, codes, copyable commands. | | `nodejs/` | The earlier Node.js version, kept for reference. | ## Config diff --git a/__pycache__/coap-inspect.cpython-312.pyc b/__pycache__/coap-inspect.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43758c5b1b44452b3ed13e4e979dd55c44c23d10 GIT binary patch literal 12746 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 literal 0 HcmV?d00001 diff --git a/coap-inspect.py b/coap-inspect.py new file mode 100755 index 0000000..2387137 --- /dev/null +++ b/coap-inspect.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Send one CoAP request over UDP and show both datagrams byte by byte. + + ./coap-inspect.py get "/hello?name=Imran" + ./coap-inspect.py post /hello Imran + ./coap-inspect.py get /health + +No CoAP library: the packet is built and decoded right here, so every byte on +screen is accounted for. COAP_HOST / COAP_PORT retarget it (127.0.0.1:5683). +""" +import os +import random +import socket +import sys +import time + +TYPES = {0: "CON confirmable", 1: "NON non-confirmable", 2: "ACK acknowledgement", 3: "RST reset"} +METHODS = {"get": 1, "post": 2, "put": 3, "delete": 4} +CODES = { + "0.00": "Empty", "0.01": "GET", "0.02": "POST", "0.03": "PUT", "0.04": "DELETE", + "2.01": "Created", "2.02": "Deleted", "2.03": "Valid", "2.04": "Changed", "2.05": "Content", + "4.00": "Bad Request", "4.04": "Not Found", "4.05": "Method Not Allowed", + "5.00": "Internal Server Error", +} +OPTIONS = {3: "Uri-Host", 7: "Uri-Port", 11: "Uri-Path", 12: "Content-Format", 15: "Uri-Query", 17: "Accept"} +FORMATS = {0: "text/plain", 50: "application/json"} + +TTY = sys.stdout.isatty() + + +def bold(text): + return f"\033[1m{text}\033[0m" if TTY else text + + +def encode_option(delta, value): + """One option: a byte of 4-bit delta + 4-bit length, then any extended bytes, then the value.""" + def nibble(n): + if n < 13: + return n, b"" + if n < 269: + return 13, bytes([n - 13]) + return 14, (n - 269).to_bytes(2, "big") + + d, d_ext = nibble(delta) + l, l_ext = nibble(len(value)) + return bytes([(d << 4) | l]) + d_ext + l_ext + value + + +def build_request(method, path, query, payload): + token = os.urandom(4) + header = bytes([(1 << 6) | (0 << 4) | len(token), METHODS[method]]) # ver 1, CON, TKL, code + header += random.randrange(1 << 16).to_bytes(2, "big") + token # message id, token + + # Options must be in ascending number order; each stores the gap from the previous one. + options = [(11, s.encode()) for s in path.split("/") if s] + options += [(15, q.encode()) for q in query.split("&") if q] + body, last = b"", 0 + for number, value in options: + body += encode_option(number - last, value) + last = number + if payload: + body += b"\xff" + payload + return header + body + + +def decode(pkt): + """Return (rows, meta). Each row is (start, end, field, meaning) over pkt[start:end].""" + if len(pkt) < 4: + raise ValueError(f"{len(pkt)} bytes is shorter than the 4-byte CoAP header") + + rows = [] + b0 = pkt[0] + ver, typ, tkl = b0 >> 6, (b0 >> 4) & 0b11, b0 & 0b1111 + rows.append((0, 1, "ver|type|tkl", + f"{ver:02b} {typ:02b} {tkl:04b} -> version {ver}, {TYPES[typ]}, token {tkl} bytes")) + + cls, detail = pkt[1] >> 5, pkt[1] & 0b11111 + code = f"{cls}.{detail:02d}" + rows.append((1, 2, "code", f"{cls:03b} {detail:05b} -> {code} {CODES.get(code, '')}")) + + mid = int.from_bytes(pkt[2:4], "big") + rows.append((2, 4, "message id", f"{mid} (0x{mid:04x})")) + + token = pkt[4:4 + tkl] + if tkl: + rows.append((4, 4 + tkl, "token", f"0x{token.hex()}")) + + i, number = 4 + tkl, 0 + while i < len(pkt): + if pkt[i] == 0xFF: + body = pkt[i + 1:] + rows.append((i, i + 1, "payload marker", "0xff -> the rest is the body")) + rows.append((i + 1, len(pkt), "payload", + f"{len(body)} bytes: {body.decode('utf-8', 'replace')}")) + break + + start = i + delta, length = pkt[i] >> 4, pkt[i] & 0b1111 + i += 1 + extended = [] + for which, n in (("delta", delta), ("length", length)): + if n == 13: + n, i = pkt[i] + 13, i + 1 + elif n == 14: + n, i = int.from_bytes(pkt[i:i + 2], "big") + 269, i + 2 + extended.append(n) + delta, length = extended + number += delta + value = pkt[i:i + length] + i += length + + if number in (12, 17): + n = int.from_bytes(value, "big") if value else 0 + shown = f"{n} = {FORMATS.get(n, 'unknown format')}" + else: + shown = f'"{value.decode("utf-8", "replace")}"' + rows.append((start, i, OPTIONS.get(number, f"option {number}"), + f"delta {delta} -> option {number}, {length} bytes: {shown}")) + + return rows, {"type": typ, "code": code, "mid": mid, "token": token} + + +def show(title, pkt): + print(bold(f"\n{title} ({len(pkt)} bytes)")) + + print(bold("\n hex + ascii")) + for off in range(0, len(pkt), 16): + chunk = pkt[off:off + 16] + hexes = " ".join(f"{b:02x}" for b in chunk) + text = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk) + print(f" {off:04x} {hexes:<47} |{text}|") + + print(bold("\n binary")) + for off in range(0, len(pkt), 4): + chunk = pkt[off:off + 4] + bits = " ".join(f"{b:08b}" for b in chunk) + text = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk) + print(f" {off:04x} {bits:<35} |{text}|") + + rows, meta = decode(pkt) + print(bold("\n fields")) + print(f" {'byte':<7} {'hex':<24} {'field':<15} meaning") + for start, end, field, meaning in rows: + span = f"{start}" if end - start == 1 else f"{start}-{end - 1}" + hexes = pkt[start:end].hex(" ") + if len(hexes) > 23: + hexes = hexes[:20] + "..." + print(f" {span:<7} {hexes:<24} {field:<15} {meaning}") + return meta + + +def main(): + args = sys.argv[1:] + if not args or args[0].lower() not in METHODS: + print(__doc__.strip()) + return 2 + + method = args[0].lower() + target = args[1] if len(args) > 1 else "/hello" + payload = args[2].encode() if len(args) > 2 else b"" + path, _, query = target.partition("?") + host = os.environ.get("COAP_HOST", "127.0.0.1") + port = int(os.environ.get("COAP_PORT", "5683")) + + request = build_request(method, path, query, payload) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM = UDP + sock.settimeout(3) + sock.connect((host, port)) + local = "%s:%d" % sock.getsockname() + + req = show(f">>> REQUEST udp {local} -> {host}:{port}", request) + sent_at = time.perf_counter() + sock.send(request) + try: + response = sock.recv(1152) + except socket.timeout: + print(f"\nno reply within 3s - is the CoAP server running on {host}:{port}?") + return 1 + except ConnectionRefusedError: + print(f"\nport unreachable - nothing is listening on udp {host}:{port}") + return 1 + elapsed = (time.perf_counter() - sent_at) * 1000 + + res = show(f"<<< RESPONSE udp {host}:{port} -> {local}", response) + + print(bold("\n exchange")) + print(f" request {TYPES[req['type']].split()[0]} {req['code']} mid 0x{req['mid']:04x} " + f"token 0x{req['token'].hex()} {len(request)} bytes") + print(f" response {TYPES[res['type']].split()[0]} {res['code']} mid 0x{res['mid']:04x} " + f"token 0x{res['token'].hex()} {len(response)} bytes after {elapsed:.2f} ms") + same_mid = "yes" if req["mid"] == res["mid"] else "NO" + same_token = "yes" if req["token"] == res["token"] else "NO" + kind = " (piggybacked: the ACK carries the answer)" if res["type"] == 2 else "" + print(f" message id matches: {same_mid} token matches: {same_token}{kind}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/healthcheck.sh b/healthcheck.sh new file mode 100755 index 0000000..628ba1f --- /dev/null +++ b/healthcheck.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Liveness check for the CoAP server. Exits 0 if healthy, 1 if not. +# ./healthcheck.sh quiet, for cron/systemd/monitoring +# ./healthcheck.sh -v print what it found +# Retargets with COAP_HOST / COAP_PORT. + +CLIENT=${CLIENT:-$(dirname "$0")/target/release/client} +verbose=false; [[ $1 == -v ]] && verbose=true +say() { $verbose && echo "$@"; } + +host=${COAP_HOST:-127.0.0.1}; port=${COAP_PORT:-5683} + +[[ -x $CLIENT ]] || { echo "healthcheck: no client binary at $CLIENT (cargo build --release)"; exit 1; } + +reply=$("$CLIENT" get /health 2>&1) + +# A bound socket proves nothing on UDP - the round trip is the real test. +if [[ $reply != "<- 2.05 "* ]]; then + echo "UNHEALTHY: coap://$host:$port/health -> ${reply:-no reply}" + exit 1 +fi + +body=${reply#<- 2.05 } +if [[ $body != *'"status":"ok"'* ]]; then + echo "UNHEALTHY: unexpected body from coap://$host:$port/health -> $body" + exit 1 +fi + +uptime=$(sed -n 's/.*"uptime":\([0-9]*\).*/\1/p' <<<"$body") +say "HEALTHY: coap://$host:$port up ${uptime}s (code 2.05)" +exit 0 diff --git a/nodejs/client.js b/nodejs/client.js deleted file mode 100644 index d9d8fb1..0000000 --- a/nodejs/client.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -// Tiny CLI client: -// node client.js get /hello?name=Imran -// node client.js post /hello Imran -// node client.js get /health - -const coap = require('coap'); - -const [, , methodArg = 'get', pathArg = '/hello', payload] = process.argv; - -const req = coap.request({ - host: process.env.COAP_HOST || 'localhost', - port: Number(process.env.COAP_PORT) || 5683, - pathname: pathArg.split('?')[0], - query: pathArg.split('?')[1] || '', - method: methodArg.toUpperCase(), - confirmable: true, -}); - -req.on('response', (res) => { - console.log(`<- ${res.code} ${res.payload.toString()}`); - process.exit(0); -}); - -req.on('error', (err) => { - console.error('request failed:', err.message); - process.exit(1); -}); - -if (payload) req.write(payload); -req.end(); diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json deleted file mode 100644 index 8b84f61..0000000 --- a/nodejs/package-lock.json +++ /dev/null @@ -1,309 +0,0 @@ -{ - "name": "coapwithudp", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "coapwithudp", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "coap": "^1.6.0" - } - }, - "node_modules/@types/node": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", - "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/readable-stream": { - "version": "4.0.24", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", - "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bl": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", - "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", - "license": "MIT", - "dependencies": { - "@types/readable-stream": "^4.0.0", - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^4.2.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/capitalize": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/capitalize/-/capitalize-2.0.4.tgz", - "integrity": "sha512-wcSyiFqXRYyCoqu0o0ekXzJAKCLMkqWS5QWGlgTJFJKwRmI6pzcN2hBl5VPq9RzLW5Uf4FF/V/lcFfjCtVak2w==", - "license": "MIT" - }, - "node_modules/cbor": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", - "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", - "license": "MIT", - "dependencies": { - "nofilter": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/coap": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coap/-/coap-1.6.0.tgz", - "integrity": "sha512-AvPHT2yQet0wOhlmIqFk11NQPnsKt30yRk6IGp9Yhb6Xnp3ozExPfklJXiE28Xb47tjU0M2OV2W++h8f350Mjg==", - "license": "MIT", - "dependencies": { - "@types/readable-stream": "^4.0.14", - "bl": "^6.0.12", - "capitalize": "^2.0.4", - "coap-oscore": "^2.2.3", - "coap-packet": "^1.1.1", - "debug": "^4.3.5", - "fastseries": "^2.0.0", - "lru-cache": "^11.0.2", - "readable-stream": "^4.5.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/coap-oscore": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/coap-oscore/-/coap-oscore-2.2.3.tgz", - "integrity": "sha512-uF592/QIDgxbwhvAHCiWJat0xIoWsc8K7ubzYK1jXM4ihe50sHLAM7pl361NzZXLOMkkYnHCBSq07DA56OIs/A==", - "license": "MIT", - "dependencies": { - "cbor": "^9.0.2" - } - }, - "node_modules/coap-packet": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/coap-packet/-/coap-packet-1.1.1.tgz", - "integrity": "sha512-Bkz2ZKI/7hU2gm6nUuo5l+MBSkdFJx7My1ZgNEhKUC7K2yYfQYVbBPRa64BBYLcEcYgaSlau4A1Uw5xfM2I0zw==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fastseries": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz", - "integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ==", - "license": "ISC" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", - "license": "MIT", - "engines": { - "node": ">=12.19" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "license": "MIT" - } - } -} diff --git a/nodejs/package.json b/nodejs/package.json deleted file mode 100644 index ee0fe54..0000000 --- a/nodejs/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "coap-hello-udp", - "version": "1.0.0", - "main": "server.js", - "scripts": { - "start": "node server.js", - "client": "node client.js" - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "Minimal CoAP-over-UDP hello world server with GET/POST routes", - "dependencies": { - "coap": "^1.6.0" - } -} diff --git a/nodejs/server.js b/nodejs/server.js deleted file mode 100644 index 74cdc6b..0000000 --- a/nodejs/server.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -const coap = require('coap'); - -const PORT = Number(process.env.COAP_PORT) || 5683; -const HOST = process.env.COAP_HOST || '0.0.0.0'; - -// --- helpers --------------------------------------------------------------- - -const TEXT = 'text/plain'; -const JSON_FMT = 'application/json'; - -/** Split "/hello?name=Bob" into ["/hello", "name=Bob"]. */ -function splitUrl(url) { - const i = url.indexOf('?'); - const path = (i === -1 ? url : url.slice(0, i)).replace(/\/+$/, '') || '/'; - return [path, i === -1 ? '' : url.slice(i + 1)]; -} - -/** Pull a name out of "?name=Bob" style queries. */ -function nameFromQuery(query) { - return new URLSearchParams(query || '').get('name'); -} - -/** Pull a name out of a POST body: raw text or {"name":"Bob"}. */ -function nameFromBody(buf) { - const body = buf.toString('utf8').trim(); - if (!body) return null; - if (body.startsWith('{')) { - try { - const parsed = JSON.parse(body); - return typeof parsed.name === 'string' ? parsed.name.trim() : null; - } catch { - return null; - } - } - return body; -} - -function greet(name) { - return `Hello, ${name && name.length ? name : 'World'}!`; -} - -function send(res, code, payload, format = TEXT) { - res.code = code; - res.setOption('Content-Format', format); - res.end(typeof payload === 'string' ? payload : JSON.stringify(payload)); -} - -// --- routes ---------------------------------------------------------------- -// key: "METHOD /path" - -const routes = { - 'GET /hello': (req, res) => { - send(res, '2.05', greet(nameFromQuery(req.query))); - }, - - 'POST /hello': (req, res) => { - const name = nameFromBody(req.payload) || nameFromQuery(req.query); - if (!name) { - return send(res, '4.00', 'Bad Request: send a name as the payload'); - } - send(res, '2.01', greet(name)); - }, - - 'GET /health': (_req, res) => { - send(res, '2.05', { status: 'ok', uptime: Math.round(process.uptime()) }, JSON_FMT); - }, -}; - -// --- server ---------------------------------------------------------------- - -const server = coap.createServer({ type: 'udp4' }); - -server.on('request', (req, res) => { - const [path, query] = splitUrl(req.url); - req.query = query; // node-coap only gives us req.url on the server side - const key = `${req.method} ${path}`; - const handler = routes[key]; - - console.log(`${req.method} ${req.url} from ${req.rsinfo.address}:${req.rsinfo.port}`); - - if (!handler) return send(res, '4.04', `Not Found: ${path}`); - - try { - handler(req, res); - } catch (err) { - console.error('handler error:', err); - send(res, '5.00', 'Internal Server Error'); - } -}); - -server.on('error', (err) => console.error('server error:', err)); - -server.listen(PORT, HOST, () => { - console.log(`CoAP server (UDP) listening on coap://${HOST}:${PORT}`); - console.log(' GET /hello?name=Imran'); - console.log(' POST /hello (payload: Imran | {"name":"Imran"})'); - console.log(' GET /health'); -}); - -process.on('SIGINT', () => server.close(() => process.exit(0))); diff --git a/src/main.rs b/src/main.rs index 51fcc9a..b344299 100644 --- a/src/main.rs +++ b/src/main.rs @@ -269,6 +269,7 @@ mod tests { } #[test] + fn reads_name_from_text_and_json_payloads() { assert_eq!(name_from_payload(b"Imran").as_deref(), Some("Imran")); assert_eq!( diff --git a/static/index.html b/static/index.html index 3b6b202..b9e7d22 100644 --- a/static/index.html +++ b/static/index.html @@ -3,7 +3,7 @@ -CoAP Hello — Test Console +CoAP Hello — UDP Reference
-

CoAP Hello — Test Console

-

This page talks HTTP to a local bridge, which reissues each - call as CoAP over UDP and shows you the raw response code.

+

CoAP Hello — UDP Reference

+

This server speaks CoAP over UDP only, so there is nothing here + for a browser to call. Type a name and copy the command for the client you + use — the page is static and works straight off disk.

- -
- - - - - -
-

Or hit an endpoint directly: - /hello?name=Imran · - /health

+ + +

Bundled client — cargo build --release first

+
+
+
+
+ +

Any other CoAP client, e.g. coap-cli

+
+ +

Set COAP_HOST / COAP_PORT to point at + another server; both default to 127.0.0.1:5683.

-

Response

-

—

-
Pick an action above.
+

Routes

+ + + + + + + + + + + +
RequestCodeResponse
GET /hello2.05Hello, World!
GET /hello?name=…2.05Hello, …!
POST /hello2.01Hello, <payload>!
GET /health2.05{"status":"ok","uptime":12}
POST /hello (empty)4.00Bad Request
GET /nope4.04Not Found
PUT /hello4.05Method Not Allowed
+

CoAP writes response codes as class.detail: + 2.05 is HTTP's 200, 4.04 its 404. + POST /hello takes raw text or {"name":"…"}.

+
+ +
+

Scripts

+

Full request/response suite against a running server

+
./test-coap.sh
+

Liveness check — exit 0 healthy, 1 not

+
./healthcheck.sh -v
+

Start the server

+
cargo run --release