146 lines
5.6 KiB
Markdown
146 lines
5.6 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
|
|
```
|
|
|
|
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 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 use 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` | CoAP reference page: routes, codes, copyable commands. |
|
|
| `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.
|