/* the Music Player Daemon (MPD) * Copyright (C) 2003-2007 by Warren Dukes (warren.dukes@gmail.com) * This project's homepage is: http://www.musicpd.org * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "inputStream_http.h" #include "utils.h" #include "log.h" #include "conf.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define HTTP_CONN_STATE_CLOSED 0 #define HTTP_CONN_STATE_INIT 1 #define HTTP_CONN_STATE_HELLO 2 #define HTTP_CONN_STATE_OPEN 3 #define HTTP_CONN_STATE_REOPEN 4 #define HTTP_BUFFER_SIZE_DEFAULT 131072 #define HTTP_PREBUFFER_SIZE_DEFAULT (HTTP_BUFFER_SIZE_DEFAULT >> 2) #define HTTP_REDIRECT_MAX 10 #define HTTP_MAX_TRIES 100 static char *proxyHost; static char *proxyPort; static char *proxyUser; static char *proxyPassword; static int bufferSize = HTTP_BUFFER_SIZE_DEFAULT; static int prebufferSize = HTTP_PREBUFFER_SIZE_DEFAULT; typedef struct _InputStreemHTTPData { char *host; char *path; char *port; int sock; int connState; char *buffer; size_t buflen; int timesRedirected; int icyMetaint; int prebuffer; int icyOffset; char *proxyAuth; char *httpAuth; /* Number of times mpd tried to get data */ int tries; } InputStreamHTTPData; void inputStream_initHttp(void) { ConfigParam *param = getConfigParam(CONF_HTTP_PROXY_HOST); char *test; if (param) { proxyHost = param->value; param = getConfigParam(CONF_HTTP_PROXY_PORT); if (!param) { FATAL("%s specified but not %s\n", CONF_HTTP_PROXY_HOST, CONF_HTTP_PROXY_PORT); } proxyPort = param->value; param = getConfigParam(CONF_HTTP_PROXY_USER); if (param) { proxyUser = param->value; param = getConfigParam(CONF_HTTP_PROXY_PASSWORD); if (!param) { FATAL("%s specified but not %s\n", CONF_HTTP_PROXY_USER, CONF_HTTP_PROXY_PASSWORD); } proxyPassword = param->value; } else { param = getConfigParam(CONF_HTTP_PROXY_PASSWORD); if (param) { FATAL("%s specified but not %s\n", CONF_HTTP_PROXY_PASSWORD, CONF_HTTP_PROXY_USER); } } } else if ((param = getConfigParam(CONF_HTTP_PROXY_PORT))) { FATAL("%s specified but not %s, line %i\n", CONF_HTTP_PROXY_PORT, CONF_HTTP_PROXY_HOST, param->line); } else if ((param = getConfigParam(CONF_HTTP_PROXY_USER))) { FATAL("%s specified but not %s, line %i\n", CONF_HTTP_PROXY_USER, CONF_HTTP_PROXY_HOST, param->line); } else if ((param = getConfigParam(CONF_HTTP_PROXY_PASSWORD))) { FATAL("%s specified but not %s, line %i\n", CONF_HTTP_PROXY_PASSWORD, CONF_HTTP_PROXY_HOST, param->line); } param = getConfigParam(CONF_HTTP_BUFFER_SIZE); if (param) { bufferSize = strtol(param->value, &test, 10); if (bufferSize <= 0 || *test != '\0') { FATAL("\"%s\" specified for %s at line %i is not a " "positive integer\n", param->value, CONF_HTTP_BUFFER_SIZE, param->line); } bufferSize *= 1024; if (prebufferSize > bufferSize) prebufferSize = bufferSize; } param = getConfigParam(CONF_HTTP_PREBUFFER_SIZE); if (param) { prebufferSize = strtol(param->value, &test, 10); if (prebufferSize <= 0 || *test != '\0') { FATAL("\"%s\" specified for %s at line %i is not a " "positive integer\n", param->value, CONF_HTTP_PREBUFFER_SIZE, param->line); } prebufferSize *= 1024; } if (prebufferSize > bufferSize) prebufferSize = bufferSize; } /* base64 code taken from xmms */ #define BASE64_LENGTH(len) (4 * (((len) + 2) / 3)) static char *base64Dup(char *s) { int i; int len = strlen(s); char *ret = xcalloc(BASE64_LENGTH(len) + 1, 1); unsigned char *p = (unsigned char *)ret; char tbl[64] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; /* Transform the 3x8 bits to 4x6 bits, as required by base64. */ for (i = 0; i < len; i += 3) { *p++ = tbl[s[0] >> 2]; *p++ = tbl[((s[0] & 3) << 4) + (s[1] >> 4)]; *p++ = tbl[((s[1] & 0xf) << 2) + (s[2] >> 6)]; *p++ = tbl[s[2] & 0x3f]; s += 3; } /* Pad the result if necessary... */ if (i == len + 1) *(p - 1) = '='; else if (i == len + 2) *(p - 1) = *(p - 2) = '='; /* ...and zero-terminate it. */ *p = '\0'; return ret; } static char *authString(char *header, char *user, char *password) { char *ret = NULL; int templen; char *temp; char *temp64; if (!user || !password) return NULL; templen = strlen(user) + strlen(password) + 2; temp = xmalloc(templen); strcpy(temp, user); strcat(temp, ":"); strcat(temp, password); temp64 = base64Dup(temp); free(temp); ret = xmalloc(strlen(temp64) + strlen(header) + 3); strcpy(ret, header); strcat(ret, temp64); strcat(ret, "\r\n"); free(temp64); return ret; } #define PROXY_AUTH_HEADER "Proxy-Authorization: Basic " #define HTTP_AUTH_HEADER "Authorization: Basic " #define proxyAuthString(x, y) authString(PROXY_AUTH_HEADER, x, y) #define httpAuthString(x, y) authString(HTTP_AUTH_HEADER, x, y) static InputStreamHTTPData *newInputStreamHTTPData(void) { InputStreamHTTPData *ret = xmalloc(sizeof(InputStreamHTTPData)); if (proxyHost) { ret->proxyAuth = proxyAuthString(proxyUser, proxyPassword); } else ret->proxyAuth = NULL; ret->httpAuth = NULL; ret->host = NULL; ret->path = NULL; ret->port = NULL; ret->connState = HTTP_CONN_STATE_CLOSED; ret->timesRedirected = 0; ret->icyMetaint = 0; ret->prebuffer = 0; ret->icyOffset = 0; ret->buffer = xmalloc(bufferSize); ret->tries = 0; return ret; } static void freeInputStreamHTTPData(InputStreamHTTPData * data) { if (data->host) free(data->host); if (data->path) free(data->path); if (data->port) free(data->port); if (data->proxyAuth) free(data->proxyAuth); if (data->httpAuth) free(data->httpAuth); free(data->buffer); free(data); } static int parseUrl(InputStreamHTTPData * data, char *url) { char *temp; char *colon; char *slash; char *at; int len; if (strncmp("http://", url, strlen("http://")) != 0) return -1; temp = url + strlen("http://"); colon = strchr(temp, ':'); at = strchr(temp, '@'); if (data->httpAuth) { free(data->httpAuth); data->httpAuth = NULL; } if (at) { char *user; char *passwd; if (colon && colon < at) { user = xmalloc(colon - temp + 1); memcpy(user, temp, colon - temp); user[colon - temp] = '\0'; passwd = xmalloc(at - colon); memcpy(passwd, colon + 1, at - colon - 1); passwd[at - colon - 1] = '\0'; } else { user = xmalloc(at - temp + 1); memcpy(user, temp, at - temp); user[at - temp] = '\0'; passwd = xstrdup(""); } data->httpAuth = httpAuthString(user, passwd); free(user); free(passwd); temp = at + 1; colon = strchr(temp, ':'); } slash = strchr(temp, '/'); if (slash && colon && slash <= colon) return -1; /* fetch the host portion */ if (colon) len = colon - temp + 1; else if (slash) len = slash - temp + 1; else len = strlen(temp) + 1; if (len <= 1) return -1; data->host = xmalloc(len); memcpy(data->host, temp, len - 1); data->host[len - 1] = '\0'; /* fetch the port */ if (colon && (!slash || slash != colon + 1)) { len = strlen(colon) - 1; if (slash) len -= strlen(slash); data->port = xmalloc(len + 1); memcpy(data->port, colon + 1, len); data->port[len] = '\0'; DEBUG(__FILE__ ": Port: %s\n", data->port); } else { data->port = xstrdup("80"); } /* fetch the path */ if (proxyHost) data->path = xstrdup(url); else data->path = xstrdup(slash ? slash : "/"); return 0; } static int initHTTPConnection(InputStream * inStream) { char *connHost; char *connPort; struct addrinfo *ans = NULL; struct addrinfo *ap = NULL; struct addrinfo hints; int error, flags; InputStreamHTTPData *data = (InputStreamHTTPData *) inStream->data; /** * Setup hints */ hints.ai_flags = 0; hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_addrlen = 0; hints.ai_addr = NULL; hints.ai_canonname = NULL; hints.ai_next = NULL; if (proxyHost) { connHost = proxyHost; connPort = proxyPort; } else { connHost = data->host; connPort = data->port; } error = getaddrinfo(connHost, connPort, &hints, &ans); if (error) { DEBUG(__FILE__ ": Error getting address info: %s\n", gai_strerror(error)); return -1; } /* loop through possible addresses */ for (ap = ans; ap != NULL; ap = ap->ai_next) { if ((data->sock = socket(ap->ai_family, ap->ai_socktype, ap->ai_protocol)) < 0) { DEBUG(__FILE__ ": unable to connect: %s\n", strerror(errno)); freeaddrinfo(ans); return -1; } flags = fcntl(data->sock, F_GETFL, 0); fcntl(data->sock, F_SETFL, flags | O_NONBLOCK); if (connect(data->sock, ap->ai_addr, ap->ai_addrlen) >= 0 || errno == EINPROGRESS) { data->connState = HTTP_CONN_STATE_INIT; data->buflen = 0; freeaddrinfo(ans); return 0; /* success */ } /* failed, get the next one */ DEBUG(__FILE__ ": unable to connect: %s\n", strerror(errno)); close(data->sock); } freeaddrinfo(ans); return -1; /* failed */ } static int finishHTTPInit(InputStream * inStream) { InputStreamHTTPData *data = (InputStreamHTTPData *) inStream->data; struct timeval tv; fd_set writeSet; fd_set errorSet; int error; socklen_t error_len = sizeof(int); int ret; int length; char request[2048]; tv.tv_sec = 0; tv.tv_usec = 0; FD_ZERO(&writeSet); FD_ZERO(&errorSet); FD_SET(data->sock, &writeSet); FD_SET(data->sock, &errorSet); ret = select(data->sock + 1, NULL, &writeSet, &errorSet, &tv); if (ret == 0 || (ret < 0 && errno == EINTR)) return 0; if (ret < 0) { DEBUG(__FILE__ ": problem select'ing: %s\n", strerror(errno)); goto close_err; } getsockopt(data->sock, SOL_SOCKET, SO_ERROR, &error, &error_len); if (error) goto close_err; /* deal with ICY metadata later, for now its fucking up stuff! */ length = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Connection: close\r\n" "User-Agent: " PACKAGE_NAME "/" PACKAGE_VERSION "\r\n" "Range: bytes=%ld-\r\n" "%s" /* authorization */ "Icy-Metadata:1\r\n" "\r\n", data->path, data->host, inStream->offset, data->proxyAuth ? data->proxyAuth : (data->httpAuth ? data->httpAuth : "")); if (length >= sizeof(request)) goto close_err; ret = write(data->sock, request, length); if (ret != length) goto close_err; data->connState = HTTP_CONN_STATE_HELLO; return 0; close_err: close(data->sock); data->connState = HTTP_CONN_STATE_CLOSED; return -1; } static int getHTTPHello(InputStream * inStream) { InputStreamHTTPData *data = (InputStreamHTTPData *) inStream->data; fd_set readSet; struct timeval tv; int ret; char *needle; char *cur = data->buffer; int rc; long readed; FD_ZERO(&readSet); FD_SET(data->sock, &readSet); tv.tv_sec = 0; tv.tv_usec = 0; ret = select(data->sock + 1, &readSet, NULL, NULL, &tv); if (ret == 0 || (ret < 0 && errno == EINTR)) return 0; if (ret < 0) { data->connState = HTTP_CONN_STATE_CLOSED; close(data->sock); data->buflen = 0; return -1; } if (data->buflen >= bufferSize - 1) { data->connState = HTTP_CONN_STATE_CLOSED; close(data->sock); return -1; } readed = recv(data->sock, data->buffer + data->buflen, bufferSize - 1 - data->buflen, 0); if (readed < 0 && (errno == EAGAIN || errno == EINTR)) return 0; if (readed <= 0) { data->connState = HTTP_CONN_STATE_CLOSED; close(data->sock); data->buflen = 0; return -1; } data->buffer[data->buflen + readed] = '\0'; data->buflen += readed; needle = strstr(data->buffer, "\r\n\r\n"); if (!needle) return 0; if (0 == strncmp(cur, "HTTP/1.0 ", 9)) { inStream->seekable = 0; rc = atoi(cur + 9); } else if (0 == strncmp(cur, "HTTP/1.1 ", 9)) { inStream->seekable = 1; rc = atoi(cur + 9); } else if (0 == strncmp(cur, "ICY 200 OK", 10)) { inStream->seekable = 0; rc = 200; } else if (0 == strncmp(cur, "ICY 400 Server Full", 19)) rc = 400; else if (0 == strncmp(cur, "ICY 404", 7)) rc = 404; else { close(data->sock); data->connState = HTTP_CONN_STATE_CLOSED; return -1; } switch (rc) { case 200: case 206: break; case 301: case 302: cur = strstr(cur, "Location: "); if (cur) { char *url; int curlen = 0; cur += strlen("Location: "); while (*(cur + curlen) != '\0' && *(cur + curlen) != '\r') { curlen++; } url = xmalloc(curlen + 1); memcpy(url, cur, curlen); url[curlen] = '\0'; ret = parseUrl(data, url); free(url); if (ret == 0 && data->timesRedirected < HTTP_REDIRECT_MAX) { data->timesRedirected++; close(data->sock); data->connState = HTTP_CONN_STATE_REOPEN; data->buflen = 0; return 0; } } case 400: case 401: case 403: case 404: default: close(data->sock); data->connState = HTTP_CONN_STATE_CLOSED; data->buflen = 0; return -1; } cur = strstr(data->buffer, "\r\n"); while (cur && cur != needle) { if (0 == strncasecmp(cur, "\r\nContent-Length: ", 18)) { if (!inStream->size) inStream->size = atol(cur + 18); } else if (0 == strncasecmp(cur, "\r\nicy-metaint:", 14)) { data->icyMetaint = atoi(cur + 14); } else if (0 == strncasecmp(cur, "\r\nicy-name:", 11) || 0 == strncasecmp(cur, "\r\nice-name:", 11)) { int incr = 11; char *temp = strstr(cur + incr, "\r\n"); if (!temp) break; *temp = '\0'; if (inStream->metaName) free(inStream->metaName); while (*(incr + cur) == ' ') incr++; inStream->metaName = xstrdup(cur + incr); *temp = '\r'; DEBUG("inputStream_http: metaName: %s\n", inStream->metaName); } else if (0 == strncasecmp(cur, "\r\nx-audiocast-name:", 19)) { int incr = 19; char *temp = strstr(cur + incr, "\r\n"); if (!temp) break; *temp = '\0'; if (inStream->metaName) free(inStream->metaName); while (*(incr + cur) == ' ') incr++; inStream->metaName = xstrdup(cur + incr); *temp = '\r'; DEBUG("inputStream_http: metaName: %s\n", inStream->metaName); } else if (0 == strncasecmp(cur, "\r\nContent-Type:", 15)) { int incr = 15; char *temp = strstr(cur + incr, "\r\n"); if (!temp) break; *temp = '\0'; if (inStream->mime) free(inStream->mime); while (*(incr + cur) == ' ') incr++; inStream->mime = xstrdup(cur + incr); *temp = '\r'; } cur = strstr(cur + 2, "\r\n"); } if (inStream->size <= 0) inStream->seekable = 0; needle += 4; /* 4 == strlen("\r\n\r\n") */ data->buflen -= (needle - data->buffer); memmove(data->buffer, needle, data->buflen); data->connState = HTTP_CONN_STATE_OPEN; data->prebuffer = 1; return 0; } int inputStream_httpOpen(InputStream * inStream, char *url) { InputStreamHTTPData *data = newInputStreamHTTPData(); inStream->data = data; if (parseUrl(data, url) < 0) { freeInputStreamHTTPData(data); return -1; } if (initHTTPConnection(inStream) < 0) { freeInputStreamHTTPData(data); return -1; } inStream->seekFunc = inputStream_httpSeek; inStream->closeFunc = inputStream_httpClose; inStream->readFunc = inputStream_httpRead; inStream->atEOFFunc = inputStream_httpAtEOF; inStream->bufferFunc = inputStream_httpBuffer; return 0; } int inputStream_httpSeek(InputStream * inStream, long offset, int whence) { InputStreamHTTPData *data; if (!inStream->seekable) return -1; switch (whence) { case SEEK_SET: inStream->offset = offset; break; case SEEK_CUR: inStream->offset += offset; break; case SEEK_END: inStream->offset = inStream->size + offset; break; default: return -1; } data = (InputStreamHTTPData *)inStream->data; close(data->sock); data->connState = HTTP_CONN_STATE_REOPEN; data->buflen = 0; inputStream_httpBuffer(inStream); return 0; } static void parseIcyMetadata(InputStream * inStream, char *metadata, int size) { char *r; char *s; char *temp = xmalloc(size + 1); memcpy(temp, metadata, size); temp[size] = '\0'; s = strtok_r(temp, ";", &r); while (s) { if (0 == strncmp(s, "StreamTitle=", 12)) { int cur = 12; if (inStream->metaTitle) free(inStream->metaTitle); if (*(s + cur) == '\'') cur++; if (s[strlen(s) - 1] == '\'') { s[strlen(s) - 1] = '\0'; } inStream->metaTitle = xstrdup(s + cur); DEBUG("inputStream_http: metaTitle: %s\n", inStream->metaTitle); } s = strtok_r(NULL, ";", &r); } free(temp); } size_t inputStream_httpRead(InputStream * inStream, void *ptr, size_t size, size_t nmemb) { InputStreamHTTPData *data = (InputStreamHTTPData *) inStream->data; long tosend = 0; long inlen = size * nmemb; long maxToSend = data->buflen; inputStream_httpBuffer(inStream); switch (data->connState) { case HTTP_CONN_STATE_OPEN: if (data->prebuffer || data->buflen < data->icyMetaint) return 0; break; case HTTP_CONN_STATE_CLOSED: if (data->buflen) break; default: return 0; } if (data->icyMetaint > 0) { if (data->icyOffset >= data->icyMetaint) { int metalen = *(data->buffer); metalen <<= 4; if (metalen < 0) metalen = 0; if (metalen + 1 > data->buflen) { /* damn that's some fucking big metadata! */ if (bufferSize < metalen + 1) { data->connState = HTTP_CONN_STATE_CLOSED; close(data->sock); data->buflen = 0; } return 0; } if (metalen > 0) { parseIcyMetadata(inStream, data->buffer + 1, metalen); } data->buflen -= metalen + 1; memmove(data->buffer, data->buffer + metalen + 1, data->buflen); data->icyOffset = 0; } maxToSend = data->icyMetaint - data->icyOffset; maxToSend = maxToSend > data->buflen ? data->buflen : maxToSend; } if (data->buflen > 0) { tosend = inlen > maxToSend ? maxToSend : inlen; tosend = (tosend / size) * size; memcpy(ptr, data->buffer, tosend); data->buflen -= tosend; data->icyOffset += tosend; memmove(data->buffer, data->buffer + tosend, data->buflen); inStream->offset += tosend; } return tosend / size; } int inputStream_httpClose(InputStream * inStream) { InputStreamHTTPData *data = (InputStreamHTTPData *) inStream->data; switch (data->connState) { case HTTP_CONN_STATE_CLOSED: break; default: close(data->sock); } freeInputStreamHTTPData(data); return 0; } int inputStream_httpAtEOF(InputStream * inStream) { InputStreamHTTPData *data = (InputStreamHTTPData *) inStream->data; switch (data->connState) { case HTTP_CONN_STATE_CLOSED: if (data->buflen == 0) return 1; default: return 0; } } int inputStream_httpBuffer(InputStream * inStream) { InputStreamHTTPData *data = (InputStreamHTTPData *) inStream->data; ssize_t readed = 0; if (data->connState == HTTP_CONN_STATE_REOPEN) { if (initHTTPConnection(inStream) < 0) return -1; } if (data->connState == HTTP_CONN_STATE_INIT) { if (finishHTTPInit(inStream) < 0) return -1; } if (data->connState == HTTP_CONN_STATE_HELLO) { if (getHTTPHello(inStream) < 0) return -1; } switch (data->connState) { case HTTP_CONN_STATE_OPEN: case HTTP_CONN_STATE_CLOSED: break; default: return -1; } if (data->buflen == 0 || data->buflen < data->icyMetaint) { data->prebuffer = 1; } else if (data->buflen > prebufferSize) data->prebuffer = 0; if (data->connState == HTTP_CONN_STATE_OPEN && data->buflen < bufferSize - 1) { readed = read(data->sock, data->buffer + data->buflen, (size_t) (bufferSize - 1 - data->buflen)); /* If the connection is currently unavailable, or interrupted (EINTR) * Don't give an error, so it's retried later. * Max times in a row to re-try this is HTTP_MAX_TRIES */ if (readed < 0 && (errno == EAGAIN || errno == EINTR) && data->tries < HTTP_MAX_TRIES) { data->tries++; DEBUG(__FILE__": Resource unavailable, trying %i times again\n", HTTP_MAX_TRIES-data->tries); readed = 0; } else if (readed <= 0) { close(data->sock); if(errno) DEBUG(__FILE__": Closing connection with error: '%s'\n", strerror(errno)); data->connState = HTTP_CONN_STATE_CLOSED; readed = 0; } else{ /* Reset the tries back to 0, because we managed to read data */ data->tries = 0; } data->buflen += readed; } if (data->buflen > prebufferSize) data->prebuffer = 0; return (readed ? 1 : 0); }