From 873f9fccca3645ffa41ad1f26355860fd925eb18 Mon Sep 17 00:00:00 2001
From: Stefan Eissing <stefan@eissing.org>
Date: Tue, 28 Feb 2023 10:07:21 +0100
Subject: [PATCH] Fixing unix domain socket use in https connects.

- refs #10633, when h2/h3 eyeballing was involved, unix domain socket
  configurations were not honoured
- configuring --unix-socket will disable HTTP/3 as candidate for eyeballing
- combinatino of --unix-socket and --http3-only will fail during initialisation
- adding pytest test_11 to reproduce
---
 lib/cf-http.c                     |   6 +-
 lib/http.c                        |   6 +-
 lib/vquic/vquic.c                 |   4 +
 tests/tests-httpd/test_11_unix.py | 129 ++++++++++++++++++++++++++++++
 4 files changed, 138 insertions(+), 7 deletions(-)
 create mode 100644 tests/tests-httpd/test_11_unix.py

Index: curl/lib/cf-http.c
===================================================================
--- curl.orig/lib/cf-http.c
+++ curl/lib/cf-http.c
@@ -266,7 +266,8 @@ static CURLcode cf_hc_connect(struct Cur
         Curl_expire(data, ctx->soft_eyeballs_timeout_ms, EXPIRE_ALPN_EYEBALLS);
     }
     else if(ctx->h21_baller.enabled)
-      cf_hc_baller_init(&ctx->h21_baller, cf, data, "h21", TRNSPRT_TCP);
+      cf_hc_baller_init(&ctx->h21_baller, cf, data, "h21",
+                       cf->conn->transport);
     ctx->state = CF_HC_CONNECT;
     /* FALLTHROUGH */
 
@@ -280,7 +281,8 @@ static CURLcode cf_hc_connect(struct Cur
     }
 
     if(time_to_start_h21(cf, data, now)) {
-      cf_hc_baller_init(&ctx->h21_baller, cf, data, "h21", TRNSPRT_TCP);
+      cf_hc_baller_init(&ctx->h21_baller, cf, data, "h21",
+                        cf->conn->transport);
     }
 
     if(cf_hc_baller_is_active(&ctx->h21_baller)) {
Index: curl/lib/http.c
===================================================================
--- curl.orig/lib/http.c
+++ curl/lib/http.c
@@ -234,14 +234,10 @@ static CURLcode http_setup_conn(struct C
   Curl_mime_initpart(&http->form);
   data->req.p.http = http;
 
-  if((data->state.httpwant == CURL_HTTP_VERSION_3)
-     || (data->state.httpwant == CURL_HTTP_VERSION_3ONLY)) {
+  if(data->state.httpwant == CURL_HTTP_VERSION_3ONLY) {
     CURLcode result = Curl_conn_may_http3(data, conn);
     if(result)
       return result;
-
-     /* TODO: HTTP lower version eyeballing */
-    conn->transport = TRNSPRT_QUIC;
   }
 
   return CURLE_OK;
Index: curl/lib/vquic/vquic.c
===================================================================
--- curl.orig/lib/vquic/vquic.c
+++ curl/lib/vquic/vquic.c
@@ -363,6 +363,10 @@ bool Curl_conn_is_http3(const struct Cur
 CURLcode Curl_conn_may_http3(struct Curl_easy *data,
                              const struct connectdata *conn)
 {
+  if(conn->transport == TRNSPRT_UNIX) {
+    /* cannot do QUIC over a unix domain socket */
+    return CURLE_QUIC_CONNECT_ERROR;
+  }
   if(!(conn->handler->flags & PROTOPT_SSL)) {
     failf(data, "HTTP/3 requested for non-HTTPS URL");
     return CURLE_URL_MALFORMAT;
Index: curl/tests/tests-httpd/test_11_unix.py
===================================================================
--- /dev/null
+++ curl/tests/tests-httpd/test_11_unix.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#***************************************************************************
+#                                  _   _ ____  _
+#  Project                     ___| | | |  _ \| |
+#                             / __| | | | |_) | |
+#                            | (__| |_| |  _ <| |___
+#                             \___|\___/|_| \_\_____|
+#
+# Copyright (C) 2008 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+import logging
+import os
+import socket
+from threading import Thread
+import pytest
+
+from testenv import Env, CurlClient
+
+
+log = logging.getLogger(__name__)
+
+class UDSFaker:
+
+    def __init__(self, path):
+        self._uds_path = path
+        self._done = False
+
+    @property
+    def path(self):
+        return self._uds_path
+
+    def start(self):
+        def process(self):
+            self._socket.listen(1)
+            self._process()
+
+        try:
+            os.unlink(self._uds_path)
+        except OSError:
+            if os.path.exists(self._uds_path):
+                raise
+        self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self._socket.bind(self._uds_path)
+        self._thread = Thread(target=process, daemon=True, args=[self])
+        self._thread.start()
+
+    def stop(self):
+        self._done = True
+        self._socket.close()
+
+    def _process(self):
+        while self._done is False:
+            try:
+                c, client_address = self._socket.accept()
+                try:
+                    data = c.recv(16)
+                    c.sendall("""HTTP/1.1 200 Ok
+Server: UdsFaker
+Content-Type: application/json
+Content-Length: 19
+
+{ "host": "faked" }""".encode())
+                finally:
+                    c.close()
+
+            except ConnectionAbortedError:
+                self._done = True
+
+
+
+@pytest.mark.skipif(condition=Env.setup_incomplete(),
+                    reason=f"missing: {Env.incomplete_reason()}")
+class TestUnix:
+
+    @pytest.fixture(scope="class")
+    def uds_faker(self, env: Env) -> UDSFaker:
+        uds_path = os.path.join(env.gen_dir, 'uds_11.sock')
+        faker = UDSFaker(path=uds_path)
+        faker.start()
+        yield faker
+        faker.stop()
+
+    # download http: via unix socket
+    def test_11_01_unix_connect_http(self, env: Env, httpd, uds_faker, repeat):
+        curl = CurlClient(env=env)
+        url = f'http://{env.domain1}:{env.http_port}/data.json'
+        r = curl.http_download(urls=[url], with_stats=True,
+                               extra_args=[
+                                 '--unix-socket', uds_faker.path,
+                               ])
+        assert r.exit_code == 0
+        r.check_stats(count=1, exp_status=200)
+
+    # download https: via unix socket
+    def test_11_02_unix_connect_http(self, env: Env, httpd, uds_faker, repeat):
+        curl = CurlClient(env=env)
+        url = f'https://{env.domain1}:{env.https_port}/data.json'
+        r = curl.http_download(urls=[url], with_stats=True,
+                               extra_args=[
+                                 '--unix-socket', uds_faker.path,
+                               ])
+        assert r.exit_code == 35  # CONNECT_ERROR (as faker is not TLS)
+
+    # download HTTP/3 via unix socket
+    def test_11_03_unix_connect_quic(self, env: Env, httpd, uds_faker, repeat):
+        curl = CurlClient(env=env)
+        url = f'https://{env.domain1}:{env.https_port}/data.json'
+        r = curl.http_download(urls=[url], with_stats=True,
+                               alpn_proto='h3',
+                               extra_args=[
+                                 '--unix-socket', uds_faker.path,
+                               ])
+        assert r.exit_code == 96  # QUIC CONNECT ERROR
