/* * tvheadend, HTTP interface * Copyright (C) 2007 Andreas Öman * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tvheadend.h" #include "tcp.h" #include "http.h" #include "access.h" #include "notify.h" #include "channels.h" void *http_server; static LIST_HEAD(, http_path) http_paths; static struct strtab HTTP_cmdtab[] = { { "GET", HTTP_CMD_GET }, { "HEAD", HTTP_CMD_HEAD }, { "POST", HTTP_CMD_POST }, { "DESCRIBE", RTSP_CMD_DESCRIBE }, { "OPTIONS", RTSP_CMD_OPTIONS }, { "SETUP", RTSP_CMD_SETUP }, { "PLAY", RTSP_CMD_PLAY }, { "TEARDOWN", RTSP_CMD_TEARDOWN }, { "PAUSE", RTSP_CMD_PAUSE }, }; static struct strtab HTTP_versiontab[] = { { "HTTP/1.0", HTTP_VERSION_1_0 }, { "HTTP/1.1", HTTP_VERSION_1_1 }, { "RTSP/1.0", RTSP_VERSION_1_0 }, }; static void http_parse_get_args(http_connection_t *hc, char *args); /** * */ const char * http_cmd2str(int val) { return val2str(val, HTTP_cmdtab); } int http_str2cmd(const char *str) { return str2val(str, HTTP_cmdtab); } const char * http_ver2str(int val) { return val2str(val, HTTP_versiontab); } int http_str2ver(const char *str) { return str2val(str, HTTP_versiontab); } /** * */ static http_path_t * http_resolve(http_connection_t *hc, char **remainp, char **argsp) { http_path_t *hp; int n = 0, cut = 0; char *v, *path = tvh_strdupa(hc->hc_url); char *npath; /* Canocalize path (or at least remove excess slashes) */ v = path; while (*v && *v != '?') { if (*v == '/' && v[1] == '/') { int l = strlen(v+1); memmove(v, v+1, l); v[l] = 0; n++; } else v++; } while (1) { LIST_FOREACH(hp, &http_paths, hp_link) { if(!strncmp(path, hp->hp_path, hp->hp_len)) { if(path[hp->hp_len] == 0 || path[hp->hp_len] == '/' || path[hp->hp_len] == '?') break; } } if(hp == NULL) return NULL; cut += hp->hp_len; if(hp->hp_path_modify == NULL) break; npath = hp->hp_path_modify(hc, path, &cut); if(npath == NULL) break; path = tvh_strdupa(npath); free(npath); } v = hc->hc_url + n + cut; *remainp = NULL; *argsp = NULL; switch(*v) { case 0: break; case '/': if(v[1] == '?') { *argsp = v + 1; break; } *remainp = v + 1; v = strchr(v + 1, '?'); if(v != NULL) { *v = 0; /* terminate remaining url */ *argsp = v + 1; } break; case '?': *argsp = v + 1; break; default: return NULL; } return hp; } /* * HTTP status code to string */ static const char * http_rc2str(int code) { switch(code) { case HTTP_STATUS_OK: return "OK"; case HTTP_STATUS_PARTIAL_CONTENT: return "Partial Content"; case HTTP_STATUS_NOT_FOUND: return "Not found"; case HTTP_STATUS_UNAUTHORIZED: return "Unauthorized"; case HTTP_STATUS_BAD_REQUEST: return "Bad request"; case HTTP_STATUS_FOUND: return "Found"; default: return "Unknown returncode"; break; } } static const char *cachedays[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static const char *cachemonths[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; /** * Transmit a HTTP reply */ void http_send_header(http_connection_t *hc, int rc, const char *content, int64_t contentlen, const char *encoding, const char *location, int maxage, const char *range, const char *disposition) { struct tm tm0, *tm; htsbuf_queue_t hdrs; time_t t; htsbuf_queue_init(&hdrs, 0); htsbuf_qprintf(&hdrs, "%s %d %s\r\n", val2str(hc->hc_version, HTTP_versiontab), rc, http_rc2str(rc)); htsbuf_qprintf(&hdrs, "Server: HTS/tvheadend\r\n"); if(maxage == 0) { htsbuf_qprintf(&hdrs, "Cache-Control: no-cache\r\n"); } else { time(&t); tm = gmtime_r(&t, &tm0); htsbuf_qprintf(&hdrs, "Last-Modified: %s, %d %s %02d %02d:%02d:%02d GMT\r\n", cachedays[tm->tm_wday], tm->tm_mday, cachemonths[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec); t += maxage; tm = gmtime_r(&t, &tm0); htsbuf_qprintf(&hdrs, "Expires: %s, %d %s %02d %02d:%02d:%02d GMT\r\n", cachedays[tm->tm_wday], tm->tm_mday, cachemonths[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec); htsbuf_qprintf(&hdrs, "Cache-Control: max-age=%d\r\n", maxage); } if(rc == HTTP_STATUS_UNAUTHORIZED) htsbuf_qprintf(&hdrs, "WWW-Authenticate: Basic realm=\"tvheadend\"\r\n"); if (hc->hc_logout_cookie == 1) { htsbuf_qprintf(&hdrs, "Set-Cookie: logout=1; Path=\"/logout\"\r\n"); } else if (hc->hc_logout_cookie == 2) { htsbuf_qprintf(&hdrs, "Set-Cookie: logout=0; Path=\"/logout'\"; expires=Thu, 01 Jan 1970 00:00:00 GMT\r\n"); } htsbuf_qprintf(&hdrs, "Connection: %s\r\n", hc->hc_keep_alive ? "Keep-Alive" : "Close"); if(encoding != NULL) htsbuf_qprintf(&hdrs, "Content-Encoding: %s\r\n", encoding); if(location != NULL) htsbuf_qprintf(&hdrs, "Location: %s\r\n", location); if(content != NULL) htsbuf_qprintf(&hdrs, "Content-Type: %s\r\n", content); if(contentlen > 0) htsbuf_qprintf(&hdrs, "Content-Length: %"PRId64"\r\n", contentlen); if(range) { htsbuf_qprintf(&hdrs, "Accept-Ranges: %s\r\n", "bytes"); htsbuf_qprintf(&hdrs, "Content-Range: %s\r\n", range); } if(disposition != NULL) htsbuf_qprintf(&hdrs, "Content-Disposition: %s\r\n", disposition); htsbuf_qprintf(&hdrs, "\r\n"); tcp_write_queue(hc->hc_fd, &hdrs); } /** * Transmit a HTTP reply */ static void http_send_reply(http_connection_t *hc, int rc, const char *content, const char *encoding, const char *location, int maxage) { http_send_header(hc, rc, content, hc->hc_reply.hq_size, encoding, location, maxage, 0, NULL); if(hc->hc_no_output) return; tcp_write_queue(hc->hc_fd, &hc->hc_reply); } /** * Send HTTP error back */ void http_error(http_connection_t *hc, int error) { const char *errtxt = http_rc2str(error); char addrstr[50]; if (!http_server) return; tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrstr, 50); if (error != HTTP_STATUS_FOUND && error != HTTP_STATUS_MOVED) tvhlog(error < 400 ? LOG_INFO : LOG_ERR, "HTTP", "%s: %s -- %d", addrstr, hc->hc_url, error); htsbuf_queue_flush(&hc->hc_reply); htsbuf_qprintf(&hc->hc_reply, "\r\n" "\r\n" "%d %s\r\n" "\r\n" "

%d %s

\r\n", error, errtxt, error, errtxt); if (error == HTTP_STATUS_UNAUTHORIZED) htsbuf_qprintf(&hc->hc_reply, "

Default Login

"); htsbuf_qprintf(&hc->hc_reply, "\r\n"); http_send_reply(hc, error, "text/html", NULL, NULL, 0); } /** * Send an HTTP OK, simple version for text/html */ void http_output_html(http_connection_t *hc) { return http_send_reply(hc, HTTP_STATUS_OK, "text/html; charset=UTF-8", NULL, NULL, 0); } /** * Send an HTTP OK, simple version for text/html */ void http_output_content(http_connection_t *hc, const char *content) { return http_send_reply(hc, HTTP_STATUS_OK, content, NULL, NULL, 0); } /** * Send an HTTP REDIRECT */ void http_redirect(http_connection_t *hc, const char *location, http_arg_list_t *req_args) { const char *loc = location; htsbuf_queue_flush(&hc->hc_reply); if (req_args) { http_arg_t *ra; htsbuf_queue_t hq; int first = 1; htsbuf_queue_init(&hq, 0); htsbuf_append(&hq, location, strlen(location)); htsbuf_append(&hq, "?", 1); TAILQ_FOREACH(ra, req_args, link) { if (!first) htsbuf_append(&hq, "&", 1); first = 0; htsbuf_append_and_escape_url(&hq, ra->key); htsbuf_append(&hq, "=", 1); htsbuf_append_and_escape_url(&hq, ra->val); } loc = htsbuf_to_string(&hq); htsbuf_queue_flush(&hq); } htsbuf_qprintf(&hc->hc_reply, "\r\n" "\r\n" "Redirect\r\n" "\r\n" "Please follow %s\r\n" "\r\n", loc, loc); http_send_reply(hc, HTTP_STATUS_FOUND, "text/html", NULL, loc, 0); if (loc != location) free((void *)loc); } /** * */ static void http_access_verify_ticket(http_connection_t *hc) { const char *ticket_id; if (hc->hc_access) return; ticket_id = http_arg_get(&hc->hc_req_args, "ticket"); hc->hc_access = access_ticket_verify2(ticket_id, hc->hc_url); if (hc->hc_access == NULL) return; char addrstr[50]; tcp_get_ip_str((struct sockaddr*)hc->hc_peer, addrstr, 50); tvhlog(LOG_INFO, "HTTP", "%s: using ticket %s for %s", addrstr, ticket_id, hc->hc_url); } /** * Return non-zero if no access */ int http_access_verify(http_connection_t *hc, int mask) { http_access_verify_ticket(hc); if (hc->hc_access == NULL) { hc->hc_access = access_get(hc->hc_username, hc->hc_password, (struct sockaddr *)hc->hc_peer); if (hc->hc_access == NULL) return -1; } return access_verify2(hc->hc_access, mask); } /** * Return non-zero if no access */ int http_access_verify_channel(http_connection_t *hc, int mask, struct channel *ch, int ticket) { int res = -1; assert(ch); if (ticket) http_access_verify_ticket(hc); if (hc->hc_access == NULL) { hc->hc_access = access_get(hc->hc_username, hc->hc_password, (struct sockaddr *)hc->hc_peer); if (hc->hc_access == NULL) return -1; } if (access_verify2(hc->hc_access, mask)) return -1; if (channel_access(ch, hc->hc_access, 0)) res = 0; return res; } /** * Execute url callback * * Returns 1 if we should disconnect * */ static int http_exec(http_connection_t *hc, http_path_t *hp, char *remain) { int err; if(http_access_verify(hc, hp->hp_accessmask)) err = HTTP_STATUS_UNAUTHORIZED; else err = hp->hp_callback(hc, remain, hp->hp_opaque); access_destroy(hc->hc_access); hc->hc_access = NULL; if(err == -1) return 1; if(err) http_error(hc, err); return 0; } /* * Dump request */ #if ENABLE_TRACE static void dump_request(http_connection_t *hc) { char buf[2048] = ""; http_arg_t *ra; int first; first = 1; TAILQ_FOREACH(ra, &hc->hc_req_args, link) { snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), first ? "?%s=%s" : "&%s=%s", ra->key, ra->val); first = 0; } first = 1; TAILQ_FOREACH(ra, &hc->hc_args, link) { snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), first ? "{{%s=%s" : ",%s=%s", ra->key, ra->val); first = 0; } snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "}}"); tvhtrace("http", "%s%s", hc->hc_url, buf); } #else static inline void dump_request(http_connection_t *hc) { } #endif /** * HTTP GET */ static int http_cmd_get(http_connection_t *hc) { http_path_t *hp; char *remain; char *args; dump_request(hc); hp = http_resolve(hc, &remain, &args); if(hp == NULL) { http_error(hc, HTTP_STATUS_NOT_FOUND); return 0; } if(args != NULL) http_parse_get_args(hc, args); return http_exec(hc, hp, remain); } /** * Initial processing of HTTP POST * * Return non-zero if we should disconnect */ static int http_cmd_post(http_connection_t *hc, htsbuf_queue_t *spill) { http_path_t *hp; char *remain, *args, *v; /* Set keep-alive status */ v = http_arg_get(&hc->hc_args, "Content-Length"); if(v == NULL) { /* No content length in POST, make us disconnect */ return -1; } hc->hc_post_len = atoi(v); if(hc->hc_post_len > 16 * 1024 * 1024) { /* Bail out if POST data > 16 Mb */ hc->hc_keep_alive = 0; return -1; } /* Allocate space for data, we add a terminating null char to ease string processing on the content */ hc->hc_post_data = malloc(hc->hc_post_len + 1); hc->hc_post_data[hc->hc_post_len] = 0; if(tcp_read_data(hc->hc_fd, hc->hc_post_data, hc->hc_post_len, spill) < 0) return -1; /* Parse content-type */ v = http_arg_get(&hc->hc_args, "Content-Type"); if(v != NULL) { char *argv[2]; int n = http_tokenize(v, argv, 2, ';'); if(n == 0) { http_error(hc, HTTP_STATUS_BAD_REQUEST); return 0; } if(!strcmp(argv[0], "application/x-www-form-urlencoded")) http_parse_get_args(hc, hc->hc_post_data); } dump_request(hc); hp = http_resolve(hc, &remain, &args); if(hp == NULL) { http_error(hc, HTTP_STATUS_NOT_FOUND); return 0; } return http_exec(hc, hp, remain); } /** * Process a HTTP request */ static int http_process_request(http_connection_t *hc, htsbuf_queue_t *spill) { switch(hc->hc_cmd) { default: http_error(hc, HTTP_STATUS_BAD_REQUEST); return 0; case HTTP_CMD_GET: return http_cmd_get(hc); case HTTP_CMD_HEAD: hc->hc_no_output = 1; return http_cmd_get(hc); case HTTP_CMD_POST: return http_cmd_post(hc, spill); } } /** * Process a request, extract info from headers, dispatch command and * clean up */ static int process_request(http_connection_t *hc, htsbuf_queue_t *spill) { char *v, *argv[2]; int n, rval = -1; uint8_t authbuf[150]; hc->hc_url_orig = tvh_strdupa(hc->hc_url); /* Set keep-alive status */ v = http_arg_get(&hc->hc_args, "connection"); switch(hc->hc_version) { case RTSP_VERSION_1_0: hc->hc_keep_alive = 1; break; case HTTP_VERSION_1_0: /* Keep-alive is default off, but can be enabled */ hc->hc_keep_alive = v != NULL && !strcasecmp(v, "keep-alive"); break; case HTTP_VERSION_1_1: /* Keep-alive is default on, but can be disabled */ hc->hc_keep_alive = !(v != NULL && !strcasecmp(v, "close")); break; } /* Extract authorization */ if((v = http_arg_get(&hc->hc_args, "Authorization")) != NULL) { if((n = http_tokenize(v, argv, 2, -1)) == 2) { n = base64_decode(authbuf, argv[1], sizeof(authbuf) - 1); if (n < 0) n = 0; authbuf[n] = 0; if((n = http_tokenize((char *)authbuf, argv, 2, ':')) == 2) { hc->hc_username = strdup(argv[0]); hc->hc_password = strdup(argv[1]); // No way to actually track this } } } if(hc->hc_username != NULL) { hc->hc_representative = strdup(hc->hc_username); } else { hc->hc_representative = malloc(50); /* Not threadsafe ? */ tcp_get_ip_str((struct sockaddr*)hc->hc_peer, hc->hc_representative, 50); } switch(hc->hc_version) { case RTSP_VERSION_1_0: break; case HTTP_VERSION_1_0: case HTTP_VERSION_1_1: rval = http_process_request(hc, spill); break; } free(hc->hc_representative); return rval; } /* * Delete all arguments associated with a connection */ void http_arg_flush(struct http_arg_list *list) { http_arg_t *ra; while((ra = TAILQ_FIRST(list)) != NULL) { TAILQ_REMOVE(list, ra, link); free(ra->key); free(ra->val); free(ra); } } /** * Find an argument associated with a connection */ char * http_arg_get(struct http_arg_list *list, const char *name) { http_arg_t *ra; TAILQ_FOREACH(ra, list, link) if(!strcasecmp(ra->key, name)) return ra->val; return NULL; } /** * Set an argument associated with a connection */ void http_arg_set(struct http_arg_list *list, const char *key, const char *val) { http_arg_t *ra; ra = malloc(sizeof(http_arg_t)); TAILQ_INSERT_TAIL(list, ra, link); ra->key = strdup(key); ra->val = strdup(val); } /* * Split a string in components delimited by 'delimiter' */ int http_tokenize(char *buf, char **vec, int vecsize, int delimiter) { int n = 0; while(1) { while((*buf > 0 && *buf < 33) || *buf == delimiter) buf++; if(*buf == 0) break; vec[n++] = buf; if(n == vecsize) break; while(*buf > 32 && *buf != delimiter) buf++; if(*buf == 0) break; *buf = 0; buf++; } return n; } /** * Add a callback for a given "virtual path" on our HTTP server */ http_path_t * http_path_add_modify(const char *path, void *opaque, http_callback_t *callback, uint32_t accessmask, http_path_modify_t *path_modify) { http_path_t *hp = malloc(sizeof(http_path_t)); char *tmp; if (tvheadend_webroot) { size_t len = strlen(tvheadend_webroot) + strlen(path) + 1; hp->hp_path = tmp = malloc(len); sprintf(tmp, "%s%s", tvheadend_webroot, path); } else hp->hp_path = strdup(path); hp->hp_len = strlen(hp->hp_path); hp->hp_opaque = opaque; hp->hp_callback = callback; hp->hp_accessmask = accessmask; hp->hp_path_modify = path_modify; LIST_INSERT_HEAD(&http_paths, hp, hp_link); return hp; } /** * Add a callback for a given "virtual path" on our HTTP server */ http_path_t * http_path_add(const char *path, void *opaque, http_callback_t *callback, uint32_t accessmask) { return http_path_add_modify(path, opaque, callback, accessmask, NULL); } /** * De-escape HTTP URL */ void http_deescape(char *s) { char v, *d = s; while(*s) { if(*s == '+') { *d++ = ' '; s++; } else if(*s == '%') { s++; switch(*s) { case '0' ... '9': v = (*s - '0') << 4; break; case 'a' ... 'f': v = (*s - 'a' + 10) << 4; break; case 'A' ... 'F': v = (*s - 'A' + 10) << 4; break; default: *d = 0; return; } s++; switch(*s) { case '0' ... '9': v |= (*s - '0'); break; case 'a' ... 'f': v |= (*s - 'a' + 10); break; case 'A' ... 'F': v |= (*s - 'A' + 10); break; default: *d = 0; return; } s++; *d++ = v; } else { *d++ = *s++; } } *d = 0; } /** * Parse arguments of a HTTP GET url, not perfect, but works for us */ static void http_parse_get_args(http_connection_t *hc, char *args) { char *k, *v; while(args) { k = args; if((args = strchr(args, '=')) == NULL) break; *args++ = 0; v = args; args = strchr(args, '&'); if(args != NULL) *args++ = 0; http_deescape(k); http_deescape(v); // printf("%s = %s\n", k, v); http_arg_set(&hc->hc_req_args, k, v); } } /** * */ static void http_serve_requests(http_connection_t *hc, htsbuf_queue_t *spill) { char *argv[3], *c, *cmdline = NULL, *hdrline = NULL; int n; htsbuf_queue_init(&hc->hc_reply, 0); do { hc->hc_no_output = 0; if (cmdline) free(cmdline); if ((cmdline = tcp_read_line(hc->hc_fd, spill)) == NULL) goto error; if((n = http_tokenize(cmdline, argv, 3, -1)) != 3) goto error; if((hc->hc_cmd = str2val(argv[0], HTTP_cmdtab)) == -1) goto error; hc->hc_url = argv[1]; if((hc->hc_version = str2val(argv[2], HTTP_versiontab)) == -1) goto error; /* parse header */ while(1) { if (hdrline) free(hdrline); if ((hdrline = tcp_read_line(hc->hc_fd, spill)) == NULL) goto error; if(!*hdrline) break; /* header complete */ if((n = http_tokenize(hdrline, argv, 2, -1)) < 2) continue; if((c = strrchr(argv[0], ':')) == NULL) goto error; *c = 0; http_arg_set(&hc->hc_args, argv[0], argv[1]); } if(process_request(hc, spill)) break; free(hc->hc_post_data); hc->hc_post_data = NULL; http_arg_flush(&hc->hc_args); http_arg_flush(&hc->hc_req_args); htsbuf_queue_flush(&hc->hc_reply); free(hc->hc_username); hc->hc_username = NULL; free(hc->hc_password); hc->hc_password = NULL; hc->hc_logout_cookie = 0; } while(hc->hc_keep_alive && http_server); error: free(hdrline); free(cmdline); } /** * */ static void http_serve(int fd, void **opaque, struct sockaddr_storage *peer, struct sockaddr_storage *self) { htsbuf_queue_t spill; http_connection_t hc; // Note: global_lock held on entry */ pthread_mutex_unlock(&global_lock); memset(&hc, 0, sizeof(http_connection_t)); *opaque = &hc; http_arg_init(&hc.hc_args); http_arg_init(&hc.hc_req_args); hc.hc_fd = fd; hc.hc_peer = peer; hc.hc_self = self; htsbuf_queue_init(&spill, 0); http_serve_requests(&hc, &spill); http_arg_flush(&hc.hc_args); http_arg_flush(&hc.hc_req_args); htsbuf_queue_flush(&hc.hc_reply); htsbuf_queue_flush(&spill); close(fd); // Note: leave global_lock held for parent pthread_mutex_lock(&global_lock); free(hc.hc_post_data); free(hc.hc_username); free(hc.hc_password); *opaque = NULL; } static void http_cancel( void *opaque ) { http_connection_t *hc = opaque; if (hc) { shutdown(hc->hc_fd, SHUT_RDWR); hc->hc_shutdown = 1; } } /** * Fire up HTTP server */ void http_server_init(const char *bindaddr) { static tcp_server_ops_t ops = { .start = http_serve, .stop = NULL, .cancel = http_cancel }; http_server = tcp_server_create(bindaddr, tvheadend_webui_port, &ops, NULL); } void http_server_register(void) { tcp_server_register(http_server); } void http_server_done(void) { http_path_t *hp; pthread_mutex_lock(&global_lock); if (http_server) tcp_server_delete(http_server); http_server = NULL; while ((hp = LIST_FIRST(&http_paths)) != NULL) { LIST_REMOVE(hp, hp_link); free((void *)hp->hp_path); free(hp); } pthread_mutex_unlock(&global_lock); }