Initial keep-alive support for the client socket.
[privoxy.git] / jcc.c
diff --git a/jcc.c b/jcc.c
index 5a3524e..5991a72 100644 (file)
--- a/jcc.c
+++ b/jcc.c
@@ -1,4 +1,4 @@
-const char jcc_rcs[] = "$Id: jcc.c,v 1.236 2009/03/25 17:30:24 fabiankeil Exp $";
+const char jcc_rcs[] = "$Id: jcc.c,v 1.245 2009/04/24 15:29:43 fabiankeil Exp $";
 /*********************************************************************
  *
  * File        :  $Source: /cvsroot/ijbswa/current/jcc.c,v $
@@ -33,6 +33,39 @@ const char jcc_rcs[] = "$Id: jcc.c,v 1.236 2009/03/25 17:30:24 fabiankeil Exp $"
  *
  * Revisions   :
  *    $Log: jcc.c,v $
+ *    Revision 1.245  2009/04/24 15:29:43  fabiankeil
+ *    Allow to limit the number of of client connections.
+ *
+ *    Revision 1.244  2009/04/17 11:34:34  fabiankeil
+ *    Style cosmetics for the IPv6 code.
+ *
+ *    Revision 1.243  2009/04/17 11:27:49  fabiankeil
+ *    Petr Pisar's privoxy-3.0.12-ipv6-3.diff.
+ *
+ *    Revision 1.242  2009/04/11 10:44:47  fabiankeil
+ *    Update a comment. We're not in Kansas anymore.
+ *
+ *    Revision 1.241  2009/04/11 10:37:23  fabiankeil
+ *    When dropping connections due to ACL, don't leak csp->ip_addr_str.
+ *
+ *    Revision 1.240  2009/04/09 10:12:54  fabiankeil
+ *    Fix two cases in which an invalid server response would result
+ *    in the client connection being closed without sending an error
+ *    message first.
+ *
+ *    Revision 1.239  2009/04/07 11:43:50  fabiankeil
+ *    If the server rudely resets the connection directly after sending the
+ *    headers, pass the mess to the client instead of sending an incorrect
+ *    connect-failed message. Fixes #2698674 reported by mybugaccount.
+ *
+ *    Revision 1.238  2009/03/27 14:42:30  fabiankeil
+ *    Correct the status code for CONNECTION_TIMEOUT_RESPONSE.
+ *
+ *    Revision 1.237  2009/03/27 14:32:04  fabiankeil
+ *    If spawning a child in listen_loop() fails, send a real
+ *    HTTP response to the client and continue listening for
+ *    new connections without artificial delay.
+ *
  *    Revision 1.236  2009/03/25 17:30:24  fabiankeil
  *    In serve(), keep the client socket open until we marked the
  *    server socket as unused. This should increase the chances
@@ -1483,7 +1516,7 @@ static const char TOO_MANY_CONNECTIONS_RESPONSE[] =
 
 /* XXX: should be a template */
 static const char CONNECTION_TIMEOUT_RESPONSE[] =
-   "HTTP/1.0 502 Connection timeout\r\n"
+   "HTTP/1.0 504 Connection timeout\r\n"
    "Proxy-Agent: Privoxy " VERSION "\r\n"
    "Content-Type: text/plain\r\n"
    "Connection: close\r\n\r\n"
@@ -2258,6 +2291,73 @@ static void wait_for_alive_connections()
    log_error(LOG_LEVEL_CONNECT, "No connections to wait for left.");
 
 }
+
+
+/*********************************************************************
+ *
+ * Function    :  save_connection_destination
+ *
+ * Description :  Remembers a connection for reuse later on.
+ *
+ * Parameters  :
+ *          1  :  sfd  = Open socket to remember.
+ *          2  :  http = The destination for the connection.
+ *          3  :  fwd  = The forwarder settings used.
+ *          3  :  server_connection  = storage.
+ *
+ * Returns     : void
+ *
+ *********************************************************************/
+void save_connection_destination(jb_socket sfd,
+                                 const struct http_request *http,
+                                 const struct forward_spec *fwd,
+                                 struct reusable_connection *server_connection)
+{
+   assert(sfd != JB_INVALID_SOCKET);
+   assert(NULL != http->host);
+   server_connection->host = strdup(http->host);
+   if (NULL == server_connection->host)
+   {
+      log_error(LOG_LEVEL_FATAL, "Out of memory saving socket.");
+   }
+   server_connection->port = http->port;
+
+   assert(NULL != fwd);
+   assert(server_connection->gateway_host == NULL);
+   assert(server_connection->gateway_port == 0);
+   assert(server_connection->forwarder_type == 0);
+   assert(server_connection->forward_host == NULL);
+   assert(server_connection->forward_port == 0);
+
+   server_connection->forwarder_type = fwd->type;
+   if (NULL != fwd->gateway_host)
+   {
+      server_connection->gateway_host = strdup(fwd->gateway_host);
+      if (NULL == server_connection->gateway_host)
+      {
+         log_error(LOG_LEVEL_FATAL, "Out of memory saving gateway_host.");
+      }
+   }
+   else
+   {
+      server_connection->gateway_host = NULL;
+   }
+   server_connection->gateway_port = fwd->gateway_port;
+
+   if (NULL != fwd->forward_host)
+   {
+      server_connection->forward_host = strdup(fwd->forward_host);
+      if (NULL == server_connection->forward_host)
+      {
+         log_error(LOG_LEVEL_FATAL, "Out of memory saving forward_host.");
+      }
+   }
+   else
+   {
+      server_connection->forward_host = NULL;
+   }
+   server_connection->forward_port = fwd->forward_port;
+}
 #endif /* FEATURE_CONNECTION_KEEP_ALIVE */
 
 
@@ -2723,7 +2823,7 @@ static void chat(struct client_state *csp)
 
    if (fwd->forward_host)
    {
-      log_error(LOG_LEVEL_CONNECT, "via %s:%d to: %s",
+      log_error(LOG_LEVEL_CONNECT, "via [%s]:%d to: %s",
          fwd->forward_host, fwd->forward_port, http->hostport);
    }
    else
@@ -2733,41 +2833,66 @@ static void chat(struct client_state *csp)
 
    /* here we connect to the server, gateway, or the forwarder */
 
-   while ((csp->sfd = forwarded_connect(fwd, http, csp))
-      && (errno == EINVAL)
-      && (forwarded_connect_retries++ < max_forwarded_connect_retries))
+#ifdef FEATURE_CONNECTION_KEEP_ALIVE
+   if ((csp->sfd != JB_INVALID_SOCKET)
+      && socket_is_still_usable(csp->sfd)
+      && connection_destination_matches(&csp->server_connection, http, fwd))
    {
-      log_error(LOG_LEVEL_ERROR,
-         "failed request #%u to connect to %s. Trying again.",
-         forwarded_connect_retries, http->hostport);
+      log_error(LOG_LEVEL_CONNECT,
+         "Reusing server socket %u. Opened for %s.",
+         csp->sfd, csp->server_connection.host);
    }
-
-   if (csp->sfd == JB_INVALID_SOCKET)
+   else
    {
-      if (fwd->type != SOCKS_NONE)
+      if (csp->sfd != JB_INVALID_SOCKET)
       {
-         /* Socks error. */
-         rsp = error_response(csp, "forwarding-failed", errno);
-      }
-      else if (errno == EINVAL)
-      {
-         rsp = error_response(csp, "no-such-domain", errno);
+         log_error(LOG_LEVEL_CONNECT,
+            "Closing server socket %u. Opened for %s.",
+            csp->sfd, csp->server_connection.host);
+         close_socket(csp->sfd);
+         mark_connection_closed(&csp->server_connection);
       }
-      else
+#endif /* def FEATURE_CONNECTION_KEEP_ALIVE */
+
+      while ((csp->sfd = forwarded_connect(fwd, http, csp))
+         && (errno == EINVAL)
+         && (forwarded_connect_retries++ < max_forwarded_connect_retries))
       {
-         rsp = error_response(csp, "connect-failed", errno);
-         log_error(LOG_LEVEL_CONNECT, "connect to: %s failed: %E",
-            http->hostport);
+         log_error(LOG_LEVEL_ERROR,
+            "failed request #%u to connect to %s. Trying again.",
+            forwarded_connect_retries, http->hostport);
       }
 
-      /* Write the answer to the client */
-      if (rsp != NULL)
+      if (csp->sfd == JB_INVALID_SOCKET)
       {
-         send_crunch_response(csp, rsp);
-      }
+         if (fwd->type != SOCKS_NONE)
+         {
+            /* Socks error. */
+            rsp = error_response(csp, "forwarding-failed", errno);
+         }
+         else if (errno == EINVAL)
+         {
+            rsp = error_response(csp, "no-such-domain", errno);
+         }
+         else
+         {
+            rsp = error_response(csp, "connect-failed", errno);
+            log_error(LOG_LEVEL_CONNECT, "connect to: %s failed: %E",
+               http->hostport);
+         }
 
-      return;
+         /* Write the answer to the client */
+         if (rsp != NULL)
+         {
+            send_crunch_response(csp, rsp);
+         }
+
+         return;
+      }
+#ifdef FEATURE_CONNECTION_KEEP_ALIVE
+      save_connection_destination(csp->sfd, http, fwd, &csp->server_connection);
    }
+#endif /* def FEATURE_CONNECTION_KEEP_ALIVE */
 
    hdr = list_to_text(csp->headers);
    if (hdr == NULL)
@@ -2895,6 +3020,9 @@ static void chat(struct client_state *csp)
       /*
        * This is the body of the browser's request,
        * just read and write it.
+       *
+       * XXX: Make sure the client doesn't use pipelining
+       * behind Privoxy's back.
        */
       if (FD_ISSET(csp->cfd, &rfds))
       {
@@ -2955,14 +3083,11 @@ static void chat(struct client_state *csp)
                mark_server_socket_tainted(csp);
                return;
             }
-
-            rsp = error_response(csp, "connect-failed", errno);
-            if (rsp)
-            {
-               send_crunch_response(csp, rsp);
-            }
-
-            return;
+            /*
+             * XXX: Consider handling the cases above the same.
+             */
+            mark_server_socket_tainted(csp);
+            len = 0;
          }
 
 #ifdef FEATURE_CONNECTION_KEEP_ALIVE
@@ -3175,9 +3300,14 @@ static void chat(struct client_state *csp)
                    * The header is incomplete and there isn't anything
                    * we can do about it.
                    */
-                  log_error(LOG_LEVEL_INFO,
-                     "MS IIS5 hack didn't produce valid headers.");
-                  break;
+                  log_error(LOG_LEVEL_ERROR, "Invalid server headers. "
+                     "Applying the MS IIS5 hack didn't help.");
+                  log_error(LOG_LEVEL_CLF,
+                     "%s - - [%T] \"%s\" 502 0", csp->ip_addr_str, http->cmd);
+                  write_socket(csp->cfd, INVALID_SERVER_HEADERS_RESPONSE,
+                     strlen(INVALID_SERVER_HEADERS_RESPONSE));
+                  mark_server_socket_tainted(csp);
+                  return;
                }
                else
                {
@@ -3314,9 +3444,15 @@ static void chat(struct client_state *csp)
              */
             if (ms_iis5_hack)
             {
-               log_error(LOG_LEVEL_INFO,
-                  "Closed server connection detected with MS IIS5 hack enabled.");
-               break;
+               log_error(LOG_LEVEL_ERROR,
+                  "Closed server connection detected. "
+                  "Applying the MS IIS5 hack didn't help.");
+               log_error(LOG_LEVEL_CLF,
+                  "%s - - [%T] \"%s\" 502 0", csp->ip_addr_str, http->cmd);
+               write_socket(csp->cfd, INVALID_SERVER_HEADERS_RESPONSE,
+                  strlen(INVALID_SERVER_HEADERS_RESPONSE));
+               mark_server_socket_tainted(csp);
+               return;
             }
          }
          continue;
@@ -3370,38 +3506,80 @@ void serve(struct client_state *csp)
 static void serve(struct client_state *csp)
 #endif /* def AMIGA */
 {
-   chat(csp);
-
-   if (csp->sfd != JB_INVALID_SOCKET)
-   {
 #ifdef FEATURE_CONNECTION_KEEP_ALIVE
-      static int monitor_thread_running = 0;
+   int continue_chatting = 0;
+   do
+   {
+      chat(csp);
+
+      continue_chatting = (csp->config->feature_flags
+         & RUNTIME_FEATURE_CONNECTION_KEEP_ALIVE)
+         && (csp->flags & CSP_FLAG_SERVER_CONNECTION_KEEP_ALIVE)
+         && (csp->cfd != JB_INVALID_SOCKET)
+         && (csp->sfd != JB_INVALID_SOCKET)
+         && socket_is_still_usable(csp->sfd);
 
-      if ((csp->config->feature_flags & RUNTIME_FEATURE_CONNECTION_KEEP_ALIVE)
-       && (csp->flags & CSP_FLAG_SERVER_CONNECTION_KEEP_ALIVE))
+      /*
+       * Get the csp in a mostly vergin state again.
+       * XXX: Should be done elsewhere.
+       */
+      csp->content_type = 0;
+      csp->content_length = 0;
+      csp->expected_content_length = 0;
+      list_remove_all(csp->headers);
+      freez(csp->iob->buf);
+      memset(csp->iob, 0, sizeof(csp->iob));
+      freez(csp->error_message);
+      free_http_request(csp->http);
+      destroy_list(csp->headers);
+      destroy_list(csp->tags);
+      free_current_action(csp->action);
+      if (NULL != csp->fwd)
       {
-         remember_connection(csp->sfd, csp->http, forward_url(csp, csp->http));
-         close_socket(csp->cfd);
-         csp->cfd = JB_INVALID_SOCKET;
-         privoxy_mutex_lock(&connection_reuse_mutex);
-         if (!monitor_thread_running)
+         unload_forward_spec(csp->fwd);
+         csp->fwd = NULL;
+      }
+
+      /* XXX: Store per-connection flags someplace else. */
+      csp->flags = CSP_FLAG_ACTIVE | (csp->flags & CSP_FLAG_TOGGLED_ON);
+
+      if (continue_chatting)
+      {
+         log_error(LOG_LEVEL_CONNECT,
+            "Waiting for the next client request. "
+            "Keeping the server socket %d to %s open.",
+            csp->sfd, csp->server_connection.host);
+
+         if ((csp->flags & CSP_FLAG_CLIENT_CONNECTION_KEEP_ALIVE)
+            && data_is_available(csp->cfd, csp->config->keep_alive_timeout)
+            && socket_is_still_usable(csp->cfd))
+         {
+            log_error(LOG_LEVEL_CONNECT, "Client request arrived in "
+               "time or the client closed the connection.");
+         }
+         else
          {
-            monitor_thread_running = 1;
-            privoxy_mutex_unlock(&connection_reuse_mutex);
-            wait_for_alive_connections();
-            privoxy_mutex_lock(&connection_reuse_mutex);
-            monitor_thread_running = 0;
+            log_error(LOG_LEVEL_CONNECT,
+               "No additional client request received in time. "
+               "Closing server socket %d, initially opened for %s.",
+               csp->sfd, csp->server_connection.host);
+            break;
          }
-         privoxy_mutex_unlock(&connection_reuse_mutex);
       }
-      else
+      else if (csp->sfd != JB_INVALID_SOCKET)
       {
-         forget_connection(csp->sfd);
-         close_socket(csp->sfd);
+         log_error(LOG_LEVEL_CONNECT,
+            "The connection on server socket %d to %s isn't reusable. "
+            "Closing.", csp->sfd, csp->server_connection.host);
       }
+   } while (continue_chatting);
 #else
-      close_socket(csp->sfd);
+   chat(csp);
 #endif /* def FEATURE_CONNECTION_KEEP_ALIVE */
+
+   if (csp->sfd != JB_INVALID_SOCKET)
+   {
+      close_socket(csp->sfd);
    }
 
    if (csp->cfd != JB_INVALID_SOCKET)
@@ -4145,7 +4323,8 @@ static void listen_loop(void)
 {
    struct client_state *csp = NULL;
    jb_socket bfd;
-   struct configuration_spec * config;
+   struct configuration_spec *config;
+   unsigned int active_threads = 0;
 
    config = load_config();
 
@@ -4175,7 +4354,7 @@ static void listen_loop(void)
       /*
        * Free data that was used by died threads
        */
-      sweep();
+      active_threads = sweep();
 
 #if defined(unix)
       /*
@@ -4206,7 +4385,7 @@ static void listen_loop(void)
       {
          /*
           * Since we were listening to the "old port", we will not see
-          * a "listen" param change until the next IJB request.  So, at
+          * a "listen" param change until the next request.  So, at
           * least 1 more request must be made for us to find the new
           * setting.  I am simply closing the old socket and binding the
           * new one.
@@ -4260,11 +4439,26 @@ static void listen_loop(void)
       {
          log_error(LOG_LEVEL_CONNECT, "Connection from %s dropped due to ACL", csp->ip_addr_str);
          close_socket(csp->cfd);
+         freez(csp->ip_addr_str);
          freez(csp);
          continue;
       }
 #endif /* def FEATURE_ACL */
 
+      if ((0 != config->max_client_connections)
+         && (active_threads >= config->max_client_connections))
+      {
+         log_error(LOG_LEVEL_CONNECT,
+            "Rejecting connection from %s. Maximum number of connections reached.",
+            csp->ip_addr_str);
+         write_socket(csp->cfd, TOO_MANY_CONNECTIONS_RESPONSE,
+            strlen(TOO_MANY_CONNECTIONS_RESPONSE));
+         close_socket(csp->cfd);
+         freez(csp->ip_addr_str);
+         freez(csp);
+         continue;
+      }
+
       /* add it to the list of clients */
       csp->next = clients->next;
       clients->next = csp;