tinyproxy/src/reqs.c

889 lines
21 KiB
C
Raw Normal View History

/* $Id: reqs.c,v 1.7 2000-03-31 22:55:22 rjkaes Exp $
*
* This is where all the work in tinyproxy is actually done. Incoming
* connections are added to the active list of connections and then the header
* is processed to determine what is being requested. tinyproxy then connects
* to the remote server and begins to relay the bytes back and forth between
* the client and the remote server. Basically, we sit there and sling bytes
* back and forth. Optionally, we can weed out headers we do not want to send,
* and also add a header of our own.
*
* Copyright (C) 1998 Steven Young
* Copyright (C) 1999 Robert James Kaes (rjkaes@flarenet.com)
* Copyright (C) 2000 Chris Lightfoot (chris@ex-parrot.com)
*
* 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, 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.
*/
#ifdef HAVE_CONFIG_H
#include <defines.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/uio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sysexits.h>
#include <assert.h>
#include "config.h"
#include "tinyproxy.h"
#include "sock.h"
#include "utils.h"
#include "conns.h"
#include "log.h"
#include "reqs.h"
#include "buffer.h"
#include "filter.h"
#include "uri.h"
#include "regexp.h"
#include "anonymous.h"
/* chris - for asynchronous DNS */
#include "dnscache.h"
#include <adns.h>
extern adns_state adns;
#ifdef XTINYPROXY
static void Add_XTinyproxy_Header(struct conn_s *connptr)
{
char *header_line;
char ipaddr[PEER_IP_LENGTH];
int length;
assert(connptr);
if (!(header_line = xmalloc(sizeof(char) * 32)))
return;
length = sprintf(header_line, "X-Tinyproxy: %s\r\n",
getpeer_ip(connptr->client_fd, ipaddr));
unshift_buffer(connptr->cbuffer, header_line, length);
}
#endif
#define HTTPPATTERN "^([a-z]+)[ \t]+([^ \t]+)([ \t]+(HTTP/[0-9]+\\.[0-9]+))?"
#define NMATCH 4
#define METHOD_IND 1
#define URI_IND 2
#define VERSION_MARK 3
#define VERSION_IND 4
#define HTTP400ERROR "Unrecognizable request. Only HTTP is allowed."
#define HTTP500ERROR "Unable to connect to remote server."
#define HTTP503ERROR "Internal server error."
/*
* Parse a client HTTP request and then establish connection.
*/
static int clientreq(struct conn_s *connptr)
{
URI *uri = NULL;
char *inbuf, *buffer, *request, *port;
char *inbuf_ptr;
regex_t preg;
regmatch_t pmatch[NMATCH];
long len;
int fd, port_no;
char peer_ipaddr[PEER_IP_LENGTH];
assert(connptr);
getpeer_ip(connptr->client_fd, peer_ipaddr);
/* chris - getpeer_string could block, so for the moment take it out */
if ((len
= readline(connptr->client_fd, connptr->cbuffer, &inbuf)) <= 0) {
return len;
}
inbuf_ptr = inbuf + len - 1;
while (*inbuf_ptr == '\r' || *inbuf_ptr == '\n')
*inbuf_ptr-- = '\0';
/* Log the incoming connection */
if (!config.restricted) {
log("Connect: %s", peer_ipaddr);
log("Request: %s", inbuf);
}
if (regcomp(&preg, HTTPPATTERN, REG_EXTENDED | REG_ICASE) != 0) {
log("ERROR clientreq: regcomp");
return 0;
}
if (regexec(&preg, inbuf, NMATCH, pmatch, 0) != 0) {
log("ERROR clientreq: regexec");
regfree(&preg);
return -1;
}
regfree(&preg);
if (pmatch[VERSION_MARK].rm_so == -1)
connptr->simple_req = TRUE;
if (pmatch[METHOD_IND].rm_so == -1 || pmatch[URI_IND].rm_so == -1) {
log("ERROR clientreq: Incomplete line from %s (%s)",
peer_ipaddr, inbuf);
httperr(connptr, 400, HTTP400ERROR);
goto COMMON_EXIT;
}
len = pmatch[URI_IND].rm_eo - pmatch[URI_IND].rm_so;
if (!(buffer = xmalloc(len + 1))) {
log("ERROR clientreq: Cannot allocate buffer for request from %s",
peer_ipaddr);
httperr(connptr, 503, HTTP503ERROR);
goto COMMON_EXIT;
}
memcpy(buffer, inbuf + pmatch[URI_IND].rm_so, len);
buffer[len] = '\0';
if (!(uri = explode_uri(buffer))) {
safefree(buffer);
log("ERROR clientreq: Problem with explode_uri");
httperr(connptr, 503, HTTP503ERROR);
goto COMMON_EXIT;
}
safefree(buffer);
if (!uri->scheme || strcasecmp(uri->scheme, "http") != 0) {
char *error_string;
if (uri->scheme) {
error_string = xmalloc(strlen(uri->scheme) + 64);
sprintf(error_string,
"Invalid scheme (%s). Only HTTP is allowed.",
uri->scheme);
} else {
error_string = strdup("Invalid scheme (NULL). Only HTTP is allowed.");
}
httperr(connptr, 400, error_string);
safefree(error_string);
goto COMMON_EXIT;
}
if (!uri->authority) {
httperr(connptr, 400, "Invalid authority.");
goto COMMON_EXIT;
}
if ((strlen(config.stathost) > 0) &&
strcasecmp(uri->authority, config.stathost) == 0) {
showstats(connptr);
goto COMMON_EXIT;
}
port_no = 80;
if ((port = strchr(uri->authority, ':'))) {
*port++ = '\0';
if (strlen(port) > 0)
port_no = atoi(port);
}
/* chris - so this can be passed on to clientreq_dnscomplete. */
connptr->port_no = port_no;
#ifdef FILTER_ENABLE
/* Filter domains out */
if (config.filter) {
if (filter_host(uri->authority)) {
log("ERROR clientreq: Filtered connection (%s)",
peer_ipaddr);
httperr(connptr, 404,
"Unable to connect to filtered host.");
goto COMMON_EXIT;
}
}
#endif /* FILTER_ENABLE */
/* Build a new request from the first line of the header */
if (!(request = xmalloc(strlen(inbuf) + 1))) {
log("ERROR clientreq: cannot allocate buffer for request from %s",
peer_ipaddr);
httperr(connptr, 503, HTTP503ERROR);
goto COMMON_EXIT;
}
/* We need to set the version number WE support */
memcpy(request, inbuf, pmatch[METHOD_IND].rm_eo);
request[pmatch[METHOD_IND].rm_eo] = '\0';
strcat(request, " ");
if (strlen(uri->path) > 0) {
strcat(request, uri->path);
if (uri->query) {
strcat(request, "?");
strcat(request, uri->query);
}
} else {
strcat(request, "/");
}
strcat(request, " HTTP/1.0\r\n");
/* chris - If domain is in dotted-quad format or is already in the
* DNS cache, then go straight to WAITCONN.
*/
if (inet_aton(uri->authority, NULL)
|| lookup(NULL, uri->authority)) {
if ((fd = opensock(uri->authority, port_no)) < 0) {
safefree(request);
httperr(connptr, 500,
"Unable to connect to host (cannot create sock)");
stats.num_badcons++;
goto COMMON_EXIT;
}
connptr->server_fd = fd;
connptr->type = WAITCONN;
}
/* Otherwise submit a DNS request and hope for the best. */
else {
if (adns_submit(adns, uri->authority, adns_r_a, adns_qf_quoteok_cname | adns_qf_cname_loose, connptr, &(connptr->adns_qu))) {
safefree(request);
httperr(connptr, 500, "Resolver error connecting to host");
stats.num_badcons++;
goto COMMON_EXIT;
} else {
#ifdef __DEBUG__
log("DNS request submitted");
#endif
/* copy domain for later caching */
connptr->domain = strdup(uri->authority);
connptr->type = DNS_WAITCONN;
}
}
#ifdef XTINYPROXY
/* Add a X-Tinyproxy header which contains our IP address */
if (config.my_domain
&& xstrstr(uri->authority, config.my_domain,
strlen(uri->authority), FALSE)) {
Add_XTinyproxy_Header(connptr);
}
#endif
/* Add the rewritten request to the buffer */
unshift_buffer(connptr->cbuffer, request, strlen(request));
COMMON_EXIT:
safefree(inbuf);
free_uri(uri);
return 0;
}
/* chris - added this to move a connection from the DNS_WAITCONN state
* to the WAITCONN state by connecting it to the server, once the name
* has been resolved.
*/
static int clientreq_dnscomplete(struct conn_s *connptr, struct in_addr *inaddr) {
int fd;
fd = opensock_inaddr(inaddr, connptr->port_no);
if (fd < 0) {
#ifdef __DEBUG__
log("Failed to open connection to server");
#endif
httperr(connptr, 500,
"Unable to connect to host (cannot create sock)");
stats.num_badcons++;
return 0;
} else {
#ifdef __DEBUG__
log("Connected to server");
#endif
connptr->server_fd = fd;
connptr->type = WAITCONN;
}
return 0;
}
/*
* Finish the client request
*/
static int clientreq_finish(struct conn_s *connptr)
{
int sockopt, len = sizeof(sockopt);
assert(connptr);
if (getsockopt
(connptr->server_fd, SOL_SOCKET, SO_ERROR, &sockopt, &len) < 0) {
log("ERROR clientreq_finish: getsockopt error (%s)",
strerror(errno));
return -1;
}
if (sockopt != 0) {
if (sockopt == EINPROGRESS)
return 0;
else if (sockopt == ETIMEDOUT) {
httperr(connptr, 408, "Connect Timed Out");
return 0;
} else {
log("ERROR clientreq_finish: could not create connection (%s)",
strerror(sockopt));
return -1;
}
}
connptr->type = RELAYCONN;
stats.num_opens++;
return 0;
}
/*
* Check to see if the line is allowed or not depending on the anonymous
* headers which are to be allowed.
*/
static int anonheader(char *line)
{
char *buffer, *ptr;
int ret;
assert(line);
if ((ptr = xstrstr(line, ":", strlen(line), FALSE)) == NULL)
return 0;
ptr++;
if ((buffer = xmalloc(ptr - line + 1)) == NULL)
return 0;
memcpy(buffer, line, ptr - line);
buffer[ptr - line] = '\0';
ret = anon_search(buffer);
free(buffer);
return ret;
}
/*
* Used to read in the lines from the header (client side) when we're doing
* the anonymous header reduction.
*/
static int readanonconn(struct conn_s *connptr)
{
char *line = NULL;
int retv;
assert(connptr);
if ((retv = readline(connptr->client_fd, connptr->cbuffer, &line)) <=
0) {
return retv;
}
if ((line[0] == '\n') || (strncmp(line, "\r\n", 2) == 0)) {
connptr->clientheader = TRUE;
} else if (!anonheader(line)) {
safefree(line);
return 0;
}
push_buffer(connptr->cbuffer, line, strlen(line));
return 0;
}
/*
* Read in the bytes from the socket
*/
static int readconn(int fd, struct buffer_s *buffptr)
{
int bytesin;
assert(fd >= 0);
assert(buffptr);
if ((bytesin = readbuff(fd, buffptr)) < 0) {
return -1;
}
#ifdef __DEBUG__
log("readconn [%d]: %d", fd, bytesin);
#endif
stats.num_rx += bytesin;
return bytesin;
}
/*
* Write the bytes from the buffer to the socket
*/
static int writeconn(int fd, struct buffer_s *buffptr)
{
int bytessent;
assert(fd >= 0);
assert(buffptr);
if ((bytessent = writebuff(fd, buffptr)) < 0) {
return -1;
}
stats.num_tx += bytessent;
return bytessent;
}
/*
* Factored out the common function to read from the client. It was used in
* two different places with no change, so no point in having the same code
* twice.
*/
static int read_from_client(struct conn_s *connptr, fd_set * readfds)
{
assert(connptr);
assert(readfds);
if (FD_ISSET(connptr->client_fd, readfds)) {
if (config.anonymous && !connptr->clientheader) {
if (readanonconn(connptr) < 0) {
shutdown(connptr->client_fd, 2);
shutdown(connptr->server_fd, 2);
connptr->type = FINISHCONN;
return -1;
}
} else if (readconn(connptr->client_fd, connptr->cbuffer) < 0) {
shutdown(connptr->client_fd, 2);
shutdown(connptr->server_fd, 2);
connptr->type = FINISHCONN;
return -1;
}
connptr->actiontime = time(NULL);
}
return 0;
}
/*
* Factored out the common write to client function since, again, it was used
* in two different places with no changes.
*/
static int write_to_client(struct conn_s *connptr, fd_set * writefds)
{
assert(connptr);
assert(writefds);
if (FD_ISSET(connptr->client_fd, writefds)) {
if (writeconn(connptr->client_fd, connptr->sbuffer) < 0) {
shutdown(connptr->client_fd, 2);
shutdown(connptr->server_fd, 2);
connptr->type = FINISHCONN;
return -1;
}
connptr->actiontime = time(NULL);
}
return 0;
}
/*
* All of the *_req functions handle the various stages a connection can go
* through. I moved them out from getreqs because they handle a lot of error
* code and it was indenting too far in. As you can see they are very simple,
* and are only called once from getreqs, hence the inlining.
*/
inline static void newconn_req(struct conn_s *connptr, fd_set * readfds)
{
#ifdef UPSTREAM_PROXY
int fd;
char peer_ipaddr[PEER_IP_LENGTH];
#endif /* UPSTREAM_PROXY */
assert(connptr);
assert(readfds);
if (FD_ISSET(connptr->client_fd, readfds)) {
#ifdef UPSTREAM_PROXY
if (config.upstream_name && config.upstream_port != 0) {
getpeer_ip(connptr->client_fd, peer_ipaddr);
/* Log the incoming connection */
if (!config.restricted) {
log("Connect (upstream): %s", peer_ipaddr);
}
if (inet_aton(config.upstream_name, NULL)
|| lookup(NULL, config.upstream_name)) {
if ((fd = opensock(config.upstream_name, config.upstream_port)) < 0) {
httperr(connptr, 500,
"Unable to connect to host (cannot create sock)");
stats.num_badcons++;
return;
}
connptr->server_fd = fd;
connptr->type = WAITCONN;
}
/* Otherwise submit a DNS request and hope for
the best. */
else {
if (adns_submit(adns, config.upstream_name, adns_r_a, adns_qf_quoteok_cname | adns_qf_cname_loose, connptr, &(connptr->adns_qu))) {
httperr(connptr, 500, "Resolver error connecting to host");
stats.num_badcons++;
return;
} else {
#ifdef __DEBUG__
log("DNS request submitted");
#endif
/* copy domain for later caching */
connptr->domain = xstrdup(config.upstream_name);
connptr->port_no = config.upstream_port;
connptr->type = DNS_WAITCONN;
}
}
} else {
#endif
if (clientreq(connptr) < 0) {
shutdown(connptr->client_fd, 2);
connptr->type = FINISHCONN;
return;
}
if (!connptr)
abort();
connptr->actiontime = time(NULL);
#ifdef UPSTREAM_PROXY
}
#endif /* UPSTREAM_PROXY */
}
}
inline static void waitconn_req(struct conn_s *connptr, fd_set * readfds,
fd_set * writefds)
{
assert(connptr);
assert(readfds);
assert(writefds);
if (read_from_client(connptr, readfds) < 0)
return;
if (FD_ISSET(connptr->server_fd, readfds)
|| FD_ISSET(connptr->server_fd, writefds)) {
if (clientreq_finish(connptr) < 0) {
shutdown(connptr->server_fd, 2);
shutdown(connptr->client_fd, 2);
connptr->type = FINISHCONN;
return;
}
connptr->actiontime = time(NULL);
}
}
inline static void relayconn_req(struct conn_s *connptr, fd_set * readfds,
fd_set * writefds)
{
assert(connptr);
assert(readfds);
assert(writefds);
if (read_from_client(connptr, readfds) < 0)
return;
if (FD_ISSET(connptr->server_fd, readfds)) {
if (connptr->serverheader) {
if (readconn(connptr->server_fd, connptr->sbuffer) < 0) {
shutdown(connptr->server_fd, 2);
connptr->type = CLOSINGCONN;
return;
}
} else {
/*
* We need to read in the first line to rewrite the
* version back down to HTTP/1.0 (if needed)
*/
char *line = NULL, *ptr, *newline;
int retv;
if (
(retv =
readline(connptr->server_fd, connptr->sbuffer,
&line)) < 0) {
shutdown(connptr->server_fd, 2);
httperr(connptr, 500, "Server Closed Early");
return;
} else if (retv == 0)
return;
connptr->serverheader = TRUE;
if (strncasecmp(line, "HTTP/1.0", 8)) {
/* Okay, we need to rewrite it then */
if (!(ptr = strchr(line, ' '))) {
shutdown(connptr->server_fd, 2);
httperr(connptr, 500,
"There was Server Error");
return;
}
ptr++;
if (!(newline = xmalloc(strlen(line) + 1))) {
shutdown(connptr->server_fd, 2);
httperr(connptr, 503,
"No Memory Available");
return;
}
sprintf(newline, "HTTP/1.0 %s", ptr);
safefree(line);
line = newline;
}
push_buffer(connptr->sbuffer, line, strlen(line));
}
connptr->actiontime = time(NULL);
}
if (write_to_client(connptr, writefds) < 0)
return;
if (FD_ISSET(connptr->server_fd, writefds)) {
if (writeconn(connptr->server_fd, connptr->cbuffer) < 0) {
shutdown(connptr->server_fd, 2);
connptr->type = CLOSINGCONN;
return;
}
connptr->actiontime = time(NULL);
}
}
inline static void closingconn_req(struct conn_s *connptr, fd_set * writefds)
{
assert(connptr);
assert(writefds);
write_to_client(connptr, writefds);
}
/*
* Check against the valid subnet to see if we should allow the access
*/
static int validuser(int fd)
{
char ipaddr[PEER_IP_LENGTH];
assert(fd >= 0);
if (config.subnet == NULL)
return 1;
if (!strncmp(config.subnet, getpeer_ip(fd, ipaddr),
strlen(config.subnet))) {
return 1;
} else {
return 0;
}
}
/*
* Loop that checks for new connections, dispatches to the correct
* routines if bytes are pending, checks to see if it's time for a
* garbage collect.
*/
int getreqs(void)
{
static unsigned int garbc = 0;
fd_set readfds, writefds, exceptfds; /* chris - ADNS expects exceptfds */
struct conn_s *connptr;
int fd;
struct timeval tv, now; /* chris - for ADNS timeouts */
char peer_ipaddr[PEER_IP_LENGTH];
if (setup_fd < 0) {
log("ERROR getreqs: setup_fd not a socket");
return -1;
}
/* Garbage collect the dead connections and close any idle ones */
if (garbc++ >= GARBCOLL_INTERVAL) {
garbcoll();
garbc = 0;
}
conncoll();
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(setup_fd, &readfds);
for (connptr = connections; connptr; connptr = connptr->next) {
#ifdef __DEBUG__
log("Connptr: %p - %d / client %d server %d", connptr,
connptr->type, connptr->client_fd, connptr->server_fd);
#endif
switch (connptr->type) {
case NEWCONN:
if (buffer_size(connptr->cbuffer) < MAXBUFFSIZE)
FD_SET(connptr->client_fd, &readfds);
else {
httperr(connptr, 414,
"Your Request is way too long.");
}
break;
/* no case here for DNS_WAITCONN */
case WAITCONN:
FD_SET(connptr->server_fd, &readfds);
FD_SET(connptr->server_fd, &writefds);
if (buffer_size(connptr->cbuffer) < MAXBUFFSIZE)
FD_SET(connptr->client_fd, &readfds);
break;
case RELAYCONN:
if (buffer_size(connptr->sbuffer) > 0)
FD_SET(connptr->client_fd, &writefds);
if (buffer_size(connptr->sbuffer) < MAXBUFFSIZE)
FD_SET(connptr->server_fd, &readfds);
if (buffer_size(connptr->cbuffer) > 0)
FD_SET(connptr->server_fd, &writefds);
if (buffer_size(connptr->cbuffer) < MAXBUFFSIZE)
FD_SET(connptr->client_fd, &readfds);
break;
case CLOSINGCONN:
if (buffer_size(connptr->sbuffer) > 0)
FD_SET(connptr->client_fd, &writefds);
else {
shutdown(connptr->client_fd, 2);
shutdown(connptr->server_fd, 2);
connptr->type = FINISHCONN;
}
break;
default:
break;
}
}
/* Set a 60 second time out */
tv.tv_sec = 1;//60;
tv.tv_usec = 0;
/* chris - Make ADNS do its stuff, too. */
{
struct timeval *tv_mod = &tv;
int foo = FD_SETSIZE;
gettimeofday(&now, NULL);
FD_ZERO(&exceptfds);
adns_beforeselect(adns, &foo, &readfds, &writefds, &exceptfds, &tv_mod, NULL, &now);
}
if (select(FD_SETSIZE, &readfds, &writefds, &exceptfds, &tv) < 0) {
#ifdef __DEBUG__
log("select error: %s", strerror(errno));
#endif
return 0;
}
/* chris - see whether any ADNS lookups have completed */
gettimeofday(&now, NULL);
adns_afterselect(adns, FD_SETSIZE, &readfds, &writefds, &exceptfds, &now);
for (connptr = connections; connptr; connptr = connptr->next) {
adns_answer *ans;
if (connptr->type == DNS_WAITCONN &&
adns_check(adns, &(connptr->adns_qu), &ans, (void**)&connptr) == 0) {
if (ans->status == adns_s_ok) {
if (connptr->domain) {
insert(ans->rrs.inaddr, connptr->domain);
free(connptr->domain);
}
clientreq_dnscomplete(connptr, ans->rrs.inaddr);
free(ans);
/* hack */
FD_SET(connptr->server_fd, &readfds);
#ifdef __DEBUG__
log("DNS resolution successful");
#endif
} else {
free(ans);
httperr(connptr, 500, "Unable to resolve hostname in URL");
#ifdef __DEBUG__
log("DNS resolution failed");
#endif
}
}
}
/* Check to see if there are new connections pending */
if (FD_ISSET(setup_fd, &readfds) && (fd = listen_sock()) >= 0) {
new_conn(fd); /* make a connection from the FD */
if (validuser(fd)) {
if (config.cutoffload && (load > config.cutoffload)) {
stats.num_refused++;
httperr(connptr, 503,
"tinyproxy is not accepting connections due to high system load");
}
} else {
httperr(connptr, 403,
"You are not authorized to use the service.");
log("AUTH Rejected connection from %s",
getpeer_ip(fd, peer_ipaddr));
}
}
/*
* Loop through the connections and dispatch them to the appropriate
* handler
*/
for (connptr = connections; connptr; connptr = connptr->next) {
switch (connptr->type) {
case NEWCONN:
newconn_req(connptr, &readfds);
break;
case WAITCONN:
waitconn_req(connptr, &readfds, &writefds);
break;
case RELAYCONN:
relayconn_req(connptr, &readfds, &writefds);
break;
case CLOSINGCONN:
closingconn_req(connptr, &writefds);
break;
default:
break;
}
}
return 0;
}