From c9c053d95a1f17ae5692f09a2087b6c5dbd98332 Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Fri, 25 May 2018 09:15:20 +0200 Subject: [PATCH] URL Redirect --- src/Path.cpp | 71 ------------ src/URI.cpp | 159 ++++++++++++++++++++++++++ src/{Path.h => URI.h} | 11 +- src/client.cpp | 242 ++++++++++++++++++++++++++++------------ src/necronda-server.cpp | 122 ++++++++++++++------ src/necronda-server.h | 9 ++ 6 files changed, 439 insertions(+), 175 deletions(-) delete mode 100644 src/Path.cpp create mode 100644 src/URI.cpp rename src/{Path.h => URI.h} (74%) diff --git a/src/Path.cpp b/src/Path.cpp deleted file mode 100644 index a55c459..0000000 --- a/src/Path.cpp +++ /dev/null @@ -1,71 +0,0 @@ - -#include "Path.h" -#include "necronda-server.h" -#include - -using namespace std; - -Path::Path(string webroot, string reqpath) { - unsigned long pos = reqpath.find('?'); - if (pos != string::npos) { - query = reqpath.substr(pos + 1, reqpath.length() - pos); - reqpath.erase(pos + 1, reqpath.length() - pos); - } - if (webroot[webroot.length() - 1] == '/') { - webroot.erase(webroot.length() - 1); - } - if (reqpath.find("/../") != string::npos) { - throw (char *) "Invalid path"; - } - if (reqpath[0] != '/') { - reqpath = '/' + reqpath; - } - this->webroot = webroot; - this->relpath = reqpath; -} - -string Path::getWebRoot() { - return webroot; -} - -string Path::getRelativePath() { - return relpath; -} - -string Path::getAbsolutePath() { - return webroot + relpath; -} - -string Path::getFilePath() { - string abs = webroot; - // TODO - return getAbsolutePath(); -} - -string Path::getRelativeFilePath() { - string rel = getRelativePath(); - // TODO -} - -string Path::getNewPath() { - string rel = getRelativeFilePath(); - // TODO - return nullptr; -} - -FILE *Path::openFile() { - return fopen64(getFilePath().c_str(), "r"); -} - -string Path::getFilePathInfo() { - return getAbsolutePath().erase(getFilePath().length(), getAbsolutePath().length()); -} - -string Path::getFileType() { - return getMimeType(getFilePath()); -} - -bool Path::isStatic() { - return true; -} - diff --git a/src/URI.cpp b/src/URI.cpp new file mode 100644 index 0000000..e3631fc --- /dev/null +++ b/src/URI.cpp @@ -0,0 +1,159 @@ + +#include "URI.h" +#include "necronda-server.h" +#include +#include + +using namespace std; + +string getExtension(string path) { + long pos = path.find_last_of('.'); + if (pos == string::npos) { + return ""; + } + return path.substr(pos + 1, path.length() - pos); +} + +string getFilename(string path) { + long pos = path.find_last_of('/'); + if (pos == string::npos) { + return ""; + } + return path.substr(pos + 1, path.length() - pos); +} + +bool isDirectory(string path) { + struct stat statbuf; + return stat(path.c_str(), &statbuf) == 0 && S_ISDIR(statbuf.st_mode) != 0; +} + +bool isFile(string path) { + struct stat statbuf; + return stat(path.c_str(), &statbuf) == 0 && S_ISDIR(statbuf.st_mode) == 0; +} + +bool fileExists(string path) { + struct stat statbuf; + return stat(path.c_str(), &statbuf) == 0; +} + +URI::URI(string webroot, string reqpath) { + unsigned long pos = reqpath.find('?'); + if (pos != string::npos) { + queryinit = true; + query = reqpath.substr(pos + 1, reqpath.length() - pos); + reqpath.erase(pos, reqpath.length() - pos); + } else { + query = ""; + queryinit = false; + } + if (webroot[webroot.length() - 1] == '/') { + webroot.erase(webroot.length() - 1); + } + if (reqpath.find("/../") != string::npos) { + throw (char *) "Invalid path"; + } + if (reqpath[0] != '/') { + reqpath = '/' + reqpath; + } + this->webroot = webroot; + this->reqpath = reqpath; + + string abs = reqpath; + if (fileExists(webroot + abs)) { + string ext = getExtension(abs); + if (ext == "php" || ext == "html") { + abs.erase(abs.length() - ext.length() - 1, abs.length()); + } + } + + string fname = getFilename(abs); + if (fname == "index") { + abs.erase(abs.length() - fname.length() - 1, abs.length()); + } + + this->filepath = webroot + reqpath; + + if (isDirectory(webroot + abs)) { + if (abs[abs.length() - 1] != '/') { + abs += "/"; + } + this->relpath = abs; + abs += "index"; + if (fileExists(webroot + abs + ".php")) { + this->filepath = webroot + abs + ".php"; + } else if (fileExists(webroot + abs + ".html")) { + this->filepath = webroot + abs + ".html"; + } + } else { + if (abs[abs.length() - 1] == '/') { + abs.erase(abs.length() - 1, abs.length() - 1); + } + this->relpath = abs; + if (fileExists(webroot + abs + ".php")) { + this->filepath = webroot + abs + ".php"; + } else if (fileExists(webroot + abs + ".html")) { + this->filepath = webroot + abs + ".html"; + } + } + +} + +string URI::getWebRoot() { + return webroot; +} + +string URI::getRelativePath() { + return relpath; +} + +string URI::getAbsolutePath() { + return webroot + relpath; +} + +string URI::getFilePath() { + return filepath; +} + +string URI::getRelativeFilePath() { + string rel = getRelativePath(); + // TODO +} + +string URI::getNewPath() { + if (isStatic()) { + if (hasQuery()) { + return getRelativePath(); + } + } + if (relpath != reqpath) { + return relpath + (queryinit? "?" + query : ""); + } else { + return ""; + } +} + +FILE *URI::openFile() { + return fopen64(getFilePath().c_str(), "rb"); +} + +string URI::getFilePathInfo() { + return getAbsolutePath().erase(getFilePath().length(), getAbsolutePath().length()); +} + +string URI::getFileType() { + return getMimeType(getFilePath()); +} + +bool URI::isStatic() { + return true; +} + +string URI::getQuery() { + return query; +} + +bool URI::hasQuery() { + return queryinit; +} + diff --git a/src/Path.h b/src/URI.h similarity index 74% rename from src/Path.h rename to src/URI.h index 4389bb6..038a130 100644 --- a/src/Path.h +++ b/src/URI.h @@ -6,14 +6,17 @@ using namespace std; -class Path { +class URI { private: string webroot; + string reqpath; string relpath; string query; + string filepath; + bool queryinit; public: - Path(string webroot, string reqpath); + URI(string webroot, string reqpath); string getWebRoot(); @@ -35,6 +38,10 @@ public: bool isStatic(); + string getQuery(); + + bool hasQuery(); + }; #endif diff --git a/src/client.cpp b/src/client.cpp index a234e74..6c5981d 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -9,12 +9,16 @@ #include #include #include +#include +#include +#include + #include "network/Socket.h" #include "network/http/HttpRequest.h" #include "network/http/HttpConnection.h" #include "necronda-server.h" #include "network/http/HttpStatusCode.h" -#include "Path.h" +#include "URI.h" /** @@ -27,6 +31,81 @@ void log(const char *prefix, const string &string) { flush(cout); } +string getETag(string filename) { + ifstream etags = ifstream("/var/necronda/ETags"); + + ifstream a = ifstream(); + string line; + int index = 0; + int i = 0; + string timestamp = getTimestamp(filename); + long size = getFileSize(filename); + while (getline(etags, line)) { + i++; + if (line == filename) { + index = i; + break; + } + long p1 = line.find(':'); + if (p1 == string::npos) continue; + long p2 = line.find(':', (unsigned) p1 + 1); + if (p2 == string::npos) continue; + long p3 = line.find(':', (unsigned) p2 + 1); + if (p3 == string::npos) continue; + string FILENAME = line.substr(0, (unsigned) p1); + string HASH = line.substr((unsigned) p1 + 1, (unsigned) (p2 - p1)); + string TIMESTAMP = line.substr((unsigned) p2 + 1, (unsigned) (p3 - p2)); + long SIZE = strtol(line.substr((unsigned) p3 + 1, line.length() - p3).c_str(), nullptr, 10); + if (FILENAME == filename) { + index = i; + if (timestamp != TIMESTAMP || size != SIZE) { + break; + } else { + etags.close(); + return HASH; + } + } + } + etags.close(); + + MD5_CTX mdContext; + MD5_Init(&mdContext); + size_t bytes; + char buffer[4096]; + FILE *file = fopen(filename.c_str(), "rb"); + if (file == nullptr) { + throw (char *) "Invalid file"; + } + while ((bytes = fread(buffer, 1, 4096, file)) != 0) { + MD5_Update(&mdContext, buffer, bytes); + } + fclose(file); + unsigned char md[16]; + MD5_Final(md, &mdContext); + char md5buff[32]; + for (int i = 0; i < 16; i++) { + sprintf(md5buff + i * 2, "%02x", md[i]); + } + string md5 = string(md5buff); + + if (index == 0) { + char buff[256]; + sprintf(buff, "%s:%s:%s:%ld\n", filename.c_str(), md5.c_str(), timestamp.c_str(), size); + FILE *f = fopen("/var/necronda/ETags", "a"); + if (f == nullptr) { + throw (char *) strerror(errno); + } + fseek(f, 0, SEEK_END); + fwrite(buff, 1, strlen(buff), f); + fflush(f); + fclose(f); + } else { + + } + + return md5; +} + /** * Handles (keep-alive) HTTP connections * @param prefix The connection prefix @@ -35,7 +114,7 @@ void log(const char *prefix, const string &string) { * @param num The Connection Number in the client * @return Should the server wait for another header? */ -bool connection_handler(const char *prefix, Socket socket, long id, long num) { +bool connection_handler(const char *prefix, Socket *socket, long id, long num) { bool error = false; try { HttpConnection req(socket); @@ -61,81 +140,103 @@ bool connection_handler(const char *prefix, Socket socket, long id, long num) { sprintf(buffer, "%s", n.c_str()); prefix = buffer; - Path path = Path(getWebRoot(host), req.getPath()); + URI path = URI(getWebRoot(host), req.getPath()); log(prefix, req.getMethod() + " " + req.getPath()); FILE *file = path.openFile(); - if (file == nullptr) { - req.setField("Cache-Control", "public, max-age=60"); - req.respond(404); + if (!path.getNewPath().empty()) { + req.redirect(302, path.getNewPath()); } else { - string type = path.getFileType(); - if (type.find("inode/") == 0) { - req.respond(403); + if (file == nullptr) { + req.setField("Cache-Control", "public, max-age=60"); + req.respond(404); } else { - req.setField("Content-Type", type); - req.setField("Last-Modified", getHttpDate(path.getAbsolutePath())); + string type = path.getFileType(); - bool invalidMethod = false; - - if (path.isStatic()) { - req.setField("Accept-Ranges", "bytes"); - req.setField("Cache-Control", "public, max-age=10"); - req.setField("Allow", "GET"); - if (req.getMethod() != "GET") { - invalidMethod = true; - } + if (type.find("inode/") == 0) { + req.respond(403); } else { - req.setField("Accept-Ranges", "none"); - req.setField("Cache-Control", "private, no-cache"); - req.setField("Allow", "GET, POST, PUT"); - if (req.getMethod() != "GET" && req.getMethod() != "POST" && req.getMethod() != "PUT") { - invalidMethod = true; - } - } + req.setField("Content-Type", type); + req.setField("Last-Modified", getHttpDate(path.getFilePath())); - if (invalidMethod) { - req.respond(405); - } else { + bool invalidMethod = false; + bool etag = false; - bool compress = type.find("text/") == 0 && req.isExistingField("Accept-Encoding") && - req.getField("Accept-Encoding").find("deflate") != string::npos; - - if (req.isExistingField("Range")) { - string range = req.getField("Range"); - if (range.find("bytes=") != 0 || !path.isStatic()) { - req.respond(416); - } else { - fseek(file, 0L, SEEK_END); - long len = ftell(file); - fseek(file, 0L, SEEK_SET); - long p = range.find('-'); - if (p == string::npos) { - req.respond(416); - } else { - string part1 = range.substr(6, p - 6); - string part2 = range.substr(p + 1, range.length() - p - 1); - long num1 = stol(part1, nullptr, 10); - long num2 = len - 1; - if (!part2.empty()) { - num2 = stol(part2, nullptr, 10); - } - if (num1 < 0 || num1 >= len || num2 < 0 || num2 >= len) { - req.respond(416); - } else { - req.setField("Content-Range", (string) "bytes " + to_string(num1) + "-" + to_string(num2) + "/" + to_string(len)); - req.respond(206, file, compress, num1, num2); - } - } + if (path.isStatic()) { + string hash = getETag(path.getFilePath()); + req.setField("ETag", hash); + req.setField("Accept-Ranges", "bytes"); + req.setField("Cache-Control", "public, max-age=30"); + req.setField("Allow", "GET"); + if (req.getMethod() != "GET") { + invalidMethod = true; + } + if (req.isExistingField("If-None-Match") && req.getField("If-None-Match") == hash) { + etag = true; } } else { - req.respond(200, file, compress); + req.setField("Accept-Ranges", "none"); + req.setField("Cache-Control", "private, no-cache"); + req.setField("Allow", "GET, POST, PUT"); + if (req.getMethod() != "GET" && req.getMethod() != "POST" && req.getMethod() != "PUT") { + invalidMethod = true; + } + system("php"); + } + + if (invalidMethod) { + req.respond(405); + } else if (etag) { + req.respond(304); + } else { + + bool compress = type.find("text/") == 0 && req.isExistingField("Accept-Encoding") && + req.getField("Accept-Encoding").find("deflate") != string::npos; + + if (compress) { + req.setField("Accept-Ranges", "none"); + } + + if (req.isExistingField("Range")) { + string range = req.getField("Range"); + if (range.find("bytes=") != 0 || !path.isStatic()) { + req.respond(416); + } else { + fseek(file, 0L, SEEK_END); + long len = ftell(file); + fseek(file, 0L, SEEK_SET); + long p = range.find('-'); + if (p == string::npos) { + req.respond(416); + } else { + string part1 = range.substr(6, (unsigned long) (p - 6)); + string part2 = range.substr((unsigned long) (p + 1), + range.length() - p - 1); + long num1 = stol(part1, nullptr, 10); + long num2 = len - 1; + if (!part2.empty()) { + num2 = stol(part2, nullptr, 10); + } + if (num1 < 0 || num1 >= len || num2 < 0 || num2 >= len) { + req.respond(416); + } else { + req.setField("Content-Range", + (string) "bytes " + to_string(num1) + "-" + + to_string(num2) + + "/" + to_string(len)); + req.respond(206, file, compress, num1, num2); + } + } + } + } else { + req.respond(200, file, compress); + } } } + fclose(file); } - fclose(file); } } HttpStatusCode status = req.getStatusCode(); @@ -151,6 +252,7 @@ bool connection_handler(const char *prefix, Socket socket, long id, long num) { error = true; } else if (msg == "Invalid path") { log(prefix, "Timeout!"); + req.setField("Connection", "close"); req.respond(400); } else { log(prefix, (string) "Unable to receive from socket: " + msg); @@ -164,11 +266,11 @@ bool connection_handler(const char *prefix, Socket socket, long id, long num) { try { if (msg == "Malformed header") { log(prefix, "Unable to parse header: Malformed header"); - socket << "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"; + socket->send("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"); error = true; } else if (msg == "timeout") { log(prefix, "Timeout!"); - socket << "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n"; + socket->send("HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n"); error = true; } else { log(prefix, (string) "Unable to receive from socket: " + msg); @@ -186,12 +288,12 @@ bool connection_handler(const char *prefix, Socket socket, long id, long num) { * @param socket The socket * @param id The client ID */ -void client_handler(Socket *socket, long id) { +void client_handler(Socket *socket, long id, bool ssl) { const char *prefix; { char const *col1; char const *col2 = "\x1B[0m"; - int group = (int) (id % 6); + auto group = (int) (id % 6); if (group == 0) { col1 = "\x1B[1;31m"; } else if (group == 1) { @@ -225,22 +327,24 @@ void client_handler(Socket *socket, long id) { } try { - if (socket->getSocketPort() == 443) { + if (ssl) { socket->sslHandshake("/home/lorenz/Documents/Projects/Necronda-Server/necronda-server-3.0/privkey.pem", "/home/lorenz/Documents/Projects/Necronda-Server/necronda-server-3.0/fullchain.pem"); } } catch (char *msg) { - log(prefix, (string) "Unable to perform handhsake: " + msg); + log(prefix, (string) "Unable to perform handshake: " + msg); err = true; } long reqnum = 0; if (!err) { - while (connection_handler(prefix, *socket, id, ++reqnum)); + while (connection_handler(prefix, socket, id, ++reqnum)); reqnum--; } - log(prefix, "Connection terminated (#:" + to_string(reqnum) + ", R:, S:, T: " + formatTime(socket->getDuration()) + ")"); + log(prefix, + "Connection terminated (#:" + to_string(reqnum) + ", R: " + formatSize(socket->getBytesReceived()) + ", S: " + + formatSize(socket->getBytesSent()) + ", T: " + formatTime(socket->getDuration()) + ")"); socket->close(); } diff --git a/src/necronda-server.cpp b/src/necronda-server.cpp index dea0f9e..9c1df0a 100644 --- a/src/necronda-server.cpp +++ b/src/necronda-server.cpp @@ -80,6 +80,26 @@ string getHttpDate(time_t time) { return string(buffer); } +std::string getTimestamp(string path) { + struct stat attrib; + stat(path.c_str(), &attrib); + return getTimestamp(attrib.st_ctime); +} + +std::string getTimestamp(time_t time) { + char buffer[64]; + struct tm *timeinfo = gmtime(&time); + strftime(buffer, sizeof(buffer), "%Y%m%d%H%M%S", timeinfo); + return string(buffer); +} + +long getFileSize(string filename) { + struct stat stat_buf; + int rc = stat(filename.c_str(), &stat_buf); + return rc == 0 ? stat_buf.st_size : -1; +} + + /** * Returns a formatted time string * @param micros Delta time to be formatted @@ -101,6 +121,22 @@ std::string formatTime(long micros) { return std::string(buffer); } +std::string formatSize(unsigned long bytes) { + char buffer[64]; + if (bytes > 0x10000000000) { + sprintf(buffer, "%.1f TiB", (double) bytes / 0x10000000000); + } else if (bytes > 0x40000000) { + sprintf(buffer, "%.1f GiB", (double) bytes / 0x40000000); + } else if (bytes > 0x100000) { + sprintf(buffer, "%.1f MiB", (double) bytes / 0x100000); + } else if (bytes > 0x400) { + sprintf(buffer, "%.1f KiB", (double) bytes / 0x400); + } else { + sprintf(buffer, "%ld B", bytes); + } + return std::string(buffer); +} + string getWebRoot(string host) { return "/home/lorenz/Documents/Projects/Necronda-Server/necronda-server-3.0/webroot"; } @@ -108,7 +144,7 @@ string getWebRoot(string host) { #include "network/Address.cpp" #include "network/Socket.cpp" -#include "Path.cpp" +#include "URI.cpp" #include "network/http/HttpStatusCode.cpp" #include "network/http/HttpHeader.cpp" #include "network/http/HttpRequest.cpp" @@ -121,54 +157,74 @@ string getWebRoot(string host) { long clientnum = 0; int main() { + cout << "Necronda Server 3.0" << endl << "by Lorenz Stechauner" << endl << endl; + + signal(SIGPIPE, SIG_IGN); SSL_load_error_strings(); SSL_library_init(); ERR_load_crypto_strings(); OpenSSL_add_all_algorithms(); - signal(SIGPIPE, SIG_IGN); + int ret = system("mkdir -p /var/necronda /etc/necronda /tmp/necronda; touch /var/necronda/ETags"); - cout << "Necronda Server 3.0" << endl << "by Lorenz Stechauner" << endl << endl; - - unsigned short PORT = 443; - - Socket *s; - try { - s = new Socket(); - } catch (char *msg) { - cout << "Unable to create socket: " << msg << endl; + if (ret != 0) { + cout << "Unable to create server files" << endl; exit(1); } - try { - s->setReuseAddress(true); - } catch (char *msg) { - cout << "Unable to set socket option: " << msg << endl; - exit(1); + list ports = {80, 443}; + + list servers = {}; + auto it = ports.begin(); + + for (int i = 0; i < ports.size(); i++) { + unsigned short port = *it; + advance(it, 1); + Socket server = Socket(); + servers.push_back(server); + + try { + server.setReuseAddress(true); + server.setReceiveTimeout(0); + server.setSendTimeout(0); + } catch (char *msg) { + cout << "Unable to set socket option: " << msg << endl; + exit(2); + } + + try { + server.bind(port); + } catch (char *msg) { + cout << "Unable to bind socket to port " << port << ": " << msg << endl; + exit(3); + } + + try { + server.listen(256); + } catch (char *msg) { + cout << "Unable to listen on socket: " << msg << endl; + exit(4); + } + } - try { - s->bind(PORT); - } catch (char *msg) { - cout << "Unable to bind socket to port " << PORT << ": " << msg << endl; - exit(2); - } - - try { - s->listen(256); - } catch (char *msg) { - cout << "Unable to listen on socket: " << msg << endl; - exit(3); - } + cout << "Ready for connections" << endl; while (true) { try { - Socket *socket = s->accept(); - clientnum++; - thread *t = new thread(client_handler, socket, clientnum); + Socket::select(servers, {}); + for (Socket server : servers) { + try { + Socket *socket = server.accept(); + clientnum++; + thread *t = new thread(client_handler, socket, clientnum, server.getSocketPort() == 443); + } catch (char *msg) { + // Nothing + } + } } catch (char *msg) { - cout << msg << endl; + cout << "Select: " << msg << endl; break; } } diff --git a/src/necronda-server.h b/src/necronda-server.h index 95c438f..4ff3ae3 100644 --- a/src/necronda-server.h +++ b/src/necronda-server.h @@ -15,6 +15,8 @@ unsigned long getMicros(); string formatTime(long micros); +string formatSize(unsigned long bytes); + string getWebRoot(string host); string getMimeType(string path); @@ -25,4 +27,11 @@ string getHttpDate(); string getHttpDate(string filename); +string getTimestamp(string path); + +string getTimestamp(time_t time); + +long getFileSize(string filename); + + #endif