/***************************************************************************** * h1conn.c: HTTP 1.x connection handling ***************************************************************************** * Copyright © 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 "conn.h" #include "message.h" static unsigned vlc_http_can_read(const char *buf, size_t len) { static const char end[4] = { '\r', '\n', '\r', '\n' }; for (unsigned i = 4; i > 0; i--) if (len >= i && !memcmp(buf + len - i, end, i)) return 4 - i; return 4; } /** * Receives HTTP headers. * * Receives HTTP 1.x response line and headers. * * @return A heap-allocated null-terminated string contained the full * headers, including the final CRLF. */ static char *vlc_https_headers_recv(struct vlc_tls *tls, size_t *restrict lenp) { size_t size = 0, len = 0, canread; char *buf = NULL; while ((canread = vlc_http_can_read(buf, len)) > 0) { ssize_t val; if (len + canread >= size) { size += 2048; if (size > 65536) goto fail; char *newbuf = realloc(buf, size); if (unlikely(newbuf == NULL)) goto fail; buf = newbuf; } assert(size - len >= canread); //vlc_cleanup_push(free, buf); val = vlc_tls_Read(tls, buf + len, canread, true); //vlc_cleanup_pop(); if (val != (ssize_t)canread) goto fail; len += val; } assert(size - len >= 1); buf[len] = '\0'; /* for convenience */ if (lenp != NULL) *lenp = len; return buf; fail: free(buf); return NULL; } /** Gets minor HTTP version */ static int vlc_http_minor(const char *msg) { int minor; if (sscanf(msg, "HTTP/1.%1d", &minor) == 1) return minor; return -1; } struct vlc_h1_conn { struct vlc_http_conn conn; struct vlc_http_stream stream; uintmax_t content_length; bool connection_close; bool active; bool released; bool proxy; void *opaque; }; #define CO(conn) ((conn)->opaque) static void vlc_h1_conn_destroy(struct vlc_h1_conn *conn); static void *vlc_h1_stream_fatal(struct vlc_h1_conn *conn) { if (conn->conn.tls != NULL) { vlc_http_dbg(CO(conn), "connection failed"); vlc_tls_Shutdown(conn->conn.tls, true); vlc_tls_Close(conn->conn.tls); conn->conn.tls = NULL; } return NULL; } static struct vlc_h1_conn *vlc_h1_stream_conn(struct vlc_http_stream *stream) { return container_of(stream, struct vlc_h1_conn, stream); } static struct vlc_http_stream *vlc_h1_stream_open(struct vlc_http_conn *c, const struct vlc_http_msg *req) { struct vlc_h1_conn *conn = container_of(c, struct vlc_h1_conn, conn); size_t len; ssize_t val; if (conn->active || conn->conn.tls == NULL) return NULL; char *payload = vlc_http_msg_format(req, &len, conn->proxy); if (unlikely(payload == NULL)) return NULL; vlc_http_dbg(CO(conn), "outgoing request:\n%.*s", (int)len, payload); val = vlc_tls_Write(conn->conn.tls, payload, len); free(payload); if (val < (ssize_t)len) return vlc_h1_stream_fatal(conn); conn->active = true; conn->content_length = 0; conn->connection_close = false; return &conn->stream; } static struct vlc_http_msg *vlc_h1_stream_wait(struct vlc_http_stream *stream) { struct vlc_h1_conn *conn = vlc_h1_stream_conn(stream); struct vlc_http_msg *resp; const char *str; size_t len; int minor; assert(conn->active); if (conn->conn.tls == NULL) return NULL; char *payload = vlc_https_headers_recv(conn->conn.tls, &len); if (payload == NULL) return vlc_h1_stream_fatal(conn); vlc_http_dbg(CO(conn), "incoming response:\n%.*s", (int)len, payload); resp = vlc_http_msg_headers(payload); minor = vlc_http_minor(payload); free(payload); if (resp == NULL) return vlc_h1_stream_fatal(conn); assert(minor >= 0); conn->content_length = vlc_http_msg_get_size(resp); conn->connection_close = false; if (minor >= 1) { if (vlc_http_msg_get_token(resp, "Connection", "close") != NULL) conn->connection_close = true; str = vlc_http_msg_get_token(resp, "Transfer-Encoding", "chunked"); if (str != NULL) { if (vlc_http_next_token(str) != NULL) { vlc_http_msg_destroy(resp); return vlc_h1_stream_fatal(conn); /* unsupported TE */ } assert(conn->content_length == UINTMAX_MAX); stream = vlc_chunked_open(stream, conn->conn.tls); if (unlikely(stream == NULL)) { vlc_http_msg_destroy(resp); return vlc_h1_stream_fatal(conn); } } } else conn->connection_close = true; vlc_http_msg_attach(resp, stream); return resp; } static block_t *vlc_h1_stream_read(struct vlc_http_stream *stream) { struct vlc_h1_conn *conn = vlc_h1_stream_conn(stream); size_t size = 2048; assert(conn->active); if (conn->conn.tls == NULL) return vlc_http_error; if (size > conn->content_length) size = conn->content_length; if (size == 0) return NULL; block_t *block = block_Alloc(size); if (unlikely(block == NULL)) return vlc_http_error; ssize_t val = vlc_tls_Read(conn->conn.tls, block->p_buffer, size, false); if (val <= 0) { block_Release(block); if (val < 0) return vlc_http_error; if (conn->content_length != UINTMAX_MAX) { errno = ECONNRESET; return vlc_http_error; } return NULL; } block->i_buffer = val; if (conn->content_length != UINTMAX_MAX) conn->content_length -= val; return block; } static void vlc_h1_stream_close(struct vlc_http_stream *stream, bool abort) { struct vlc_h1_conn *conn = vlc_h1_stream_conn(stream); assert(conn->active); if (conn->connection_close) /* Server requested closing the connection after this stream. */ abort = true; if (conn->content_length > 0 && conn->content_length != UINTMAX_MAX) /* Client left some data to be read, so pipelining is impossible. */ abort = true; if (abort) /* Shut the underlying connection down and prevent reuse. */ vlc_h1_stream_fatal(conn); conn->active = false; if (conn->released) vlc_h1_conn_destroy(conn); } static const struct vlc_http_stream_cbs vlc_h1_stream_callbacks = { vlc_h1_stream_wait, vlc_h1_stream_read, vlc_h1_stream_close, }; static void vlc_h1_conn_destroy(struct vlc_h1_conn *conn) { assert(!conn->active); assert(conn->released); if (conn->conn.tls != NULL) { vlc_tls_Shutdown(conn->conn.tls, true); vlc_tls_Close(conn->conn.tls); } free(conn); } static void vlc_h1_conn_release(struct vlc_http_conn *c) { struct vlc_h1_conn *conn = container_of(c, struct vlc_h1_conn, conn); assert(!conn->released); conn->released = true; if (!conn->active) vlc_h1_conn_destroy(conn); } static const struct vlc_http_conn_cbs vlc_h1_conn_callbacks = { vlc_h1_stream_open, vlc_h1_conn_release, }; struct vlc_http_conn *vlc_h1_conn_create(void *ctx, vlc_tls_t *tls, bool proxy) { struct vlc_h1_conn *conn = malloc(sizeof (*conn)); if (unlikely(conn == NULL)) return NULL; conn->conn.cbs = &vlc_h1_conn_callbacks; conn->conn.tls = tls; conn->stream.cbs = &vlc_h1_stream_callbacks; conn->active = false; conn->released = false; conn->proxy = proxy; conn->opaque = ctx; return &conn->conn; } struct vlc_http_stream *vlc_h1_request(void *ctx, const char *hostname, unsigned port, bool proxy, const struct vlc_http_msg *req, bool idempotent, struct vlc_http_conn **restrict connp) { struct addrinfo hints = { .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP, }, *res; vlc_http_dbg(ctx, "resolving %s ...", hostname); int val = vlc_getaddrinfo_i11e(hostname, port, &hints, &res); if (val != 0) { /* TODO: C locale for gai_strerror() */ vlc_http_err(ctx, "cannot resolve %s: %s", hostname, gai_strerror(val)); return NULL; } for (const struct addrinfo *p = res; p != NULL; p = p->ai_next) { vlc_tls_t *tcp = vlc_tls_SocketOpenAddrInfo(p, idempotent); if (tcp == NULL) { vlc_http_err(ctx, "socket error: %s", vlc_strerror_c(errno)); continue; } struct vlc_http_conn *conn = vlc_h1_conn_create(ctx, tcp, proxy); if (unlikely(conn == NULL)) { vlc_tls_SessionDelete(tcp); continue; } /* Send the HTTP request */ struct vlc_http_stream *stream = vlc_http_stream_open(conn, req); if (stream != NULL) { if (connp != NULL) *connp = conn; else vlc_http_conn_release(conn); freeaddrinfo(res); return stream; } vlc_http_conn_release(conn); if (!idempotent) break; /* If the request is nonidempotent, it cannot be resent. */ } /* All address info failed. */ freeaddrinfo(res); return NULL; }