Author: brane
Date: Sat Dec 13 02:11:51 2025
New Revision: 1930478

Log:
Add a simple HTTP server to aid testing of authentication handlers.
Created speifically for SERF-195, but could be useful in other contexts.

Incidentally fix serf_get so that authentication works when using
multiple connections.

* test/manual/authserver.py: New.
* test/serf_get.c
  (handler_baton_t): New field conn_count.
  (credentials_callback): Allow as many authentication attempts
   as there are concurrent connections.
  (main): Initialize handler_baton_t::conn_count.

Added:
   serf/trunk/test/manual/
   serf/trunk/test/manual/authserver.py   (contents, props changed)
Modified:
   serf/trunk/test/serf_get.c

Added: serf/trunk/test/manual/authserver.py
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ serf/trunk/test/manual/authserver.py        Sat Dec 13 02:11:51 2025        
(r1930478)
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# ====================================================================
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+# ====================================================================
+
+"""
+A simple threaded HTTP server that requires HTTP Basic auth for all
+requests, but doesn't bother to check credentials. Generates random
+text data for GET and fakes random response size for HEAD requests.
+"""
+
+from base64 import standard_b64encode
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from random import randbytes, randint
+from time import sleep
+from typing import Generator
+
+
+class Handler(BaseHTTPRequestHandler):
+    LATENCY = 1.0                   # Average request latency in seconds
+    LENGTH = 68                     # Base-64 response line length
+    MIN_SIZE = 2000                 # Decoded response data min and max sizes
+    MAX_SIZE = 7000
+
+    def handle_one_request(self) -> None:
+        self.close_connection = False
+        self.protocol_version = "HTTP/1.1"
+        self.server_version = "AuthServer/36"
+        return super().handle_one_request()
+
+    def do_HEAD(self) -> None:
+        """like do_GET() but don't generate response data."""
+        if self._check_auth():
+            self._add_latency()
+            length = randint(self.MIN_SIZE, self.MAX_SIZE)
+            self.send_response(200)
+            self._send_headers(length)
+
+    def do_GET(self) -> None:
+        """Return a random-sized text response."""
+        if self._check_auth():
+            self._add_latency()
+            data = self._make_random_data()
+            self.send_response(200)
+            self._send_headers(len(data))
+            self.wfile.write(data)
+
+    def _check_auth(self) -> bool:
+        """Require that authentication data is present."""
+        if self.headers.get("Authorization") is None:
+            message = b"Authentication required\n"
+            self.send_response(401)
+            self.send_header("WWW-Authenticate", "Basic realm=AuthServer")
+            self._send_headers(len(message), error=True)
+            self.wfile.write(message)
+            return False
+        return True
+
+    def _send_headers(self, length: int, error: bool = False) -> None:
+        """Send a standard set of response headers."""
+        if error:
+            self.close_connection = True
+            self.send_header("Connection", "close")
+        self.send_header("Content-Type", "text/plain")
+        self.send_header("Content-Length", str(length))
+        if not error:
+            self.send_header("Last-Modified", self.date_time_string())
+        self.end_headers()
+
+    @classmethod
+    def _add_latency(cls) -> None:
+        """Add random response latency."""
+        sleep(cls.LATENCY * randint(50, 150) / 100)
+
+    @classmethod
+    def _make_random_data(cls) -> bytes:
+        """Generate Base64-encoded random data with constraind line length."""
+        def splitlines(data: bytes) -> Generator[bytes, None, None]:
+            for start in range(0, len(data), cls.LENGTH):
+                if len(data) - cls.LENGTH < start:
+                    end = len(data)
+                else:
+                    end = start + cls.LENGTH
+                yield data[start:end] + b"\n"
+
+        data = randbytes(randint(cls.MIN_SIZE, cls.MAX_SIZE))
+        return b"".join(splitlines(standard_b64encode(data)))
+
+
+def serve() -> None:
+    """Run the server."""
+    addr, port = "127.0.0.1", 8087
+    print(f"Listening on http://{addr}:{port}. Press ^C to stop.")
+    ThreadingHTTPServer((addr, port), Handler).serve_forever()
+
+
+if __name__ == "__main__":
+    serve()

Modified: serf/trunk/test/serf_get.c
==============================================================================
--- serf/trunk/test/serf_get.c  Sat Dec 13 02:06:00 2025        (r1930477)
+++ serf/trunk/test/serf_get.c  Sat Dec 13 02:11:51 2025        (r1930478)
@@ -329,6 +329,7 @@ typedef struct handler_baton_t {
     const char *username;
     const char *password;
     int auth_attempts;
+    int conn_count;
     serf_bucket_t *req_hdrs;
 } handler_baton_t;
 
@@ -493,7 +494,8 @@ credentials_callback(char **username,
 {
     handler_baton_t *ctx = baton;
 
-    if (ctx->auth_attempts > 0)
+    /* Every connection should be allowed to connect once. */
+    if (ctx->auth_attempts > ctx->conn_count)
     {
         return SERF_ERROR_AUTHN_FAILED;
     }
@@ -873,6 +875,7 @@ int main(int argc, const char **argv)
     handler_ctx.username = username;
     handler_ctx.password = password;
     handler_ctx.auth_attempts = 0;
+    handler_ctx.conn_count = conn_count;
 
     handler_ctx.req_body_path = req_body_path;
 

Reply via email to