Compare commits
No commits in common. "49d0ebfed000974ea154f8bbf6f552bb496cc07c" and "93d983424ed7f205df663aed6748291ef6a41e9e" have entirely different histories.
49d0ebfed0
...
93d983424e
|
@ -17,16 +17,3 @@ jobs:
|
||||||
compiler: ldc-latest
|
compiler: ldc-latest
|
||||||
- name: Build and Test
|
- name: Build and Test
|
||||||
run: dub -q test
|
run: dub -q test
|
||||||
|
|
||||||
integration-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup DLang
|
|
||||||
uses: dlang-community/setup-dlang@v2
|
|
||||||
with:
|
|
||||||
compiler: ldc-latest
|
|
||||||
|
|
||||||
- name: http1-test
|
|
||||||
working-directory: integration-tests
|
|
||||||
run: dub run --single http1-test.d
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
http1-test
|
|
|
@ -1,47 +0,0 @@
|
||||||
/+ dub.sdl:
|
|
||||||
dependency "handy-http-transport" path="../"
|
|
||||||
dependency "requests" version="~>2.1"
|
|
||||||
+/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This tests the basic HTTP functionality of the Http1Transport implementation
|
|
||||||
* by starting a server, sending a request, and checking the response.
|
|
||||||
*/
|
|
||||||
module integration_tests.http1_test;
|
|
||||||
|
|
||||||
import handy_http_primitives;
|
|
||||||
import handy_http_transport;
|
|
||||||
import slf4d;
|
|
||||||
import slf4d.default_provider;
|
|
||||||
import requests;
|
|
||||||
|
|
||||||
import core.thread;
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
auto loggingProvider = new shared DefaultProvider(true, Levels.INFO);
|
|
||||||
configureLoggingProvider(loggingProvider);
|
|
||||||
|
|
||||||
HttpTransport transport = new Http1Transport(HttpRequestHandler.of((ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
|
||||||
response.headers.add("Content-Type", "text/plain");
|
|
||||||
response.headers.add("Content-Length", "13");
|
|
||||||
response.outputStream.writeToStream(cast(ubyte[]) "Hello, world!");
|
|
||||||
}));
|
|
||||||
Thread thread = transport.startInNewThread();
|
|
||||||
scope(exit) {
|
|
||||||
transport.stop();
|
|
||||||
thread.join();
|
|
||||||
}
|
|
||||||
info("Started server in another thread.");
|
|
||||||
Thread.sleep(msecs(100)); // Wait for the server to start.
|
|
||||||
|
|
||||||
// Send a simple GET request to the server.
|
|
||||||
auto content = getContent("http://localhost:8080");
|
|
||||||
ubyte[] data = content.data;
|
|
||||||
if (data.length != 13 || (cast(string) data) != "Hello, world!") {
|
|
||||||
error("Received unexpected content: " ~ cast(string) data);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
info("Test completed successfully.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
|
@ -11,18 +11,16 @@ import streams;
|
||||||
* Returns: The string that was read, or a stream error.
|
* Returns: The string that was read, or a stream error.
|
||||||
*/
|
*/
|
||||||
Either!(string, "value", StreamError, "error") consumeUntil(S)(
|
Either!(string, "value", StreamError, "error") consumeUntil(S)(
|
||||||
S inputStream,
|
ref S inputStream,
|
||||||
string target
|
string target
|
||||||
) if (isByteInputStream!S) {
|
) if (isByteInputStream!S) {
|
||||||
import slf4d;
|
|
||||||
// info("consumeUntil called.");
|
|
||||||
ubyte[1024] buffer;
|
ubyte[1024] buffer;
|
||||||
size_t idx;
|
size_t idx;
|
||||||
while (true) {
|
while (true) {
|
||||||
auto result = inputStream.readFromStream(buffer[idx .. idx + 1]);
|
auto result = inputStream.readFromStream(buffer[idx .. idx + 1]);
|
||||||
if (result.hasError) return Either!(string, "value", StreamError, "error")(result.error);
|
if (result.hasError) return Either!(string, "value", StreamError, "error")(result.error);
|
||||||
if (result.count != 1) return Either!(string, "value", StreamError, "error")(
|
if (result.count != 1) return Either!(string, "value", StreamError, "error")(
|
||||||
StreamError("Failed to read a single element", 0)
|
StreamError("Failed to read a single element", 1)
|
||||||
);
|
);
|
||||||
idx++;
|
idx++;
|
||||||
if (idx >= target.length && buffer[idx - target.length .. idx] == target) {
|
if (idx >= target.length && buffer[idx - target.length .. idx] == target) {
|
||||||
|
|
|
@ -23,12 +23,6 @@ class Http1Transport : HttpTransport {
|
||||||
private const ushort port;
|
private const ushort port;
|
||||||
private bool running = false;
|
private bool running = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new Http1Transport server instance.
|
|
||||||
* Params:
|
|
||||||
* requestHandler = The request handler to use for all requests.
|
|
||||||
* port = The port to bind to.
|
|
||||||
*/
|
|
||||||
this(HttpRequestHandler requestHandler, ushort port = 8080) {
|
this(HttpRequestHandler requestHandler, ushort port = 8080) {
|
||||||
assert(requestHandler !is null);
|
assert(requestHandler !is null);
|
||||||
this.serverSocket = new TcpSocket();
|
this.serverSocket = new TcpSocket();
|
||||||
|
@ -36,33 +30,18 @@ class Http1Transport : HttpTransport {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the server. Internally, this starts the Photon event loop and
|
|
||||||
* accepts incoming connections in a separate fiber. Then, clients are
|
|
||||||
* handled in their own separate fiber (think "coroutine").
|
|
||||||
*/
|
|
||||||
void start() {
|
void start() {
|
||||||
debugF!"Starting server on port %d."(port);
|
debugF!"Starting HTTP1Transport server on port %d."(port);
|
||||||
startloop();
|
startloop();
|
||||||
go(() => runServer());
|
go(() => runServer());
|
||||||
runFibers();
|
runFibers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the server. This will mark the server as no longer running, so
|
|
||||||
* no more connections will be accepted.
|
|
||||||
*/
|
|
||||||
void stop() {
|
void stop() {
|
||||||
debugF!"Stopping server on port %d."(port);
|
debugF!"Stopping HTTP1Transport server on port %d."(port);
|
||||||
this.running = false;
|
this.running = false;
|
||||||
// Send a dummy request to cause the server's blocking accept() call to end.
|
this.serverSocket.shutdown(SocketShutdown.BOTH);
|
||||||
try {
|
this.serverSocket.close();
|
||||||
Socket dummySocket = new TcpSocket(this.serverSocket.localAddress());
|
|
||||||
dummySocket.shutdown(SocketShutdown.BOTH);
|
|
||||||
dummySocket.close();
|
|
||||||
} catch (SocketOSException e) {
|
|
||||||
warn("Failed to send empty request to stop server.", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runServer() {
|
private void runServer() {
|
||||||
|
@ -79,8 +58,6 @@ class Http1Transport : HttpTransport {
|
||||||
warn("Failed to accept socket connection.", e);
|
warn("Failed to accept socket connection.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.serverSocket.shutdown(SocketShutdown.BOTH);
|
|
||||||
this.serverSocket.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,14 +76,10 @@ void handleClient(Socket clientSocket, HttpRequestHandler requestHandler) {
|
||||||
// Get remote address from the socket.
|
// Get remote address from the socket.
|
||||||
import handy_http_primitives.address;
|
import handy_http_primitives.address;
|
||||||
ClientAddress addr = getAddress(clientSocket);
|
ClientAddress addr = getAddress(clientSocket);
|
||||||
debugF!"Handling client request from %s."(addr.toString());
|
traceF!"Handling client request from %s."(addr.toString());
|
||||||
auto result = readHttpRequest(&bufferedInput, addr);
|
auto result = readHttpRequest(&bufferedInput, addr);
|
||||||
debug_("Finished reading HTTP request from client.");
|
|
||||||
if (result.hasError) {
|
if (result.hasError) {
|
||||||
if (result.error.code != -1) {
|
warnF!"Failed to read HTTP request: %s"(result.error.message);
|
||||||
// Only warn if we didn't read an empty request.
|
|
||||||
warnF!"Failed to read HTTP request: %s"(result.error.message);
|
|
||||||
}
|
|
||||||
inputStream.closeStream();
|
inputStream.closeStream();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -209,17 +182,9 @@ alias HttpRequestParseResult = Either!(ServerHttpRequest, "request", StreamError
|
||||||
*/
|
*/
|
||||||
HttpRequestParseResult readHttpRequest(S)(S inputStream, in ClientAddress addr) if (isByteInputStream!S) {
|
HttpRequestParseResult readHttpRequest(S)(S inputStream, in ClientAddress addr) if (isByteInputStream!S) {
|
||||||
auto methodStr = consumeUntil(inputStream, " ");
|
auto methodStr = consumeUntil(inputStream, " ");
|
||||||
if (methodStr.hasError) {
|
if (methodStr.hasError) return HttpRequestParseResult(methodStr.error);
|
||||||
if (methodStr.error.code == 0) {
|
|
||||||
// Set a custom code to indicate an empty request.
|
|
||||||
return HttpRequestParseResult(StreamError(methodStr.error.message, -1));
|
|
||||||
}
|
|
||||||
return HttpRequestParseResult(methodStr.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto urlStr = consumeUntil(inputStream, " ");
|
auto urlStr = consumeUntil(inputStream, " ");
|
||||||
if (urlStr.hasError) return HttpRequestParseResult(urlStr.error);
|
if (urlStr.hasError) return HttpRequestParseResult(urlStr.error);
|
||||||
|
|
||||||
auto versionStr = consumeUntil(inputStream, "\r\n");
|
auto versionStr = consumeUntil(inputStream, "\r\n");
|
||||||
if (versionStr.hasError) return HttpRequestParseResult(versionStr.error);
|
if (versionStr.hasError) return HttpRequestParseResult(versionStr.error);
|
||||||
|
|
||||||
|
|
|
@ -4,21 +4,3 @@ interface HttpTransport {
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
void stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
import core.thread;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a new thread to run an HTTP transport implementation in, separate
|
|
||||||
* from the calling thread. This is useful for running a server in the
|
|
||||||
* background, like for integration tests.
|
|
||||||
* Params:
|
|
||||||
* transport = The transport implementation to start.
|
|
||||||
* Returns: The thread that was started.
|
|
||||||
*/
|
|
||||||
Thread startInNewThread(HttpTransport transport) {
|
|
||||||
Thread t = new Thread(() {
|
|
||||||
transport.start();
|
|
||||||
});
|
|
||||||
t.start();
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue