looper/subprojects/mpg123/src/streamdump.c
2024-09-28 10:31:18 -07:00

702 lines
17 KiB
C

/*
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 <fcntl.h>
#include <errno.h>
#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<hn; ++hi)
{
if(!strcasecmp(n, head[hi]))
break;
}
if(hi == hn)
{
debug("skipping uninteresting header");
hi = -1;
continue;
}
if(param.verbose > 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; i<purl.fill; ++i)
{
if(purl.p[i] == '?' || purl.p[i] == '#')
{
purl.p[i] = 0;
purl.fill = i+1;
break;
}
}
// now the last slash, keeping it
ptmp = strrchr(purl.p, '/');
if(ptmp != NULL)
{
purl.fill = ptmp-purl.p+2;
purl.p[purl.fill-1] = 0;
}
}
// only the prefix left
debug1("prefix=%s", purl.p);
mpg123_add_string(location, purl.p);
}
mpg123_add_string(location, workbuf.p);
mpg123_free_string(&workbuf);
mpg123_free_string(&purl);
}
#endif
static void stream_init(struct stream *sd)
{
sd->bufp = 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;
}