diff options
author | cflip <cflip@cflip.net> | 2023-05-08 21:25:21 -0600 |
---|---|---|
committer | cflip <cflip@cflip.net> | 2023-05-08 21:25:21 -0600 |
commit | 7f1d6bbc335288df1e24e7c8f305c32afe6b050a (patch) | |
tree | 3ca1784ab73315e44dd9e03b2f0e244a59158fbc | |
parent | 83fb0b96c94e7f596f81d5bc346150904457ed64 (diff) |
Begin rewriting cfws in C
-rw-r--r-- | Makefile | 26 | ||||
-rw-r--r-- | cfws.c | 102 | ||||
-rw-r--r-- | http.c | 37 | ||||
-rw-r--r-- | http.h | 25 | ||||
-rw-r--r-- | src/CGIScript.cpp | 69 | ||||
-rw-r--r-- | src/CGIScript.h | 25 | ||||
-rw-r--r-- | src/ClientConnection.cpp | 47 | ||||
-rw-r--r-- | src/ClientConnection.h | 18 | ||||
-rw-r--r-- | src/HttpRequest.cpp | 28 | ||||
-rw-r--r-- | src/HttpRequest.h | 16 | ||||
-rw-r--r-- | src/HttpResponse.cpp | 55 | ||||
-rw-r--r-- | src/HttpResponse.h | 30 | ||||
-rw-r--r-- | src/ServerConnection.cpp | 46 | ||||
-rw-r--r-- | src/ServerConnection.h | 13 | ||||
-rw-r--r-- | src/main.cpp | 156 |
15 files changed, 174 insertions, 519 deletions
@@ -1,31 +1,25 @@ -CXX=g++ -LD=g++ +CC=gcc +LD=gcc -CFLAGS=-pedantic -Wall --std=c++17 +CFLAGS=-Wall -Wextra -pedantic -std=c89 +LDFLAGS= -OBJS=src/main.o \ - src/CGIScript.o \ - src/ClientConnection.o \ - src/ServerConnection.o \ - src/HttpRequest.o \ - src/HttpResponse.o +OBJS=cfws.o http.o DESTDIR=/usr/local/bin/ +.PHONY: all clean install + all: cfws -%.o: %.cpp - $(CXX) $< -o $@ -c $(CFLAGS) +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ cfws: $(OBJS) - $(LD) $^ -o $@ $(CFLAGS) - -.PHONY: clean + $(LD) $(LDFLAGS) $^ -o $@ clean: rm -f $(OBJS) cfws -.PHONY: install - install: all install -m 0755 ./cfws -t $(DESTDIR) @@ -0,0 +1,102 @@ +#include <arpa/inet.h> +#include <netdb.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <sys/types.h> +#include <unistd.h> + +#include "http.h" + +#define CFWS_MAXCONN 10 /* Max connections allowed by listen(). */ +#define CFWS_MAXREAD 1024 /* Size of buffer used for reading from client. */ + +#define CFWS_DEFAULT_PORT 8080 + +int initialize_server(int); +void handle_connection(); + +int main(int argc, char *argv[]) +{ + int port = CFWS_DEFAULT_PORT; + int serverfd, clientfd; + + serverfd = initialize_server(port); + if (serverfd == -1) + return 1; + + printf("Serving a directory at localhost:%d\n", port); + + while (1) { + clientfd = accept(serverfd, NULL, NULL); + handle_connection(clientfd); + close(clientfd); + } + + close(serverfd); + return 0; +} + +int initialize_server(int port) +{ + struct sockaddr_in addr; + int rc; + int sockopts = 1; + int sockfd; + + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd == -1) { + perror("Failed to create server socket"); + return -1; + } + + /* Allow the port to be reused, prevents errors when quickly starting + * and restarting the server. */ + /* TODO: Also use SO_REUSEPORT? */ + setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &sockopts, sizeof(int)); + + memset(&addr, 0, sizeof(struct sockaddr_in)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(port); + + rc = bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr)); + if (rc == -1) { + perror("Failed to bind server socket"); + return -1; + } + + rc = listen(sockfd, CFWS_MAXCONN); + if (rc == -1) { + perror("Failed to listen on server socket"); + return -1; + } + + return sockfd; +} + +void handle_connection(int connfd) +{ + char msgbuf[128]; + char *resbuf; + char readbuf[CFWS_MAXREAD]; + struct http_request req; + + memset(readbuf, 0, CFWS_MAXREAD); + read(connfd, readbuf, CFWS_MAXREAD - 1); + + req = http_parse_request(readbuf); + + snprintf(msgbuf, 128, "Welcome to %s", req.uri); + + http_build_response(&resbuf, HTTP_RESPONSE_OK, msgbuf); + write(connfd, resbuf, strlen(resbuf)); + + free(resbuf); + http_free_request(&req); +} + @@ -0,0 +1,37 @@ +#include "http.h" + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +struct http_request http_parse_request(const char *reqstr) +{ + struct http_request req; + char uribuf[CFWS_MAXURI]; + size_t urilen; + + sscanf(reqstr, "GET %s HTTP/1.1", uribuf); + + /* TODO: Support other method types, notably POST */ + req.method = HTTP_METHOD_GET; + + urilen = strlen(uribuf); + req.uri = malloc(urilen + 1); + memcpy(req.uri, uribuf, urilen + 1); + + /* TODO: Parse request headers */ + + return req; +} + +void http_free_request(struct http_request *req) +{ + free(req->uri); +} + +void http_build_response(char **res, enum http_res_code code, const char *msg) +{ + *res = malloc(128); + sprintf(*res, "HTTP/1.1 200 OK\r\n\r\n%s\r\n", msg); +} @@ -0,0 +1,25 @@ +#ifndef _H_HTTP +#define _H_HTTP + +#define CFWS_MAXURI 128 + +enum http_req_method { + HTTP_METHOD_GET +}; + +enum http_res_code { + HTTP_RESPONSE_OK = 200, + HTTP_RESPONSE_NOTFOUND = 404 +}; + +struct http_request { + int method; + char *uri; +}; + +struct http_request http_parse_request(const char *); +void http_free_request(struct http_request *); + +void http_build_response(char **, enum http_res_code, const char *); + +#endif diff --git a/src/CGIScript.cpp b/src/CGIScript.cpp deleted file mode 100644 index 3ef1f3f..0000000 --- a/src/CGIScript.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "CGIScript.h" - -#include <cstdlib> -#include <filesystem> -#include <iostream> -#include <sstream> -#include <string> -#include <unistd.h> - -CGIScript::CGIScript(const std::string& script_path) - : m_script_path(script_path) -{ - set_environment("SCRIPT_NAME", script_path.c_str()); - set_environment("SCRIPT_FILENAME", script_path.c_str()); - set_environment("SERVER_PROTOCOL", "HTTP/1.1"); - set_environment("SERVER_SOFTWARE", "cfws"); -} - -CGIScript::~CGIScript() -{ - pclose(m_pipe); - - for (const auto* key : m_environment_variables) - unsetenv(key); -} - -void CGIScript::set_environment(const char* key, const char* value) -{ - m_environment_variables.push_back(key); - setenv(key, value, 1); -} - -bool CGIScript::open() -{ - m_pipe = popen(m_script_path.c_str(), "r"); - if (m_pipe == nullptr) { - perror("cfws: popen"); - return false; - } - - m_is_open = true; - return true; -} - -std::string CGIScript::read_output() -{ - std::stringstream sstream; - - char ch = 0; - while ((ch = fgetc(m_pipe)) != EOF) - sstream << ch; - - return sstream.str(); -} - -void CGIScript::validate_path(const std::string& script_path) -{ - namespace fs = std::filesystem; - - if (!fs::exists(script_path)) { - std::cerr << "cfws: Script not found: " << script_path << std::endl; - exit(1); - } - - if (access(script_path.c_str(), X_OK)) { - std::cerr << "cfws: Script does not have execute permissions: " << script_path << std::endl; - exit(1); - } -} diff --git a/src/CGIScript.h b/src/CGIScript.h deleted file mode 100644 index 3ac00d9..0000000 --- a/src/CGIScript.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include <cstdio> -#include <string> -#include <vector> - -class CGIScript { -public: - CGIScript(const std::string& script_path); - ~CGIScript(); - - void set_environment(const char* key, const char* value); - bool open(); - - std::string read_output(); - - static void validate_path(const std::string& path); - -private: - FILE* m_pipe {}; - const std::string& m_script_path; - bool m_is_open { false }; - - std::vector<const char*> m_environment_variables; -}; diff --git a/src/ClientConnection.cpp b/src/ClientConnection.cpp deleted file mode 100644 index c434212..0000000 --- a/src/ClientConnection.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "ClientConnection.h" - -#include <cstring> -#include <iostream> -#include <sstream> -#include <unistd.h> - -ClientConnection::ClientConnection(int socket) - : m_socket_fd(socket) -{ -} - -HttpRequest ClientConnection::read_request() const -{ - // TODO: Clean up this code to ensure it works with multiple lines - // and not risk a buffer overflow. - constexpr int BUFFER_SIZE = 4096; - char buffer[BUFFER_SIZE + 1]; - int n = 0; - - memset(buffer, 0, BUFFER_SIZE); - while ((n = read(m_socket_fd, buffer, BUFFER_SIZE - 1)) > 0) { - if (buffer[n - 1] == '\n') - break; - - memset(buffer, 0, BUFFER_SIZE); - } - - return HttpRequest(buffer); -} - -bool ClientConnection::send(const HttpResponse& response) const -{ - if (!m_is_open) - return false; - - std::string result = response.to_string(); - write(m_socket_fd, result.c_str(), result.length()); - - return true; -} - -void ClientConnection::close_connection() -{ - m_is_open = false; - close(m_socket_fd); -} diff --git a/src/ClientConnection.h b/src/ClientConnection.h deleted file mode 100644 index fee0aeb..0000000 --- a/src/ClientConnection.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "HttpRequest.h" -#include "HttpResponse.h" - -class ClientConnection { -public: - ClientConnection(int socket); - - HttpRequest read_request() const; - - bool send(const HttpResponse&) const; - void close_connection(); - -private: - int m_socket_fd; - bool m_is_open { true }; -}; diff --git a/src/HttpRequest.cpp b/src/HttpRequest.cpp deleted file mode 100644 index 37c85fd..0000000 --- a/src/HttpRequest.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "HttpRequest.h" - -#include <iostream> - -HttpRequest::HttpRequest(const std::string& request_string) -{ - size_t pos = 0; - std::string s = request_string; - std::string line; - while ((pos = s.find("\r\n")) != std::string::npos) { - line = s.substr(0, pos); - - if (line.find("GET ") != std::string::npos) { - m_uri = s.substr(4, line.find(' ', 5) - 4); - std::cout << m_uri << std::endl; - } - - // If the line contains a colon, we assume it's a header. - // TODO: This may not always be the case. - size_t delim_pos = 0; - if ((delim_pos = line.find(':')) != std::string::npos) { - std::string header_key = s.substr(0, delim_pos); - std::string header_value = s.substr(delim_pos + 2, s.find("\r\n") - delim_pos - 2); - m_headers[header_key] = header_value; - } - s.erase(0, pos + 2); - } -} diff --git a/src/HttpRequest.h b/src/HttpRequest.h deleted file mode 100644 index cb98bea..0000000 --- a/src/HttpRequest.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include <map> -#include <string> - -class HttpRequest { -public: - HttpRequest(const std::string& request_string); - - std::string uri() const { return m_uri; } - std::string header(const std::string& header_key) const { return m_headers.at(header_key); }; - -private: - std::map<std::string, std::string> m_headers; - std::string m_uri; -}; diff --git a/src/HttpResponse.cpp b/src/HttpResponse.cpp deleted file mode 100644 index c8afc63..0000000 --- a/src/HttpResponse.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "HttpResponse.h" - -#include <sstream> - -void HttpResponse::add_headers_and_content(const std::string& input) -{ - bool is_parsing_headers = true; - - size_t pos = 0; - std::string s = input; - std::string line; - while ((pos = s.find('\n')) != std::string::npos) { - line = s.substr(0, pos + 1); - - if (is_parsing_headers) { - size_t delim_pos = 0; - if ((delim_pos = line.find(':')) != std::string::npos) { - std::string header_key = s.substr(0, delim_pos); - std::string header_value = s.substr(delim_pos + 2, s.find('\n') - delim_pos - 2); - m_headers[header_key] = header_value; - } else { - is_parsing_headers = false; - } - } else { - m_content += line; - } - s.erase(0, pos + 1); - } -} - -static std::string status_code_string(HttpStatusCode status_code) -{ - switch (status_code) { - case HttpStatusCode::OK: - return "200 OK"; - case HttpStatusCode::Forbidden: - return "403 Forbidden"; - case HttpStatusCode::NotFound: - return "404 Not Found"; - case HttpStatusCode::InternalServerError: - return "500 Internal Server Error"; - } -} - -std::string HttpResponse::to_string() const -{ - std::stringstream string_stream; - string_stream << "HTTP/1.0 " << status_code_string(m_status_code) << "\r\n"; - for (const auto& header : m_headers) - string_stream << header.first << ": " << header.second << "\r\n"; - string_stream << "\r\n" - << m_content; - - return string_stream.str(); -} diff --git a/src/HttpResponse.h b/src/HttpResponse.h deleted file mode 100644 index b55e01a..0000000 --- a/src/HttpResponse.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include <map> -#include <string> - -enum class HttpStatusCode { - OK = 200, - Forbidden = 403, - NotFound = 404, - InternalServerError = 500, -}; - -class HttpResponse { -public: - void add_header(const std::string& header, const std::string& value) - { - m_headers[header] = value; - } - - void set_status_code(HttpStatusCode status_code) { m_status_code = status_code; } - void set_content(const std::string& content) { m_content = content; } - void add_headers_and_content(const std::string&); - - std::string to_string() const; - -private: - HttpStatusCode m_status_code; - std::map<std::string, std::string> m_headers; - std::string m_content; -}; diff --git a/src/ServerConnection.cpp b/src/ServerConnection.cpp deleted file mode 100644 index 64692b6..0000000 --- a/src/ServerConnection.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "ServerConnection.h" - -#include <arpa/inet.h> -#include <csignal> -#include <netdb.h> -#include <sys/ioctl.h> -#include <sys/socket.h> -#include <sys/time.h> -#include <sys/types.h> -#include <unistd.h> - -#include "ClientConnection.h" - -static void error_and_die(const char* message) -{ - perror(message); - exit(1); -} - -ServerConnection::ServerConnection(int port) -{ - sockaddr_in address {}; - int socket_options = 1; - - if ((m_socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) - error_and_die("Failed to create socket"); - - if (setsockopt(m_socket_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &socket_options, sizeof(socket_options)) != 0) - error_and_die("setsockopt"); - - address.sin_family = AF_INET; - address.sin_addr.s_addr = htonl(INADDR_ANY); - address.sin_port = htons(port); - - if ((bind(m_socket_fd, (sockaddr*)&address, sizeof(address))) < 0) - error_and_die("bind"); - - if ((listen(m_socket_fd, 10)) < 0) - error_and_die("listen"); -} - -ClientConnection ServerConnection::accept_client_connection() const -{ - int client_socket = accept(m_socket_fd, (sockaddr*)nullptr, nullptr); - return { client_socket }; -} diff --git a/src/ServerConnection.h b/src/ServerConnection.h deleted file mode 100644 index 8f0af70..0000000 --- a/src/ServerConnection.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -class ClientConnection; - -class ServerConnection { -public: - ServerConnection(int port); - - ClientConnection accept_client_connection() const; - -private: - int m_socket_fd; -}; diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index c0c9bb3..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,156 +0,0 @@ -#include <getopt.h> - -#include <filesystem> -#include <fstream> -#include <iostream> -#include <sstream> - -#include "CGIScript.h" -#include "ClientConnection.h" -#include "HttpRequest.h" -#include "HttpResponse.h" -#include "ServerConnection.h" - -static std::string content_type_for_extension(const std::filesystem::path& extension) -{ - if (extension == ".html") - return "text/html"; - if (extension == ".css") - return "text/css"; - if (extension == ".gif") - return "image/gif"; - if (extension == ".png") - return "image/png"; - if (extension == ".webp") - return "image/webp"; - return "text/plain"; -} - -static HttpResponse serve_from_filesystem(const HttpRequest& request) -{ - namespace fs = std::filesystem; - - HttpResponse response; - response.add_header("Server", "cfws"); - - // Remove leading slash from the path if it exists - std::string relative_request_path = request.uri(); - while (*relative_request_path.begin() == '/') - relative_request_path.erase(0, 1); - - fs::path request_path = fs::current_path() / relative_request_path; - - // Look for an index.html if the requested path is a directory - if (fs::is_directory(request_path)) { - request_path /= "index.html"; - } - - if (fs::exists(request_path)) { - std::string file_path = request_path.string(); - std::ifstream input_file(file_path); - if (!input_file.is_open()) { - std::cerr << "Failed to open file " << file_path << std::endl; - } - - std::string file_contents((std::istreambuf_iterator<char>(input_file)), std::istreambuf_iterator<char>()); - - response.set_status_code(HttpStatusCode::OK); - response.add_header("Content-Type", content_type_for_extension(request_path.extension())); - response.set_content(file_contents); - } else { - response.set_status_code(HttpStatusCode::NotFound); - response.add_header("Content-Type", "text/plain"); - response.set_content("Page not found!"); - } - - return response; -} - -static HttpResponse serve_from_cgi(const std::string& script_path, const HttpRequest& request) -{ - HttpResponse response; - response.add_header("Server", "cfws"); - - // Split URI between the path and the query parameter string - std::stringstream uri_stream(request.uri()); - std::string segment; - std::vector<std::string> segment_list; - - while (std::getline(uri_stream, segment, '?')) { - segment_list.push_back(segment); - } - - CGIScript script(script_path); - script.set_environment("REQUEST_METHOD", "GET"); - script.set_environment("REQUEST_URI", request.uri().c_str()); - script.set_environment("PATH_INFO", segment_list[0].c_str()); - if (segment_list.size() > 1) - script.set_environment("QUERY_STRING", segment_list[1].c_str()); - script.set_environment("CONTENT_LENGTH", "0"); - - if (!script.open()) { - response.set_status_code(HttpStatusCode::InternalServerError); - response.add_header("Content-Type", "text/plain"); - response.set_content("Failed to open CGI script!"); - return response; - } - - response.set_status_code(HttpStatusCode::OK); - response.add_headers_and_content(script.read_output()); - return response; -} - -static option long_options[] = { - { "cgi", required_argument, nullptr, 'c' }, - { "port", required_argument, nullptr, 'p' }, -}; - -int main(int argc, char** argv) -{ - int port = 8080; - bool in_cgi_mode = false; - std::string cgi_program_name; - - int c = 0; - int option_index = 0; - while ((c = getopt_long(argc, argv, "c:p:", long_options, &option_index)) != -1) { - switch (c) { - case 'c': - in_cgi_mode = true; - cgi_program_name = optarg; - break; - case 'p': - port = atoi(optarg); - if (port == 0) { - std::cerr << "cfws: Specified port is not a valid number" << std::endl; - exit(1); - } - break; - default: - break; - } - } - - // Check the script path to ensure that it is a valid executable - // script before attempting to start the server. - if (in_cgi_mode) - CGIScript::validate_path(cgi_program_name); - - ServerConnection server(port); - std::cout << "Serving a " << (in_cgi_mode ? "CGI script" : "directory") << " on port " << port << std::endl; - - while (true) { - ClientConnection client = server.accept_client_connection(); - HttpRequest request = client.read_request(); - HttpResponse response; - - if (in_cgi_mode) { - response = serve_from_cgi(cgi_program_name, request); - } else { - response = serve_from_filesystem(request); - } - - client.send(response); - client.close_connection(); - } -} |