tests/net: Implement testserver.py TLS for HttpsTests
This patch is a followup to 0dde5052b
which added testserver.py, a
HTTP echo server for the HttpTests and HttpsTests in the ServicesKit
test suite. This patch implements `testserver.py --use-tls` which
allows for re-enabling HttpsTests.
If `--use-tls` is used, then a self-signed TLS certificate is
generated in a temporary directory which is used by the test
server. This option is used when running HttpsTests.
There doesn't seem to be a good way to have these tests trust the
certificate generated by this test at the moment. Until that API
exists I've just made these tests ignore certificate validation. We'll
want to resolve this and update these tests to actually verify that
validation works as expected.
Some minor tweaks had to be made to testserver.py to take care of
differences in the response body when serving HTTP and HTTPS requests.
Some additional changes:
* Don't depend on any files outside of src/tests/kits/net/service for
these tests. UploadTest was uploading a file from /boot, but I
copied it into the test source directory to avoid having these tests
break if someone makes an unrelated change. It doesn't really matter
what the contents of this file is as long as it doesn't change.
* Use BThreadedTestCase. This speeds up the tests considerably, mostly
because it means that the different test cases can share the same
HttpTest instance, which means there is only a single TestServer
instance, and it takes around half a second to bootstrap the test
server on my system, and even longer if --use-tls is used.
Change-Id: I6d93d390ebd56115365a85109140d175085e1f01
Reviewed-on: https://review.haiku-os.org/c/haiku/+/2260
Reviewed-by: Adrien Destugues <pulkomandy@gmail.com>
This commit is contained in:
parent
9176b546e3
commit
762f26bac8
@ -13,13 +13,15 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <posix/libgen.h>
|
||||
#include <string>
|
||||
|
||||
#include <AutoDeleter.h>
|
||||
#include <HttpRequest.h>
|
||||
#include <NetworkKit.h>
|
||||
#include <UrlProtocolListener.h>
|
||||
|
||||
#include <cppunit/TestCaller.h>
|
||||
#include <tools/cppunit/ThreadedTestCaller.h>
|
||||
|
||||
#include "TestServer.h"
|
||||
|
||||
@ -66,6 +68,28 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
virtual bool CertificateVerificationFailed(
|
||||
BUrlRequest* caller,
|
||||
BCertificate& certificate,
|
||||
const char* message)
|
||||
{
|
||||
// TODO: Add tests that exercize this behavior.
|
||||
//
|
||||
// At the moment there doesn't seem to be any public API for providing
|
||||
// an alternate certificate authority, or for constructing a
|
||||
// BCertificate to be sent to BUrlContext::AddCertificateException().
|
||||
// Once we have such a public API then it will be useful to create
|
||||
// test scenarios that exercize the validation performed by the
|
||||
// undrelying TLS implementaiton to verify that it is configured
|
||||
// to do so.
|
||||
//
|
||||
// For now we just disable TLS certificate validation entirely because
|
||||
// we are generating a self-signed TLS certificate for these tests.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void Verify()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(fExpectedResponseBody, fActualResponseBody);
|
||||
@ -101,7 +125,10 @@ void SendAuthenticatedRequest(
|
||||
{
|
||||
TestListener listener(expectedResponseBody, expectedResponseHeaders);
|
||||
|
||||
BHttpRequest request(testUrl, false, "HTTP", &listener, &context);
|
||||
BHttpRequest request(testUrl, testUrl.Protocol() == "https");
|
||||
request.SetContext(&context);
|
||||
request.SetListener(&listener);
|
||||
|
||||
request.SetUserName("walter");
|
||||
request.SetPassword("secret");
|
||||
|
||||
@ -120,6 +147,18 @@ void SendAuthenticatedRequest(
|
||||
listener.Verify();
|
||||
}
|
||||
|
||||
|
||||
// Return the path of a file path relative to this source file.
|
||||
std::string TestFilePath(const std::string& relativePath)
|
||||
{
|
||||
char *testFileSource = strdup(__FILE__);
|
||||
MemoryDeleter _(testFileSource);
|
||||
|
||||
std::string testSrcDir(::dirname(testFileSource));
|
||||
|
||||
return testSrcDir + "/" + relativePath;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -168,9 +207,13 @@ HttpTest::GetTest()
|
||||
expectedResponseHeaders["Content-Type"] = "text/plain";
|
||||
expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
|
||||
expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
|
||||
|
||||
TestListener listener(expectedResponseBody, expectedResponseHeaders);
|
||||
|
||||
BHttpRequest request(testUrl, false, "HTTP", &listener, context);
|
||||
BHttpRequest request(testUrl, testUrl.Protocol() == "https");
|
||||
request.SetContext(context);
|
||||
request.SetListener(&listener);
|
||||
|
||||
CPPUNIT_ASSERT(request.Run());
|
||||
while (request.IsRunning())
|
||||
snooze(1000);
|
||||
@ -202,7 +245,7 @@ HttpTest::ProxyTest()
|
||||
c->AcquireReference();
|
||||
c->SetProxy("120.203.214.182", 83);
|
||||
|
||||
BHttpRequest t(testUrl);
|
||||
BHttpRequest t(testUrl, testUrl.Protocol() == "https");
|
||||
t.SetContext(c);
|
||||
|
||||
BUrlProtocolListener l;
|
||||
@ -230,12 +273,14 @@ HttpTest::ProxyTest()
|
||||
void
|
||||
HttpTest::UploadTest()
|
||||
{
|
||||
std::string testFilePath = TestFilePath("testfile.txt");
|
||||
|
||||
// The test server will echo the POST body back to us in the HTTP response,
|
||||
// so here we load it into memory so that we can compare to make sure that
|
||||
// the server received it.
|
||||
std::string fileContents;
|
||||
{
|
||||
std::ifstream inputStream("/system/data/licenses/MIT");
|
||||
std::ifstream inputStream(testFilePath);
|
||||
CPPUNIT_ASSERT(inputStream.is_open());
|
||||
fileContents = std::string(
|
||||
std::istreambuf_iterator<char>(inputStream),
|
||||
@ -254,14 +299,14 @@ HttpTest::UploadTest()
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Services Kit (Haiku)\r\n"
|
||||
"Content-Type: multipart/form-data; boundary=<<BOUNDARY-ID>>\r\n"
|
||||
"Content-Length: 1381\r\n"
|
||||
"Content-Length: 1404\r\n"
|
||||
"\r\n"
|
||||
"Request body:\r\n"
|
||||
"-------------\r\n"
|
||||
"--<<BOUNDARY-ID>>\r\n"
|
||||
"Content-Disposition: form-data; name=\"_uploadfile\";"
|
||||
" filename=\"MIT\"\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
" filename=\"testfile.txt\"\r\n"
|
||||
"Content-Type: application/octet-stream\r\n"
|
||||
"\r\n"
|
||||
+ fileContents
|
||||
+ "\r\n"
|
||||
@ -273,7 +318,7 @@ HttpTest::UploadTest()
|
||||
"\r\n");
|
||||
HttpHeaderMap expectedResponseHeaders;
|
||||
expectedResponseHeaders["Content-Encoding"] = "gzip";
|
||||
expectedResponseHeaders["Content-Length"] = "900";
|
||||
expectedResponseHeaders["Content-Length"] = "913";
|
||||
expectedResponseHeaders["Content-Type"] = "text/plain";
|
||||
expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
|
||||
expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
|
||||
@ -282,13 +327,16 @@ HttpTest::UploadTest()
|
||||
BUrl testUrl(fTestServer.BaseUrl(), "/post");
|
||||
|
||||
BUrlContext context;
|
||||
BHttpRequest request(testUrl, false, "HTTP", &listener, &context);
|
||||
|
||||
BHttpRequest request(testUrl, testUrl.Protocol() == "https");
|
||||
request.SetContext(&context);
|
||||
request.SetListener(&listener);
|
||||
|
||||
BHttpForm form;
|
||||
form.AddString("hello", "world");
|
||||
CPPUNIT_ASSERT_EQUAL(
|
||||
B_OK,
|
||||
form.AddFile("_uploadfile", BPath("/system/data/licenses/MIT")));
|
||||
form.AddFile("_uploadfile", BPath(testFilePath.c_str())));
|
||||
|
||||
request.SetPostFields(form);
|
||||
|
||||
@ -303,7 +351,7 @@ HttpTest::UploadTest()
|
||||
dynamic_cast<const BHttpResult &>(request.Result());
|
||||
CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
|
||||
CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
|
||||
CPPUNIT_ASSERT_EQUAL(900, result.Length());
|
||||
CPPUNIT_ASSERT_EQUAL(913, result.Length());
|
||||
|
||||
listener.Verify();
|
||||
}
|
||||
@ -326,12 +374,12 @@ HttpTest::AuthBasicTest()
|
||||
"Accept-Encoding: gzip\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Services Kit (Haiku)\r\n"
|
||||
"Referer: http://127.0.0.1:PORT/auth/basic/walter/secret\r\n"
|
||||
"Referer: SCHEME://127.0.0.1:PORT/auth/basic/walter/secret\r\n"
|
||||
"Authorization: Basic d2FsdGVyOnNlY3JldA==\r\n");
|
||||
|
||||
HttpHeaderMap expectedResponseHeaders;
|
||||
expectedResponseHeaders["Content-Encoding"] = "gzip";
|
||||
expectedResponseHeaders["Content-Length"] = "210";
|
||||
expectedResponseHeaders["Content-Length"] = "212";
|
||||
expectedResponseHeaders["Content-Type"] = "text/plain";
|
||||
expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
|
||||
expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
|
||||
@ -362,7 +410,7 @@ HttpTest::AuthDigestTest()
|
||||
"Accept-Encoding: gzip\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Services Kit (Haiku)\r\n"
|
||||
"Referer: http://127.0.0.1:PORT/auth/digest/walter/secret\r\n"
|
||||
"Referer: SCHEME://127.0.0.1:PORT/auth/digest/walter/secret\r\n"
|
||||
"Authorization: Digest username=\"walter\","
|
||||
" realm=\"user@shredder\","
|
||||
" nonce=\"f3a95f20879dd891a5544bf96a3e5518\","
|
||||
@ -377,7 +425,7 @@ HttpTest::AuthDigestTest()
|
||||
|
||||
HttpHeaderMap expectedResponseHeaders;
|
||||
expectedResponseHeaders["Content-Encoding"] = "gzip";
|
||||
expectedResponseHeaders["Content-Length"] = "401";
|
||||
expectedResponseHeaders["Content-Length"] = "403";
|
||||
expectedResponseHeaders["Content-Type"] = "text/plain";
|
||||
expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
|
||||
expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
|
||||
@ -409,23 +457,16 @@ HttpTest::AuthDigestTest()
|
||||
/* static */ template<class T> void
|
||||
HttpTest::_AddCommonTests(BString prefix, CppUnit::TestSuite& suite)
|
||||
{
|
||||
BString name;
|
||||
T* test = new T();
|
||||
BThreadedTestCaller<T>* testCaller
|
||||
= new BThreadedTestCaller<T>(prefix.String(), test);
|
||||
|
||||
name = prefix;
|
||||
name << "GetTest";
|
||||
suite.addTest(new CppUnit::TestCaller<T>(name.String(), &T::GetTest));
|
||||
testCaller->addThread("GetTest", &T::GetTest);
|
||||
testCaller->addThread("UploadTest", &T::UploadTest);
|
||||
testCaller->addThread("BasicAuthTest", &T::AuthBasicTest);
|
||||
testCaller->addThread("DigestAuthTest", &T::AuthDigestTest);
|
||||
|
||||
name = prefix;
|
||||
name << "UploadTest";
|
||||
suite.addTest(new CppUnit::TestCaller<T>(name.String(), &T::UploadTest));
|
||||
|
||||
name = prefix;
|
||||
name << "AuthBasicTest";
|
||||
suite.addTest(new CppUnit::TestCaller<T>(name.String(), &T::AuthBasicTest));
|
||||
|
||||
name = prefix;
|
||||
name << "AuthDigestTest";
|
||||
suite.addTest(new CppUnit::TestCaller<T>(name.String(), &T::AuthDigestTest));
|
||||
suite.addTest(testCaller);
|
||||
}
|
||||
|
||||
|
||||
@ -446,9 +487,6 @@ HttpTest::AddTests(BTestSuite& parent)
|
||||
parent.addTest("HttpTest", &suite);
|
||||
}
|
||||
|
||||
// The HTTPS tests are disabled for now until --use-tls is implemented
|
||||
// in testserver.py.
|
||||
#if 0
|
||||
{
|
||||
CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpsTest");
|
||||
|
||||
@ -457,7 +495,6 @@ HttpTest::AddTests(BTestSuite& parent)
|
||||
|
||||
parent.addTest("HttpsTest", &suite);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
@ -12,11 +12,12 @@
|
||||
#include <TestSuite.h>
|
||||
|
||||
#include <cppunit/TestSuite.h>
|
||||
#include <tools/cppunit/ThreadedTestCase.h>
|
||||
|
||||
#include "TestServer.h"
|
||||
|
||||
|
||||
class HttpTest: public BTestCase {
|
||||
class HttpTest: public BThreadedTestCase {
|
||||
public:
|
||||
HttpTest(TestServerMode mode
|
||||
= TEST_SERVER_MODE_HTTP);
|
||||
|
21
src/tests/kits/net/service/testfile.txt
Normal file
21
src/tests/kits/net/service/testfile.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -26,7 +26,10 @@ import optparse
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
|
||||
|
||||
@ -148,6 +151,14 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
# server port will be stripped from these headers when
|
||||
# echoed to the response body.
|
||||
header_value = re.sub(r':[0-9]+', ':PORT', header_value)
|
||||
|
||||
# The scheme will also be in this header value, and we want
|
||||
# to return the same reguardless of whether http:// or
|
||||
# https:// was used.
|
||||
header_value = re.sub(
|
||||
r'https?://',
|
||||
'SCHEME://',
|
||||
header_value)
|
||||
if header == 'Content-Type':
|
||||
match = MULTIPART_FORM_BOUNDARY_RE.match(
|
||||
self.headers.get('Content-Type', 'text/plain'))
|
||||
@ -341,6 +352,20 @@ def extract_desired_status_code_from_path(path, default=200):
|
||||
return status_code
|
||||
|
||||
|
||||
def generate_self_signed_tls_cert(common_name, cert_path, key_path):
|
||||
subprocess.check_call([
|
||||
'openssl',
|
||||
'req',
|
||||
'-x509',
|
||||
'-nodes',
|
||||
'-subj', '/CN={}'.format(common_name),
|
||||
'-newkey', 'rsa:4096',
|
||||
'-keyout', key_path,
|
||||
'-out', cert_path,
|
||||
'-days', '1'
|
||||
])
|
||||
|
||||
|
||||
def compute_digest_challenge_response_hash(
|
||||
request_method,
|
||||
request_uri,
|
||||
@ -419,33 +444,54 @@ def parse_kv_pair_header(header_value, sep=','):
|
||||
def main():
|
||||
options = parse_args(sys.argv)
|
||||
|
||||
if options.use_tls:
|
||||
# TODO: Generate a self-signed TLS cert to test HTTPS.
|
||||
raise NotImplementedError()
|
||||
bind_addr = (
|
||||
options.bind_addr,
|
||||
0 if options.port is None else options.port)
|
||||
|
||||
bind_addr = (options.bind_addr, options.port)
|
||||
server = http.server.HTTPServer(
|
||||
bind_addr,
|
||||
RequestHandler,
|
||||
bind_and_activate=False)
|
||||
if options.port is None:
|
||||
server.server_port = server.socket.getsockname()[1]
|
||||
else:
|
||||
server.server_port = options.port
|
||||
|
||||
if options.server_socket_fd:
|
||||
server = http.server.HTTPServer(
|
||||
bind_addr,
|
||||
RequestHandler,
|
||||
bind_and_activate=False)
|
||||
server.socket = socket.fromfd(
|
||||
options.server_socket_fd,
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM)
|
||||
server.server_port = server.socket.getsockname()[1]
|
||||
else:
|
||||
# A socket hasn't been open for us already, so we'll just use
|
||||
# a random port here.
|
||||
server = http.server.HTTPServer(bind_addr, RequestHandler)
|
||||
|
||||
try:
|
||||
def run_server():
|
||||
if not options.server_socket_fd:
|
||||
server.server_bind()
|
||||
server.server_activate()
|
||||
print(
|
||||
'Test server listening on port',
|
||||
server.server_port,
|
||||
file=sys.stderr)
|
||||
server.serve_forever(0.01)
|
||||
|
||||
try:
|
||||
if options.use_tls:
|
||||
with tempfile.TemporaryDirectory() as temp_cert_dir:
|
||||
common_name = options.bind_addr + ':' + str(options.port)
|
||||
cert_file = os.path.join(temp_cert_dir, 'cert.pem')
|
||||
key_file = os.path.join(temp_cert_dir, 'key.pem')
|
||||
generate_self_signed_tls_cert(
|
||||
common_name,
|
||||
cert_file,
|
||||
key_file)
|
||||
server.socket = ssl.wrap_socket(
|
||||
server.socket,
|
||||
certfile=cert_file,
|
||||
keyfile=key_file,
|
||||
server_side=True,
|
||||
do_handshake_on_connect=False)
|
||||
run_server()
|
||||
else:
|
||||
run_server()
|
||||
except KeyboardInterrupt:
|
||||
server.server_close()
|
||||
|
||||
@ -469,7 +515,7 @@ def parse_args(argv):
|
||||
parser.add_option(
|
||||
'--port',
|
||||
dest='port',
|
||||
default=0,
|
||||
default=None,
|
||||
type='int',
|
||||
help='If not specified a random port will be used.')
|
||||
parser.add_option(
|
||||
|
Loading…
Reference in New Issue
Block a user