Skip to content

Commit

Permalink
Http: Add HttpWebServer to serve files from a directory
Browse files Browse the repository at this point in the history
  • Loading branch information
Pagghiu committed Aug 11, 2024
1 parent 4d3a5b4 commit 9f4700f
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 6 deletions.
1 change: 1 addition & 0 deletions Bindings/cpp/SC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "../../Libraries/Http/HttpParser.cpp"
#include "../../Libraries/Http/HttpServer.cpp"
#include "../../Libraries/Http/HttpURLParser.cpp"
#include "../../Libraries/Http/HttpWebServer.cpp"
#include "../../Libraries/Plugin/Plugin.cpp"
#include "../../Libraries/Process/Process.cpp"
#include "../../Libraries/SerializationText/SerializationJson.cpp"
Expand Down
15 changes: 9 additions & 6 deletions Documentation/Libraries/Http.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,33 @@ Http library contains a hand-written http 1.1 parser, client and server.

# Status
🟥 Draft
In current state the library is not even able to host a simple static website.
In current state the library is able to host simple static website but it cannot be used for any internet facing application.

# Description
The HTTP parser is an incremental parser, that will emit events as soon as a valid element has been successfully parsed.
This allows handling incomplete responses without needing holding it entirely in memory.

The HTTP client and server are for now just some toy implementations missing almost everything needed for real usage.
They only contain what's used in the test so far, so really can't be defined as more than a Draft.
The HTTP client and server are for now just some basic implementations and are missing some important feature.

## HttpServer
@copydoc SC::HttpServer

## HttpWebServer
@copydoc SC::HttpWebServer

## HttpClient
@copydoc SC::HttpClient

# Examples

No examples are provided so far as the API is very likely to change drastically going towards MVP.
If you like to see where we are just a take a look at the associated unit test.
- [SCExample](@ref page_examples) features the `WebServerExample` sample showing how to use SC::HttpWebServer and SC::HttpServer
- Unit tests show how to use SC::HttpWebServer, SC::HttpServer and SC::HttpClient

# Roadmap

🟨 MVP
- Server+Client: Support mostly used HTTP verbs / methods
- Server: Ability to host a local website (handle content encoding)
- Server+Client: HTTP 1.1 Chunked Encoding

🟩 Usable Features:
- Implement ([Web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API))
Expand Down
104 changes: 104 additions & 0 deletions Libraries/Http/HttpWebServer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
#include "HttpWebServer.h"
#include "../FileSystem/FileSystem.h"
#include "../FileSystem/Path.h"
#include "../Strings/StringBuilder.h"

struct SC::HttpWebServer::Internal
{
static Result writeGMTHeaderTime(StringView headerName, HttpResponse& response,
const Time::Absolute::ParseResult& local);
static StringView getContentType(const StringView extension);

static Result readFile(StringView initialDirectory, HttpRequest& request, HttpResponse& response);
};

SC::Result SC::HttpWebServer::init(StringView directoryToServe)
{
SC_TRY_MSG(FileSystem().existsAndIsDirectory(directoryToServe), "Invalid directory");
SC_TRY(directory.assign(directoryToServe));
return Result(true);
}

SC::Result SC::HttpWebServer::stopAsync() { return Result(true); }

void SC::HttpWebServer::serveFile(HttpRequest& request, HttpResponse& response)
{
if (not Internal::readFile(directory.view(), request, response))
{
SC_TRUST_RESULT(response.startResponse(404));
SC_TRUST_RESULT(response.end("Error"));
}
}

SC::Result SC::HttpWebServer::Internal::readFile(StringView directory, HttpRequest& request, HttpResponse& response)
{
if (not request.getURL().startsWith("/"))
{
return Result::Error("Wrong url");
}
StringView url = request.getURL().sliceStart(1);
if (url.isEmpty())
{
url = "index.html";
}
FileSystem fileSystem;
SC_TRY(fileSystem.init(directory));
if (fileSystem.existsAndIsFile(url))
{
FileSystem::FileStat fileStat;
SC_TRY(fileSystem.getFileStat(url, fileStat));
StringView name, extension;
SC_TRY(Path::parseNameExtension(url, name, extension));
Vector<char> data;
SC_TRY(fileSystem.read(url, data));
SC_TRY(response.startResponse(200));
SC_TRY(response.addHeader("Connection", "Closed"));
SC_TRY(response.addHeader("Content-Type", Internal::getContentType(extension)));
SC_TRY(response.addHeader("Server", "SC"));
Time::Absolute::ParseResult local;
SC_TRY(Time::Absolute::now().parseUTC(local));
SC_TRY(Internal::writeGMTHeaderTime("Date", response, local));
SC_TRY(fileStat.modifiedTime.parseUTC(local));
SC_TRY(Internal::writeGMTHeaderTime("Last-Modified", response, local));
SC_TRY(response.end(data.toSpanConst()));
}
return Result(true);
}

SC::Result SC::HttpWebServer::Internal::writeGMTHeaderTime(StringView headerName, HttpResponse& response,
const Time::Absolute::ParseResult& local)
{
SmallString<512> buffer;
SC_TRY(StringBuilder(buffer).format("{}, {:02} {} {} {:02}:{:02}:{:02} GMT", local.getDay(), local.dayOfMonth,
local.getMonth(), local.year, local.hour, local.minutes, local.seconds));

SC_TRY(response.addHeader(headerName, buffer.view()));
return Result(true);
}

SC::StringView SC::HttpWebServer::Internal::getContentType(const StringView extension)
{
if (extension == "htm" or extension == "html")
{
return "text/html";
}
if (extension == "css")
{
return "text/css";
}
if (extension == "png")
{
return "image/png";
}
if (extension == "jpeg" or extension == "jpg")
{
return "image/jpg";
}
if (extension == "svg")
{
return "image/svg+xml";
}
return "text/html";
}
36 changes: 36 additions & 0 deletions Libraries/Http/HttpWebServer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
#pragma once
#include "../Strings/String.h"
#include "HttpServer.h"

namespace SC
{
struct SC_COMPILER_EXPORT HttpWebServer;
}

/// @brief Http web server helps statically serves files from a directory.
/// @n
/// It can be used in conjunction with SC::HttpServer, by calling SC::HttpWebServer::serveFile
/// inside the SC::HttpServer::onRequest callback to statically serve files.
///
/// @see SC::HttpServer
///
/// \snippet Libraries/Http/Tests/HttpWebServerTest.cpp HttpWebServerSnippet
struct SC::HttpWebServer
{
/// @brief Initialize the web server on the given file system directory to serve
Result init(StringView directoryToServe);

/// @brief Release all resources allocated by this web server
Result stopAsync();

/// @brief Serve the efile requested by this Http Client on its channel
/// Call this method in response to HttpServer::onRequest to serve a file
void serveFile(HttpRequest& request, HttpResponse& response);

private:
String directory;

struct Internal;
};
69 changes: 69 additions & 0 deletions Libraries/Http/Tests/HttpWebServerTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
#include "../HttpWebServer.h"
#include "../../FileSystem/FileSystem.h"
#include "../../Testing/Testing.h"
#include "../HttpClient.h"
namespace SC
{
struct HttpWebServerTest;
} // namespace SC

struct SC::HttpWebServerTest : public SC::TestCase
{
HttpWebServerTest(SC::TestReport& report) : TestCase(report, "HttpServerTest")
{
if (test_section("HttpWebServer"))
{
httpWebServerTest();
}
}
void httpWebServerTest();
};

void SC::HttpWebServerTest::httpWebServerTest()
{
// Create a test file in the application root directory
FileSystem fs;
SC_TEST_EXPECT(fs.init(report.applicationRootDirectory));
SC_TEST_EXPECT(fs.write("file.html", "<html><body>Response from file</body></html>"));
AsyncEventLoop eventLoop;
SC_TEST_EXPECT(eventLoop.create());

//! [HttpWebServerSnippet]
// Creates an HttpServer that serves files from application root directory
HttpServer server;
HttpWebServer webServer;
SC_TEST_EXPECT(server.start(eventLoop, 16, "127.0.0.1", 8090));
SC_TEST_EXPECT(webServer.init(report.applicationRootDirectory));

server.onRequest = [&](HttpRequest& req, HttpResponse& res) { webServer.serveFile(req, res); };
//! [HttpWebServerSnippet]

struct Context
{
int numRequests = 0;
HttpWebServer& httpWebServer;
HttpServer& httpServer;
} context = {0, webServer, server};

// Create an Http Client request for that file
HttpClient client;
SC_TEST_EXPECT(client.get(eventLoop, "http://localhost:8090/file.html"));
client.callback = [this, &context](HttpClient& result)
{
context.numRequests++;
SC_TEST_EXPECT(result.getResponse().containsString("Response from file"));
SC_TEST_EXPECT(context.httpServer.stopAsync());
SC_TEST_EXPECT(context.httpWebServer.stopAsync());
};
SC_TEST_EXPECT(eventLoop.run());

// Remove the test file
SC_TEST_EXPECT(fs.removeFile("file.html"));
}

namespace SC
{
void runHttpWebServerTest(SC::TestReport& report) { HttpWebServerTest test(report); }
} // namespace SC
2 changes: 2 additions & 0 deletions Tests/SCTest/SCTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ void runHashingTest(TestReport& report);
void runHttpClientTest(TestReport& report);
void runHttpParserTest(TestReport& report);
void runHttpServerTest(TestReport& report);
void runHttpWebServerTest(TestReport& report);
void runHttpURLParserTest(TestReport& report);

// Plugin
Expand Down Expand Up @@ -160,6 +161,7 @@ int main(int argc, const char* argv[])
runHttpParserTest(report);
runHttpClientTest(report);
runHttpServerTest(report);
runHttpWebServerTest(report);
runHttpURLParserTest(report);

// Plugin tests
Expand Down

0 comments on commit 9f4700f

Please sign in to comment.