/***************************************************************************** * message.c: HTTP request/response ***************************************************************************** * Copyright (C) 2015 Rémi Denis-Courmont * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "message.h" #include "h2frame.h" struct vlc_http_msg { short status; char *method; char *scheme; char *authority; char *path; char *(*headers)[2]; unsigned count; struct vlc_http_stream *payload; }; /* Maximum alignment for safe conversion to/from any specific pointer type */ static const char alignas (max_align_t) vlc_http_error_loc; void *const vlc_http_error = (char *)&vlc_http_error_loc; static bool vlc_http_is_token(const char *); static ssize_t vlc_http_msg_find_header(const struct vlc_http_msg *m, const char *name) { for (unsigned i = 0; i < m->count; i++) if (!strcasecmp(m->headers[i][0], name)) return i; return -1; } static int vlc_http_msg_vadd_header(struct vlc_http_msg *m, const char *name, const char *fmt, va_list ap) { if (!vlc_http_is_token(name)) { /* Not a valid field name, i.e. not an HTTP token */ errno = EINVAL; return -1; } char *value; int len = vasprintf(&value, fmt, ap); if (unlikely(len < 0)) return -1; /* IETF RFC7230 §3.2.4 */ for (char *p = value; *p; p++) if (*p == '\r' || *p == '\n') *p = ' '; /* Discard leading OWS */ size_t crop = strspn(value, "\t "); if (crop > 0) { assert((unsigned)len >= crop); memmove(value, value + crop, len - crop + 1); len -= crop; } /* Discard trailing OWS */ while (len > 0 && (value[len - 1] == '\t' || value[len - 1] == ' ')) value[--len] = '\0'; /* Fold identically named header field values. This is unfortunately not * possible for Set-Cookie, while Cookie requires a special separator. */ ssize_t idx = vlc_http_msg_find_header(m, name); if (idx >= 0 && strcasecmp(name, "Set-Cookie")) { char *merged; char sep = strcasecmp(name, "Cookie") ? ',' : ';'; int val = asprintf(&merged, "%s%c %s", m->headers[idx][1], sep, value); free(value); if (unlikely(val == -1)) return -1; free(m->headers[idx][1]); m->headers[idx][1] = merged; return 0; } char *(*h)[2] = realloc(m->headers, sizeof (char *[2]) * (m->count + 1)); if (unlikely(h == NULL)) { free(value); return -1; } m->headers = h; h += m->count; h[0][0] = strdup(name); if (unlikely(h[0][0] == NULL)) { free(value); return -1; } h[0][1] = value; m->count++; return 0; } int vlc_http_msg_add_header(struct vlc_http_msg *m, const char *name, const char *fmt, ...) { va_list ap; int ret; va_start(ap, fmt); ret = vlc_http_msg_vadd_header(m, name, fmt, ap); va_end(ap); return ret; } const char *vlc_http_msg_get_header(const struct vlc_http_msg *m, const char *name) { ssize_t idx = vlc_http_msg_find_header(m, name); if (idx < 0) { errno = ENOENT; return NULL; } return m->headers[idx][1]; } int vlc_http_msg_get_status(const struct vlc_http_msg *m) { return m->status; } const char *vlc_http_msg_get_method(const struct vlc_http_msg *m) { return m->method; } const char *vlc_http_msg_get_scheme(const struct vlc_http_msg *m) { return m->scheme; } const char *vlc_http_msg_get_authority(const struct vlc_http_msg *m) { return m->authority; } const char *vlc_http_msg_get_path(const struct vlc_http_msg *m) { return m->path; } void vlc_http_msg_destroy(struct vlc_http_msg *m) { if (m->payload != NULL) vlc_http_stream_close(m->payload, false); for (unsigned i = 0; i < m->count; i++) { free(m->headers[i][0]); free(m->headers[i][1]); } free(m->headers); free(m->path); free(m->authority); free(m->scheme); free(m->method); free(m); } struct vlc_http_msg * vlc_http_req_create(const char *method, const char *scheme, const char *authority, const char *path) { struct vlc_http_msg *m = malloc(sizeof (*m)); if (unlikely(m == NULL)) return NULL; assert(method != NULL); m->status = -1; m->method = strdup(method); m->scheme = (scheme != NULL) ? strdup(scheme) : NULL; m->authority = (authority != NULL) ? strdup(authority) : NULL; m->path = (path != NULL) ? strdup(path) : NULL; m->count = 0; m->headers = NULL; m->payload = NULL; if (unlikely(m->method == NULL || (scheme != NULL && m->scheme == NULL) || (authority != NULL && m->authority == NULL) || (path != NULL && m->path == NULL))) { vlc_http_msg_destroy(m); m = NULL; } return m; } struct vlc_http_msg *vlc_http_resp_create(unsigned status) { struct vlc_http_msg *m = malloc(sizeof (*m)); if (unlikely(m == NULL)) return NULL; assert(status < 1000); m->status = status; m->method = NULL; m->scheme = NULL; m->authority = NULL; m->path = NULL; m->count = 0; m->headers = NULL; m->payload = NULL; return m; } void vlc_http_msg_attach(struct vlc_http_msg *m, struct vlc_http_stream *s) { assert(m->payload == NULL); m->payload = s; } struct vlc_http_msg *vlc_http_msg_iterate(struct vlc_http_msg *m) { struct vlc_http_msg *next = vlc_http_stream_read_headers(m->payload); if (next != NULL) m->payload = NULL; vlc_http_msg_destroy(m); return next; } struct vlc_http_msg *vlc_http_msg_get_initial(struct vlc_http_stream *s) { struct vlc_http_msg *m = vlc_http_stream_read_headers(s); if (m == NULL) vlc_http_stream_close(s, false); return m; } struct vlc_http_msg *vlc_http_msg_get_final(struct vlc_http_msg *m) { while (m != NULL && (vlc_http_msg_get_status(m) / 100) == 1) m = vlc_http_msg_iterate(m); return m; } block_t *vlc_http_msg_read(struct vlc_http_msg *m) { if (m->payload == NULL) return NULL; return vlc_http_stream_read(m->payload); } /* Serialization and deserialization */ char *vlc_http_msg_format(const struct vlc_http_msg *m, size_t *restrict lenp, bool proxied) { struct vlc_memstream stream; vlc_memstream_open(&stream); if (m->status < 0) { vlc_memstream_printf(&stream, "%s ", m->method); if (proxied) vlc_memstream_printf(&stream, "%s://%s", m->scheme, m->authority); vlc_memstream_printf(&stream, "%s HTTP/1.1\r\nHost: %s\r\n", m->path ? m->path : m->authority, m->authority); } else vlc_memstream_printf(&stream, "HTTP/1.1 %03hd .\r\n", m->status); for (unsigned i = 0; i < m->count; i++) vlc_memstream_printf(&stream, "%s: %s\r\n", m->headers[i][0], m->headers[i][1]); vlc_memstream_puts(&stream, "\r\n"); if (vlc_memstream_close(&stream)) return NULL; if (lenp != NULL) *lenp = stream.length; return stream.ptr; } struct vlc_http_msg *vlc_http_msg_headers(const char *msg) { struct vlc_http_msg *m; unsigned short code; if (sscanf(msg, "HTTP/1.%*1u %3hu %*s", &code) == 1) { m = vlc_http_resp_create(code); if (unlikely(m == NULL)) return NULL; } else return NULL; /* TODO: request support */ msg = strstr(msg, "\r\n"); if (msg == NULL) goto error; while (strcmp(msg + 2, "\r\n")) { const char *eol = msg; do { eol = strstr(eol + 2, "\r\n"); if (eol == NULL) goto error; } /* Deal with legacy obs-fold (i.e. multi-line header) */ while (eol[2] == ' ' || eol[2] == '\t'); msg += 2; /* skip CRLF */ const char *colon = memchr(msg, ':', eol - msg); if (colon == NULL || colon == msg) goto error; char *name = strndup(msg, colon - msg); if (unlikely(name == NULL)) goto error; colon++; colon += strspn(colon, " \t"); if (unlikely(vlc_http_msg_add_header(m, name, "%.*s", (int)(eol - colon), colon))) { free(name); goto error; } free(name); msg = eol; } return m; error: vlc_http_msg_destroy(m); return NULL; } struct vlc_h2_frame *vlc_http_msg_h2_frame(const struct vlc_http_msg *m, uint_fast32_t stream_id, bool eos) { for (unsigned j = 0; j < m->count; j++) { /* No HTTP 1 specific headers */ assert(strcasecmp(m->headers[j][0], "Connection")); assert(strcasecmp(m->headers[j][0], "Upgrade")); assert(strcasecmp(m->headers[j][0], "HTTP2-Settings")); } const char *(*headers)[2] = vlc_alloc(m->count + 5, sizeof (char *[2])); if (unlikely(headers == NULL)) return NULL; struct vlc_h2_frame *f; unsigned i = 0; char status[4]; if (m->status >= 0) { assert(m->status < 1000); sprintf(status, "%hd", m->status); headers[i][0] = ":status"; headers[i][1] = status; i++; } if (m->method != NULL) { headers[i][0] = ":method"; headers[i][1] = m->method; i++; } if (m->scheme != NULL) { headers[i][0] = ":scheme"; headers[i][1] = m->scheme; i++; } if (m->authority != NULL) { headers[i][0] = ":authority"; headers[i][1] = m->authority; i++; } if (m->path != NULL) { headers[i][0] = ":path"; headers[i][1] = m->path; i++; } if (m->count > 0) { memcpy(headers + i, m->headers, m->count * sizeof (*headers)); i += m->count; } f = vlc_h2_frame_headers(stream_id, VLC_H2_DEFAULT_MAX_FRAME, eos, i, headers); free(headers); return f; } struct vlc_http_msg *vlc_http_msg_h2_headers(unsigned n, const char *const hdrs[][2]) { struct vlc_http_msg *m = vlc_http_resp_create(0); if (unlikely(m == NULL)) return NULL; for (unsigned i = 0; i < n; i++) { const char *name = hdrs[i][0]; const char *value = hdrs[i][1]; /* NOTE: HPACK always returns lower-case names, so strcmp() is fine. */ if (!strcmp(name, ":status")) { char *end; unsigned long status = strtoul(value, &end, 10); if (m->status != 0 || status > 999 || *end != '\0') goto error; /* Not a three decimal digits status! */ m->status = status; continue; } if (!strcmp(name, ":method")) { if (m->method != NULL) goto error; m->method = strdup(value); if (unlikely(m->method == NULL)) goto error; m->status = -1; /* this is a request */ continue; } if (!strcmp(name, ":scheme")) { if (m->scheme != NULL) goto error; m->scheme = strdup(value); if (unlikely(m->scheme == NULL)) goto error; continue; } if (!strcmp(name, ":authority")) { if (m->authority != NULL) goto error; m->authority = strdup(value); if (unlikely(m->authority == NULL)) goto error; continue; } if (!strcmp(name, ":path")) { if (m->path != NULL) goto error; m->path = strdup(value); if (unlikely(m->path == NULL)) goto error; continue; } if (vlc_http_msg_add_header(m, name, "%s", value)) goto error; } if ((m->status < 0) == (m->method == NULL)) { /* Must be either a request or response. Not both, not neither. */ error: vlc_http_msg_destroy(m); m = NULL; } return m; } /* Header helpers */ static int vlc_http_istoken(int c) { /* IETF RFC7230 §3.2.6 */ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c && strchr("!#$%&'*+-.^_`|~", c) != NULL); } static int vlc_http_isctext(int c) { /* IETF RFC7230 §3.2.6 */ return (c == '\t') || (c == ' ') || (c >= 0x21 && c <= 0x27) || (c >= 0x2A && c <= 0x5B) || (c >= 0x5D && c <= 0x7E) || (c >= 0x80); } static size_t vlc_http_token_length(const char *str) { size_t i = 0; while (vlc_http_istoken(str[i])) i++; return i; } static size_t vlc_http_quoted_length(const char *str) { size_t i = 0; unsigned char c; if (str[i++] != '"') return 0; do { c = str[i++]; if (c == '\0') return 0; if (c == '\\') /* Quoted pair */ { unsigned char q = str[i++]; if (q < 32 && q != '\t') return 0; } } while (c != '"'); return i; } static bool vlc_http_is_token(const char *str) { size_t len = vlc_http_token_length(str); return len > 0 && str[len] == '\0'; } const char *vlc_http_next_token(const char *value) { /* We handle either token or token = token / quoted-string */ value += strcspn(value, ",\""); if (!*value) return NULL; value += vlc_http_quoted_length(value); return value + strspn(value, "\t ,"); } static const char *vlc_http_get_token(const char *value, const char *token) { const size_t length = strlen(token); while (value != NULL) { if (vlc_http_token_length(value) == length && !strncasecmp(token, value, length)) return value; value = vlc_http_next_token(value); } return NULL; } static char *vlc_http_get_token_value(const char *value, const char *token) { value = vlc_http_get_token(value, token); if (value == NULL) return NULL; value += vlc_http_token_length(value); value += strspn(value, " \t"); /* BWS */ if (*value != '=') return NULL; value++; value += strspn(value, " \t"); /* BWS */ size_t len = vlc_http_quoted_length(value); if (len == 0) return NULL; assert(len >= 2); value++; len -= 2; char *out = malloc(len + 1), *p; if (unlikely(out == NULL)) return NULL; for (p = out; len > 0; len--) { char c = *(value++); if (c == '\\') /* Quoted pair */ { c = *(value++); len--; } *(p++) = c; } *p = '\0'; return out; } const char *vlc_http_msg_get_token(const struct vlc_http_msg *msg, const char *field, const char *token) { return vlc_http_get_token(vlc_http_msg_get_header(msg, field), token); } static size_t vlc_http_comment_length(const char *str) { /* IETF RFC7230 §3.2.6 */ if (*str != '(') return 0; size_t i = 1; for (size_t nested = 1; nested > 0; i++) { unsigned char c = str[i]; if (c == ')') nested--; else if (c == '(') /* Nested comment */ nested++; else if (c == '\\') /* Quoted pair */ { i++; if (str[i] < 32) return 0; } else if (!vlc_http_isctext(c)) return 0; } return i; } static bool vlc_http_is_agent(const char *s) { /* IETF RFC7231 §5.5.3 and §7.4.2 */ if (!vlc_http_istoken(*s)) return false; for (;;) { size_t l = vlc_http_token_length(s); if (l != 0) /* product */ { if (s[l] == '/') /* product version */ { s += l + 1; l = vlc_http_token_length(s); } } else l = vlc_http_comment_length(s); if (l == 0) break; s += l; if (*s == '\0') return true; l = strspn(s, "\t "); /* RWS */ if (l == 0) break; s += l; } return false; } int vlc_http_msg_add_agent(struct vlc_http_msg *m, const char *str) { const char *hname = (m->status < 0) ? "User-Agent" : "Server"; if (!vlc_http_is_agent(str)) { errno = EINVAL; return -1; } return vlc_http_msg_add_header(m, hname, "%s", str); } const char *vlc_http_msg_get_agent(const struct vlc_http_msg *m) { const char *hname = (m->status < 0) ? "User-Agent" : "Server"; const char *str = vlc_http_msg_get_header(m, hname); return (str != NULL && vlc_http_is_agent(str)) ? str : NULL; } static const char vlc_http_days[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static const char vlc_http_months[12][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; int vlc_http_msg_add_time(struct vlc_http_msg *m, const char *hname, const time_t *t) { struct tm tm; if (gmtime_r(t, &tm) == NULL) return -1; return vlc_http_msg_add_header(m, hname, "%s, %02d %s %04d %02d:%02d:%02d GMT", vlc_http_days[tm.tm_wday], tm.tm_mday, vlc_http_months[tm.tm_mon], 1900 + tm.tm_year, tm.tm_hour, tm.tm_min, tm.tm_sec); } int vlc_http_msg_add_atime(struct vlc_http_msg *m) { time_t now; time(&now); return vlc_http_msg_add_time(m, "Date", &now); } static time_t vlc_http_mktime(const char *str) { /* IETF RFC7231 §7.1.1.1 */ struct tm tm; char mon[4]; /* Internet Message Format date */ if (sscanf(str, "%*c%*c%*c, %2d %3s %4d %2d:%2d:%2d", &tm.tm_mday, mon, &tm.tm_year, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 /* ANSI C format */ || sscanf(str, "%*3s %3s %2d %2d:%2d:%2d %4d", mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &tm.tm_year) == 6) tm.tm_year -= 1900; /* RFC850 date */ else if (sscanf(str, "%*[^,], %2d-%3s-%2d %2d:%2d:%2d", &tm.tm_mday, mon, &tm.tm_year, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { if (tm.tm_year <= 75) tm.tm_year += 100; /* Y2K compat, sort of */ } else /* Unknown format */ goto error; for (tm.tm_mon = 0; tm.tm_mon < 12; tm.tm_mon++) if (!strcmp(mon, vlc_http_months[tm.tm_mon])) /* found month */ return timegm(&tm); error: errno = EINVAL; return -1; /* invalid month */ } time_t vlc_http_msg_get_time(const struct vlc_http_msg *m, const char *name) { const char *str = vlc_http_msg_get_header(m, name); if (str == NULL) return -1; return vlc_http_mktime(str); } time_t vlc_http_msg_get_atime(const struct vlc_http_msg *m) { return vlc_http_msg_get_time(m, "Date"); } time_t vlc_http_msg_get_mtime(const struct vlc_http_msg *m) { return vlc_http_msg_get_time(m, "Last-Modified"); } unsigned vlc_http_msg_get_retry_after(const struct vlc_http_msg *m) { const char *str = vlc_http_msg_get_header(m, "Retry-After"); if (str == NULL) return 0; char *end; unsigned long delay = strtoul(str, &end, 10); if (end != str && *end == '\0') return delay; time_t t = vlc_http_mktime(str); if (t != (time_t)-1) { time_t now; time(&now); if (t >= now) return t - now; } return 0; } uintmax_t vlc_http_msg_get_size(const struct vlc_http_msg *m) { /* IETF RFC7230 §3.3.3 */ if ((m->status / 100) == 1 /* Informational 1xx (implicitly void) */ || m->status == 204 /* No Content (implicitly void) */ || m->status == 205 /* Reset Content (must be explicitly void) */ || m->status == 304 /* Not Modified */) return 0; const char *str = vlc_http_msg_get_header(m, "Transfer-Encoding"); if (str != NULL) /* Transfer-Encoding preempts Content-Length */ return -1; str = vlc_http_msg_get_header(m, "Content-Length"); if (str == NULL) { if (m->status < 0) return 0; /* Requests are void by default */ return -1; /* Response of unknown size (e.g. chunked) */ } uintmax_t length; if (sscanf(str, "%ju", &length) == 1) return length; errno = EINVAL; return -1; } void vlc_http_msg_get_cookies(const struct vlc_http_msg *m, vlc_http_cookie_jar_t *jar, const char *host, const char *path) { if (jar == NULL) return; for (unsigned i = 0; i < m->count; i++) if (!strcasecmp(m->headers[i][0], "Set-Cookie")) vlc_http_cookies_store(jar, m->headers[i][1], host, path); } int vlc_http_msg_add_cookies(struct vlc_http_msg *m, vlc_http_cookie_jar_t *jar) { char *host, *cookies; int val = 0; bool secure; if (m->scheme == NULL || m->authority == NULL || m->path == NULL) { errno = EINVAL; return -1; } if (!strcasecmp(m->scheme, "https")) secure = true; else if (!strcasecmp(m->scheme, "http")) secure = false; else return 0; if (jar == NULL) return 0; if (m->authority[0] == '[') host = strndup(m->authority + 1, strcspn(m->authority + 1, "]")); else host = strndup(m->authority, strcspn(m->authority, ":")); if (unlikely(host == NULL)) return -1; cookies = vlc_http_cookies_fetch(jar, secure, host, m->path); free(host); if (cookies != NULL) { val = vlc_http_msg_add_header(m, "Cookie", "%s", cookies); free(cookies); } return val; } char *vlc_http_msg_get_basic_realm(const struct vlc_http_msg *m) { const char *auth; /* TODO: Support other authentication schemes. */ /* NOTE: In principles, RFC7235 allows for multiple authentication schemes, * including multiple times Basic with a different realm each. There are no * UI paradigms though. */ auth = vlc_http_msg_get_token(m, "WWW-Authenticate", "Basic"); if (auth == NULL) return NULL; auth += 5; auth += strspn(auth, " "); /* 1*SP */ return vlc_http_get_token_value(auth, "realm"); } int vlc_http_msg_add_creds_basic(struct vlc_http_msg *m, bool proxy, const char *username, const char *password) { char *str, *token; int ret; unsigned char c; /* CTL characters and colons not permitted in username */ for (size_t len = 0; (c = username[len]) != '\0'; len++) if (c < 32 || c == 127 || c == ':') { errno = EINVAL; return -1; } /* CTL characters not permitted in password */ for (size_t len = 0; (c = password[len]) != '\0'; len++) if (c < 32 || c == 127) { errno = EINVAL; return -1; } ret = asprintf(&str, "%s:%s", username, password); if (unlikely(ret < 0)) return -1; token = vlc_b64_encode_binary((unsigned char *)str, ret); free(str); if (unlikely(token == NULL)) return -1; ret = vlc_http_msg_add_header(m, proxy ? "Proxy-Authorization" : "Authorization", "Basic %s", token); free(token); return ret; }