Avoid reading too much data before TLS handshake
[exim.git] / src / src / smtp_in.c
index 9c647efda8db520f56f0d46499497c376bfbde55..b882c1e2abf78e58554ead1c57f7401d61df9723 100644 (file)
@@ -600,7 +600,7 @@ next_cmd:
   }
 }
 
-static void
+void
 bdat_flush_data(void)
 {
 while (chunking_data_left > 0)
@@ -897,7 +897,8 @@ if (get_ok == 0)
 *       Check if host is required proxy host     *
 *************************************************/
 /* The function determines if inbound host will be a regular smtp host
-or if it is configured that it must use Proxy Protocol.
+or if it is configured that it must use Proxy Protocol.  A local
+connection cannot.
 
 Arguments: none
 Returns:   bool
@@ -907,12 +908,10 @@ static BOOL
 check_proxy_protocol_host()
 {
 int rc;
-/* Cannot configure local connection as a proxy inbound */
-if (sender_host_address == NULL) return proxy_session;
 
-rc = verify_check_this_host(CUSS &hosts_proxy, NULL, NULL,
-                           sender_host_address, NULL);
-if (rc == OK)
+if (  sender_host_address
+   && (rc = verify_check_this_host(CUSS &hosts_proxy, NULL, NULL,
+                           sender_host_address, NULL)) == OK)
   {
   DEBUG(D_receive)
     debug_printf("Detected proxy protocol configured host\n");
@@ -922,6 +921,52 @@ return proxy_session;
 }
 
 
+/*************************************************
+*    Read data until newline or end of buffer    *
+*************************************************/
+/* While SMTP is server-speaks-first, TLS is client-speaks-first, so we can't
+read an entire buffer and assume there will be nothing past a proxy protocol
+header.  Our approach normally is to use stdio, but again that relies upon
+"STARTTLS\r\n" and a server response before the client starts TLS handshake, or
+reading _nothing_ before client TLS handshake.  So we don't want to use the
+usual buffering reads which may read enough to block TLS starting.
+
+So unfortunately we're down to "read one byte at a time, with a syscall each,
+and expect a little overhead", for all proxy-opened connections which are v1,
+just to handle the TLS-on-connect case.  Since SSL functions wrap the
+underlying fd, we can't assume that we can feed them any already-read content.
+
+We need to know where to read to, the max capacity, and we'll read until we
+get a CR and one more character.  Let the caller scream if it's CR+!LF.
+
+Return the amount read.
+*/
+
+static int
+swallow_until_crlf(int fd, void *vto, int capacity)
+{
+  uschar *to = (uschar *)vto;
+  int have = 0;
+  int ret;
+  int last = 0;
+
+  while (capacity > 0) {
+    do { ret = recv(fd, to, 1, 0); } while (ret == -1 && errno == EINTR);
+    if (ret == -1)
+      return -1;
+    have++;
+    if (last)
+      return have;
+    if (*to == '\r')
+      last = 1;
+    capacity--;
+    to++;
+  }
+  // reached end without having room for a final newline, abort
+  errno = EOVERFLOW;
+  return -1;
+}
+
 /*************************************************
 *         Setup host for proxy protocol          *
 *************************************************/
@@ -934,7 +979,7 @@ Arguments: none
 Returns:   Boolean success
 */
 
-static BOOL
+static void
 setup_proxy_protocol_host()
 {
 union {
@@ -977,11 +1022,13 @@ struct sockaddr_in6 tmpaddr6;
 int get_ok = 0;
 int size, ret;
 int fd = fileno(smtp_in);
+#define PROXY_INITIAL_READ 12
 const char v2sig[12] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
-uschar *iptype;  /* To display debug info */
+uschar * iptype;  /* To display debug info */
 struct timeval tv;
 struct timeval tvtmp;
 socklen_t vslen = sizeof(struct timeval);
+BOOL yield = FALSE;
 
 /* Save current socket timeout values */
 get_ok = getsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, CS &tvtmp, &vslen);
@@ -991,13 +1038,16 @@ get_ok = getsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, CS &tvtmp, &vslen);
 tv.tv_sec  = PROXY_NEGOTIATION_TIMEOUT_SEC;
 tv.tv_usec = PROXY_NEGOTIATION_TIMEOUT_USEC;
 if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, CS &tv, sizeof(tv)) < 0)
-  return FALSE;
+  goto bad;
 
 do
   {
   /* The inbound host was declared to be a Proxy Protocol host, so
      don't do a PEEK into the data, actually slurp it up. */
-  ret = recv(fd, &hdr, sizeof(hdr), 0);
+  /* We assume that the zie of a v2sig is less than the size of a complete
+     PROXYv1 line, so that we will _have_ to read more data; "PROXY TCPn\r\n"
+     is 12, so an IP address pushes it over, so we're good. */
+  ret = recv(fd, &hdr, PROXY_INITIAL_READ, 0);
   }
   while (ret == -1 && errno == EINTR);
 
@@ -1006,8 +1056,19 @@ if (ret == -1)
 
 if (ret >= 16 && memcmp(&hdr.v2, v2sig, 12) == 0)
   {
-  uint8_t ver = (hdr.v2.ver_cmd & 0xf0) >> 4;
-  uint8_t cmd = (hdr.v2.ver_cmd & 0x0f);
+  uint8_t ver, cmd;
+  int ret2nd;
+
+  /* It's now safe to read the rest. */
+  do {
+    ret2nd = recv(fd, (uschar*)&hdr + ret, sizeof(hdr)-ret, 0);
+  } while (ret2nd == -1 && errno == EINTR);
+  if (ret2nd == -1)
+    goto proxyfail;
+  ret += ret2nd;
+
+  ver = (hdr.v2.ver_cmd & 0xf0) >> 4;
+  cmd = (hdr.v2.ver_cmd & 0x0f);
 
   /* May 2014: haproxy combined the version and command into one byte to
      allow two full bytes for the length field in order to proxy SSL
@@ -1095,6 +1156,7 @@ if (ret >= 16 && memcmp(&hdr.v2, v2sig, 12) == 0)
       break;
     case 0x00: /* LOCAL command */
       /* Keep local connection address for LOCAL */
+      iptype = US"local";
       break;
     default:
       DEBUG(D_receive)
@@ -1104,20 +1166,31 @@ if (ret >= 16 && memcmp(&hdr.v2, v2sig, 12) == 0)
   }
 else if (ret >= 8 && memcmp(hdr.v1.line, "PROXY", 5) == 0)
   {
-  uschar *p = string_copy(hdr.v1.line);
-  uschar *end = memchr(p, '\r', ret - 1);
+  uschar *p;
+  uschar *end;
   uschar *sp;     /* Utility variables follow */
   int     tmp_port;
+  int     r2;
   char   *endc;
 
+  /* get the rest of the line */
+  r2 = swallow_until_crlf(fd, (uschar*)&hdr + ret, sizeof(hdr)-ret);
+  if (r2 == -1)
+    goto proxyfail;
+  ret += r2;
+
+  p = string_copy(hdr.v1.line);
+  end = memchr(p, '\r', ret - 1);
+
   if (!end || end[1] != '\n')
     {
     DEBUG(D_receive) debug_printf("Partial or invalid PROXY header\n");
     goto proxyfail;
     }
   *end = '\0'; /* Terminate the string */
-  size = end + 2 - hdr.v1.line; /* Skip header + CRLF */
+  size = end + 2 - p; /* Skip header + CRLF */
   DEBUG(D_receive) debug_printf("Detected PROXYv1 header\n");
+  DEBUG(D_receive) debug_printf("Bytes read not within PROXY header: %d\n", ret - size);
   /* Step through the string looking for the required fields. Ensure
      strict adherence to required formatting, exit for any error. */
   p += 5;
@@ -1209,26 +1282,41 @@ else if (ret >= 8 && memcmp(hdr.v1.line, "PROXY", 5) == 0)
     }
   proxy_external_port = tmp_port;
   /* Already checked for /r /n above. Good V1 header received. */
-  goto done;
   }
 else
   {
   /* Wrong protocol */
   DEBUG(D_receive) debug_printf("Invalid proxy protocol version negotiation\n");
+  (void) swallow_until_crlf(fd, (uschar*)&hdr + ret, sizeof(hdr)-ret);
   goto proxyfail;
   }
 
+done:
+  DEBUG(D_receive)
+    debug_printf("Valid %s sender from Proxy Protocol header\n", iptype);
+  yield = proxy_session;
+
+/* Don't flush any potential buffer contents. Any input on proxyfail
+should cause a synchronization failure */
+
 proxyfail:
-restore_socket_timeout(fd, get_ok, &tvtmp, vslen);
-/* Don't flush any potential buffer contents. Any input should cause a
-   synchronization failure */
-return FALSE;
+  restore_socket_timeout(fd, get_ok, &tvtmp, vslen);
 
-done:
-restore_socket_timeout(fd, get_ok, &tvtmp, vslen);
-DEBUG(D_receive)
-  debug_printf("Valid %s sender from Proxy Protocol header\n", iptype);
-return proxy_session;
+bad:
+  if (yield)
+    {
+    sender_host_name = NULL;
+    (void) host_name_lookup();
+    host_build_sender_fullhost();
+    }
+  else
+    {
+    proxy_session_failed = TRUE;
+    DEBUG(D_receive)
+      debug_printf("Failure to extract proxied host, only QUIT allowed\n");
+    }
+
+return;
 }
 #endif
 
@@ -1306,14 +1394,11 @@ if required. */
 
 for (p = cmd_list; p < cmd_list_end; p++)
   {
-  #ifdef SUPPORT_PROXY
+#ifdef SUPPORT_PROXY
   /* Only allow QUIT command if Proxy Protocol parsing failed */
-  if (proxy_session && proxy_session_failed)
-    {
-    if (p->cmd != QUIT_CMD)
-      continue;
-    }
-  #endif
+  if (proxy_session && proxy_session_failed && p->cmd != QUIT_CMD)
+    continue;
+#endif
   if (  p->len
      && strncmpic(smtp_cmd_buffer, US p->name, p->len) == 0
      && (  smtp_cmd_buffer[p->len-1] == ':'    /* "mail from:" or "rcpt to:" */
@@ -1674,7 +1759,13 @@ uschar *n;
 uschar *v = smtp_cmd_data + Ustrlen(smtp_cmd_data) - 1;
 while (isspace(*v)) v--;
 v[1] = 0;
-while (v > smtp_cmd_data && *v != '=' && !isspace(*v)) v--;
+while (v > smtp_cmd_data && *v != '=' && !isspace(*v))
+  {
+  /* Take care to not stop at a space embedded in a quoted local-part */
+
+  if (*v == '"') do v--; while (*v != '"' && v > smtp_cmd_data+1);
+  v--;
+  }
 
 n = v;
 if (*v == '=')
@@ -2398,14 +2489,6 @@ if (!sender_host_unknown)
        "bad value for smtp_receive_timeout: '%s'", exp ? exp : US"");
     }
 
-  /* Start up TLS if tls_on_connect is set. This is for supporting the legacy
-  smtps port for use with older style SSL MTAs. */
-
-  #ifdef SUPPORT_TLS
-  if (tls_in.on_connect && tls_server_start(tls_require_ciphers) != OK)
-    return FALSE;
-  #endif
-
   /* Test for explicit connection rejection */
 
   if (verify_check_host(&host_reject_connection) == OK)
@@ -2425,19 +2508,17 @@ if (!sender_host_unknown)
   value of errno is 0 or ENOENT (which happens if /etc/hosts.{allow,deny} does
   not exist). */
 
-  #ifdef USE_TCP_WRAPPERS
+#ifdef USE_TCP_WRAPPERS
   errno = 0;
-  tcp_wrappers_name = expand_string(tcp_wrappers_daemon_name);
-  if (tcp_wrappers_name == NULL)
-    {
+  if (!(tcp_wrappers_name = expand_string(tcp_wrappers_daemon_name)))
     log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Expansion of \"%s\" "
       "(tcp_wrappers_name) failed: %s", string_printing(tcp_wrappers_name),
         expand_string_message);
-    }
+
   if (!hosts_ctl(tcp_wrappers_name,
-         (sender_host_name == NULL)? STRING_UNKNOWN : CS sender_host_name,
-         (sender_host_address == NULL)? STRING_UNKNOWN : CS sender_host_address,
-         (sender_ident == NULL)? STRING_UNKNOWN : CS sender_ident))
+         sender_host_name ? CS sender_host_name : STRING_UNKNOWN,
+         sender_host_address ? CS sender_host_address : STRING_UNKNOWN,
+         sender_ident ? CS sender_ident : STRING_UNKNOWN))
     {
     if (errno == 0 || errno == ENOENT)
       {
@@ -2459,7 +2540,7 @@ if (!sender_host_unknown)
       }
     return FALSE;
     }
-  #endif
+#endif
 
   /* Check for reserved slots. The value of smtp_accept_count has already been
   incremented to include this process. */
@@ -2530,27 +2611,25 @@ if (!sender_host_unknown)
 
 if (smtp_batched_input) return TRUE;
 
-#ifdef SUPPORT_PROXY
 /* If valid Proxy Protocol source is connecting, set up session.
  * Failure will not allow any SMTP function other than QUIT. */
+
+#ifdef SUPPORT_PROXY
 proxy_session = FALSE;
 proxy_session_failed = FALSE;
 if (check_proxy_protocol_host())
-  if (!setup_proxy_protocol_host())
-    {
-    proxy_session_failed = TRUE;
-    DEBUG(D_receive)
-      debug_printf("Failure to extract proxied host, only QUIT allowed\n");
-    }
-  else
-    {
-    sender_host_name = NULL;
-    (void)host_name_lookup();
-    host_build_sender_fullhost();
-    }
+  setup_proxy_protocol_host();
+#endif
+
+  /* Start up TLS if tls_on_connect is set. This is for supporting the legacy
+  smtps port for use with older style SSL MTAs. */
+
+#ifdef SUPPORT_TLS
+  if (tls_in.on_connect && tls_server_start(tls_require_ciphers) != OK)
+    return FALSE;
 #endif
 
-/* Run the ACL if it exists */
+/* Run the connect ACL if it exists */
 
 user_msg = NULL;
 if (acl_smtp_connect)