/* streamdump: Dumping a copy of the input data. This evolved into the generic I/O interposer for direct file or http stream access, with explicit buffering for getline. copyright 2010-2023 by the mpg123 project - free software under the terms of the LGPL 2.1 see COPYING and AUTHORS files in distribution or http://mpg123.org initially written by Michael Hipp */ // setenv #define _XOPEN_SOURCE 600 #define _POSIX_C_SOURCE 200112L #include "streamdump.h" #include #include #include "common/debug.h" #ifndef O_BINARY #define O_BINARY 0 #endif #ifndef HTTP_MAX_RELOCATIONS #define HTTP_MAX_RELOCATIONS 20 #endif #if defined(NETWORK) && !defined(NET123) #error "NETWORK only with NET123 from now on!" #endif #if !defined(NETWORK) && defined(NET123) #error "NET123 only with NETWORK from now on!" #endif #ifdef NETWORK const char *net123_backends[] = { "internal" #ifdef NET123_EXEC , "wget" , "curl" #endif #ifdef NET123_WINHTTP , "winhttp" #endif #ifdef NET123_WININET , "wininet" #endif , NULL }; // Net123 variant for our internal code that has safer legacy support for HTTP shoutcast. static size_t net123_read_internal( struct net123_handle_struct *nh, void *buf, size_t bufsize ) { if(!nh) return 0; int *fdp = nh->parts; #ifdef WANT_WIN32_SOCKETS return win32_net_read(*fdp, buf, bufsize); #else return INT123_unintr_read(*fdp, buf, bufsize); #endif } static void net123_close_internal(struct net123_handle_struct *nh) { if(nh) return; int *fdp = nh->parts; #ifdef WANT_WIN32_SOCKETS if(*fdp != SOCKET_ERROR) win32_net_close(*fdp); #else close(*fdp); #endif free(fdp); free(nh); } static net123_handle *net123_open_internal( const char *url , const char * const *client_head, struct httpdata *hd ) { net123_handle *nh = malloc(sizeof(net123_handle)); if(!nh) return NULL; int *fdp = malloc(sizeof(int)); if(!fdp) { free(nh); return NULL; } nh->parts = fdp; nh->read = net123_read_internal; nh->close = net123_close_internal; // Handles win32_net internally. *fdp = http_open(url, hd, client_head); if(*fdp >= 0) return nh; free(fdp); free(nh); return NULL; } // Decide which backend to load. static net123_handle *net123_open( const char *url , const char * const *client_head, struct httpdata *hd ) { int autochoose = !strcmp("auto", param.network_backend); int https = !strncasecmp("https://", url, 8); if(param.proxyurl) { if(strcmp(param.proxyurl, "none")) { #ifdef HAVE_SETENV setenv("http_proxy", param.proxyurl, 1); setenv("HTTP_PROXY", param.proxyurl, 1); setenv("https_proxy", param.proxyurl, 1); setenv("HTTPS_PROXY", param.proxyurl, 1); #endif } else { #ifdef HAVE_UNSETENV unsetenv("http_proxy"); unsetenv("HTTP_PROXY"); unsetenv("https_proxy"); unsetenv("HTTPS_PROXY"); #endif } } if( (autochoose && !https) || !strcmp("internal", param.network_backend) ) { if(https && !param.quiet) fprintf(stderr, "Note: HTTPS will fail with internal network code.\n"); return net123_open_internal(url, client_head, hd); } #ifdef NET123_EXEC if( autochoose || !strcmp("wget", param.network_backend) || !strcmp("curl", param.network_backend) ) return net123_open_exec(url, client_head); #endif #ifdef NET123_WININET if(autochoose || !strcmp("wininet", param.network_backend)) return net123_open_wininet(url, client_head); #endif #ifdef NET123_WINHTTP if(autochoose || !strcmp("winhttp", param.network_backend)) return net123_open_winhttp(url, client_head); #endif merror("no network backend for %s", https ? "HTTPS" : "HTTP"); return NULL; } #endif /* Stream dump descriptor. */ static int dump_fd = -1; // Read without the buffer. This is used to fill the buffer explicitly in getline. // This is the function that finally wraps around all the different types of input. static mpg123_ssize_t stream_read_raw(struct stream *sd, void *buf, size_t count) { mpg123_ssize_t ret = -1; #ifdef NETWORK if(sd->nh) ret = (mpg123_ssize_t) sd->nh->read(sd->nh, buf, count); #endif if(sd->fd >= 0) // plain file or network socket ret = (mpg123_ssize_t) INT123_unintr_read(sd->fd, buf, count); return ret; } static mpg123_ssize_t stream_read(struct stream *sd, void *buf, size_t count) { if(!sd) return -1; char *bbuf = buf; mpg123_ssize_t ret = 0; if(count > SSIZE_MAX) return -1; while(count) { size_t get = 0; if(sd->fill) { // drain the buffer get = sd->fill > count ? count : sd->fill; memcpy(bbuf, sd->bufp, get); sd->fill -= get; sd->bufp += get; } else { // get it from the source mpg123_ssize_t rret = stream_read_raw(sd, bbuf, count); if(rret < 0) return ret > 0 ? ret : -1; if(rret == 0) return ret; get = rret; } bbuf += get; count -= get; ret += get; } return ret; } static off_t stream_seek(struct stream *sd, off_t pos, int whence) { if(!sd) return -1; #ifdef NET123 if(sd->nh) return -1; #endif return lseek(sd->fd, pos, whence); } // Read into the stream buffer, look for line endings there, copy stuff. // This should work with \n and \r\n sequences ... even just \r sequences? // Yes, either \r or \n ends a line, a following \n or \r is just swallowed. // Need to catch the case where the buffer ends with \r and the next buffer // contents start with the matching \n, and the other way round. mpg123_ssize_t stream_getline(struct stream *sd, mpg123_string *line) { if(!sd || !line) return -1; line->fill = 0; // this is EOF char lend = 0; while(1) { mdebug("getline loop with %d", sd->fill); // If we got just an \r, we need to ensure that we swalloed the matching \n, // too. This implies that we got a line stored already. if(sd->fill && lend) { // finish skipping over an earlier line end if( (*sd->bufp == '\n' || *sd->bufp == '\r') && *sd->bufp != lend) { ++sd->bufp; --sd->fill; } // whatever happened, no half-line-end lurking anymore return line->fill; } if(sd->fill) { // look for line end here, copy things size_t i = 0; while(i < sd->fill && sd->bufp[i] != '\n' && sd->bufp[i] != '\r') ++i; // either found an end, or hit the end if(!mpg123_add_substring(line, sd->bufp, 0, i)) return -1; // out of memory // skip over a line end if found if(i == sd->fill) { // not done yet, refill, please sd->fill = 0; } else { // got end and stored complete line, but need to go on to capture full line end lend = sd->bufp[i]; // either \r or \n sd->bufp += i+1; sd->fill -= i+1; } } else { debug("re-filling buffer"); // refill buffer mpg123_ssize_t ret = stream_read_raw(sd, sd->buf, sizeof(sd->buf)); mdebug("raw read return: %zd", ret); if(ret < 0) return -1; else if(ret == 0) return line->fill; // A line ends at end of file. else { sd->fill = ret; sd->bufp = sd->buf; } } } } #ifdef NETWORK // Return 0 on success, non-zero when there is an error or more work to do. // -1: error, 1: redirection to given location static int stream_parse_headers(struct stream *sd, mpg123_string *location) { int ret = 0; mpg123_string line; mpg123_init_string(&line); mpg123_string icyint; mpg123_init_string(&icyint); int redirect = 0; location->fill = 0; const char *head[] = { "content-type", "icy-name" , "icy-url", "icy-metaint", "location" }; mpg123_string *val[] = { &sd->htd.content_type, &sd->htd.icy_name , &sd->htd.icy_url, &icyint, location }; int hn = sizeof(head)/sizeof(char*); int hi = -1; int got_ok = 0; int got_line = 0; debug("parsing headers"); while(stream_getline(sd, &line) > 0) { got_line = 1; mdebug("HTTP in: %s", line.p); if(line.p[0] == 0) { break; // This is the content separator line. } // React to HTTP error codes, but do not enforce an OK being sent as Shoutcast // only produces very minimal headers, not even a HTTP response code. // Well, ICY 200 OK could be there, but then we got other headers to know // things are fine. if(!strncasecmp("http/", line.p, 5)) { // HTTP/1.1 200 OK char *tok = line.p; while(*tok && *tok != ' ' && *tok != '\t') ++tok; while(*tok && (*tok == ' ' || *tok == '\t')) ++tok; if(tok && *tok != '2') { if(*tok == '3') { redirect = ret = 1; if(param.verbose > 2) fprintf(stderr, "Note: HTTP redirect\n"); } else { merror("HTTP error response: %s", line.p); ret = -1; break; } } else if(tok && *tok == '2') { if(param.verbose > 2) fprintf(stderr, "Note: got a positive HTTP response\n"); got_ok = 1; } } if(hi >= 0 && (line.p[0] == ' ' || line.p[0] == '\t')) { debug("header continuation"); // nh continuation line, appending to already stored value. char *v = line.p+1; while(*v == ' ' || *v == '\t'){ ++v; } if(!mpg123_add_string(val[hi], v)) { merror("failed to grow header value for %s", head[hi]); hi = -1; continue; } } char *n = line.p; char *v = strchr(line.p, ':'); if(!v) continue; // No proper header line. // Got a header line. *v = 0; // Terminate the header name. if(param.verbose > 2) fprintf(stderr, "Note: got header: %s\n", n); ++v; // Value starts after : and whitespace. while(*v == ' ' || *v == '\t'){ ++v; } for(hi = 0; hi 2) fprintf(stderr, "Note: storing HTTP header %s: %s\n", head[hi], v); got_ok = 1; // When we got some header to store, things seem fine. if(!mpg123_set_string(val[hi], v)) { error("failed to allocate header value storage"); hi = -1; continue; } } if(!redirect && icyint.fill) { sd->htd.icy_interval = atol(icyint.p); if(param.verbose > 1) fprintf(stderr, "Info: ICY interval %li\n", (long)sd->htd.icy_interval); } if(!got_line) { error("no data at all from network resource"); ret = -1; } else if(redirect && !location->fill) { error("redirect but no location given"); } else if(!got_ok) { error("missing positive server response"); ret = -1; } mpg123_free_string(&icyint); mpg123_free_string(&line); return ret; } // resolve relative locations given the initial full URL // full URL ensured to either start with http:// or https:// // (case-insensitive), location non-empty static void relocate_url(mpg123_string *location, const char *url) { if(!strncasecmp(location->p, "http://", 7) || !strncasecmp(location->p, "https://", 8)) return; if(!url || (strncasecmp(url, "http://", 7) && strncasecmp(url, "https://", 8))) { mpg123_resize_string(location, 0); return; } if(!param.quiet) fprintf(stderr, "NOTE: no complete URL in redirect, constructing one\n"); mpg123_string purl; mpg123_string workbuf; mpg123_init_string(&purl); mpg123_init_string(&workbuf); if(mpg123_set_string(&purl, url) && mpg123_move_string(location, &workbuf)) { debug1("relocate request_url: %s", purl.p); // location somewhat relative, either /some/path or even just some/path char* ptmp = NULL; // Though it's not RFC (?), accept relative URIs as wget does. if(workbuf.p[0] == '/') { // server-absolute only prepend http://server/ // I null the first / after http:// or https:// size_t off = (purl.p[4] == 's') ? 8 : 7; ptmp = strchr(purl.p+off,'/'); if(ptmp != NULL) { purl.fill = ptmp-purl.p+1; purl.p[purl.fill-1] = 0; } } else { // relative to current directory // prepend http://server/path/ // first cutting off parameter stuff from URL for(size_t i=0; ibufp = sd->buf; sd->fill = 0; sd->fd = -1; #ifdef NET123 sd->nh = NULL; #endif httpdata_init(&sd->htd); } // Clean up things for another connection. // Does not reset the network flag. static void stream_reset(struct stream *sd) { #ifdef NET123 if(sd->nh) sd->nh->close(sd->nh); sd->nh = NULL; #endif if(sd->fd >= 0) // plain file or network socket close(sd->fd); sd->fd = -1; httpdata_free(&sd->htd); httpdata_init(&sd->htd); sd->bufp = sd->buf; sd->fill = 0; } struct stream *stream_open(const char *url) { struct stream *sd = malloc(sizeof(struct stream)); if(!sd) return NULL; stream_init(sd); mdebug("opening resource %s", url); if(!strcmp(url, "-")) { sd->fd = STDIN_FILENO; INT123_compat_binmode(STDIN_FILENO, TRUE); } #ifdef NETWORK else if(!strncasecmp("http://", url, 7) || !strncasecmp("https://", url, 8)) { // Network stream with header parsing. const char *client_head[] = { NULL, NULL, NULL }; client_head[0] = param.talk_icy ? icy_yes : icy_no; mpg123_string accept; mpg123_init_string(&accept); append_accept(&accept); client_head[1] = accept.p; mpg123_string location; mpg123_string urlcopy; mpg123_init_string(&location); mpg123_init_string(&urlcopy); int numrelocs = 0; while(sd) { sd->nh = net123_open(url, client_head, &sd->htd); if(!sd->nh) { stream_close(sd); sd = NULL; break; } location.fill = 0; if(stream_parse_headers(sd, &location)) { stream_reset(sd); if(location.fill) { if(++numrelocs > HTTP_MAX_RELOCATIONS) { merror("too many HTTP redirections: %i", numrelocs); url = NULL; } else { relocate_url(&location, url); // resolve relative locations mpg123_copy_string(&location, &urlcopy); url = urlcopy.p; } } else { url = NULL; } if(!url) { stream_close(sd); sd = NULL; } } else break; // Successful end. } mpg123_free_string(&urlcopy); mpg123_free_string(&location); mpg123_free_string(&accept); // Either sd is NULL or we got a stream ready. } #endif else { // plain file access if(!strncasecmp("file://", url, 7)) url+= 7; // Might be useful to prepend file scheme prefix for local stuff. errno = 0; sd->fd = INT123_compat_open(url, O_RDONLY|O_BINARY); if(sd->fd < 0) { merror("failed to open file: %s: %s", url, INT123_strerror(errno)); stream_close(sd); return NULL; } } return sd; } void stream_close(struct stream *sd) { if(!sd) return; stream_reset(sd); free(sd); } /* Read data from input, write copy to dump file. */ static mpg123_ssize_t dump_read(void *handle, void *buf, size_t count) { struct stream *sd = handle; mpg123_ssize_t ret = stream_read(sd, buf, count); if(ret > 0 && dump_fd > -1) { ret = INT123_unintr_write(dump_fd, buf, ret); } return ret; } /* Also mirror seeks, to prevent messed up dumps of seekable streams. */ static off_t dump_seek(void *handle, off_t pos, int whence) { struct stream *sd = handle; off_t ret = stream_seek(sd, pos, whence); if(ret >= 0 && dump_fd > -1) { ret = lseek(dump_fd, pos, whence); } return ret; } /* External API... open and close. */ int dump_setup(struct stream *sd, mpg123_handle *mh) { int ret = MPG123_OK; int do_replace = 0; // full replacement with handle // paranoia: if buffer active, ensure handle I/O if(sd->fill) do_replace = 1; #ifdef NET123 if(sd->nh) do_replace = 1; #endif if(param.streamdump) { do_replace = 1; // open freshly or keep open if(dump_fd < 0) { if(!param.quiet) fprintf(stderr, "Note: Dumping stream to %s\n", param.streamdump); dump_fd = INT123_compat_open(param.streamdump, O_CREAT|O_TRUNC|O_RDWR); } if(dump_fd < 0) { error1("Failed to open dump file: %s\n", INT123_strerror(errno)); return -1; } #ifdef WIN32 _setmode(dump_fd, _O_BINARY); #endif } if( MPG123_OK != mpg123_param(mh, MPG123_ICY_INTERVAL , param.icy_interval ? param.icy_interval : sd->htd.icy_interval, 0) ) error1("Cannot set ICY interval: %s", mpg123_strerror(mh)); if(param.icy_interval > 0 && param.verbose > 1) fprintf(stderr, "Info: Forced ICY interval %li\n", param.icy_interval); if(do_replace) { mpg123_replace_reader_handle(mh, dump_read, dump_seek, NULL); ret = mpg123_open_handle(mh, sd); } else { mpg123_replace_reader(mh, NULL, NULL); ret = mpg123_open_fd(mh, sd->fd); } if(ret != MPG123_OK) { error1("Unable to replace reader/open track for stream dump: %s\n", mpg123_strerror(mh)); dump_close(); return -1; } else return 0; } void dump_close(void) { if(dump_fd > -1) INT123_compat_close(dump_fd); dump_fd = -1; }