summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile26
-rw-r--r--cfws.c102
-rw-r--r--http.c37
-rw-r--r--http.h25
-rw-r--r--src/CGIScript.cpp69
-rw-r--r--src/CGIScript.h25
-rw-r--r--src/ClientConnection.cpp47
-rw-r--r--src/ClientConnection.h18
-rw-r--r--src/HttpRequest.cpp28
-rw-r--r--src/HttpRequest.h16
-rw-r--r--src/HttpResponse.cpp55
-rw-r--r--src/HttpResponse.h30
-rw-r--r--src/ServerConnection.cpp46
-rw-r--r--src/ServerConnection.h13
-rw-r--r--src/main.cpp156
15 files changed, 174 insertions, 519 deletions
diff --git a/Makefile b/Makefile
index feaf3e3..81c0b3d 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/cfws.c b/cfws.c
new file mode 100644
index 0000000..bc9d0fa
--- /dev/null
+++ b/cfws.c
@@ -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);
+}
+
diff --git a/http.c b/http.c
new file mode 100644
index 0000000..d6f495d
--- /dev/null
+++ b/http.c
@@ -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);
+}
diff --git a/http.h b/http.h
new file mode 100644
index 0000000..b39e271
--- /dev/null
+++ b/http.h
@@ -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();
- }
-}