tinyproxy/src/reqs.c

1788 lines
62 KiB
C

/* tinyproxy - A fast light-weight HTTP proxy
* Copyright (C) 1998 Steven Young <sdyoung@miranda.org>
* Copyright (C) 1999-2005 Robert James Kaes <rjkaes@users.sourceforge.net>
* Copyright (C) 2000 Chris Lightfoot <chris@ex-parrot.com>
* Copyright (C) 2002 Petr Lampa <lampa@fit.vutbr.cz>
*
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/* This is where all the work in tinyproxy is actually done. Incoming
* connections have a new child created for them. The child then
* processes the headers from the client, the response from the server,
* and then relays the bytes between the two.
*/
#include "main.h"
#include "acl.h"
#include "anonymous.h"
#include "buffer.h"
#include "conns.h"
#include "filter.h"
#include "hsearch.h"
#include "orderedmap.h"
#include "heap.h"
#include "html-error.h"
#include "log.h"
#include "network.h"
#include "reqs.h"
#include "sock.h"
#include "stats.h"
#include "text.h"
#include "utils.h"
#include "sblist.h"
#include "reverse-proxy.h"
#include "transparent-proxy.h"
#include "upstream.h"
#include "connect-ports.h"
#include "conf.h"
#include "basicauth.h"
#include "loop.h"
#include "mypoll.h"
/*
* Maximum length of a HTTP line
*/
#define HTTP_LINE_LENGTH (MAXBUFFSIZE / 6)
/*
* Macro to help test if the Upstream proxy supported is compiled in and
* enabled.
*/
#ifdef UPSTREAM_SUPPORT
# define UPSTREAM_CONFIGURED() (config->upstream_list != NULL)
# define UPSTREAM_HOST(host) upstream_get(host, config->upstream_list)
# define UPSTREAM_IS_HTTP(conn) (conn->upstream_proxy != NULL && conn->upstream_proxy->type == PT_HTTP)
#else
# define UPSTREAM_CONFIGURED() (0)
# define UPSTREAM_HOST(host) (NULL)
# define UPSTREAM_IS_HTTP(up) (0)
#endif
/*
* Codify the test for the carriage return and new line characters.
*/
#define CHECK_CRLF(header, len) \
(((len) == 1 && header[0] == '\n') || \
((len) == 2 && header[0] == '\r' && header[1] == '\n'))
/*
* Codify the test for header fields folded over multiple lines.
*/
#define CHECK_LWS(header, len) \
((len) > 0 && (header[0] == ' ' || header[0] == '\t'))
/*
* Read in the first line from the client (the request line for HTTP
* connections. The request line is allocated from the heap, but it must
* be freed in another function.
*/
static int read_request_line (struct conn_s *connptr)
{
ssize_t len;
retry:
len = readline (connptr->client_fd, &connptr->request_line);
if (len <= 0) {
log_message (LOG_ERR,
"read_request_line: Client (file descriptor: %d) "
"closed socket before read.", connptr->client_fd);
return -1;
}
/*
* Strip the new line and carriage return from the string.
*/
if (chomp (connptr->request_line, len) == len) {
/*
* If the number of characters removed is the same as the
* length then it was a blank line. Free the buffer and
* try again (since we're looking for a request line.)
*/
safefree (connptr->request_line);
goto retry;
}
log_message (LOG_CONN, "Request (file descriptor %d): %s",
connptr->client_fd, connptr->request_line);
return 0;
}
/*
* Free all the memory allocated in a request.
*/
static void free_request_struct (struct request_s *request)
{
if (!request)
return;
safefree (request->method);
safefree (request->protocol);
if (request->host)
safefree (request->host);
if (request->path)
safefree (request->path);
safefree (request);
}
/*
* Take a host string and if there is a username/password part, strip
* it off.
*/
static void strip_username_password (char *host)
{
char *p;
assert (host);
assert (strlen (host) > 0);
if ((p = strchr (host, '@')) == NULL)
return;
/*
* Move the pointer past the "@" and then copy from that point
* until the NUL to the beginning of the host buffer.
*/
p++;
while (*p)
*host++ = *p++;
*host = '\0';
}
/*
* Take a host string and if there is a port part, strip
* it off and set proper port variable i.e. for www.host.com:8001
*/
static int strip_return_port (char *host)
{
char *ptr1;
char *ptr2;
int port;
ptr1 = strrchr (host, ':');
if (ptr1 == NULL)
return 0;
/* Check for IPv6 style literals */
ptr2 = strchr (ptr1, ']');
if (ptr2 != NULL)
return 0;
*ptr1++ = '\0';
if (sscanf (ptr1, "%d", &port) != 1) /* one conversion required */
return 0;
return port;
}
/*
* Pull the information out of the URL line.
* This expects urls with the initial '<proto>://'
* part stripped and hence can handle http urls,
* (proxied) ftp:// urls and https-requests that
* come in without the proto:// part via CONNECT.
*/
static int extract_url (const char *url, int default_port,
struct request_s *request)
{
char *p;
int port;
/* Split the URL on the slash to separate host from path */
p = strchr (url, '/');
if (p != NULL) {
int len;
len = p - url;
request->host = (char *) safemalloc (len + 1);
memcpy (request->host, url, len);
request->host[len] = '\0';
request->path = safestrdup (p);
} else {
request->host = safestrdup (url);
request->path = safestrdup ("/");
}
if (!request->host || !request->path)
goto ERROR_EXIT;
/* Remove the username/password if they're present */
strip_username_password (request->host);
/* Find a proper port in www.site.com:8001 URLs */
port = strip_return_port (request->host);
request->port = (port != 0) ? port : default_port;
/* Remove any surrounding '[' and ']' from IPv6 literals */
p = strrchr (request->host, ']');
if (p && (*(request->host) == '[')) {
memmove(request->host, request->host + 1,
strlen(request->host) - 2);
*p = '\0';
p--;
*p = '\0';
}
return 0;
ERROR_EXIT:
if (request->host)
safefree (request->host);
if (request->path)
safefree (request->path);
return -1;
}
/*
* Create a connection for HTTP connections.
*/
static int
establish_http_connection (struct conn_s *connptr, struct request_s *request)
{
char portbuff[7];
char dst[sizeof(struct in6_addr)];
/* Build a port string if it's not a standard port */
if (request->port != HTTP_PORT && request->port != HTTP_PORT_SSL)
snprintf (portbuff, 7, ":%u", request->port);
else
portbuff[0] = '\0';
if (inet_pton(AF_INET6, request->host, dst) > 0) {
/* host is an IPv6 address literal, so surround it with
* [] */
return write_message (connptr->server_fd,
"%s %s HTTP/1.%u\r\n"
"Host: [%s]%s\r\n"
"Connection: close\r\n",
request->method, request->path,
connptr->protocol.major != 1 ? 0 :
connptr->protocol.minor,
request->host, portbuff);
} else if (connptr->upstream_proxy &&
connptr->upstream_proxy->type == PT_HTTP &&
connptr->upstream_proxy->ua.authstr) {
return write_message (connptr->server_fd,
"%s %s HTTP/1.%u\r\n"
"Host: %s%s\r\n"
"Connection: close\r\n"
"Proxy-Authorization: Basic %s\r\n",
request->method, request->path,
connptr->protocol.major != 1 ? 0 :
connptr->protocol.minor,
request->host, portbuff,
connptr->upstream_proxy->ua.authstr);
} else {
return write_message (connptr->server_fd,
"%s %s HTTP/1.%u\r\n"
"Host: %s%s\r\n"
"Connection: close\r\n",
request->method, request->path,
connptr->protocol.major != 1 ? 0 :
connptr->protocol.minor,
request->host, portbuff);
}
}
/*
* These two defines are for the SSL tunnelling.
*/
#define SSL_CONNECTION_RESPONSE "HTTP/1.0 200 Connection established"
#define PROXY_AGENT "Proxy-agent: " PACKAGE "/" VERSION
/*
* Send the appropriate response to the client to establish a SSL
* connection.
*/
static int send_ssl_response (struct conn_s *connptr)
{
return write_message (connptr->client_fd,
"%s\r\n"
"%s\r\n"
"\r\n", SSL_CONNECTION_RESPONSE, PROXY_AGENT);
}
/*
* Break the request line apart and figure out where to connect and
* build a new request line. Finally connect to the remote server.
*/
static struct request_s *process_request (struct conn_s *connptr,
orderedmap hashofheaders)
{
char *url;
struct request_s *request;
int ret;
size_t request_len;
/* NULL out all the fields so frees don't cause segfaults. */
request =
(struct request_s *) safecalloc (1, sizeof (struct request_s));
if (!request)
return NULL;
request_len = strlen (connptr->request_line) + 1;
request->method = (char *) safemalloc (request_len);
url = (char *) safemalloc (request_len);
request->protocol = (char *) safemalloc (request_len);
if (!request->method || !url || !request->protocol) {
goto fail;
}
ret = sscanf (connptr->request_line, "%[^ ] %[^ ] %[^ ]",
request->method, url, request->protocol);
if (ret == 2 && !strcasecmp (request->method, "GET")) {
request->protocol[0] = 0;
/* Indicate that this is a HTTP/0.9 GET request */
connptr->protocol.major = 0;
connptr->protocol.minor = 9;
} else if (ret == 3 && !strncasecmp (request->protocol, "HTTP/", 5)) {
/*
* Break apart the protocol and update the connection
* structure.
*/
ret = sscanf (request->protocol + 5, "%u.%u",
&connptr->protocol.major,
&connptr->protocol.minor);
/*
* If the conversion doesn't succeed, drop down below and
* send the error to the user.
*/
if (ret != 2)
goto BAD_REQUEST_ERROR;
} else {
BAD_REQUEST_ERROR:
log_message (LOG_ERR,
"process_request: Bad Request on file descriptor %d",
connptr->client_fd);
indicate_http_error (connptr, 400, "Bad Request",
"detail", "Request has an invalid format",
"url", url, NULL);
goto fail;
}
#ifdef REVERSE_SUPPORT
if (config->reversepath_list != NULL) {
/*
* Rewrite the URL based on the reverse path. After calling
* reverse_rewrite_url "url" can be freed since we either
* have the newly rewritten URL, or something failed and
* we'll be closing anyway.
*/
char *reverse_url;
int reverse_status;
reverse_url = reverse_rewrite_url (connptr, hashofheaders, url, &reverse_status);
if (reverse_url != NULL) {
if (reverse_status == 301) {
char buf[PATH_MAX];
snprintf (buf, sizeof buf, "Location: %s\r\n", reverse_url);
send_http_headers (connptr, 301, "Moved Permanently", buf);
goto fail;
}
safefree (url);
url = reverse_url;
} else if (config->reverseonly) {
log_message (LOG_ERR,
"Bad request, no mapping for '%s' found",
url);
indicate_http_error (connptr, 400, "Bad Request",
"detail", "No mapping found for "
"requested url", "url", url, NULL);
goto fail;
}
}
#endif
if (strncasecmp (url, "http://", 7) == 0
|| (UPSTREAM_CONFIGURED () && strncasecmp (url, "ftp://", 6) == 0))
{
char *skipped_type = strstr (url, "//") + 2;
if (extract_url (skipped_type, HTTP_PORT, request) < 0) {
indicate_http_error (connptr, 400, "Bad Request",
"detail", "Could not parse URL",
"url", url, NULL);
goto fail;
}
} else if (strcmp (request->method, "CONNECT") == 0) {
if (extract_url (url, HTTP_PORT_SSL, request) < 0) {
indicate_http_error (connptr, 400, "Bad Request",
"detail", "Could not parse URL",
"url", url, NULL);
goto fail;
}
/* Verify that the port in the CONNECT method is allowed */
if (!check_allowed_connect_ports (request->port,
config->connect_ports))
{
indicate_http_error (connptr, 403, "Access violation",
"detail",
"The CONNECT method not allowed "
"with the port you tried to use.",
"url", url, NULL);
log_message (LOG_INFO,
"Refused CONNECT method on port %d",
request->port);
goto fail;
}
connptr->connect_method = TRUE;
} else {
#ifdef TRANSPARENT_PROXY
if (!do_transparent_proxy
(connptr, hashofheaders, request, config, &url)) {
goto fail;
}
#else
indicate_http_error (connptr, 501, "Not Implemented",
"detail",
"Unknown method or unsupported protocol.",
"url", url, NULL);
log_message (LOG_INFO, "Unknown method (%s) or protocol (%s)",
request->method, url);
goto fail;
#endif
}
#ifdef FILTER_ENABLE
/*
* Filter restricted domains/urls
*/
if (config->filter) {
int fu = config->filter_opts & FILTER_OPT_URL;
ret = filter_run (fu ? url : request->host);
if (ret) {
update_stats (STAT_DENIED);
log_message (LOG_NOTICE,
"Proxying refused on filtered %s \"%s\"",
fu ? "url" : "domain",
fu ? url : request->host);
indicate_http_error (connptr, 403, "Filtered",
"detail",
"The request you made has been filtered",
"url", url, NULL);
goto fail;
}
}
#endif
/*
* Check to see if they're requesting the stat host
*/
if (config->stathost && strcmp (config->stathost, request->host) == 0) {
log_message (LOG_NOTICE, "Request for the stathost.");
connptr->show_stats = TRUE;
goto fail;
}
safefree (url);
return request;
fail:
safefree (url);
free_request_struct (request);
return NULL;
}
/*
* pull_client_data is used to pull across any client data (like in a
* POST) which needs to be handled before an error can be reported, or
* server headers can be processed.
* - rjkaes
*/
static int pull_client_data (struct conn_s *connptr, long int length, int iehack)
{
char *buffer;
ssize_t len;
int ret;
buffer =
(char *) safemalloc (min (MAXBUFFSIZE, (unsigned long int) length));
if (!buffer)
return -1;
do {
len = safe_read (connptr->client_fd, buffer,
min (MAXBUFFSIZE, (unsigned long int) length));
if (len <= 0)
goto ERROR_EXIT;
if (!connptr->error_variables) {
if (safe_write (connptr->server_fd, buffer, len) < 0)
goto ERROR_EXIT;
}
length -= len;
} while (length > 0);
if (iehack) {
/*
* BUG FIX: Internet Explorer will leave two bytes (carriage
* return and line feed) at the end of a POST message. These
* need to be eaten for tinyproxy to work correctly.
*/
ret = socket_nonblocking (connptr->client_fd);
if (ret != 0) {
log_message(LOG_ERR, "Failed to set the client socket "
"to non-blocking: %s", strerror(errno));
goto ERROR_EXIT;
}
len = recv (connptr->client_fd, buffer, 2, MSG_PEEK);
ret = socket_blocking (connptr->client_fd);
if (ret != 0) {
log_message(LOG_ERR, "Failed to set the client socket "
"to blocking: %s", strerror(errno));
goto ERROR_EXIT;
}
if (len < 0 && errno != EAGAIN)
goto ERROR_EXIT;
if ((len == 2) && CHECK_CRLF (buffer, len)) {
ssize_t bytes_read;
bytes_read = read (connptr->client_fd, buffer, 2);
if (bytes_read == -1) {
log_message
(LOG_WARNING,
"Could not read two bytes from POST message");
}
}
}
safefree (buffer);
return 0;
ERROR_EXIT:
safefree (buffer);
return -1;
}
/* pull chunked client data */
static int pull_client_data_chunked (struct conn_s *connptr) {
char *buffer = 0;
ssize_t len;
long chunklen;
while(1) {
if (buffer) safefree(buffer);
len = readline (connptr->client_fd, &buffer);
if (len <= 0)
goto ERROR_EXIT;
if (!connptr->error_variables) {
if (safe_write (connptr->server_fd, buffer, len) < 0)
goto ERROR_EXIT;
}
chunklen = strtol (buffer, (char**)0, 16);
if (pull_client_data (connptr, chunklen+2, 0) < 0)
goto ERROR_EXIT;
if(!chunklen) break;
}
safefree (buffer);
return 0;
ERROR_EXIT:
safefree (buffer);
return -1;
}
#ifdef XTINYPROXY_ENABLE
/*
* Add the X-Tinyproxy header to the collection of headers being sent to
* the server.
* -rjkaes
*/
static int add_xtinyproxy_header (struct conn_s *connptr)
{
assert (connptr && connptr->server_fd >= 0);
return write_message (connptr->server_fd,
"X-Tinyproxy: %s\r\n", connptr->client_ip_addr);
}
#endif /* XTINYPROXY */
/*
* Take a complete header line and break it apart (into a key and the data.)
* Now insert this information into the hashmap for the connection so it
* can be retrieved and manipulated later.
*/
static int
add_header_to_connection (orderedmap hashofheaders, char *header, size_t len)
{
char *sep;
/* Get rid of the new line and return at the end */
len -= chomp (header, len);
sep = strchr (header, ':');
if (!sep)
return 0; /* just skip invalid header, do not give error */
/* Blank out colons, spaces, and tabs. */
while (*sep == ':' || *sep == ' ' || *sep == '\t')
*sep++ = '\0';
/* Calculate the new length of just the data */
len -= sep - header - 1;
return orderedmap_append (hashofheaders, header, sep);
}
/*
* Define maximum number of headers that we accept.
* This should be big enough to handle legitimate cases,
* but limited to avoid DoS.
*/
#define MAX_HEADERS 10000
/*
* Read all the headers from the stream
*/
static int get_all_headers (int fd, orderedmap hashofheaders)
{
char *line = NULL;
char *header = NULL;
int count;
char *tmp;
ssize_t linelen;
ssize_t len = 0;
unsigned int double_cgi = FALSE; /* boolean */
assert (fd >= 0);
assert (hashofheaders != NULL);
for (count = 0; count < MAX_HEADERS; count++) {
if ((linelen = readline (fd, &line)) <= 0) {
safefree (header);
safefree (line);
return -1;
}
/*
* If we received a CR LF or a non-continuation line, then add
* the accumulated header field, if any, to the hashmap, and
* reset it.
*/
if (CHECK_CRLF (line, linelen) || !CHECK_LWS (line, linelen)) {
if (!double_cgi
&& len > 0
&& add_header_to_connection (hashofheaders, header,
len) < 0) {
safefree (header);
safefree (line);
return -1;
}
len = 0;
}
/*
* If we received just a CR LF on a line, the headers are
* finished.
*/
if (CHECK_CRLF (line, linelen)) {
safefree (header);
safefree (line);
return 0;
}
/*
* BUG FIX: The following code detects a "Double CGI"
* situation so that we can handle the nonconforming system.
* This problem was found when accessing cgi.ebay.com, and it
* turns out to be a wider spread problem as well.
*
* If "Double CGI" is in effect, duplicate headers are
* ignored.
*
* FIXME: Might need to change this to a more robust check.
*/
if (linelen >= 5 && strncasecmp (line, "HTTP/", 5) == 0) {
double_cgi = TRUE;
}
/*
* Append the new line to the current header field.
*/
tmp = (char *) saferealloc (header, len + linelen);
if (tmp == NULL) {
safefree (header);
safefree (line);
return -1;
}
header = tmp;
memcpy (header + len, line, linelen);
len += linelen;
safefree (line);
}
/*
* If we get here, this means we reached MAX_HEADERS count.
* Bail out with error.
*/
safefree (header);
safefree (line);
return -1;
}
/*
* Extract the headers to remove. These headers were listed in the Connection
* and Proxy-Connection headers.
*/
static int remove_connection_headers (orderedmap hashofheaders)
{
static const char *headers[] = {
"connection",
"proxy-connection"
};
char *data;
char *ptr;
ssize_t len;
int i;
for (i = 0; i != (sizeof (headers) / sizeof (char *)); ++i) {
/* Look for the connection header. If it's not found, return. */
data = orderedmap_find(hashofheaders, headers[i]);
if (!data)
return 0;
len = strlen(data);
/*
* Go through the data line and replace any special characters
* with a NULL.
*/
ptr = data;
while ((ptr = strpbrk (ptr, "()<>@,;:\\\"/[]?={} \t")))
*ptr++ = '\0';
/*
* All the tokens are separated by NULLs. Now go through the
* token and remove them from the hashofheaders.
*/
ptr = data;
while (ptr < data + len) {
orderedmap_remove (hashofheaders, ptr);
/* Advance ptr to the next token */
ptr += strlen (ptr) + 1;
while (ptr < data + len && *ptr == '\0')
ptr++;
}
/* Now remove the connection header it self. */
orderedmap_remove (hashofheaders, headers[i]);
}
return 0;
}
/*
* If there is a Content-Length header, then return the value; otherwise, return
* -1.
*/
static long get_content_length (orderedmap hashofheaders)
{
char *data;
long content_length = -1;
data = orderedmap_find (hashofheaders, "content-length");
if (data)
content_length = atol (data);
return content_length;
}
static int is_chunked_transfer (orderedmap hashofheaders)
{
char *data;
data = orderedmap_find (hashofheaders, "transfer-encoding");
return data ? !strcmp (data, "chunked") : 0;
}
/*
* Search for Via header in a hash of headers and either write a new Via
* header, or append our information to the end of an existing Via header.
*
* FIXME: Need to add code to "hide" our internal information for security
* purposes.
*/
static int
write_via_header (int fd, orderedmap hashofheaders,
unsigned int major, unsigned int minor)
{
char hostname[512];
char *data;
int ret;
if (config->disable_viaheader) {
ret = 0;
goto done;
}
if (config->via_proxy_name) {
strlcpy (hostname, config->via_proxy_name, sizeof (hostname));
} else if (gethostname (hostname, sizeof (hostname)) < 0) {
strlcpy (hostname, "unknown", 512);
}
/*
* See if there is a "Via" header. If so, again we need to do a bit
* of processing.
*/
data = orderedmap_find (hashofheaders, "via");
if (data) {
ret = write_message (fd,
"Via: %s, %hu.%hu %s (%s/%s)\r\n",
data, major, minor, hostname, PACKAGE,
VERSION);
orderedmap_remove (hashofheaders, "via");
} else {
ret = write_message (fd,
"Via: %hu.%hu %s (%s/%s)\r\n",
major, minor, hostname, PACKAGE, VERSION);
}
done:
return ret;
}
/*
* Number of buckets to use internally in the hashmap.
*/
#define HEADER_BUCKETS 32
/*
* Here we loop through all the headers the client is sending. If we
* are running in anonymous mode, we will _only_ send the headers listed
* (plus a few which are required for various methods).
* - rjkaes
*/
static int
process_client_headers (struct conn_s *connptr, orderedmap hashofheaders)
{
static const char *skipheaders[] = {
"host",
"keep-alive",
"proxy-connection",
"te",
"trailers",
"upgrade"
};
int i;
size_t iter;
int ret = 0;
char *data, *header;
/*
* Don't send headers if there's already an error, if the request was
* a stats request, or if this was a CONNECT method (unless upstream
* http proxy is in use.)
*/
if (connptr->server_fd == -1 || connptr->show_stats
|| (connptr->connect_method && ! UPSTREAM_IS_HTTP(connptr))) {
log_message (LOG_INFO,
"Not sending client headers to remote machine");
return 0;
}
/*
* See if there is a "Content-Length" header. If so, again we need
* to do a bit of processing.
*/
connptr->content_length.client = get_content_length (hashofheaders);
/* Check whether client sends chunked data. */
if (connptr->content_length.client == -1 && is_chunked_transfer (hashofheaders))
connptr->content_length.client = -2;
/*
* See if there is a "Connection" header. If so, we need to do a bit
* of processing. :)
*/
remove_connection_headers (hashofheaders);
/*
* Delete the headers listed in the skipheaders list
*/
for (i = 0; i != (sizeof (skipheaders) / sizeof (char *)); i++) {
orderedmap_remove (hashofheaders, skipheaders[i]);
}
/* Send, or add the Via header */
ret = write_via_header (connptr->server_fd, hashofheaders,
connptr->protocol.major,
connptr->protocol.minor);
if (ret < 0) {
indicate_http_error (connptr, 503,
"Could not send data to remote server",
"detail",
"A network error occurred while "
"trying to write data to the remote web server.",
NULL);
goto PULL_CLIENT_DATA;
}
/*
* Output all the remaining headers to the remote machine.
*/
iter = 0;
while((iter = orderedmap_next(hashofheaders, iter, &data, &header))) {
if (!is_anonymous_enabled (config)
|| anonymous_search (config, data) > 0) {
ret =
write_message (connptr->server_fd,
"%s: %s\r\n", data, header);
if (ret < 0) {
indicate_http_error (connptr, 503,
"Could not send data to remote server",
"detail",
"A network error occurred while "
"trying to write data to the "
"remote web server.",
NULL);
goto PULL_CLIENT_DATA;
}
}
}
#if defined(XTINYPROXY_ENABLE)
if (config->add_xtinyproxy)
add_xtinyproxy_header (connptr);
#endif
/* Write the final "blank" line to signify the end of the headers */
if (safe_write (connptr->server_fd, "\r\n", 2) < 0)
return -1;
/*
* Spin here pulling the data from the client.
*/
PULL_CLIENT_DATA:
if (connptr->content_length.client > 0) {
ret = pull_client_data (connptr,
connptr->content_length.client, 1);
} else if (connptr->content_length.client == -2)
ret = pull_client_data_chunked (connptr);
return ret;
}
/*
* Loop through all the headers (including the response code) from the
* server.
*/
static int process_server_headers (struct conn_s *connptr)
{
static const char *skipheaders[] = {
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"proxy-connection",
};
char *response_line;
orderedmap hashofheaders;
size_t iter;
char *data, *header;
ssize_t len;
int i;
int ret;
#ifdef REVERSE_SUPPORT
struct reversepath *reverse = config->reversepath_list;
#endif
/* Get the response line from the remote server. */
retry:
len = readline (connptr->server_fd, &response_line);
if (len <= 0)
return -1;
/*
* Strip the new line and character return from the string.
*/
if (chomp (response_line, len) == len) {
/*
* If the number of characters removed is the same as the
* length then it was a blank line. Free the buffer and
* try again (since we're looking for a request line.)
*/
safefree (response_line);
goto retry;
}
hashofheaders = orderedmap_create (HEADER_BUCKETS);
if (!hashofheaders) {
safefree (response_line);
return -1;
}
/*
* Get all the headers from the remote server in a big hash
*/
if (get_all_headers (connptr->server_fd, hashofheaders) < 0) {
log_message (LOG_WARNING,
"Could not retrieve all the headers from the remote server.");
orderedmap_destroy (hashofheaders);
safefree (response_line);
indicate_http_error (connptr, 503,
"Could not retrieve all the headers",
"detail",
PACKAGE_NAME " "
"was unable to retrieve and process headers from "
"the remote web server.", NULL);
return -1;
}
/*
* At this point we've received the response line and all the
* headers. However, if this is a simple HTTP/0.9 request we
* CAN NOT send any of that information back to the client.
* Instead we'll free all the memory and return.
*/
if (connptr->protocol.major < 1) {
orderedmap_destroy (hashofheaders);
safefree (response_line);
return 0;
}
/* Send the saved response line first */
ret = write_message (connptr->client_fd, "%s\r\n", response_line);
safefree (response_line);
if (ret < 0)
goto ERROR_EXIT;
/*
* If there is a "Content-Length" header, retrieve the information
* from it for later use.
*/
connptr->content_length.server = get_content_length (hashofheaders);
/*
* See if there is a connection header. If so, we need to to a bit of
* processing.
*/
remove_connection_headers (hashofheaders);
/*
* Delete the headers listed in the skipheaders list
*/
for (i = 0; i != (sizeof (skipheaders) / sizeof (char *)); i++) {
orderedmap_remove (hashofheaders, skipheaders[i]);
}
/* Send, or add the Via header */
ret = write_via_header (connptr->client_fd, hashofheaders,
connptr->protocol.major,
connptr->protocol.minor);
if (ret < 0)
goto ERROR_EXIT;
#ifdef REVERSE_SUPPORT
/* Write tracking cookie for the magical reverse proxy path hack */
if (config->reversemagic && connptr->reversepath) {
ret = write_message (connptr->client_fd,
"Set-Cookie: " REVERSE_COOKIE
"=%s; path=/\r\n", connptr->reversepath);
if (ret < 0)
goto ERROR_EXIT;
}
/* Rewrite the HTTP redirect if needed */
if (config->reversebaseurl &&
(header = orderedmap_find (hashofheaders, "location"))) {
/* Look for a matching entry in the reversepath list */
while (reverse) {
if (strncasecmp (header,
reverse->url, (len =
strlen (reverse->
url))) == 0)
break;
reverse = reverse->next;
}
if (reverse) {
ret =
write_message (connptr->client_fd,
"Location: %s%s%s\r\n",
config->reversebaseurl,
(reverse->path + 1), (header + len));
if (ret < 0)
goto ERROR_EXIT;
log_message (LOG_INFO,
"Rewriting HTTP redirect: %s -> %s%s%s",
header, config->reversebaseurl,
(reverse->path + 1), (header + len));
orderedmap_remove (hashofheaders, "location");
}
}
#endif
/*
* All right, output all the remaining headers to the client.
*/
iter = 0;
while ((iter = orderedmap_next(hashofheaders, iter, &data, &header))) {
ret = write_message (connptr->client_fd,
"%s: %s\r\n", data, header);
if (ret < 0)
goto ERROR_EXIT;
}
orderedmap_destroy (hashofheaders);
/* Write the final blank line to signify the end of the headers */
if (safe_write (connptr->client_fd, "\r\n", 2) < 0)
return -1;
return 0;
ERROR_EXIT:
orderedmap_destroy (hashofheaders);
return -1;
}
/*
* Switch the sockets into nonblocking mode and begin relaying the bytes
* between the two connections. We continue to use the buffering code
* since we want to be able to buffer a certain amount for slower
* connections (as this was the reason why I originally modified
* tinyproxy oh so long ago...)
* - rjkaes
*/
static void relay_connection (struct conn_s *connptr)
{
int ret;
ssize_t bytes_received;
for (;;) {
pollfd_struct fds[2] = {0};
fds[0].fd = connptr->client_fd;
fds[1].fd = connptr->server_fd;
if (buffer_size (connptr->sbuffer) > 0)
fds[0].events |= MYPOLL_WRITE;
if (buffer_size (connptr->cbuffer) > 0)
fds[1].events |= MYPOLL_WRITE;
if (buffer_size (connptr->sbuffer) < MAXBUFFSIZE)
fds[1].events |= MYPOLL_READ;
if (buffer_size (connptr->cbuffer) < MAXBUFFSIZE)
fds[0].events |= MYPOLL_READ;
ret = mypoll(fds, 2, config->idletimeout);
if (ret == 0) {
log_message (LOG_INFO,
"Idle Timeout (after " SELECT_OR_POLL ")");
return;
} else if (ret < 0) {
log_message (LOG_ERR,
"relay_connection: " SELECT_OR_POLL "() error \"%s\". "
"Closing connection (client_fd:%d, server_fd:%d)",
strerror (errno), connptr->client_fd,
connptr->server_fd);
return;
}
if (fds[1].revents & MYPOLL_READ) {
bytes_received =
read_buffer (connptr->server_fd, connptr->sbuffer);
if (bytes_received < 0)
break;
connptr->content_length.server -= bytes_received;
if (connptr->content_length.server == 0)
break;
}
if ((fds[0].revents & MYPOLL_READ)
&& read_buffer (connptr->client_fd, connptr->cbuffer) < 0) {
break;
}
if ((fds[1].revents & MYPOLL_WRITE)
&& write_buffer (connptr->server_fd, connptr->cbuffer) < 0) {
break;
}
if ((fds[0].revents & MYPOLL_WRITE)
&& write_buffer (connptr->client_fd, connptr->sbuffer) < 0) {
break;
}
}
while (buffer_size (connptr->sbuffer) > 0) {
if (write_buffer (connptr->client_fd, connptr->sbuffer) < 0)
break;
}
shutdown (connptr->client_fd, SHUT_WR);
/*
* Try to send any remaining data to the server if we can.
*/
ret = socket_blocking (connptr->server_fd);
if (ret != 0) {
log_message(LOG_ERR,
"Failed to set server socket to blocking: %s",
strerror(errno));
return;
}
while (buffer_size (connptr->cbuffer) > 0) {
if (write_buffer (connptr->server_fd, connptr->cbuffer) < 0)
break;
}
return;
}
static int
connect_to_upstream_proxy(struct conn_s *connptr, struct request_s *request)
{
unsigned len;
unsigned char buff[512]; /* won't use more than 7 + 255 */
unsigned short port;
size_t ulen, passlen;
struct upstream *cur_upstream = connptr->upstream_proxy;
ulen = cur_upstream->ua.user ? strlen(cur_upstream->ua.user) : 0;
passlen = cur_upstream->pass ? strlen(cur_upstream->pass) : 0;
log_message(LOG_CONN,
"Established connection to %s proxy \"%s\" using file descriptor %d.",
proxy_type_name(cur_upstream->type), cur_upstream->host, connptr->server_fd);
if (cur_upstream->type == PT_SOCKS4) {
buff[0] = 4; /* socks version */
buff[1] = 1; /* connect command */
port = htons(request->port);
memcpy(&buff[2], &port, 2); /* dest port */
memcpy(&buff[4], "\0\0\0\1" /* socks4a fake ip */
"\0" /* user */, 5);
len = strlen(request->host);
if(len>255)
return -1;
memcpy(&buff[9], request->host, len+1);
if (9+len+1 != safe_write(connptr->server_fd, buff, 9+len+1))
return -1;
if (8 != safe_read(connptr->server_fd, buff, 8))
return -1;
if (buff[0]!=0 || buff[1]!=90)
return -1;
} else if (cur_upstream->type == PT_SOCKS5) {
/* init */
int n_methods = ulen ? 2 : 1;
buff[0] = 5; /* socks version */
buff[1] = n_methods; /* number of methods */
buff[2] = 0; /* no auth method */
if (ulen) buff[3] = 2; /* auth method -> username / password */
if (2+n_methods != safe_write(connptr->server_fd, buff, 2+n_methods))
return -1;
if (2 != safe_read(connptr->server_fd, buff, 2))
return -1;
if (buff[0] != 5 || (buff[1] != 0 && buff[1] != 2))
return -1;
if (buff[1] == 2) {
/* authentication */
char in[2];
char out[515];
char *cur = out;
size_t c;
*cur++ = 1; /* version */
c = ulen & 0xFF;
*cur++ = c;
memcpy(cur, cur_upstream->ua.user, c);
cur += c;
c = passlen & 0xFF;
*cur++ = c;
memcpy(cur, cur_upstream->pass, c);
cur += c;
if((cur - out) != safe_write(connptr->server_fd, out, cur - out))
return -1;
if(2 != safe_read(connptr->server_fd, in, 2))
return -1;
if(in[1] != 0 || !(in[0] == 5 || in[0] == 1)) {
return -1;
}
}
/* connect */
buff[0] = 5; /* socks version */
buff[1] = 1; /* connect */
buff[2] = 0; /* reserved */
buff[3] = 3; /* domainname */
len=strlen(request->host);
if(len>255)
return -1;
buff[4] = len; /* length of domainname */
memcpy(&buff[5], request->host, len); /* dest ip */
port = htons(request->port);
memcpy(&buff[5+len], &port, 2); /* dest port */
if (7+len != safe_write(connptr->server_fd, buff, 7+len))
return -1;
if (4 != safe_read(connptr->server_fd, buff, 4))
return -1;
if (buff[0]!=5 || buff[1]!=0)
return -1;
switch(buff[3]) {
case 1: len=4; break; /* ip v4 */
case 4: len=16; break; /* ip v6 */
case 3: /* domainname */
if (1 != safe_read(connptr->server_fd, buff, 1))
return -1;
len = buff[0]; /* max = 255 */
break;
default: return -1;
}
if (2+len != safe_read(connptr->server_fd, buff, 2+len))
return -1;
} else {
return -1;
}
if (connptr->connect_method)
return 0;
return establish_http_connection(connptr, request);
}
/*
* Establish a connection to the upstream proxy server.
*/
static int
connect_to_upstream (struct conn_s *connptr, struct request_s *request)
{
#ifndef UPSTREAM_SUPPORT
/*
* This function does nothing if upstream support was not compiled
* into tinyproxy.
*/
return -1;
#else
char *combined_string;
int len;
struct upstream *cur_upstream = connptr->upstream_proxy;
if (!cur_upstream) {
log_message (LOG_WARNING,
"No upstream proxy defined for %s.",
request->host);
indicate_http_error (connptr, 502,
"Unable to connect to upstream proxy.");
return -1;
}
connptr->server_fd =
opensock (cur_upstream->host, cur_upstream->port,
connptr->server_ip_addr);
if (connptr->server_fd < 0) {
log_message (LOG_WARNING,
"Could not connect to upstream proxy.");
indicate_http_error (connptr, 502,
"Unable to connect to upstream proxy",
"detail",
"A network error occurred while trying to "
"connect to the upstream web proxy.",
NULL);
return -1;
}
if (cur_upstream->type != PT_HTTP)
return connect_to_upstream_proxy(connptr, request);
log_message (LOG_CONN,
"Established connection to upstream proxy \"%s\" "
"using file descriptor %d.",
cur_upstream->host, connptr->server_fd);
/*
* We need to re-write the "path" part of the request so that we
* can reuse the establish_http_connection() function. It expects a
* method and path.
*/
if (connptr->connect_method) {
len = strlen (request->host) + 7;
combined_string = (char *) safemalloc (len);
if (!combined_string) {
return -1;
}
snprintf (combined_string, len, "%s:%d", request->host,
request->port);
} else {
len = strlen (request->host) + strlen (request->path) + 14;
combined_string = (char *) safemalloc (len);
if (!combined_string) {
return -1;
}
snprintf (combined_string, len, "http://%s:%d%s", request->host,
request->port, request->path);
}
if (request->path)
safefree (request->path);
request->path = combined_string;
return establish_http_connection (connptr, request);
#endif
}
/* this function "drains" remaining bytes in the read pipe from
the client. it's usually only called on error before displaying
an error code/page. */
static int
get_request_entity(struct conn_s *connptr)
{
int ret;
pollfd_struct fds[1] = {0};
fds[0].fd = connptr->client_fd;
fds[0].events |= MYPOLL_READ;
ret = mypoll(fds, 1, config->idletimeout);
if (ret == -1) {
log_message (LOG_ERR,
"Error calling " SELECT_OR_POLL " on client fd %d: %s",
connptr->client_fd, strerror(errno));
} else if (ret == 0) {
log_message (LOG_INFO, "no entity");
} else if (ret == 1 && (fds[0].revents & MYPOLL_READ)) {
ssize_t nread;
nread = read_buffer (connptr->client_fd, connptr->cbuffer);
if (nread < 0) {
log_message (LOG_ERR,
"Error reading readable client_fd %d (%s)",
connptr->client_fd, strerror(errno));
ret = -1;
} else {
log_message (LOG_INFO,
"Read request entity of %ld bytes",
(long) nread);
ret = 0;
}
} else {
log_message (LOG_ERR, "strange situation after " SELECT_OR_POLL ": "
"ret = %d, but client_fd (%d) is not readable...",
ret, connptr->client_fd);
ret = -1;
}
return ret;
}
static void handle_connection_failure(struct conn_s *connptr, int got_headers)
{
/*
* First, get the body if there is one.
* If we don't read all there is from the socket first,
* it is still marked for reading and we won't be able
* to send our data properly.
*/
if (!got_headers && get_request_entity (connptr) < 0) {
log_message (LOG_WARNING,
"Could not retrieve request entity");
indicate_http_error (connptr, 400, "Bad Request",
"detail",
"Could not retrieve the request entity "
"the client.", NULL);
update_stats (STAT_BADCONN);
}
if (connptr->error_variables) {
send_http_error_message (connptr);
} else if (connptr->show_stats) {
showstats (connptr);
}
}
/*
* This is the main drive for each connection. As you can tell, for the
* first few steps we are using a blocking socket. If you remember the
* older tinyproxy code, this use to be a very confusing state machine.
* Well, no more! :) The sockets are only switched into nonblocking mode
* when we start the relay portion. This makes most of the original
* tinyproxy code, which was confusing, redundant. Hail progress.
* - rjkaes
* this function is called directly from child_thread() with the newly
* received fd from accept().
*/
void handle_connection (struct conn_s *connptr, union sockaddr_union* addr)
{
#define HC_FAIL() \
do {handle_connection_failure(connptr, got_headers); goto done;} \
while(0)
int got_headers = 0, fd = connptr->client_fd;
size_t i;
struct request_s *request = NULL;
orderedmap hashofheaders = NULL;
char sock_ipaddr[IP_LENGTH];
char peer_ipaddr[IP_LENGTH];
getpeer_information (addr, peer_ipaddr, sizeof(peer_ipaddr));
if (config->bindsame)
getsock_ip (fd, sock_ipaddr);
log_message (LOG_CONN, config->bindsame ?
"Connect (file descriptor %d): %s at [%s]" :
"Connect (file descriptor %d): %s",
fd, peer_ipaddr, sock_ipaddr);
if(!conn_init_contents (connptr, peer_ipaddr,
config->bindsame ? sock_ipaddr : NULL)) {
close (fd);
return;
}
set_socket_timeout(fd);
if (connection_loops (addr)) {
log_message (LOG_CONN,
"Prevented endless loop (file descriptor %d): %s",
fd, peer_ipaddr);
indicate_http_error(connptr, 400, "Bad Request",
"detail",
"You tried to connect to the "
"machine the proxy is running on",
NULL);
HC_FAIL();
}
if (check_acl (peer_ipaddr, addr, config->access_list) <= 0) {
update_stats (STAT_DENIED);
indicate_http_error (connptr, 403, "Access denied",
"detail",
"The administrator of this proxy has not configured "
"it to service requests from your host.",
NULL);
HC_FAIL();
}
if (read_request_line (connptr) < 0) {
update_stats (STAT_BADCONN);
goto done;
}
/*
* The "hashofheaders" store the client's headers.
*/
hashofheaders = orderedmap_create (HEADER_BUCKETS);
if (hashofheaders == NULL) {
update_stats (STAT_BADCONN);
indicate_http_error (connptr, 503, "Internal error",
"detail",
"An internal server error occurred while processing "
"your request. Please contact the administrator.",
NULL);
HC_FAIL();
}
/*
* Get all the headers from the client in a big hash.
*/
if (get_all_headers (connptr->client_fd, hashofheaders) < 0) {
log_message (LOG_WARNING,
"Could not retrieve all the headers from the client");
indicate_http_error (connptr, 400, "Bad Request",
"detail",
"Could not retrieve all the headers from "
"the client.", NULL);
update_stats (STAT_BADCONN);
HC_FAIL();
}
got_headers = 1;
if (config->basicauth_list != NULL) {
char *authstring;
int failure = 1, stathost_connect = 0;
authstring = orderedmap_find (hashofheaders, "proxy-authorization");
if (!authstring && config->stathost) {
authstring = orderedmap_find (hashofheaders, "host");
if (authstring && !strncmp(authstring, config->stathost, strlen(config->stathost))) {
authstring = orderedmap_find (hashofheaders, "authorization");
stathost_connect = 1;
} else authstring = 0;
}
if (!authstring) {
if (stathost_connect) goto e401;
update_stats (STAT_DENIED);
indicate_http_error (connptr, 407, "Proxy Authentication Required",
"detail",
"This proxy requires authentication.",
NULL);
HC_FAIL();
}
if ( /* currently only "basic" auth supported */
(strncmp(authstring, "Basic ", 6) == 0 ||
strncmp(authstring, "basic ", 6) == 0) &&
basicauth_check (config->basicauth_list, authstring + 6) == 1)
failure = 0;
if(failure) {
e401:
update_stats (STAT_DENIED);
indicate_http_error (connptr, 401, "Unauthorized",
"detail",
"The administrator of this proxy has not configured "
"it to service requests from you.",
NULL);
HC_FAIL();
}
orderedmap_remove (hashofheaders, "proxy-authorization");
}
/*
* Add any user-specified headers (AddHeader directive) to the
* outgoing HTTP request.
*/
if (config->add_headers)
for (i = 0; i < sblist_getsize (config->add_headers); i++) {
http_header_t *header = sblist_get (config->add_headers, i);
orderedmap_append (hashofheaders, header->name, header->value);
}
request = process_request (connptr, hashofheaders);
if (!request) {
if (!connptr->show_stats) {
update_stats (STAT_BADCONN);
}
HC_FAIL();
}
connptr->upstream_proxy = UPSTREAM_HOST (request->host);
if (connptr->upstream_proxy != NULL) {
if (connect_to_upstream (connptr, request) < 0) {
HC_FAIL();
}
} else {
connptr->server_fd = opensock (request->host, request->port,
connptr->server_ip_addr);
if (connptr->server_fd < 0) {
indicate_http_error (connptr, 500, "Unable to connect",
"detail",
PACKAGE_NAME " "
"was unable to connect to the remote web server.",
"error", strerror (errno), NULL);
HC_FAIL();
}
log_message (LOG_CONN,
"Established connection to host \"%s\" using "
"file descriptor %d.", request->host,
connptr->server_fd);
if (!connptr->connect_method)
establish_http_connection (connptr, request);
}
if (process_client_headers (connptr, hashofheaders) < 0) {
update_stats (STAT_BADCONN);
log_message (LOG_INFO,
"process_client_headers failed: %s. host \"%s\" using "
"file descriptor %d.", strerror(errno),
request->host,
connptr->server_fd);
HC_FAIL();
}
if (!connptr->connect_method || UPSTREAM_IS_HTTP(connptr)) {
if (process_server_headers (connptr) < 0) {
update_stats (STAT_BADCONN);
log_message (LOG_INFO,
"process_server_headers failed: %s. host \"%s\" using "
"file descriptor %d.", strerror(errno),
request->host,
connptr->server_fd);
HC_FAIL();
}
} else {
if (send_ssl_response (connptr) < 0) {
log_message (LOG_ERR,
"handle_connection: Could not send SSL greeting "
"to client.");
update_stats (STAT_BADCONN);
HC_FAIL();
}
}
relay_connection (connptr);
log_message (LOG_INFO,
"Closed connection between local client (fd:%d) "
"and remote client (fd:%d)",
connptr->client_fd, connptr->server_fd);
done:
free_request_struct (request);
orderedmap_destroy (hashofheaders);
conn_destroy_contents (connptr);
return;
#undef HC_FAIL
}