/* MiddleMan filtering proxy server Copyright (C) 2002 Jason McLaughlin 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include "../libntlm/ntlm.h" #include "proto.h" extern TEMPLATES *templates; extern REWRITE_LIST *rewrite_list; extern EXTERNAL *external; extern MIME_LIST *mime_list; extern REDIRECT_LIST *redirect_list; extern KEYWORD_LIST *keyword_list; extern THREADLIST threads[]; int protocol_start(CONNECTION * connection) { int proxyfd, x; switch (connection->proxy_type) { case PROXY_NORMAL: proxyfd = net_connect(connection->proxy_host, connection->proxy_port); if (proxyfd < 0) putlog(MMLOG_ERROR, "connection to proxy server failed"); else connection->server = sock_new(proxyfd); break; case PROXY_SOCKS4: proxyfd = net_connect(connection->proxy_host, connection->proxy_port); if (proxyfd >= 0) { connection->server = sock_new(proxyfd); x = net_socks4(connection, connection->header->host, connection->header->port); if (x < 0) { putlog(MMLOG_ERROR, "connection to socks4 server failed"); template_send(templates, error_to_template(x), connection, 404); return -1; } } break; default: proxyfd = net_connect(connection->header->host, connection->header->port); if (proxyfd >= 0) connection->server = sock_new(proxyfd); } if (proxyfd < 0) { template_send(templates, error_to_template(proxyfd), connection, 404); connection->server = NULL; return -1; } return proxyfd; } int protocol_reconnect(CONNECTION * connection) { int ret; sock_close(connection->server); sock_flush(connection->client); ret = protocol_start(connection); return ret; } /* This function handles HTTP requests */ int protocol_http(CONNECTION * connection) { int auth = FALSE, ret; char *headbuf, buf[8096], *ptr; FILEBUF *filebuf, *ext_filebuf, *postdata = NULL; struct EXTERNAL_LIST_LIST *external_list = NULL; struct MIME_LIST_LIST *mime_list_list; struct url_command_t **url_command; pthread_mutex_lock(&threads[connection->thread].lock); threads[connection->thread].flags |= THREAD_HTTP; pthread_mutex_unlock(&threads[connection->thread].lock); if (connection->header->content_length != -1) { /* the browser wants to send something to the remote site after the header (POST, PUT, etc.) */ putlog(MMLOG_DEBUG, "post length: %d", connection->header->content_length); /* unfortunately, this _needs_ to be buffered.. otherwise we won't have any POST data after authentication */ postdata = http_transfer_filebuf(connection, CLIENT); rewrite_do(rewrite_list, connection, postdata, REWRITE_POST, TRUE); connection->header->chunked = FALSE; connection->header->content_length = postdata->size; } header_send(connection->header, connection, SERVER, (connection->proxy_type == PROXY_NORMAL) ? HEADER_FORWARD : HEADER_DIRECT); /* I really hate using goto's */ auth_finished: if (postdata != NULL) net_filebuf_send(postdata, connection, SERVER); reget_header: headbuf = header_get(connection, SERVER, TIMEOUT); if (headbuf == NULL) { if (!connection->keepalive_server && auth == FALSE) { template_send(templates, "noconnect", connection, 503); if (postdata != NULL) filebuf_free(postdata); return -1; } else { /* keep-alive connection closed, or authorization is being done on a non-keepalive proxy */ ret = protocol_reconnect(connection); if (ret == -1) { if (postdata != NULL) filebuf_free(postdata); return -1; } header_send(connection->header, connection, SERVER, (connection->proxy_type == PROXY_NORMAL) ? HEADER_FORWARD : HEADER_DIRECT); if (postdata != NULL) net_filebuf_send(postdata, connection, SERVER); headbuf = header_get(connection, SERVER, TIMEOUT); if (headbuf == NULL) { if (postdata != NULL) filebuf_free(postdata); return -1; } } } /* pass the server's header through the rewrite rules */ filebuf = filebuf_new(); filebuf->data = headbuf; filebuf->size = strlen(headbuf) + 1; rewrite_do(rewrite_list, connection, filebuf, REWRITE_SERVER, TRUE); headbuf = filebuf->data; filebuf->data = NULL; filebuf_free(filebuf); connection->rheader = http_header_parse_response(headbuf); if (connection->rheader == NULL) { if (postdata != NULL) filebuf_free(postdata); template_send(templates, "badresponse", connection, 400); xfree(headbuf); return -1; } xfree(headbuf); if (connection->rheader->code == 100) { /* server sent notice to continue POST, go back and retrieve final header (according to the RFC, the server should not send this unless the client sends an Expect: header; however... IIS will sometimes send this anyways) */ http_header_free(connection->rheader); goto reget_header; } connection->rheader->host = xstrdup(connection->header->host); connection->rheader->file = xstrdup(connection->header->file); if (connection->keepalive_client) { if (connection->rheader->content_length == -1 && !connection->rheader->chunked) { /* connection can't be kept alive if we can't determine when the remote site's data ends */ connection->keepalive_client = FALSE; connection->keepalive_server = FALSE; } else if (connection->rheader->keepalive == TRUE || connection->rheader->proxy_keepalive == TRUE) connection->keepalive_server = TRUE; else if (connection->rheader->version == HTTP_HTTP11) /* keep-alive is assumed for HTTP/1.1 unless any of the above conditions are met */ connection->keepalive_server = TRUE; else connection->keepalive_server = FALSE; putlog(MMLOG_DEBUG, "keepalive_server = %d", connection->keepalive_server); } else connection->keepalive_server = FALSE; if (connection->rheader->code == 407 && connection->proxy_type == PROXY_NORMAL && auth == FALSE) { /* authenticate with the other proxy */ if (proxy_authenticate(connection)) { http_header_free(connection->rheader); /* only try once */ auth = TRUE; /* authentication finished, retrieve the header again */ goto auth_finished; } FREE_AND_NULL(connection->rheader->proxy_authenticate); /* just continue, user will see 407 warning from proxy in browser */ } if (postdata != NULL) filebuf_free(postdata); /* send template instead of web server's content when a template name matches the code */ snprintf(buf, sizeof(buf), "%d", connection->rheader->code); ret = template_send(templates, buf, connection, connection->rheader->code); if (ret == TRUE) goto out; if (connection->rheader->location != NULL && (connection->rheader->code == 301 || connection->rheader->code == 302)) { /* redirect_do will replace the Location: header if any rules match */ redirect_do(redirect_list, connection, REDIRECT_HEADER); /* add URL command to location: header so the feature(s) are still bypassed when browser follows redirect */ if (connection->header->url_command != NULL) { ptr = url_command_add(connection, connection->rheader->location); xfree(connection->rheader->location); connection->rheader->location = ptr; } } /* no message body regardless of what header says for HEAD requests and certain return codes */ if (!strcasecmp(connection->header->method, "HEAD") || connection->rheader->code == 304 || connection->rheader->code == 204 || (connection->rheader->code >= 100 && connection->rheader->code < 200)) { header_send(connection->rheader, connection, CLIENT, HEADER_RESP); goto out; } for (url_command = connection->header->url_command; url_command && *url_command; url_command++) { if (!strcasecmp((*url_command)->command, "mime")) { /* show matching MIME filter entries, if any, for the requested URL */ mime_check_show(connection); connection->keepalive_server = FALSE; goto out; } } mime_list_list = mime_check(mime_list, connection); if (mime_list_list != NULL) { putlog(MMLOG_MIME, "blocked mime-type %s from %s%s", (connection->rheader->content_type != NULL) ? connection->rheader->content_type : "(none)", connection->header->host, connection->header->file); template_send(templates, (mime_list_list->template != NULL) ? mime_list_list->template : (mime_list->dtemplate != NULL) ? mime_list->dtemplate : "blocked", connection, 200); /* we have to drop the connection since we're already transferring something */ connection->keepalive_server = FALSE; pthread_rwlock_unlock(&mime_list->lock); goto out; } pthread_rwlock_unlock(&mime_list->lock); putlog(MMLOG_REQUEST, "%s %s:%d%s", connection->header->method, connection->header->host, connection->header->port, connection->header->file); if (BUFFERMAX == -1 || connection->rheader->content_length <= BUFFERMAX) { connection->buffer = rewrite_do(rewrite_list, connection, NULL, REWRITE_BODY, FALSE); if (connection->buffer == FALSE) { external_list = external_find(external, connection); pthread_rwlock_unlock(&external->lock); if (external_list != NULL) connection->buffer = TRUE; } if (connection->buffer == FALSE) connection->buffer = keyword_check(keyword_list, connection, NULL, FALSE); if (connection->buffer == FALSE) { for (url_command = connection->header->url_command; url_command && *url_command; url_command++) { if (!strcasecmp((*url_command)->command, "score")) connection->buffer = TRUE; } } } else connection->buffer = FALSE; if (connection->buffer == FALSE) { header_send(connection->rheader, connection, CLIENT, HEADER_RESP); http_transfer(connection, SERVER); } else { /* this file is to be buffered and processed before sending it to the browser */ filebuf = http_transfer_filebuf(connection, SERVER); /* decompress gzip-encoded content */ if (connection->rheader->content_encoding != NULL) { if (!strcasecmp(connection->rheader->content_encoding, "gzip")) { ret = filebuf_ungzip(filebuf); if (ret == FALSE) { putlog(MMLOG_ERROR, "failed to decompress gzip encoded content"); /* failed to decompress, just send as-is */ goto bypass; } } else { putlog(MMLOG_WARN, "unhandled content encoding: %s", connection->rheader->content_encoding); goto bypass; } } /* check keyword score */ ret = keyword_check(keyword_list, connection, filebuf, TRUE); for (url_command = connection->header->url_command; url_command && *url_command; url_command++) { if (!strcasecmp((*url_command)->command, "score")) { score_show(connection, ret); filebuf_free(filebuf); goto out; } } if (ret) { pthread_rwlock_rdlock(&keyword_list->lock); if (ret >= keyword_list->threshold) { putlog(MMLOG_KEYWORDS, "page above threshold with a score of %d", ret); template_send(templates, (keyword_list->template != NULL) ? keyword_list->template : "blocked", connection, 200); pthread_rwlock_unlock(&keyword_list->lock); filebuf_free(filebuf); goto out; } pthread_rwlock_unlock(&keyword_list->lock); } rewrite_do(rewrite_list, connection, filebuf, REWRITE_BODY, TRUE); external_list = external_find(external, connection); if (external_list != NULL && external_list->exec != NULL) { ext_filebuf = external_exec(connection, external_list->exec, filebuf, external_list->type); if (ext_filebuf != NULL) { filebuf_free(filebuf); filebuf = ext_filebuf; if (external_list->newmime != NULL) { FREE_AND_NULL(connection->rheader->content_type); if (!strcasecmp(external_list->newmime, "STDIN")) { /* try extracting Content-type header from external program's output */ connection->rheader->content_type = external_getmime(filebuf); } else connection->rheader->content_type = xstrdup(external_list->newmime); } } else putlog(MMLOG_ERROR, "failed to execute external parser '%s'", external_list->exec); } pthread_rwlock_unlock(&external->lock); FREE_AND_NULL(connection->rheader->content_encoding); if (connection->header->accept_encoding != NULL) { /* compress the content if the browser supports gzip encoding */ ptr = strcasestr(connection->header->accept_encoding, "gzip"); if (ptr != NULL) { ret = filebuf_gzip(filebuf); if (ret == TRUE) connection->rheader->content_encoding = xstrdup("gzip"); } } bypass: putlog(MMLOG_DEBUG, "filebuf size: %d", filebuf->size); connection->rheader->chunked = FALSE; connection->rheader->content_length = filebuf->size; if (connection->keepalive_client != FALSE && connection->header->keepalive != FALSE && connection->header->proxy_keepalive != FALSE) { /* we may be able to do keep-alive now since the page was buffered and the size is now known */ if (connection->header->keepalive == TRUE || connection->header->proxy_keepalive == TRUE) connection->keepalive_client = TRUE; else if (connection->header->version == HTTP_HTTP11) connection->keepalive_client = TRUE; } header_send(connection->rheader, connection, CLIENT, HEADER_RESP); net_filebuf_send(filebuf, connection, CLIENT); filebuf_free(filebuf); } out: http_header_free(connection->rheader); return 1; } /* This function handles CONNECT requests (HTTPS) */ int protocol_connect(CONNECTION * connection) { pthread_mutex_lock(&threads[connection->thread].lock); threads[connection->thread].flags |= THREAD_CONNECT; pthread_mutex_unlock(&threads[connection->thread].lock); putlog(MMLOG_REQUEST, "CONNECT %s:%d", connection->header->host, connection->header->port); if (connection->proxy_type == PROXY_NORMAL) header_send(connection->header, connection, SERVER, HEADER_FORWARD); else putsock(connection->client, "HTTP/1.1 Connection Established\r\n\r\n"); net_proxy(connection, -1); return 0; } /* get size of next chunk for pages using chunked encoding */ unsigned int next_chunksize(CONNECTION * connection, int flags) { int x; char buf[64]; x = sock_getline((flags == SERVER) ? connection->server : connection->client, buf, sizeof(buf)); if (x <= 0) return 0; return strtol(buf, NULL, 16); } /* transfer http body from one socket to another */ void http_transfer(CONNECTION * connection, int direction) { int x; HEADER *header; header = (direction == SERVER) ? connection->rheader : connection->header; if (header->content_length != -1) net_transfer(connection, direction, header->content_length); else if (header->chunked) do { x = next_chunksize(connection, direction); putsock((direction == SERVER) ? connection->client : connection->server, "%x\r\n", x); /* rfc says chunks must end in \r\n */ net_transfer(connection, direction, x + 2); } while (x > 0); else net_transfer(connection, direction, -1); } /* transfer http body into filebuf */ FILEBUF *http_transfer_filebuf(CONNECTION * connection, int direction) { int x; FILEBUF *filebuf; HEADER *header; SOCKET *sock; filebuf = filebuf_new(); header = (direction == SERVER) ? connection->rheader : connection->header; sock = (direction == SERVER) ? connection->server : connection->client; if (header->content_length != -1) net_filebuf_read(filebuf, connection, direction, header->content_length); else if (header->chunked) do { x = next_chunksize(connection, direction); net_filebuf_read(filebuf, connection, direction, x); /* discard the \r\n from the server */ sock_getline(sock, NULL, -1); } while (x > 0); else net_filebuf_read(filebuf, connection, direction, -1); return filebuf; } /* read the body and discard it */ void http_transfer_discard(CONNECTION * connection, int direction) { int x; HEADER *header; SOCKET *sock; header = (direction == SERVER) ? connection->rheader : connection->header; sock = (direction == SERVER) ? connection->server : connection->client; if (header->content_length != -1) net_filebuf_read(NULL, connection, direction, header->content_length); else if (header->chunked) do { x = next_chunksize(connection, direction); net_filebuf_read(NULL, connection, direction, x); /* discard the \r\n from the server */ sock_getline(sock, NULL, -1); } while (x > 0); else net_filebuf_read(NULL, connection, direction, -1); } int proxy_authenticate(CONNECTION * connection) { if (connection->rheader->proxy_authenticate == NULL) return FALSE; if (!strcasecmp(connection->rheader->proxy_authenticate, "NTLM")) { /* NTLM authentication */ return send_ntlm_response(connection); } else if (!strncasecmp(connection->rheader->proxy_authenticate, "Basic", 5)) { /* Basic authentication */ return send_basic_response(connection); } return FALSE; } /* perform an NTLM handshake, the sequence of events looks like this: 1: C --> S GET ... 2: C <-- S 401 Unauthorized Proxy-Authenticate: NTLM 3: C --> S GET ... Proxy-Authorization: NTLM 4: C <-- S 401 Unauthorized Proxy-Authenticate: NTLM 5: C --> S GET ... Proxy-Authorization: NTLM 6: C <-- S 200 Ok */ int send_ntlm_response(CONNECTION * connection) { int ret, oldkeep; unsigned char buf[4096], buf2[4096], *headbuf; HEADER *header; /* client must do keep-alive during authentication */ oldkeep = connection->keepalive_server; connection->keepalive_server = TRUE; /* don't need the body of the 407 message */ http_transfer_discard(connection, SERVER); /* send type-1 message */ /* note: buildSmbNtml* knows how to deal with NULL arguments */ buildSmbNtlmAuthRequest((tSmbNtlmAuthRequest *) buf, connection->proxy_username, connection->proxy_domain); to64frombits(buf2, buf, SmbLength((tSmbNtlmAuthResponse *) buf)); FREE_AND_NULL(connection->proxy_auth); snprintf(buf, sizeof(buf), "NTLM %s", buf2); connection->proxy_auth = xstrdup(buf); header_send(connection->header, connection, SERVER, HEADER_FORWARD); headbuf = header_get(connection, SERVER, TIMEOUT); if (headbuf == NULL) { /* squid and maybe some other proxies will disconnect after sending the first 407 message regardless of Connection header */ ret = protocol_reconnect(connection); if (ret == -1) goto error; header_send(connection->header, connection, SERVER, HEADER_FORWARD); headbuf = header_get(connection, SERVER, TIMEOUT); if (headbuf == NULL) goto error; } header = http_header_parse_response(headbuf); xfree(headbuf); if (header == NULL) goto error; http_header_free(connection->rheader); connection->rheader = header; if (header->proxy_authenticate == NULL || strncasecmp(header->proxy_authenticate, "NTLM ", 5)) goto error; http_transfer_discard(connection, SERVER); /* parse type-2 message */ from64tobits(buf, &header->proxy_authenticate[5]); buildSmbNtlmAuthResponse((tSmbNtlmAuthChallenge *) buf, (tSmbNtlmAuthResponse *) buf2, connection->proxy_username, connection->proxy_password); /* send type-3 message */ to64frombits(buf, buf2, SmbLength((tSmbNtlmAuthResponse *) buf2)); snprintf(buf2, sizeof(buf2), "NTLM %s", buf); FREE_AND_NULL(connection->proxy_auth); connection->proxy_auth = xstrdup(buf2); header_send(connection->header, connection, SERVER, HEADER_FORWARD); connection->keepalive_server = oldkeep; return TRUE; error: connection->keepalive_server = oldkeep; return FALSE; } /* basic proxy authentication */ int send_basic_response(CONNECTION * connection) { char buf[4096], buf2[4096]; /* discard body of 407 authentication required message */ http_transfer_discard(connection, SERVER); snprintf(buf, sizeof(buf), "%s:%s", (connection->proxy_username != NULL) ? connection->proxy_username : "", (connection->proxy_password != NULL) ? connection->proxy_password : ""); to64frombits(buf2, buf, strlen(buf)); snprintf(buf, sizeof(buf), "Basic %s", buf2); FREE_AND_NULL(connection->proxy_auth); connection->proxy_auth = xstrdup(buf); header_send(connection->header, connection, SERVER, HEADER_FORWARD); return TRUE; }