Salta al contenuto
Note per Studenti Esercizio - Servizio REST con JSON (sul modello della prova pratica)

Esercizio - Servizio REST con JSON (sul modello della prova pratica)

In questa pagina 6

Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD).

Scrivere in C un servizio web REST che gestisce in memoria un elenco di compiti (task), ciascuno con id, title e done, rappresentati in JSON:

operazione richiesta risposta di successo
elencare GET /tasks 200, array JSON
creare POST /tasks con {"title":"..."} 201 Created con Location: /tasks/ID e il compito in JSON
leggere GET /tasks/ID 200 oppure 404
sostituire PUT /tasks/ID con {"title":"...","done":true} 200 oppure 404
eliminare DELETE /tasks/ID 204 No Content oppure 404
metodi permessi OPTIONS 204 con l'header Allow

Gli errori sono oggetti JSON {"error":"..."} con i codici giusti: 400 (JSON o campi non validi), 404, 405 (con Allow), 411 (manca Content-Length), 413 (corpo troppo grande), 415 (Content-Type non application/json), 507 (archivio pieno). Il programma deve leggere il corpo con Content-Length, estrarre i campi dal JSON gestendo le sequenze di escape e generare JSON con l'escape corretto.

Teoria: Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →, HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding →, Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →.


Idea

Un servizio REST è un server HTTP con tre aggiunte (Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →):

  1. Instradamento: dal request-target e dal metodo si ricava l'operazione. /tasks è la collezione e /tasks/ID un elemento; il metodo dice cosa farne.
  2. Corpo con Content-Length: POST e PUT portano un corpo JSON da leggere esattamente (rb_readn), dopo aver controllato Content-Length (411/413) e Content-Type (415).
  3. JSON: estrarre title e done dal corpo, e produrre l'uscita con le stringhe correttamente escapate.

Le risposte seguono le convenzioni REST: la creazione risponde 201 e dice dove vive la nuova risorsa con Location; l'eliminazione risponde 204 senza corpo né Content-Type; gli errori sono JSON con un messaggio. Lo stato è un array statico di 64 compiti (used, id, done, title) con next_id che cresce: i dati si perdono alla chiusura del programma.

JSON in C. Non c'è una libreria standard. Le due funzioni di estrazione, json_get_string e json_get_bool, sono volutamente minime: cercano la chiave con strstr e leggono il valore. Per oggetti piatti come questi bastano, ma non validano il documento intero (per un parser vero si usano cJSON o jansson). json_get_string gestisce le sequenze di escape (\", \\, \/, \n, \t) e rifiuta i caratteri di controllo non escapati e le sequenze non gestite (\uXXXX); json_quote fa l'operazione inversa per l'uscita.

Codice

c
/* rest_server.c - piccolo servizio REST in C: risorsa /tasks (elenco di compiti) in memoria, rappresentata in JSON.
 *   GET    /tasks        -> 200, array JSON         POST   /tasks      -> 201 + Location, corpo JSON {"title":"..."}
 *   GET    /tasks/ID     -> 200 o 404               PUT    /tasks/ID   -> 200 o 404, corpo {"title":"...","done":true}
 *   DELETE /tasks/ID     -> 204 o 404               OPTIONS             -> 204 + Allow
 *
 * Compilare:  gcc -Wall -Wextra -o rest_server rest_server.c
 * Avviare:    ./rest_server 8080
 * Provare:    curl -i -X POST -H 'Content-Type: application/json' -d '{"title":"Comprare il latte"}' http://127.0.0.1:8080/tasks
 *             curl -i http://127.0.0.1:8080/tasks            curl -i http://127.0.0.1:8080/tasks/1
 *             curl -i -X PUT -H 'Content-Type: application/json' -d '{"title":"Latte","done":true}' http://127.0.0.1:8080/tasks/1
 *             curl -i -X DELETE http://127.0.0.1:8080/tasks/1
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_TASKS 64
#define MAX_TITLE 128
#define MAX_LINE 2048
#define MAX_BODY 4096

struct task {
    int used, id, done;
    char title[MAX_TITLE];
};

static struct task tasks[MAX_TASKS];
static int next_id = 1;

static int write_all(int fd, const char *buf, size_t n)
{
    while (n > 0) {
        ssize_t w = write(fd, buf, n);
        if (w < 0) {
            if (errno == EINTR)
                continue;
            return -1;
        }
        buf += w;
        n -= (size_t)w;
    }
    return 0;
}

struct rbuf {
    int fd;
    char buf[4096];
    size_t pos, len;
};

static int rb_fill(struct rbuf *r)
{
    while (r->pos == r->len) {
        ssize_t k = read(r->fd, r->buf, sizeof r->buf);
        if (k < 0 && errno == EINTR)
            continue;
        if (k <= 0)
            return -1;
        r->pos = 0;
        r->len = (size_t)k;
    }
    return 0;
}

static int rb_line(struct rbuf *r, char *line, size_t max)
{
    size_t n = 0;
    for (;;) {
        if (rb_fill(r) < 0)
            return -1;
        char c = r->buf[r->pos++];
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')
                n--;
            line[n] = '\0';
            return (int)n;
        }
        if (n + 1 >= max)
            return -2;
        line[n++] = c;
    }
}

static int rb_readn(struct rbuf *r, char *dst, size_t n)
{
    while (n > 0) {
        if (rb_fill(r) < 0)
            return -1;
        size_t take = r->len - r->pos < n ? r->len - r->pos : n;
        memcpy(dst, r->buf + r->pos, take);
        r->pos += take;
        dst += take;
        n -= take;
    }
    return 0;
}

/* ---------- JSON minimo: oggetti piatti con stringhe e booleani ---------- */

/* Cerca "key" in un oggetto JSON e copia il valore stringa (con le sequenze \" \\ \n \t \/ ) in out.
 * Torna 1 se trovato, 0 se manca la chiave, -1 se il valore non e' una stringa valida. */
static int json_get_string(const char *json, const char *key, char *out, size_t size)
{
    char pat[64];
    snprintf(pat, sizeof pat, "\"%s\"", key);
    const char *p = strstr(json, pat);
    if (p == NULL)
        return 0;
    p += strlen(pat);
    while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
        p++;
    if (*p != ':')
        return -1;
    p++;
    while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
        p++;
    if (*p != '"')
        return -1;
    p++;
    size_t o = 0;
    while (*p != '"') {
        if (*p == '\0' || (unsigned char)*p < 0x20)
            return -1;                             /* stringa non chiusa o carattere di controllo non escapato */
        char c = *p++;
        if (c == '\\') {
            switch (*p++) {
            case '"': c = '"'; break;
            case '\\': c = '\\'; break;
            case '/': c = '/'; break;
            case 'n': c = '\n'; break;
            case 't': c = '\t'; break;
            default: return -1;                    /* \uXXXX e altre sequenze: non gestite in questo esempio */
            }
        }
        if (o + 1 >= size)
            return -1;
        out[o++] = c;
    }
    out[o] = '\0';
    return 1;
}

/* Valore booleano: torna 1 se la chiave e' true, 0 se false, -1 se assente o non valido. */
static int json_get_bool(const char *json, const char *key)
{
    char pat[64];
    snprintf(pat, sizeof pat, "\"%s\"", key);
    const char *p = strstr(json, pat);
    if (p == NULL)
        return -1;
    p += strlen(pat);
    while (*p == ' ' || *p == ':' || *p == '\t')
        p++;
    if (strncmp(p, "true", 4) == 0)
        return 1;
    if (strncmp(p, "false", 5) == 0)
        return 0;
    return -1;
}

/* Scrive una stringa JSON (con le virgolette) in out, facendo l'escape di ", \ e dei caratteri di controllo. */
static size_t json_quote(const char *s, char *out, size_t size)
{
    size_t o = 0;
    if (o + 1 < size)
        out[o++] = '"';
    for (; *s && o + 7 < size; s++) {
        unsigned char c = (unsigned char)*s;
        if (c == '"' || c == '\\') {
            out[o++] = '\\';
            out[o++] = (char)c;
        } else if (c < 0x20) {
            o += (size_t)snprintf(out + o, size - o, "\\u%04x", c);
        } else {
            out[o++] = (char)c;
        }
    }
    if (o + 1 < size)
        out[o++] = '"';
    out[o] = '\0';
    return o;
}

static int task_json(const struct task *t, char *out, size_t size)
{
    char q[2 * MAX_TITLE + 16];
    json_quote(t->title, q, sizeof q);
    return snprintf(out, size, "{\"id\":%d,\"title\":%s,\"done\":%s}", t->id, q, t->done ? "true" : "false");
}

/* ---------- risposta ---------- */
static const char *reason_of(int code)
{
    switch (code) {
    case 200: return "OK";
    case 201: return "Created";
    case 204: return "No Content";
    case 400: return "Bad Request";
    case 404: return "Not Found";
    case 405: return "Method Not Allowed";
    case 411: return "Length Required";
    case 413: return "Content Too Large";
    case 415: return "Unsupported Media Type";
    case 507: return "Insufficient Storage";
    default:  return "Error";
    }
}

static void respond(int fd, int code, const char *extra, const char *json)
{
    char head[512];
    size_t blen = json ? strlen(json) : 0;
    int n = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\n", code, reason_of(code));
    if (code != 204)                               /* 204: niente corpo e niente Content-Type */
        n += snprintf(head + n, sizeof head - (size_t)n, "Content-Type: application/json\r\nContent-Length: %zu\r\n", blen);
    n += snprintf(head + n, sizeof head - (size_t)n, "%sConnection: close\r\n\r\n", extra);
    write_all(fd, head, (size_t)n);
    if (code != 204 && blen > 0)
        write_all(fd, json, blen);
}

static void respond_error(int fd, int code, const char *extra, const char *msg)
{
    char body[256];
    snprintf(body, sizeof body, "{\"error\":\"%s\"}", msg);   /* msg e' una costante senza virgolette */
    respond(fd, code, extra, body);
}

static struct task *find_task(int id)
{
    for (int i = 0; i < MAX_TASKS; i++)
        if (tasks[i].used && tasks[i].id == id)
            return &tasks[i];
    return NULL;
}

static void handle(int fd)
{
    struct rbuf in = {.fd = fd};
    char line[MAX_LINE], method[16], target[MAX_LINE], ver[16];
    if (rb_line(&in, line, sizeof line) < 0 || sscanf(line, "%15s %2047s %15s", method, target, ver) != 3 ||
        strncmp(ver, "HTTP/1.", 7) != 0) {
        respond_error(fd, 400, "", "richiesta non valida");
        return;
    }
    long clen = -1;
    int is_json = 0, chunked = 0;
    for (;;) {
        int len = rb_line(&in, line, sizeof line);
        if (len < 0) {
            respond_error(fd, 400, "", "richiesta non valida");
            return;
        }
        if (len == 0)
            break;
        char *colon = strchr(line, ':');
        if (colon == NULL)
            continue;
        *colon = '\0';
        const char *v = colon + 1;
        while (*v == ' ' || *v == '\t')
            v++;
        if (strcasecmp(line, "Content-Length") == 0)
            clen = atol(v);
        else if (strcasecmp(line, "Content-Type") == 0)
            is_json = strncasecmp(v, "application/json", 16) == 0;
        else if (strcasecmp(line, "Transfer-Encoding") == 0)
            chunked = 1;
    }

    /* riconoscimento della risorsa: /tasks oppure /tasks/ID */
    char *q = strchr(target, '?');
    if (q)
        *q = '\0';
    int id = -1, is_collection = 0;
    if (strcmp(target, "/tasks") == 0 || strcmp(target, "/tasks/") == 0) {
        is_collection = 1;
    } else if (strncmp(target, "/tasks/", 7) == 0) {
        char *end;
        long v = strtol(target + 7, &end, 10);
        if (*end != '\0' || end == target + 7 || v < 1 || v > 1000000) {
            respond_error(fd, 404, "", "risorsa inesistente");
            return;
        }
        id = (int)v;
    } else {
        respond_error(fd, 404, "", "risorsa inesistente");
        return;
    }

    /* corpo (solo per POST e PUT) */
    char body[MAX_BODY + 1] = "";
    int has_body_method = strcmp(method, "POST") == 0 || strcmp(method, "PUT") == 0;
    if (has_body_method) {
        if (chunked || clen < 0) {
            respond_error(fd, 411, "", "serve Content-Length");
            return;
        }
        if (clen > MAX_BODY) {
            respond_error(fd, 413, "", "corpo troppo grande");
            return;
        }
        if (!is_json) {
            respond_error(fd, 415, "", "serve Content-Type application/json");
            return;
        }
        if (rb_readn(&in, body, (size_t)clen) < 0) {
            respond_error(fd, 400, "", "corpo incompleto");
            return;
        }
        body[clen] = '\0';
    }

    char out[8192], one[512];
    if (strcmp(method, "OPTIONS") == 0) {
        respond(fd, 204, is_collection ? "Allow: GET, POST, OPTIONS\r\n" : "Allow: GET, PUT, DELETE, OPTIONS\r\n", NULL);
    } else if (is_collection && strcmp(method, "GET") == 0) {
        size_t o = (size_t)snprintf(out, sizeof out, "[");
        int first = 1;
        for (int i = 0; i < MAX_TASKS && o + 600 < sizeof out; i++)
            if (tasks[i].used) {
                o += (size_t)snprintf(out + o, sizeof out - o, "%s", first ? "" : ",");
                o += (size_t)task_json(&tasks[i], out + o, sizeof out - o);
                first = 0;
            }
        snprintf(out + o, sizeof out - o, "]");
        respond(fd, 200, "", out);
    } else if (is_collection && strcmp(method, "POST") == 0) {
        char title[MAX_TITLE];
        int rc = json_get_string(body, "title", title, sizeof title);
        if (rc != 1 || title[0] == '\0') {
            respond_error(fd, 400, "", "serve un campo title (stringa non vuota)");
            return;
        }
        struct task *slot = NULL;
        for (int i = 0; i < MAX_TASKS && slot == NULL; i++)
            if (!tasks[i].used)
                slot = &tasks[i];
        if (slot == NULL) {
            respond_error(fd, 507, "", "archivio pieno");
            return;
        }
        slot->used = 1;
        slot->id = next_id++;
        slot->done = 0;
        snprintf(slot->title, sizeof slot->title, "%s", title);
        task_json(slot, one, sizeof one);
        char extra[64];
        snprintf(extra, sizeof extra, "Location: /tasks/%d\r\n", slot->id);   /* dove vive la nuova risorsa */
        respond(fd, 201, extra, one);
    } else if (!is_collection && strcmp(method, "GET") == 0) {
        struct task *t = find_task(id);
        if (t == NULL) {
            respond_error(fd, 404, "", "compito inesistente");
            return;
        }
        task_json(t, one, sizeof one);
        respond(fd, 200, "", one);
    } else if (!is_collection && strcmp(method, "PUT") == 0) {
        struct task *t = find_task(id);
        if (t == NULL) {
            respond_error(fd, 404, "", "compito inesistente");
            return;
        }
        char title[MAX_TITLE];
        int rc = json_get_string(body, "title", title, sizeof title);
        int done = json_get_bool(body, "done");
        if (rc != 1 || title[0] == '\0' || done < 0) {   /* PUT sostituisce TUTTA la risorsa: servono entrambi i campi */
            respond_error(fd, 400, "", "servono title e done");
            return;
        }
        snprintf(t->title, sizeof t->title, "%s", title);
        t->done = done;
        task_json(t, one, sizeof one);
        respond(fd, 200, "", one);
    } else if (!is_collection && strcmp(method, "DELETE") == 0) {
        struct task *t = find_task(id);
        if (t == NULL) {
            respond_error(fd, 404, "", "compito inesistente");
            return;
        }
        t->used = 0;
        respond(fd, 204, "", NULL);
    } else {
        respond_error(fd, 405, is_collection ? "Allow: GET, POST, OPTIONS\r\n" : "Allow: GET, PUT, DELETE, OPTIONS\r\n",
                      "metodo non permesso");
    }
    fprintf(stderr, "%s %s\n", method, target);
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "uso: %s porta\n", argv[0]);
        return 1;
    }
    signal(SIGPIPE, SIG_IGN);
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        return 1;
    }
    int yes = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons((unsigned short)atoi(argv[1]));
    if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0 || listen(lfd, 16) < 0) {
        perror("bind/listen");
        return 1;
    }
    for (;;) {
        int cfd = accept(lfd, NULL, NULL);
        if (cfd < 0) {
            if (errno == EINTR)
                continue;
            perror("accept");
            break;
        }
        handle(cfd);
        close(cfd);
    }
    return 0;
}

Compilare e provare

$ gcc -Wall -Wextra -o rest_server rest_server.c
$ ./rest_server 8080 &

Una sessione completa con curl (-i mostra gli header; -d invia un corpo e imposta Content-Type: application/x-www-form-urlencoded, quindi per il JSON si specifica l'header):

$ curl -i -X POST -H 'Content-Type: application/json' -d '{"title":"Comprare il latte"}' http://127.0.0.1:8080/tasks
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 49
Location: /tasks/1
Connection: close

{"id":1,"title":"Comprare il latte","done":false}

$ curl -i http://127.0.0.1:8080/tasks
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 51

[{"id":1,"title":"Comprare il latte","done":false}]

$ curl -i http://127.0.0.1:8080/tasks/9
HTTP/1.1 404 Not Found
Content-Length: 31

{"error":"compito inesistente"}

$ curl -i -X PUT -H 'Content-Type: application/json' -d '{"title":"Latte","done":true}' http://127.0.0.1:8080/tasks/1
HTTP/1.1 200 OK
Content-Length: 36

{"id":1,"title":"Latte","done":true}

$ curl -i -X DELETE http://127.0.0.1:8080/tasks/1
HTTP/1.1 204 No Content
Connection: close

$ curl -i -X OPTIONS http://127.0.0.1:8080/tasks
HTTP/1.1 204 No Content
Allow: GET, POST, OPTIONS

Gli errori (le risposte sono quelle del programma):

$ curl -i -X PUT -H 'Content-Type: application/json' -d '{"title":"Latte"}' http://127.0.0.1:8080/tasks/1     # manca done
HTTP/1.1 400 Bad Request ... {"error":"servono title e done"}
$ curl -i -X POST -H 'Content-Type: application/json' -d '{"nome":"x"}' http://127.0.0.1:8080/tasks            # manca title
HTTP/1.1 400 Bad Request ... {"error":"serve un campo title (stringa non vuota)"}
$ curl -i -X POST -H 'Content-Type: text/plain' -d '{"title":"x"}' http://127.0.0.1:8080/tasks                # tipo sbagliato
HTTP/1.1 415 Unsupported Media Type ... {"error":"serve Content-Type application/json"}
$ curl -i -X PATCH http://127.0.0.1:8080/tasks/2                                                                 # metodo non permesso
HTTP/1.1 405 Method Not Allowed
Allow: GET, PUT, DELETE, OPTIONS
$ curl -i http://127.0.0.1:8080/altro                                                                            # risorsa inesistente
HTTP/1.1 404 Not Found ... {"error":"risorsa inesistente"}

Un titolo con virgolette e a-capo ('{"title":"Dire \"ciao\"\n e \\ ok"}') viene letto correttamente e restituito con l'escape: {"id":1,"title":"Dire \"ciao\"\u000a e \\ ok","done":false} (il carattere di controllo \n esce come \u000a, valido in JSON).

Spiegazione dei punti chiave

handle: lettura della richiesta. Come negli altri server: request line con sscanf e controllo della versione (400), header con rb_line; si memorizzano Content-Length, Content-Type (vero se inizia per application/json, senza distinguere maiuscole) e la presenza di Transfer-Encoding.

Instradamento. Si toglie la query (?...). strcmp(target, "/tasks") (o con / finale) è la collezione; /tasks/ seguito da un numero è un elemento: strtol(target + 7, &end, 10) con controllo *end != '\0' (il percorso deve finire subito dopo il numero) e intervallo valido; altro → 404. Il metodo viene dopo: GET, POST, PUT, DELETE, OPTIONS; per ogni altra combinazione 405 con l'Allow della risorsa.

Il corpo di POST e PUT (ordine dei controlli).

  1. chunked o Content-Length assente → 411 Length Required;
  2. Content-Length > MAX_BODY (4096) → 413;
  3. Content-Type diverso da application/json → 415;
  4. rb_readn legge esattamente Content-Length byte (anche quelli già nel buffer); se la connessione cade → 400; si termina il buffer con '\0'.

Il limite sulla dimensione è fondamentale: senza, un Content-Length enorme farebbe allocare memoria o attendere all'infinito.

json_get_string. Cerca "chiave" con strstr, salta gli spazi, richiede :, salta gli spazi, richiede ", poi copia carattere per carattere fino alla " finale:

  • un \ introduce una sequenza di escape: \" e \\ e \/ e \n e \t si convertono, qualunque altra (per esempio è) fa fallire la funzione (limite dichiarato);
  • un carattere sotto 0x20 non escapato, o la fine della stringa C prima della ", → errore (JSON non valido);
  • se il valore non entra nel buffer (o + 1 >= size) → errore, non troncamento silenzioso.

Restituisce 1 se trovato, 0 se la chiave manca, -1 se il valore non è una stringa valida. Limite: strstr non conosce la struttura: {"nota":"il title è qui","title":"x"} troverebbe "title" dentro il valore di nota. Per dati non fidati serve un parser vero.

json_get_bool cerca true o false dopo i due punti; per PUT (che sostituisce l'intera risorsa) servono entrambi i campi: se manca uno → 400.

json_quote. Scrive la stringa fra virgolette facendo l'escape di " e \ e dei caratteri di controllo (\u00XX con %04x): è ciò che rende valido il JSON generato. Dimenticarlo permette di rompere la struttura (o iniettare campi) con un titolo come x","done":true,"y":".

respond. Compone la status-line con reason_of; per ogni codice diverso da 204 aggiunge Content-Type: application/json e Content-Length (la lunghezza del corpo, che è sempre nota: niente chunked); gli header supplementari (Location, Allow) vengono da extra, già terminati da \r\n. Per 204 non c'è né corpo né Content-Type né Content-Length: la risposta finisce con la riga vuota.

Le operazioni.

  • POST: estrae title (non vuoto) → 400 altrimenti; cerca uno slot libero (507 se l'archivio è pieno); assegna id = next_id++, done = 0; risponde 201 con Location: /tasks/ID e il JSON del nuovo compito. L'identificatore lo sceglie il server.
  • GET /tasks: costruisce l'array scorrendo gli slot usati, con la virgola fra gli elementi.
  • GET /tasks/ID, PUT, DELETE: find_task(id); 404 se non esiste. DELETE azzera used e risponde 204; ripetuta, dà 404 (l'effetto sul server è lo stesso: idempotenza).

Dove sta REST. Gli URI indicano nomi (/tasks/1, non /leggiCompito?id=1); i metodi hanno il loro significato (GET sicuro e idempotente, PUT idempotente, POST non idempotente); i codici di stato comunicano l'esito (201 + Location, 204, 404, 415); non c'è stato di sessione sul server (ogni richiesta è completa).

Errori tipici

  • Rispondere 200 per tutto, anche per gli errori, con l'errore nel corpo.
  • POST che risponde 200 senza Location, o DELETE che risponde 200 con un corpo vuoto senza Content-Length; con 204 non si invia il corpo.
  • Non controllare Content-Length: leggere più o meno byte del corpo desincronizza la connessione.
  • Non limitare la dimensione del corpo.
  • Non verificare Content-Type, e interpretare come JSON un corpo qualsiasi.
  • Generare JSON concatenando stringhe senza escape (injection nel JSON, JSON non valido).
  • Interpretare JSON con strstr su dati non fidati (confusione fra chiavi e valori).
  • Usare PUT per modifiche parziali: sostituisce tutta la risorsa.
  • Verbi negli URI (/creaCompito) o metodi usati male (GET che modifica dati).
  • Buffer fissi senza controllo di lunghezza nelle copie di title.

Varianti per esercitarsi

Versione ripasso

  • Testo. Risorsa /tasks in memoria (id, title, done) in JSON: GET /tasks (200), POST (201 + Location: /tasks/ID), GET /tasks/ID (200/404), PUT (200/404, sostituisce tutto), DELETE (204/404), OPTIONS (204 + Allow); errori JSON {"error":"..."}: 400, 404, 405 + Allow, 411, 413, 415, 507.
  • Instradamento. Query tolta; /tasks = collezione; /tasks/ID con strtol e *end == '\0' = elemento; altro 404; combinazione metodo/risorsa non prevista -> 405 con Allow (collezione: GET, POST, OPTIONS; elemento: GET, PUT, DELETE, OPTIONS).
  • Corpo di POST/PUT. (1) chunked o niente Content-Length -> 411; (2) > MAX_BODY (4096) -> 413; (3) Content-Type non application/json -> 415; (4) rb_readn di esattamente Content-Length byte, poi '\0'.
  • json_get_string. strstr("\"chiave\""), spazi, :, spazi, "; copia con escape (\" \\ \/ \n \t); altre sequenze (\uXXXX), caratteri di controllo non escapati, stringa non chiusa, buffer pieno -> errore (-1); 0 se la chiave manca. Non è un parser: strstr può trovare la chiave dentro un valore. json_get_bool: true/false dopo i due punti; PUT richiede title e done.
  • json_quote. Virgolette, escape di ", \ e dei caratteri sotto 0x20 (\u%04x): necessario perché il JSON generato sia valido e non iniettabile.
  • respond. Status-line; per ogni codice tranne 204: Content-Type: application/json e Content-Length; extra per Location e Allow; 204 = nessun corpo, Content-Type né Content-Length.
  • Operazioni. POST: title non vuoto (400), slot libero (507), id = next_id++, done = 0, 201 + Location. GET collezione: array con virgole. PUT: titolo e done obbligatori. DELETE: used = 0, 204; ripetuta 404 (idempotente).
  • REST. URI = nomi; metodi con il loro significato (GET sicuro, PUT/DELETE idempotenti, POST no); codici di stato come contratto; nessuna sessione sul server.
  • Prove. curl -i -X POST -H 'Content-Type: application/json' -d '{"title":"Comprare il latte"}' http://127.0.0.1:8080/tasks -> 201, Location: /tasks/1, corpo di 49 byte; PUT -> 200; DELETE -> 204; -H 'Content-Type: text/plain' -> 415; PATCH -> 405; titolo con " e \n restituito con escape (\u000a).
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static int json_get_string(const char *json, const char *key, char *out, size_t size)
{
    char pat[64];
    snprintf(pat, sizeof pat, "\"%s\"", key);
    const char *p = strstr(json, pat);
    if (p == NULL)
        return 0;
    p += strlen(pat);
    while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
        p++;
    if (*p != ':')
        return -1;
    p++;
    while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
        p++;
    if (*p != '"')
        return -1;
    p++;
    size_t o = 0;
    while (*p != '"') {
        if (*p == '\0' || (unsigned char)*p < 0x20)
            return -1;
        char c = *p++;
        if (c == '\\') {
            switch (*p++) {
            case '"': c = '"'; break;
            case '\\': c = '\\'; break;
            case '/': c = '/'; break;
            case 'n': c = '\n'; break;
            case 't': c = '\t'; break;
            default: return -1;
            }
        }
        if (o + 1 >= size)
            return -1;
        out[o++] = c;
    }
    out[o] = '\0';
    return 1;
}

static size_t json_quote(const char *s, char *out, size_t size)
{
    size_t o = 0;
    if (o + 1 < size)
        out[o++] = '"';
    for (; *s && o + 7 < size; s++) {
        unsigned char c = (unsigned char)*s;
        if (c == '"' || c == '\\') {
            out[o++] = '\\';
            out[o++] = (char)c;
        } else if (c < 0x20) {
            o += (size_t)snprintf(out + o, size - o, "\\u%04x", c);
        } else {
            out[o++] = (char)c;
        }
    }
    if (o + 1 < size)
        out[o++] = '"';
    out[o] = '\0';
    return o;
}

static void respond(int fd, int code, const char *extra, const char *json)
{
    char head[512];
    size_t blen = json ? strlen(json) : 0;
    int n = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\n", code, reason_of(code));
    if (code != 204)
        n += snprintf(head + n, sizeof head - (size_t)n, "Content-Type: application/json\r\nContent-Length: %zu\r\n", blen);
    n += snprintf(head + n, sizeof head - (size_t)n, "%sConnection: close\r\n\r\n", extra);
    write_all(fd, head, (size_t)n);
    if (code != 204 && blen > 0)
        write_all(fd, json, blen);
}
c
static int task_json(const struct task *t, char *out, size_t size)
{
    char q[2 * MAX_TITLE + 16];
    json_quote(t->title, q, sizeof q);
    return snprintf(out, size, "{\"id\":%d,\"title\":%s,\"done\":%s}", t->id, q, t->done ? "true" : "false");
}

static struct task *find_task(int id)
{
    for (int i = 0; i < MAX_TASKS; i++)
        if (tasks[i].used && tasks[i].id == id)
            return &tasks[i];
    return NULL;
}
  • Errori tipici: sempre 200; POST senza 201/Location; 204 con corpo; Content-Length non controllato o corpo senza limite; Content-Type ignorato; JSON generato senza escape; strstr su dati non fidati; PUT per modifiche parziali; verbi negli URI; stato in memoria creduto condiviso fra processi fork.

Teoria collegata