Skip to content

Commit f8f87ae

Browse files
committed
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. git-svn-id: https://svn.apache.org/repos/asf/serf/trunk@1930478 13f79535-47bb-0310-9956-ffa450edef68
1 parent e97f2ba commit f8f87ae

2 files changed

Lines changed: 117 additions & 1 deletion

File tree

test/manual/authserver.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python3
2+
# ====================================================================
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
# ====================================================================
20+
21+
"""
22+
A simple threaded HTTP server that requires HTTP Basic auth for all
23+
requests, but doesn't bother to check credentials. Generates random
24+
text data for GET and fakes random response size for HEAD requests.
25+
"""
26+
27+
from base64 import standard_b64encode
28+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
29+
from random import randbytes, randint
30+
from time import sleep
31+
from typing import Generator
32+
33+
34+
class Handler(BaseHTTPRequestHandler):
35+
LATENCY = 1.0 # Average request latency in seconds
36+
LENGTH = 68 # Base-64 response line length
37+
MIN_SIZE = 2000 # Decoded response data min and max sizes
38+
MAX_SIZE = 7000
39+
40+
def handle_one_request(self) -> None:
41+
self.close_connection = False
42+
self.protocol_version = "HTTP/1.1"
43+
self.server_version = "AuthServer/36"
44+
return super().handle_one_request()
45+
46+
def do_HEAD(self) -> None:
47+
"""like do_GET() but don't generate response data."""
48+
if self._check_auth():
49+
self._add_latency()
50+
length = randint(self.MIN_SIZE, self.MAX_SIZE)
51+
self.send_response(200)
52+
self._send_headers(length)
53+
54+
def do_GET(self) -> None:
55+
"""Return a random-sized text response."""
56+
if self._check_auth():
57+
self._add_latency()
58+
data = self._make_random_data()
59+
self.send_response(200)
60+
self._send_headers(len(data))
61+
self.wfile.write(data)
62+
63+
def _check_auth(self) -> bool:
64+
"""Require that authentication data is present."""
65+
if self.headers.get("Authorization") is None:
66+
message = b"Authentication required\n"
67+
self.send_response(401)
68+
self.send_header("WWW-Authenticate", "Basic realm=AuthServer")
69+
self._send_headers(len(message), error=True)
70+
self.wfile.write(message)
71+
return False
72+
return True
73+
74+
def _send_headers(self, length: int, error: bool = False) -> None:
75+
"""Send a standard set of response headers."""
76+
if error:
77+
self.close_connection = True
78+
self.send_header("Connection", "close")
79+
self.send_header("Content-Type", "text/plain")
80+
self.send_header("Content-Length", str(length))
81+
if not error:
82+
self.send_header("Last-Modified", self.date_time_string())
83+
self.end_headers()
84+
85+
@classmethod
86+
def _add_latency(cls) -> None:
87+
"""Add random response latency."""
88+
sleep(cls.LATENCY * randint(50, 150) / 100)
89+
90+
@classmethod
91+
def _make_random_data(cls) -> bytes:
92+
"""Generate Base64-encoded random data with constraind line length."""
93+
def splitlines(data: bytes) -> Generator[bytes, None, None]:
94+
for start in range(0, len(data), cls.LENGTH):
95+
if len(data) - cls.LENGTH < start:
96+
end = len(data)
97+
else:
98+
end = start + cls.LENGTH
99+
yield data[start:end] + b"\n"
100+
101+
data = randbytes(randint(cls.MIN_SIZE, cls.MAX_SIZE))
102+
return b"".join(splitlines(standard_b64encode(data)))
103+
104+
105+
def serve() -> None:
106+
"""Run the server."""
107+
addr, port = "127.0.0.1", 8087
108+
print(f"Listening on http://{addr}:{port}. Press ^C to stop.")
109+
ThreadingHTTPServer((addr, port), Handler).serve_forever()
110+
111+
112+
if __name__ == "__main__":
113+
serve()

test/serf_get.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ typedef struct handler_baton_t {
329329
const char *username;
330330
const char *password;
331331
int auth_attempts;
332+
int conn_count;
332333
serf_bucket_t *req_hdrs;
333334
} handler_baton_t;
334335

@@ -493,7 +494,8 @@ credentials_callback(char **username,
493494
{
494495
handler_baton_t *ctx = baton;
495496

496-
if (ctx->auth_attempts > 0)
497+
/* Every connection should be allowed to connect once. */
498+
if (ctx->auth_attempts > ctx->conn_count)
497499
{
498500
return SERF_ERROR_AUTHN_FAILED;
499501
}
@@ -873,6 +875,7 @@ int main(int argc, const char **argv)
873875
handler_ctx.username = username;
874876
handler_ctx.password = password;
875877
handler_ctx.auth_attempts = 0;
878+
handler_ctx.conn_count = conn_count;
876879

877880
handler_ctx.req_body_path = req_body_path;
878881

0 commit comments

Comments
 (0)