Ready. Set. Go!
This commit is contained in:
commit
e52806bc7a
12
CMakeLists.txt
Normal file
12
CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.8..3.22)
|
||||
project(webcxx LANGUAGES CXX)
|
||||
|
||||
add_library(${PROJECT_NAME} STATIC ${CMAKE_CURRENT_LIST_DIR}/src/App.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Socket.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HTMLTemplate.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MIMETypes.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Response.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Request.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HTTPStatus.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Map.cpp)
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/include/webcxx ${CMAKE_CURRENT_LIST_DIR}/src)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17)
|
||||
|
||||
set(webcxx_include ${CMAKE_CURRENT_LIST_DIR}/include)
|
||||
set(webcxx_libs ${PROJECT_NAME})
|
70
include/webcxx/App.h
Normal file
70
include/webcxx/App.h
Normal file
@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <netinet/in.h>
|
||||
#include <string>
|
||||
|
||||
namespace webcxx {
|
||||
class Response;
|
||||
class Request;
|
||||
class ErrorWatcher;
|
||||
class App {
|
||||
public:
|
||||
static std::shared_ptr<App> create();
|
||||
int run(int port);
|
||||
void terminate();
|
||||
|
||||
static std::shared_ptr<App> current_app();
|
||||
|
||||
void on(int methods, std::string path,
|
||||
std::function<Response(Request &)> action);
|
||||
|
||||
void on_error(int error, std::function<Response(void)> action);
|
||||
|
||||
void set_static_resource_path(const std::string& path);
|
||||
|
||||
void set_favicon(const std::string& favicon_path);
|
||||
|
||||
private:
|
||||
App() = default;
|
||||
int socket_init(int);
|
||||
int main_loop();
|
||||
bool m_is_running;
|
||||
int m_socket_fd;
|
||||
static std::shared_ptr<App> s_instance;
|
||||
|
||||
std::string static_resource_path = "/static";
|
||||
|
||||
int m_current_connection_fd;
|
||||
|
||||
sockaddr_in m_addr;
|
||||
|
||||
int handle_request(Request& req);
|
||||
|
||||
void get_client_addr();
|
||||
std::string m_client_ip;
|
||||
|
||||
struct Action {
|
||||
std::function<Response(Request &)> get_action;
|
||||
std::function<Response(Request &)> post_action;
|
||||
std::function<Response(Request &)> put_action;
|
||||
std::function<Response(Request &)> delete_action;
|
||||
};
|
||||
|
||||
std::map<std::string, Action> m_action_map;
|
||||
|
||||
std::map<int, std::function<Response(void)>> m_error_action_map;
|
||||
|
||||
static void app_terminate(int); // Terminate the current app.
|
||||
|
||||
friend class ErrorWatcher;
|
||||
};
|
||||
|
||||
Response text(const std::string &content);
|
||||
Response json(const std::string &content);
|
||||
Response html(const std::string &content, const std::string& title = "");
|
||||
Response redirect(const std::string &url, int status_code = 302);
|
||||
Response error(int status_code = 500);
|
||||
Response file(const std::string& item_path);
|
||||
} // namespace webcxx
|
23
include/webcxx/Map.h
Normal file
23
include/webcxx/Map.h
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace webcxx
|
||||
{
|
||||
/* Wrapper around std::map that provides a read-only interface with a contains() function */
|
||||
class Map {
|
||||
public:
|
||||
typedef std::map<std::string, std::string> MapType;
|
||||
Map(const MapType& map);
|
||||
|
||||
std::string at(const std::string& key);
|
||||
bool contains(const std::string& key);
|
||||
std::string operator[](const std::string& key);
|
||||
MapType::const_iterator begin() const noexcept;
|
||||
MapType::const_iterator cbegin() const noexcept;
|
||||
MapType::const_iterator end() const noexcept;
|
||||
MapType::const_iterator cend() const noexcept;
|
||||
private:
|
||||
const MapType m_map;
|
||||
};
|
||||
}
|
46
include/webcxx/Request.h
Normal file
46
include/webcxx/Request.h
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace webcxx {
|
||||
enum RequestType { GET = 1 << 0, POST = 1 << 1, PUT = 1 << 2, DELETE = 1 << 3 };
|
||||
|
||||
class App;
|
||||
class Map;
|
||||
|
||||
class Request {
|
||||
public:
|
||||
RequestType method() { return m_method; }
|
||||
std::string ip() { return m_ip; }
|
||||
|
||||
Map headers() const;
|
||||
|
||||
Map args() const;
|
||||
|
||||
std::string path() const { return m_path; }
|
||||
std::string content() const { return m_content; }
|
||||
|
||||
private:
|
||||
explicit Request(int fd);
|
||||
|
||||
bool is_bad_request() { return m_bad_request; };
|
||||
|
||||
RequestType m_method;
|
||||
std::string m_path;
|
||||
std::string m_real_path;
|
||||
std::string m_content;
|
||||
std::map<std::string, std::string> m_headers;
|
||||
bool m_bad_request = false;
|
||||
std::string m_ip;
|
||||
|
||||
std::map<std::string, std::string> m_args;
|
||||
|
||||
std::pair<std::string, std::string> split_header(std::string input,
|
||||
std::string delim);
|
||||
void decode_http_header(std::string header);
|
||||
|
||||
void log(int status_code);
|
||||
|
||||
friend class App;
|
||||
};
|
||||
} // namespace webcxx
|
24
include/webcxx/Response.h
Normal file
24
include/webcxx/Response.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace webcxx {
|
||||
class App;
|
||||
class Response {
|
||||
public:
|
||||
explicit Response(int status_code, std::string content = "",
|
||||
std::string content_type = "text/html",
|
||||
std::map<std::string, std::string> extra_headers = {});
|
||||
std::string to_string();
|
||||
|
||||
void set_status(int status_code);
|
||||
|
||||
private:
|
||||
int status;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string content;
|
||||
int send(int fd);
|
||||
|
||||
friend class App;
|
||||
};
|
||||
} // namespace webcxx
|
419
src/App.cpp
Normal file
419
src/App.cpp
Normal file
@ -0,0 +1,419 @@
|
||||
#include "App.h"
|
||||
#include "HTTPStatus.h"
|
||||
#include "Request.h"
|
||||
#include "Response.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <poll.h>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/signal.h>
|
||||
#include <sys/socket.h>
|
||||
#include "HTMLTemplate.h"
|
||||
#include <fstream>
|
||||
#include <limits.h>
|
||||
#include <sys/stat.h>
|
||||
#include <filesystem>
|
||||
#include "MIMETypes.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
webcxx::Response webcxx::text(const std::string &content) {
|
||||
return Response{200, content, {"text/plain"}};
|
||||
}
|
||||
|
||||
webcxx::Response webcxx::json(const std::string &content) {
|
||||
return Response{200, content, {"application/json"}};
|
||||
}
|
||||
|
||||
webcxx::Response webcxx::html(const std::string& content, const std::string& title) {
|
||||
return Response{200, _webcxx_internal::build_html_from_template(content, title)};
|
||||
}
|
||||
|
||||
webcxx::Response webcxx::redirect(const std::string &url, int status_code) {
|
||||
return Response{
|
||||
status_code,
|
||||
_webcxx_internal::build_html_from_template("<p>If you aren't redirected automatically, please click <a href=\"" +
|
||||
url + "\">here</a>.</p>","Redirecting..."),
|
||||
"text/html",
|
||||
{{"Location", url}}};
|
||||
}
|
||||
|
||||
static webcxx::Response _webcxx_default_error(int status_code)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
ss << "<h1>";
|
||||
ss << _webcxx_internal::http_status_codes[status_code];
|
||||
ss << "</h1>";
|
||||
ss << "<p>";
|
||||
ss << "<i>webcxx/0.1</i>";
|
||||
ss << "</p>";
|
||||
std::ostringstream title;
|
||||
title << status_code;
|
||||
title << " ";
|
||||
title << _webcxx_internal::http_status_codes[status_code];
|
||||
return webcxx::Response{status_code, _webcxx_internal::build_html_from_template(ss.str(),title.str())};
|
||||
}
|
||||
|
||||
class webcxx::ErrorWatcher
|
||||
{
|
||||
public:
|
||||
std::map<int, std::function<Response(void)>>& get_app_error_map(std::shared_ptr<App> app)
|
||||
{
|
||||
return app->m_error_action_map;
|
||||
}
|
||||
};
|
||||
|
||||
webcxx::Response webcxx::error(int status_code) {
|
||||
static bool is_recursing = false;
|
||||
if(is_recursing) return _webcxx_default_error(status_code);
|
||||
is_recursing = true;
|
||||
ErrorWatcher e;
|
||||
auto& error_map = e.get_app_error_map(webcxx::App::current_app());
|
||||
if(error_map.find(status_code) != error_map.end())
|
||||
{
|
||||
Response resp = error_map.at(status_code)();
|
||||
resp.set_status(status_code);
|
||||
is_recursing = false;
|
||||
return resp;
|
||||
} else {
|
||||
is_recursing = false;
|
||||
return _webcxx_default_error(status_code);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<webcxx::App> webcxx::App::s_instance;
|
||||
|
||||
std::shared_ptr<webcxx::App> webcxx::App::create() {
|
||||
s_instance = std::shared_ptr<App>(new App);
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
std::shared_ptr<webcxx::App> webcxx::App::current_app() {
|
||||
if (!s_instance) {
|
||||
throw std::runtime_error(
|
||||
"Trying to access the current app before creating an app");
|
||||
}
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
int webcxx::App::run(int port) {
|
||||
if (signal(SIGINT, app_terminate) == SIG_ERR) {
|
||||
perror("signal(SIGINT)");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int rc = socket_init(port);
|
||||
|
||||
if (rc != 0)
|
||||
return rc;
|
||||
|
||||
printf("webcxx - running on port %d\n", port);
|
||||
fflush(stdout);
|
||||
|
||||
return main_loop();
|
||||
}
|
||||
|
||||
void webcxx::App::app_terminate(int __signum) {
|
||||
if (!s_instance)
|
||||
exit(1);
|
||||
s_instance->terminate();
|
||||
}
|
||||
|
||||
int webcxx::App::socket_init(int port) {
|
||||
m_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (m_socket_fd < 0) {
|
||||
perror("socket");
|
||||
return 1;
|
||||
}
|
||||
|
||||
m_addr.sin_port = htons(port);
|
||||
m_addr.sin_family = AF_INET;
|
||||
m_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
if (bind(m_socket_fd, (struct sockaddr *)&m_addr, sizeof(sockaddr)) < 0) {
|
||||
perror("bind");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (listen(m_socket_fd, 20) < 0) {
|
||||
perror("listen");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void webcxx::App::terminate() { m_is_running = false; }
|
||||
|
||||
void webcxx::App::get_client_addr() {
|
||||
char result[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &m_addr.sin_addr.s_addr, result, sizeof(result));
|
||||
m_client_ip = result;
|
||||
}
|
||||
|
||||
void webcxx::App::on(int methods, std::string path,
|
||||
std::function<Response(Request &)> action) {
|
||||
Action act;
|
||||
if (m_action_map.find(path) != m_action_map.end()) {
|
||||
act = m_action_map.at(path);
|
||||
}
|
||||
if (methods & (int)RequestType::GET) {
|
||||
act.get_action = action;
|
||||
}
|
||||
if (methods & (int)RequestType::POST) {
|
||||
act.post_action = action;
|
||||
}
|
||||
if (methods & (int)RequestType::PUT) {
|
||||
act.put_action = action;
|
||||
}
|
||||
if (methods & (int)RequestType::DELETE) {
|
||||
act.delete_action = action;
|
||||
}
|
||||
m_action_map[path] = act;
|
||||
}
|
||||
|
||||
void webcxx::App::on_error(int error,
|
||||
std::function<Response(void)> action) {
|
||||
if(error < 400)
|
||||
{
|
||||
throw std::runtime_error("on_error can only register handlers for status codes 400 and over");
|
||||
}
|
||||
m_error_action_map[error] = action;
|
||||
}
|
||||
|
||||
void webcxx::App::set_favicon(const std::string& favicon_path)
|
||||
{
|
||||
std::string path = favicon_path;
|
||||
on(GET, "/favicon.ico", [=](Request& req){
|
||||
return file(path);
|
||||
});
|
||||
}
|
||||
|
||||
void webcxx::App::set_static_resource_path(const std::string& path)
|
||||
{
|
||||
static_resource_path = path;
|
||||
}
|
||||
|
||||
int webcxx::App::main_loop() {
|
||||
m_is_running = true;
|
||||
struct pollfd poll_info;
|
||||
poll_info.fd = m_socket_fd;
|
||||
poll_info.events = POLL_IN;
|
||||
while (m_is_running) {
|
||||
|
||||
bool is_ready = false;
|
||||
m_current_connection_fd = -1;
|
||||
while (!is_ready) {
|
||||
if (!m_is_running) {
|
||||
break;
|
||||
}
|
||||
int rc = poll(&poll_info, 1, 2000);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR) {
|
||||
close(m_socket_fd);
|
||||
return 0;
|
||||
}
|
||||
perror("poll");
|
||||
close(m_socket_fd);
|
||||
return 1;
|
||||
}
|
||||
if (rc == 0) {
|
||||
continue;
|
||||
}
|
||||
if (poll_info.revents & POLL_IN) {
|
||||
auto addrlen = sizeof(sockaddr);
|
||||
m_current_connection_fd = accept(
|
||||
m_socket_fd, (struct sockaddr *)&m_addr, (socklen_t *)&addrlen);
|
||||
if (m_current_connection_fd < 0) {
|
||||
perror("accept");
|
||||
close(m_socket_fd);
|
||||
return 1;
|
||||
}
|
||||
get_client_addr();
|
||||
is_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_is_running) {
|
||||
close(m_current_connection_fd);
|
||||
break;
|
||||
}
|
||||
|
||||
Request req(m_current_connection_fd);
|
||||
req.m_ip = m_client_ip;
|
||||
|
||||
int status;
|
||||
|
||||
if((status = handle_request(req)))
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
close(m_current_connection_fd);
|
||||
}
|
||||
close(m_socket_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline bool string_starts_with(const std::string& string, const std::string& substring)
|
||||
{
|
||||
return string.rfind(substring, 0) == 0;
|
||||
}
|
||||
|
||||
static std::string get_current_working_directory()
|
||||
{
|
||||
char buffer[PATH_MAX];
|
||||
getcwd(buffer, PATH_MAX);
|
||||
return std::move(std::string(buffer));
|
||||
}
|
||||
|
||||
static inline void trim_string_with_substring(std::string& string, const std::string& substring)
|
||||
{
|
||||
string.assign(string.begin() + substring.length(), string.end());
|
||||
}
|
||||
|
||||
static inline size_t get_file_size(std::ifstream& file)
|
||||
{
|
||||
file.seekg(0, file.end);
|
||||
std::size_t result = file.tellg();
|
||||
file.seekg(0, file.beg);
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string read_whole_file(std::ifstream& file)
|
||||
{
|
||||
std::string result;
|
||||
while(file.good())
|
||||
{
|
||||
char ch = file.get();
|
||||
if(ch != -1) result.push_back(ch);
|
||||
}
|
||||
return std::move(result);
|
||||
}
|
||||
|
||||
static std::string remove_trailing_newline(std::string str)
|
||||
{
|
||||
while(str[str.size() - 1] == '\n')
|
||||
{
|
||||
str.pop_back();
|
||||
}
|
||||
return std::move(str);
|
||||
}
|
||||
|
||||
webcxx::Response webcxx::file(const std::string& item_path)
|
||||
{
|
||||
std::string path = fs::absolute(item_path).string();
|
||||
struct stat statbuf;
|
||||
if(stat(path.c_str(), &statbuf) < 0)
|
||||
{
|
||||
if(errno == EACCES)
|
||||
{
|
||||
return error(403);
|
||||
}
|
||||
return error(404);
|
||||
}
|
||||
if(S_ISDIR(statbuf.st_mode))
|
||||
{
|
||||
return error(403);
|
||||
}
|
||||
std::ifstream item(path, std::ios::binary);
|
||||
std::string content = read_whole_file(item);
|
||||
Response res = Response{200, content, remove_trailing_newline(_webcxx_internal::get_mime_type_from_filepath(path))};
|
||||
item.close();
|
||||
return res;
|
||||
}
|
||||
|
||||
int webcxx::App::handle_request(Request& req)
|
||||
{
|
||||
Response res(200);
|
||||
|
||||
if (req.is_bad_request()) {
|
||||
res = error(400);
|
||||
} else if (!static_resource_path.empty() && string_starts_with(req.path(), static_resource_path)) {
|
||||
std::string path = req.path();
|
||||
trim_string_with_substring(path, static_resource_path);
|
||||
std::string item_path = fs::path("static").concat(path);
|
||||
res = file(item_path);
|
||||
} else if (m_action_map.find(req.path()) != m_action_map.end()) {
|
||||
Action act = m_action_map.at(req.path());
|
||||
switch (req.m_method) {
|
||||
case RequestType::GET:
|
||||
if (act.get_action) {
|
||||
try {
|
||||
res = act.get_action(req);
|
||||
} catch (...) {
|
||||
res = error(500);
|
||||
req.log(res.status);
|
||||
res.send(m_current_connection_fd);
|
||||
close(m_current_connection_fd);
|
||||
close(m_socket_fd);
|
||||
std::rethrow_exception(std::current_exception());
|
||||
}
|
||||
} else {
|
||||
res = error(405);
|
||||
}
|
||||
break;
|
||||
case RequestType::POST:
|
||||
if (act.post_action) {
|
||||
try {
|
||||
res = act.post_action(req);
|
||||
} catch (...) {
|
||||
res = error(500);
|
||||
req.log(res.status);
|
||||
res.send(m_current_connection_fd);
|
||||
close(m_current_connection_fd);
|
||||
close(m_socket_fd);
|
||||
std::rethrow_exception(std::current_exception());
|
||||
}
|
||||
} else {
|
||||
res = error(405);
|
||||
}
|
||||
break;
|
||||
case RequestType::PUT:
|
||||
if (act.put_action) {
|
||||
try {
|
||||
res = act.put_action(req);
|
||||
} catch (...) {
|
||||
res = error(500);
|
||||
req.log(res.status);
|
||||
res.send(m_current_connection_fd);
|
||||
close(m_current_connection_fd);
|
||||
close(m_socket_fd);
|
||||
std::rethrow_exception(std::current_exception());
|
||||
}
|
||||
} else {
|
||||
res = error(405);
|
||||
}
|
||||
break;
|
||||
case RequestType::DELETE:
|
||||
if (act.delete_action) {
|
||||
try {
|
||||
res = act.delete_action(req);
|
||||
} catch (...) {
|
||||
res = error(500);
|
||||
req.log(res.status);
|
||||
res.send(m_current_connection_fd);
|
||||
close(m_current_connection_fd);
|
||||
close(m_socket_fd);
|
||||
std::rethrow_exception(std::current_exception());
|
||||
}
|
||||
} else {
|
||||
res = error(405);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
res = error(404);
|
||||
}
|
||||
|
||||
req.log(res.status);
|
||||
if (res.send(m_current_connection_fd) < 0) {
|
||||
perror("send");
|
||||
close(m_current_connection_fd);
|
||||
close(m_socket_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
25
src/HTMLTemplate.cpp
Normal file
25
src/HTMLTemplate.cpp
Normal file
@ -0,0 +1,25 @@
|
||||
#include "HTMLTemplate.h"
|
||||
#include <sstream>
|
||||
|
||||
std::string _webcxx_internal::build_html_from_template(const std::string& content, const std::string& title)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
ss << "<!DOCTYPE html>"
|
||||
"<html>"
|
||||
"<head>"
|
||||
"<meta charset=\"UTF-8\">"
|
||||
"<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">"
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
|
||||
if (!title.empty())
|
||||
{
|
||||
ss << "<title>";
|
||||
ss << title;
|
||||
ss << "</title>";
|
||||
}
|
||||
ss << "</head>"
|
||||
"<body>";
|
||||
ss << content;
|
||||
ss << "</body>"
|
||||
"</html>";
|
||||
return std::move(ss.str());
|
||||
}
|
6
src/HTMLTemplate.h
Normal file
6
src/HTMLTemplate.h
Normal file
@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace _webcxx_internal {
|
||||
std::string build_html_from_template(const std::string& content, const std::string& title = "");
|
||||
}
|
58
src/HTTPStatus.cpp
Normal file
58
src/HTTPStatus.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
#include "HTTPStatus.h"
|
||||
|
||||
std::map<int, std::string> _webcxx_internal::http_status_codes = {
|
||||
{100, "Continue"},
|
||||
{101, "Switching Protocols"},
|
||||
{103, "Early Hints"},
|
||||
{200, "OK"},
|
||||
{201, "Created"},
|
||||
{202, "Accepted"},
|
||||
{203, "Non-Authoritative Information"},
|
||||
{204, "No Content"},
|
||||
{205, "Reset Content"},
|
||||
{206, "Partial Content"},
|
||||
{300, "Multiple Choices"},
|
||||
{301, "Moved Permanently"},
|
||||
{302, "Found"},
|
||||
{303, "See Other"},
|
||||
{304, "Not Modified"},
|
||||
{307, "Temporary Redirect"},
|
||||
{308, "Permanent Redirect"},
|
||||
{400, "Bad Request"},
|
||||
{401, "Unauthorized"},
|
||||
{402, "Payment Required"},
|
||||
{403, "Forbidden"},
|
||||
{404, "Not Found"},
|
||||
{405, "Method Not Allowed"},
|
||||
{406, "Not Acceptable"},
|
||||
{407, "Proxy Authentication Required"},
|
||||
{408, "Request Timeout"},
|
||||
{409, "Conflict"},
|
||||
{410, "Gone"},
|
||||
{411, "Length Required"},
|
||||
{412, "Precondition Failed"},
|
||||
{413, "Payload Too Large"},
|
||||
{414, "URI Too Long"},
|
||||
{415, "Unsupported Media Type"},
|
||||
{416, "Range Not Satisfiable"},
|
||||
{417, "Expectation Failed"},
|
||||
{418, "I'm a teapot"},
|
||||
{422, "Unprocessable Entity"},
|
||||
{425, "Too Early"},
|
||||
{426, "Upgrade Required"},
|
||||
{428, "Precondition Required"},
|
||||
{429, "Too Many Requests"},
|
||||
{431, "Request Header Fields Too Large"},
|
||||
{451, "Unavailable For Legal Reasons"},
|
||||
{500, "Internal Server Error"},
|
||||
{501, "Not Implemented"},
|
||||
{502, "Bad Gateway"},
|
||||
{503, "Service Unavailable"},
|
||||
{504, "Gateway Timeout"},
|
||||
{505, "HTTP Version Not Supported"},
|
||||
{506, "Variant Also Negotiates"},
|
||||
{507, "Insufficient Storage"},
|
||||
{508, "Loop Detected"},
|
||||
{510, "Not Extended"},
|
||||
{511, "Network Authentication Required"},
|
||||
};
|
7
src/HTTPStatus.h
Normal file
7
src/HTTPStatus.h
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace _webcxx_internal {
|
||||
extern std::map<int, std::string> http_status_codes;
|
||||
}
|
69
src/MIMETypes.cpp
Normal file
69
src/MIMETypes.cpp
Normal file
@ -0,0 +1,69 @@
|
||||
#include "MIMETypes.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#define OS_UNIX_LIKE
|
||||
#elif defined(__unix__)
|
||||
#define OS_UNIX_LIKE
|
||||
#elif defined(_WIN32) || defined(WIN32)
|
||||
#define OS_WINDOWS
|
||||
#endif
|
||||
|
||||
#ifdef OS_UNIX_LIKE
|
||||
#include <unistd.h>
|
||||
#elif defined(OS_WINDOWS)
|
||||
#include <Windows.h>
|
||||
#include <urlmon.h>
|
||||
#else
|
||||
#error "Unsupported platform"
|
||||
#endif
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef OS_UNIX_LIKE
|
||||
std::string _webcxx_internal::get_mime_type_from_filepath(const std::string& filepath)
|
||||
{
|
||||
std::string extension;
|
||||
size_t dot = filepath.rfind('.');
|
||||
if(dot == filepath.npos) goto use_file_command;
|
||||
extension = filepath.substr(dot+1);
|
||||
if(extension == "js") return "text/javascript";
|
||||
if(extension == "json") return "application/json";
|
||||
if(extension == "css") return "text/css";
|
||||
|
||||
use_file_command:
|
||||
FILE* pipe = popen(("file --mime-type -b " + filepath).c_str(), "r");
|
||||
if(!pipe)
|
||||
{
|
||||
throw std::runtime_error("Unable to determine mimetype for file " + filepath);
|
||||
}
|
||||
|
||||
std::string result;
|
||||
int ch;
|
||||
|
||||
while((ch=fgetc(pipe)) != EOF)
|
||||
{
|
||||
result.push_back(ch);
|
||||
}
|
||||
|
||||
pclose(pipe);
|
||||
|
||||
return std::move(result);
|
||||
}
|
||||
#else
|
||||
std::string _webcxx_internal::get_mime_type_from_filepath(const std::string& filepath)
|
||||
{
|
||||
std::wstring str = filepath;
|
||||
LPWSTR pwzMimeOut = NULL;
|
||||
HRESULT hr = FindMimeFromData( NULL, str.c_str(), NULL, 0,
|
||||
NULL, FMFD_URLASFILENAME, &pwzMimeOut, 0x0 );
|
||||
if ( SUCCEEDED( hr ) ) {
|
||||
std::wstring strResult( pwzMimeOut );
|
||||
// Despite the documentation stating to call operator delete, the
|
||||
// returned string must be cleaned up using CoTaskMemFree
|
||||
CoTaskMemFree( pwzMimeOut );
|
||||
return std::move(std::string(strResult));
|
||||
}
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
#endif
|
7
src/MIMETypes.h
Normal file
7
src/MIMETypes.h
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace _webcxx_internal
|
||||
{
|
||||
std::string get_mime_type_from_filepath(const std::string& filepath);
|
||||
}
|
43
src/Map.cpp
Normal file
43
src/Map.cpp
Normal file
@ -0,0 +1,43 @@
|
||||
#include "Map.h"
|
||||
|
||||
namespace webcxx {
|
||||
|
||||
Map::Map(const MapType& map) : m_map(map)
|
||||
{
|
||||
}
|
||||
|
||||
std::string Map::at(const std::string& key)
|
||||
{
|
||||
return m_map.at(key);
|
||||
}
|
||||
|
||||
std::string Map::operator[](const std::string& key)
|
||||
{
|
||||
return std::move(at(key));
|
||||
}
|
||||
|
||||
bool Map::contains(const std::string& key)
|
||||
{
|
||||
return m_map.find(key) != m_map.end();
|
||||
}
|
||||
|
||||
Map::MapType::const_iterator Map::begin() const noexcept
|
||||
{
|
||||
return m_map.cbegin();
|
||||
}
|
||||
|
||||
Map::MapType::const_iterator Map::cbegin() const noexcept
|
||||
{
|
||||
return m_map.cbegin();
|
||||
}
|
||||
|
||||
Map::MapType::const_iterator Map::end() const noexcept
|
||||
{
|
||||
return m_map.cend();
|
||||
}
|
||||
|
||||
Map::MapType::const_iterator Map::cend() const noexcept
|
||||
{
|
||||
return m_map.cend();
|
||||
}
|
||||
}
|
163
src/Request.cpp
Normal file
163
src/Request.cpp
Normal file
@ -0,0 +1,163 @@
|
||||
#include "Request.h"
|
||||
#include "Socket.h"
|
||||
#include <fcntl.h>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
#include "Map.h"
|
||||
|
||||
using namespace _webcxx_internal;
|
||||
|
||||
static std::pair<std::string, std::string> _split_header(std::string input,
|
||||
std::string delim) {
|
||||
int start = 0;
|
||||
int end = input.find(delim);
|
||||
if (end == std::string::npos) {
|
||||
return {"", ""};
|
||||
}
|
||||
return {input.substr(start, end),
|
||||
input.substr(end + delim.size(), input.size() - end - delim.size())};
|
||||
}
|
||||
|
||||
static bool is_pair_empty(const std::pair<std::string, std::string>& p) {
|
||||
return (p.first.size() == 0) && (p.second.size() == 0);
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string>
|
||||
webcxx::Request::split_header(std::string input, std::string delim) {
|
||||
auto result = _split_header(input, delim);
|
||||
if (is_pair_empty(result))
|
||||
m_bad_request = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::pair<std::string, std::string>
|
||||
remove_newlines(std::pair<std::string, std::string> header) {
|
||||
if (header.first[header.first.size() - 1] == '\n') {
|
||||
header.first.pop_back();
|
||||
}
|
||||
if (header.second[header.second.size() - 1] == '\n') {
|
||||
header.second.pop_back();
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
void webcxx::Request::decode_http_header(std::string header) {
|
||||
auto rc = split_header(header, " ");
|
||||
if (rc.first == "GET") {
|
||||
m_method = RequestType::GET;
|
||||
} else if (rc.first == "POST") {
|
||||
m_method = RequestType::POST;
|
||||
} else if (rc.first == "PUT") {
|
||||
m_method = RequestType::PUT;
|
||||
} else if (rc.first == "DELETE") {
|
||||
m_method = RequestType::DELETE;
|
||||
} else {
|
||||
m_bad_request = true;
|
||||
return;
|
||||
}
|
||||
rc = split_header(rc.second, " ");
|
||||
m_path = rc.first;
|
||||
m_real_path = rc.first;
|
||||
if (m_path.find('?') != std::string::npos) {
|
||||
auto path_parts = split_header(m_path, "?");
|
||||
m_path = path_parts.first;
|
||||
std::vector<std::string> arg_strings;
|
||||
std::pair<std::string, std::string> p =
|
||||
_split_header(path_parts.second, "&");
|
||||
while (!is_pair_empty(p)) {
|
||||
arg_strings.push_back(p.first);
|
||||
if(p.second.find("&") != std::string::npos) p = split_header(p.second, "&");
|
||||
else break;
|
||||
}
|
||||
if (arg_strings.size() == 0) {
|
||||
arg_strings.push_back(path_parts.second);
|
||||
} else {
|
||||
arg_strings.push_back(p.second);
|
||||
}
|
||||
for (auto &arg : arg_strings) {
|
||||
if (arg.find('=') != std::string::npos) {
|
||||
auto _p = split_header(arg, "=");
|
||||
m_args[_p.first] = _p.second;
|
||||
} else {
|
||||
m_args[arg] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rc.second != "HTTP/1.1\n") {
|
||||
m_bad_request = true;
|
||||
}
|
||||
}
|
||||
|
||||
webcxx::Request::Request(int fd) {
|
||||
std::string m_http_header_line = "";
|
||||
std::string m_request_header = "";
|
||||
char buffer[4096];
|
||||
if (sockgetline(fd, buffer, sizeof(buffer)) > 2) {
|
||||
m_http_header_line = buffer;
|
||||
decode_http_header(m_http_header_line);
|
||||
while (sockgetline(fd, buffer, sizeof(buffer)) > 2) {
|
||||
m_request_header = buffer;
|
||||
m_headers.insert(remove_newlines(split_header(m_request_header, ": ")));
|
||||
}
|
||||
} else {
|
||||
m_bad_request = true;
|
||||
}
|
||||
if(!(m_method == POST || m_method == PUT)) { m_content = ""; return; }
|
||||
try {
|
||||
std::string content_length = m_headers.at("Content-Length");
|
||||
int content_length_as_int;
|
||||
int rc = sscanf(content_length.c_str(), "%d", &content_length_as_int);
|
||||
if (rc != 1) {
|
||||
printf("%d\n", rc);
|
||||
m_bad_request = true;
|
||||
return;
|
||||
}
|
||||
char *buf = (char *)malloc(content_length_as_int);
|
||||
if (read(fd, buf, content_length_as_int) < 0) {
|
||||
m_bad_request = true;
|
||||
return;
|
||||
}
|
||||
m_content = buf;
|
||||
free(buf);
|
||||
} catch (std::out_of_range e) {
|
||||
m_content = "";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string method_to_string(webcxx::RequestType method) {
|
||||
switch (method) {
|
||||
case webcxx::RequestType::GET:
|
||||
return "GET";
|
||||
case webcxx::RequestType::POST:
|
||||
return "POST";
|
||||
case webcxx::RequestType::PUT:
|
||||
return "PUT";
|
||||
case webcxx::RequestType::DELETE:
|
||||
return "DELETE";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void webcxx::Request::log(int status_code) {
|
||||
printf("%s - \"%s %s HTTP/1.1\" - %d\n", m_ip.c_str(),
|
||||
method_to_string(m_method).c_str(),
|
||||
m_real_path.c_str(),
|
||||
status_code);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
webcxx::Map webcxx::Request::headers() const
|
||||
{
|
||||
return Map(m_headers);
|
||||
}
|
||||
|
||||
webcxx::Map webcxx::Request::args() const
|
||||
{
|
||||
return Map(m_args);
|
||||
}
|
52
src/Response.cpp
Normal file
52
src/Response.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include "Response.h"
|
||||
#include "HTTPStatus.h"
|
||||
#include <sstream>
|
||||
#include <sys/socket.h>
|
||||
|
||||
static int socket_send(int fd, const char *str, size_t size, int flags) {
|
||||
return send(fd, str, size, flags);
|
||||
};
|
||||
|
||||
void webcxx::Response::set_status(int status_code)
|
||||
{
|
||||
status = status_code;
|
||||
}
|
||||
|
||||
webcxx::Response::Response(int status_code, std::string content,
|
||||
std::string content_type,
|
||||
std::map<std::string, std::string> extra_headers)
|
||||
: status(status_code), headers(extra_headers), content(content) {
|
||||
headers["Content-Type"] = content_type;
|
||||
if (content != "") {
|
||||
std::ostringstream ss;
|
||||
ss << content.size();
|
||||
headers["Content-Length"] = ss.str();
|
||||
}
|
||||
headers["Server"] = "webcxx/0.1";
|
||||
headers["Connection"] = "close";
|
||||
}
|
||||
|
||||
std::string webcxx::Response::to_string() {
|
||||
std::ostringstream result;
|
||||
result << "HTTP/1.1 ";
|
||||
result << status;
|
||||
result << " ";
|
||||
result << _webcxx_internal::http_status_codes[status];
|
||||
result << "\r\n";
|
||||
for (auto &header : headers) {
|
||||
result << header.first;
|
||||
result << ": ";
|
||||
result << header.second;
|
||||
result << "\r\n";
|
||||
}
|
||||
result << "\r\n";
|
||||
if (content != "") {
|
||||
result << content;
|
||||
}
|
||||
return result.str();
|
||||
}
|
||||
|
||||
int webcxx::Response::send(int fd) {
|
||||
std::string as_string = to_string();
|
||||
return socket_send(fd, as_string.c_str(), as_string.size(), 0);
|
||||
}
|
42
src/Socket.cpp
Normal file
42
src/Socket.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
#include "Socket.h"
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace _webcxx_internal {
|
||||
char sockgetchar(int fd) {
|
||||
char c;
|
||||
if (recv(fd, &c, 1, 0) < 0) {
|
||||
return -1;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
char sockpeekchar(int fd) {
|
||||
char c;
|
||||
if (recv(fd, &c, 1, MSG_PEEK) < 0) {
|
||||
return -1;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
int sockgetline(int fd, char *buf, size_t n) {
|
||||
int i = 0;
|
||||
char c = '\0';
|
||||
|
||||
while ((i < n - 1) && (c != '\n')) {
|
||||
c = sockgetchar(fd);
|
||||
if (c > 0) {
|
||||
if (c == '\r') {
|
||||
c = sockpeekchar(fd);
|
||||
if ((c > 0) && (c == '\n'))
|
||||
c = sockgetchar(fd);
|
||||
else
|
||||
c = '\n';
|
||||
}
|
||||
buf[i] = c;
|
||||
i++;
|
||||
} else
|
||||
c = '\n';
|
||||
}
|
||||
buf[i] = '\0';
|
||||
|
||||
return i;
|
||||
}
|
||||
} // namespace _webcxx_internal
|
8
src/Socket.h
Normal file
8
src/Socket.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
|
||||
namespace _webcxx_internal {
|
||||
char sockgetchar(int fd);
|
||||
char sockpeekchar(int fd);
|
||||
int sockgetline(int fd, char *buf, size_t n);
|
||||
} // namespace _webcxx_internal
|
Loading…
Reference in New Issue
Block a user