#include "qtv.h"
#define MINPLUGVER "4239"
//main reason to use connection close is because we're lazy and don't want to give sizes in advance (yes, we could use chunks..)
size_t SHA1(unsigned char *digest, size_t maxdigestsize, const unsigned char *string, size_t stringlen);
void tobase64(unsigned char *out, int outlen, unsigned char *in, int inlen)
{
static unsigned char tab[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','+','/'
};
unsigned int usedbits = 0;
unsigned int val = 0;
outlen--;
while(inlen)
{
while(usedbits < 24 && inlen)
{
val <<= 8;
val |= (*in++);
inlen--;
usedbits += 8;
}
if (outlen < 4)
return;
val <<= 24 - usedbits;
*out++ = (usedbits > 0)?tab[(val>>18)&0x3f]:'=';
*out++ = (usedbits > 6)?tab[(val>>12)&0x3f]:'=';
*out++ = (usedbits > 12)?tab[(val>>6)&0x3f]:'=';
*out++ = (usedbits > 18)?tab[(val>>0)&0x3f]:'=';
val=0;
usedbits = 0;
}
*out = 0;
}
static const char qfont_table[256] = {
'\0', '#', '#', '#', '#', '.', '#', '#',
'#', 9, 10, '#', ' ', 13, '.', '.',
'[', ']', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '.', '<', '=', '>',
' ', '!', '"', '#', '$', '%', '&', '\'',
'(', ')', '*', '+', ',', '-', '.', '/',
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', ':', ';', '<', '=', '>', '?',
'@', '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', '.', '<', '=', '>',
' ', '!', '"', '#', '$', '%', '&', '\'',
'(', ')', '*', '+', ',', '-', '.', '/',
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', ':', ';', '<', '=', '>', '?',
'@', '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', '{', '|', '}', '~', '<'
};
static void HTMLprintf(char *outb, int outl, char *fmt, ...)
{
va_list val;
char qfmt[8192*4];
char *inb = qfmt;
unsigned char inchar;
va_start(val, fmt);
vsnprintf(qfmt, sizeof(qfmt), fmt, val);
va_end(val);
qfmt[sizeof(qfmt)-1] = 0;
outl--;
outl -= 5;
while (outl > 0 && *inb)
{
inchar = qfont_table[*(unsigned char*)inb];
if (inchar == '<')
{
*outb++ = '&';
*outb++ = 'l';
*outb++ = 't';
*outb++ = ';';
outl -= 4;
}
else if (inchar == '>')
{
*outb++ = '&';
*outb++ = 'g';
*outb++ = 't';
*outb++ = ';';
outl -= 4;
}
else if (inchar == '\n')
{
*outb++ = '<';
*outb++ = 'b';
*outb++ = 'r';
*outb++ = '/';
*outb++ = '>';
outl -= 5;
}
else if (inchar == '&')
{
*outb++ = '&';
*outb++ = 'a';
*outb++ = 'm';
*outb++ = 'p';
*outb++ = ';';
outl -= 5;
}
else
{
*outb++ = inchar;
outl -= 1;
}
inb++;
}
*outb++ = 0;
}
static void HTTPSV_SendHTTPHeader(cluster_t *cluster, oproxy_t *dest, char *error_code, char *content_type, qboolean nocache)
{
char *s;
char buffer[2048];
if (nocache)
{
s = "HTTP/1.1 %s OK\r\n"
"Content-Type: %s\r\n"
"Cache-Control: no-cache, must-revalidate\r\n"
"Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n"
"Connection: close\r\n"
"\r\n";
}
else
{
s = "HTTP/1.1 %s OK\r\n"
"Cache-Control: public, max-age=3600\r\n"
"Content-Type: %s\r\n"
"Connection: close\r\n"
"\r\n";
}
snprintf(buffer, sizeof(buffer), s, error_code, content_type);
Net_ProxySend(cluster, dest, buffer, strlen(buffer));
}
static void HTTPSV_SendHTMLHeader(cluster_t *cluster, oproxy_t *dest, char *title, char *args)
{
char *s;
char buffer[2048];
qboolean plugin = false;
while (*args && *args != ' ')
{
if (*args == 'p')
plugin = true;
args++;
}
s = "\n"
"\n"
"
\n"
" \n"
" %s\n"
" \n"
"\n"
"";
snprintf(buffer, sizeof(buffer), s, title,
plugin?"?p":"",
plugin?"?p":"",
(!*cluster->adminpassword)?"":(plugin?"Admin":"Admin"),
plugin?"Basic":"Plugin"
);
Net_ProxySend(cluster, dest, buffer, strlen(buffer));
}
static void HTTPSV_SendHTMLFooter(cluster_t *cluster, oproxy_t *dest)
{
char *s;
char buffer[2048];
/*Proxy version*/
snprintf(buffer, sizeof(buffer), "
Server Version: "QTV_VERSION_STRING" "PROXYWEBSITE"");
Net_ProxySend(cluster, dest, buffer, strlen(buffer));
/*terminate html page*/
s = "\n"
"\n";
Net_ProxySend(cluster, dest, s, strlen(s));
}
#define HTMLPRINT(str) Net_ProxySend(cluster, dest, str "\n", strlen(str "\n"))
static void HTTPSV_GenerateNowPlaying(cluster_t *cluster, oproxy_t *dest, char *args)
{
int player;
char *s;
char buffer[1024];
char plname[64];
sv_t *streams;
qboolean plugin = false;
qboolean activeonly = false;
HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/html", true);
HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Now Playing", args);
while (*args && *args != ' ')
{
if (*args == 'p')
plugin = true;
else if (*args == 'a')
activeonly = true;
args++;
}
if (!strcmp(cluster->hostname, DEFAULT_HOSTNAME))
snprintf(buffer, sizeof(buffer), "QuakeTV: Now Playing
"); //don't show the hostname if its set to the default
else
snprintf(buffer, sizeof(buffer), "%s: Now Playing
", cluster->hostname);
Net_ProxySend(cluster, dest, buffer, strlen(buffer));
HTMLPRINT("");
for (streams = cluster->servers; streams; streams = streams->next)
{
if (activeonly)
{
for (player = 0; player < MAX_CLIENTS; player++)
{
if (streams->isconnected && streams->map.thisplayer == player)
continue;
if (*streams->map.players[player].userinfo)
{
break;
}
}
if (player == MAX_CLIENTS)
continue;
}
HTMLPRINT("- ");
HTMLprintf(buffer, sizeof(buffer), "%s (%s: %s)", streams->server, streams->map.gamedir, streams->map.mapname);
Net_ProxySend(cluster, dest, buffer, strlen(buffer));
if (plugin && !strncmp(streams->server, "udp:", 4))
snprintf(buffer, sizeof(buffer), " [ Join ]", streams->server+4);
else
snprintf(buffer, sizeof(buffer), " [ Watch Now ]", streams->streamid);
Net_ProxySend(cluster, dest, buffer, strlen(buffer));
HTMLPRINT("
");
for (player = 0; player < MAX_CLIENTS; player++)
{
if (streams->isconnected && streams->map.thisplayer == player)
continue;
if (*streams->map.players[player].userinfo)
{
Info_ValueForKey(streams->map.players[player].userinfo, "name", plname, sizeof(plname));
if (streams->map.players[player].frags < -90)
{
HTMLPRINT("- ");
}
else
{
HTMLPRINT("
- ");
}
HTMLprintf(buffer, sizeof(buffer), "%s", plname);
Net_ProxySend(cluster, dest, buffer, strlen(buffer));
HTMLPRINT("
");
}
}
HTMLPRINT("
");
}
HTMLPRINT("
");
if (!cluster->servers)
{
s = "No streams are currently being played
";
Net_ProxySend(cluster, dest, s, strlen(s));
}
HTTPSV_SendHTMLFooter(cluster, dest);
}
static void HTTPSV_GenerateCSSFile(cluster_t *cluster, oproxy_t *dest)
{
HTTPSV_SendHTTPHeader(cluster, dest, "200", "text/css", false);
HTMLPRINT("* { font-family: Verdana, Helvetica, sans-serif; }");
HTMLPRINT("body { color: #000; background-color: #fff; padding: 0 0px; }");
HTMLPRINT("a { color: #00f; }");
HTMLPRINT("div.topdiv { display:flex; align-items: stretch; position: absolute; top: 0; right: 0; bottom: 0; left: 0; }");
HTMLPRINT("div.left { resize: horizontal; overflow: auto; flex 0 0 25%;}");
HTMLPRINT("div.right { padding:0; margin: 0; flex: auto; }");
HTMLPRINT("a.qtvfile { font-weight: bold; }");
HTMLPRINT("a:visited { color: #00f; }");
HTMLPRINT("a:hover { background-color: black; color: yellow; }");
HTMLPRINT("li.spectator { color: #666; font-size: 0.9ex; }");
HTMLPRINT("dl.nowplaying dd { margin: 0 0 2em 0; }");
HTMLPRINT("dl.nowplaying dt { margin: 1em 0 0 0; font-size: 1.1em; font-weight: bold; }");
HTMLPRINT("dl.nowplaying li { list-style: none; margin: 0 0 0 1em; padding: 0; }");
HTMLPRINT("dl.nowplaying ul { margin: 0 0 0 1em; padding: 0; }");
HTMLPRINT("canvas.emscripten { border: 0px none; padding:0; margin: 0; width: 100%; height: 100%;}");
HTMLPRINT("#navigation { background-color: #eef; }");
HTMLPRINT("#navigation li { display: inline; list-style: none; margin: 0 3em; }");
}
static qboolean HTTPSV_GetHeaderField(char *s, char *field, char *buffer, int buffersize)
{
char *e;
char *colon;
int fieldnamelen = strlen(field);
buffer[0] = 0;
e = s;
while(*e)
{
if (*e == '\n')
{
*e = '\0';
colon = strchr(s, ':');
*e = '\n';
if (!colon)
{
if (!strncmp(field, s, fieldnamelen))
{
if (s[fieldnamelen] <= ' ')
{
return true;
}
}
}
else
{
if (fieldnamelen == colon - s)
{
if (!strncmp(field, s, colon-s))
{
colon++;
while (*colon == ' ')
colon++;
while (buffersize > 2)
{
if (!*colon || *colon == '\r' || *colon == '\n')
break;
*buffer++ = *colon++;
buffersize--;
}
*buffer = 0;
return true;
}
}
}
s = e+1;
}
e++;
}
return false;
}
static void HTTPSV_GenerateQTVStub(cluster_t *cluster, oproxy_t *dest, char *streamtype, char *streamid)
{
char *s;
char hostname[128];
char buffer[1024];
char fname[256];
s = fname;
while (*streamid > ' ')
{
if (s > fname + sizeof(fname)-4) //4 cos I'm too lazy to work out what the actual number should be
break;
if (*streamid == '%')
{
*s = 0;
streamid++;
if (*streamid >= 'a' && *streamid <= 'f')
*s += 10 + *streamid-'a';
else if (*streamid >= 'A' && *streamid <= 'F')
*s += 10 + *streamid-'A';
else if (*streamid >= '0' && *streamid <= '9')
*s += *streamid-'0';
else
break;
*s <<= 4;
streamid++;
if (*streamid >= 'a' && *streamid <= 'f')
*s += 10 + *streamid-'a';
else if (*streamid >= 'A' && *streamid <= 'F')
*s += 10 + *streamid-'A';
else if (*streamid >= '0' && *streamid <= '9')
*s += *streamid-'0';
else
break;
//don't let hackers try adding extra commands to it.
if (*s == '$' || *s == ';' || *s == '\r' || *s == '\n')
continue;
streamid++;
s++;
}
else if (*streamid == '$' || *streamid == ';' || *streamid == '\r' || *streamid == '\n')
{
//don't let hackers try adding extra commands to it.
streamid++;
}
else
*s++ = *streamid++;
}
*s = 0;
streamid = fname;
if (!HTTPSV_GetHeaderField((char*)dest->inbuffer, "Host", hostname, sizeof(hostname)))
{
HTTPSV_SendHTTPHeader(cluster, dest, "400", "text/html", true);
HTTPSV_SendHTMLHeader(cluster, dest, "QuakeTV: Error", "");
s = "Your client did not send a Host field, which is required in HTTP/1.1\n
"
"Please try a different browser.\n"
"