Browsers speak HTTP over TCP and cannot open coap:// URLs, which made the server awkward to try without the CLI client. The bridge accepts an ordinary HTTP request, reissues it as CoAP over UDP, and maps the response code back onto an HTTP status (the raw code is also returned in X-Coap-Code). It serves a test console at / with a name field and a button per route, so GET, POST, 4.04 and 4.05 can all be exercised by clicking. The CoAP request logic moves from src/bin/client.rs into src/lib.rs so the client and the bridge share one implementation. The bridge is a development convenience: a small HTTP/1.1 subset with no keep-alive, chunked bodies or TLS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
5.7 KiB
Markdown
147 lines
5.7 KiB
Markdown
# CoAP Hello (UDP) — Rust MVP
|
|
|
|
A minimal CoAP server over UDP in Rust, built on [`coap-lite`](https://crates.io/crates/coap-lite)
|
|
(the RFC 7252 binary codec) and a [`tokio`](https://tokio.rs) `UdpSocket`.
|
|
|
|
CoAP is "HTTP semantics on a UDP datagram" — the same methods, paths and query
|
|
strings, but each exchange is one compact binary datagram. Response codes are
|
|
written `class.detail`: `2.05 Content`, `2.01 Created`, `4.00 Bad Request`,
|
|
`4.04 Not Found`, `4.05 Method Not Allowed`.
|
|
|
|
`coap-lite` does message encoding only — the socket, the read loop and the
|
|
routing are all in `src/main.rs`, so nothing about the UDP layer is hidden.
|
|
|
|
## Run
|
|
|
|
```bash
|
|
cargo run --release
|
|
```
|
|
|
|
Listens on `udp://0.0.0.0:5683` (CoAP's IANA-assigned port).
|
|
|
|
## Routes
|
|
|
|
| Method | Path | Response |
|
|
|--------|---------------------|--------------------------------------|
|
|
| GET | `/hello` | `2.05` `Hello, World!` |
|
|
| GET | `/hello?name=Imran` | `2.05` `Hello, Imran!` |
|
|
| POST | `/hello` | `2.01` `Hello, <payload>!` |
|
|
| GET | `/health` | `2.05` `{"status":"ok","uptime":12}` |
|
|
|
|
`POST /hello` takes either a raw text payload (`Imran`) or JSON
|
|
(`{"name":"Imran"}`). An empty payload returns `4.00`. An unknown path returns
|
|
`4.04`; a known path with the wrong method returns `4.05`.
|
|
|
|
## Check it in a browser
|
|
|
|
Browsers speak HTTP over TCP; this server speaks CoAP over UDP, so `coap://`
|
|
URLs cannot be opened directly. `src/bin/bridge.rs` is a small HTTP -> CoAP
|
|
proxy that closes the gap. Run both:
|
|
|
|
```bash
|
|
cargo run --release --bin coap-hello-udp
|
|
```
|
|
|
|
```bash
|
|
cargo run --release --bin bridge
|
|
```
|
|
|
|
Then open <http://localhost:8080/> 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.
|
|
|
|
These are now ordinary URLs — clickable, curl-able, bookmarkable:
|
|
|
|
| URL | Response |
|
|
|--------------------------------------------------|------------------------------|
|
|
| `http://localhost:8080/` | the test console |
|
|
| `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:
|
|
|
|
```bash
|
|
curl -X POST -d "Imran" http://localhost:8080/hello
|
|
```
|
|
|
|
The bridge is a development convenience: a deliberately small HTTP/1.1 subset
|
|
with no keep-alive, no chunked bodies and no TLS. Do not expose it. Set
|
|
`HTTP_PORT` if 8080 is taken.
|
|
|
|
## Try it from the CLI
|
|
|
|
With the bundled client:
|
|
|
|
```bash
|
|
cargo run --bin client -- get "/hello?name=Imran"
|
|
```
|
|
|
|
```bash
|
|
cargo run --bin client -- post /hello "Imran"
|
|
```
|
|
|
|
Or with any other CoAP client, e.g. [`coap-cli`](https://www.npmjs.com/package/coap-cli):
|
|
|
|
```bash
|
|
coap get "coap://localhost:5683/hello?name=Imran"
|
|
```
|
|
|
|
## Test
|
|
|
|
```bash
|
|
cargo test
|
|
```
|
|
|
|
Five unit tests cover the greeting, payload parsing (text + JSON) and query
|
|
decoding.
|
|
|
|
## Layout
|
|
|
|
| Path | What it is |
|
|
|---------------------|---------------------------------------------------------|
|
|
| `src/main.rs` | The server: socket, read loop, route table, helpers. |
|
|
| `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 `/`. |
|
|
| `nodejs/` | The earlier Node.js version, kept for reference. |
|
|
|
|
## Config
|
|
|
|
| Env var | Default | Used by |
|
|
|-------------|-----------|----------------|
|
|
| `COAP_PORT` | `5683` | server, client, bridge |
|
|
| `COAP_HOST` | `0.0.0.0` | server, client, bridge |
|
|
| `HTTP_PORT` | `8080` | bridge |
|
|
|
|
## Design notes
|
|
|
|
- **One task, no per-request spawn.** Handling a datagram is pure CPU work with
|
|
no `.await` inside, so it runs inline on the receive loop — no task-spawn
|
|
overhead, and replies keep their arrival order. Spawn per datagram once a
|
|
handler does real I/O (a database, an upstream call).
|
|
- **A fixed 1152-byte receive buffer**, reused across requests, so the steady
|
|
state allocates only for the response body. 1152 is the RFC 7252 guidance for
|
|
staying inside an unfragmented IPv6 datagram.
|
|
- **Hand-rolled JSON field read.** `json_string_field` handles a flat
|
|
`{"name":"..."}` and nothing more; it exists to avoid a `serde_json`
|
|
dependency for one field. Swap it out the moment bodies get real structure.
|
|
- **Lenient percent-decoding.** RFC 7252 §6.4 says the *client* decodes before
|
|
splitting a URI into options, so a strict client sends `name=Md Imran`
|
|
already decoded. Not all clients do, so the server decodes again — meaning a
|
|
literal `%` in a name would be misread. Drop `percent_decode` if your clients
|
|
are known-conformant.
|
|
|
|
## Next steps
|
|
|
|
- **DTLS.** Plaintext CoAP has no encryption or authentication. Production uses
|
|
`coaps://` on port 5684 — see `webrtc-dtls` or a gateway like `libcoap`.
|
|
- **Block-wise transfer** (RFC 7959) for payloads past ~1 KB; `coap-lite` ships
|
|
a `BlockHandler` for this.
|
|
- **Observe** (RFC 7641) for server-pushed updates; `coap-lite` provides
|
|
`Subject` and `create_notification`.
|
|
- **Deduplication.** A CoAP server should track recent message IDs and replay
|
|
the cached response, since UDP clients retransmit. Fine to skip for
|
|
idempotent greetings, needed once a POST has side effects.
|