/* $Id: reqs.c,v 1.1.1.1 2000-02-16 17:32:23 sdyoung 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 #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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" /* chris - for asynchronous DNS */ #include "dnscache.h" #include 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 (strcasecmp(uri->scheme, "http") != 0) { char *error_string = xmalloc(strlen(uri->scheme) + 64); sprintf(error_string, "Invalid scheme (%s). Only HTTP is allowed.", uri->scheme); httperr(connptr, 400, error_string); safefree(error_string); 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, " "); strcat(request, uri->path); if (uri->query) { strcat(request, "?"); strcat(request, uri->query); } 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) == 0) { 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)); /* * HACK HACK HACK: When we're sending a POST there is no restriction * on the length of the header. If we don't let all the header lines * through, the POST will not work. This _definitely_ needs to be * fixed. - rjkaes */ if (!xstrstr(inbuf, "POST ", 5, FALSE)) { connptr->clientheader = TRUE; } 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) { struct allowedhdr_s *allowedptr = allowedhdrs; assert(line); assert(allowedhdrs); if (!xstrstr(line, "GET ", 4, FALSE) || !xstrstr(line, "POST ", 5, FALSE) || !xstrstr(line, "HEAD ", 5, FALSE)) return 1; for (allowedptr = allowedhdrs; allowedptr; allowedptr = allowedptr->next) { if (!strncasecmp (line, allowedptr->hdrname, strlen(allowedptr->hdrname))) { return 1; } } return 0; } /* * 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) { assert(connptr); assert(readfds); if (FD_ISSET(connptr->client_fd, readfds)) { if (clientreq(connptr) < 0) { shutdown(connptr->client_fd, 2); connptr->type = FINISHCONN; return; } if (!connptr) abort(); connptr->actiontime = time(NULL); } } 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; }