#include #include /** * Necronda Web Server 3.0 * client.cpp - Client and Connection handler * Lorenz Stechauner, 2018-05-16 */ #include #include #include #include #include #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 "URI.h" #include "procopen.h" #include "network/Address.h" typedef struct { string host; string cc; string country; string prov; string provname; string city; string timezone; string localdate; } IpAddressInfo; void log_to_file(const char *prefix, const string &str, string host) { //FILE *file = fopen((getWebRoot(std::move(host)) + ".access.log").c_str(), "a"); //fprintf(file, "%s%s\r\n", prefix, str.c_str()); //fflush(file); //fclose(file); } void log_error_to_file(const char *prefix, const string &str, string host) { log_to_file(prefix, "\x1B[1;31m" + str + "\x1B[0m", std::move(host)); } /** * Writes log messages to the console * @param prefix The connection prefix * @param str The string to be written */ void log(const char *prefix, const string &str) { printf("%s%s\r\n", prefix, str.c_str()); flush(cout); } void log_error(const char *prefix, const string &str) { log(prefix, "\x1B[1;31m" + str + "\x1B[0m"); } void php_error_handler(const char *prefix, FILE *stderr) { string line; while (!(line = read_line(stderr)).empty()) { log_error(prefix, line); } fclose(stderr); } IpAddressInfo get_ip_address_info(Address* addr) { FILE *name = popen(("/opt/ipinfo/ipinfo.py " + addr->toString()).c_str(), "r"); char hostbuffer[1024]; memset(hostbuffer, 0, 1024); size_t size = fread(hostbuffer, 1, 1024, name); istringstream buffer(hostbuffer); string line; IpAddressInfo info; int num = 0; while (std::getline(buffer, line)) { switch (num) { case 0: info.host = line; break; case 1: info.cc = line; break; case 2: info.country = line; break; case 3: info.prov = line; break; case 4: info.provname = line; break; case 5: info.city = line; break; case 6: info.timezone = line; break; case 7: info.localdate = line; break; } num++; } return info; } string get_os_info(int fd) { struct tcp_info ti; socklen_t tisize = sizeof(ti); getsockopt(fd, IPPROTO_TCP, TCP_INFO, &ti, &tisize); int ttl; socklen_t ttlsize = sizeof(ttl); getsockopt(fd, IPPROTO_IP, IP_TTL, &ttl, &ttlsize); return "win_size=" + to_string(ti.tcpi_rcv_space) + ", ttl=" + to_string(ttl); } 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; } #include #include #include long getPosition(std::string str, char c, int occurence) { int tempOccur = 0; int num = 0; for (auto it : str) { num++; if (it == c) { if (++tempOccur == occurence) { return num; } } } return -1; } int websocket_handler(Socket *socket, stds *pipes) { fd_set readfd; int maxfd = (socket->getFd() > pipes->stdout->_fileno) ? socket->getFd() : pipes->stdout->_fileno; FD_ZERO(&readfd); FD_SET(socket->getFd(), &readfd); FD_SET(pipes->stdout->_fileno, &readfd); while (true) { int ret = ::select(maxfd + 1, &readfd, nullptr, nullptr, nullptr); if (ret < 0) { throw (char *) strerror(errno); } } } /** * Handles (keep-alive) HTTP connections * @param prefix The connection prefix * @param socket The socket * @param id The client ID * @param num The Connection Number in the client * @return Should the server wait for another header? */ bool connection_handler(const char *preprefix, const char *col1, const char *col2, Socket *socket, long id, long num, IpAddressInfo *info) { bool error = false; char buffer[1024]; char *prefix = (char *) preprefix; HttpConnection req; try { req = HttpConnection(socket); } catch (char *msg) { try { if (msg == "Malformed header") { log(prefix, "Unable to parse header: Malformed header"); 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->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); error = true; } } catch (char *msg2) { } } try { bool noRedirect, redir, invalidMethod, etag, compress, websocket; URI path; pid_t childpid; FILE *file; int statuscode; string hash, type, host; thread *t; long pos; stds pipes; if (req.isExistingField("Connection") && req.getField("Connection") == "keep-alive") { req.setField("Connection", "keep-alive"); req.setField("Keep-Alive", "timeout=3600, max=100"); } else { req.setField("Connection", "close"); error = true; } host = ""; if (!req.isExistingField("Host")) { req.respond(400); goto respond; } host = req.getField("Host"); pos = host.find(':'); if (pos != string::npos) { host.erase(pos, host.length() - pos); } /* FILE *name = popen(("dig @8.8.8.8 +time=1 -x " + socket->getPeerAddress()->toString() + " | grep -oP \"^[^;].*\\t\\K([^ ]*)\\w\"").c_str(), "r"); char hostbuffer[1024]; memset(hostbuffer, 0, 1024); size_t size = fread(hostbuffer, 1, 1024, name); hostbuffer[size - 1] = 0; // remove \n if (size <= 1) { sprintf(hostbuffer, "%s", socket->getPeerAddress()->toString().c_str()); } */ sprintf(buffer, "[\x1B[1m%s\x1B[0m][%i]%s[%s][%i]%s ", host.c_str(), socket->getSocketPort(), col1, info->host.c_str(), socket->getPeerPort(), col2); prefix = buffer; log(prefix, "\x1B[1m" + req.getMethod() + " " + req.getPath() + "\x1B[0m"); log_to_file(prefix, "\x1B[1m" + req.getMethod() + " " + req.getPath() + "\x1B[0m", host); noRedirect = req.getPath().find("/.well-known/") == 0 || (req.getPath().find("/files/") == 0); redir = true; if (!noRedirect) { if (getWebRoot(host).empty()) { req.redirect(303, "https://www.necronda.net" + req.getPath()); } else if (socket->getSocketPort() != 443) { req.redirect(302, "https://" + host + req.getPath()); } else { redir = false; } } else { redir = false; } path = URI(getWebRoot(host), req.getPath()); childpid = 0; if (redir) { goto respond; } else if (!path.getNewPath().empty() && req.getMethod() != "POST") { req.redirect(303, path.getNewPath()); goto respond; } file = path.openFile(); if (file == nullptr) { req.setField("Cache-Control", "public, max-age=60"); req.respond(404); goto respond; } type = path.getFileType(); if (type.find("inode/") == 0 || (path.getRelativeFilePath().find("/.") != string::npos && !noRedirect)) { req.respond(403); goto respond; } req.setField("Content-Type", type); req.setField("Last-Modified", getHttpDate(path.getFilePath())); invalidMethod = false; etag = false; if (path.isStatic()) { hash = getETag(path.getFilePath()); req.setField("ETag", hash); req.setField("Accept-Ranges", "bytes"); if (type.find("text/") == 0) { req.setField("Cache-Control", "public, max-age=3600"); } else { req.setField("Cache-Control", "public, max-age=86400"); } 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.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; } } if (invalidMethod) { req.respond(405); goto respond; } else if (etag) { req.respond(304); goto respond; } statuscode = 0; if (!path.isStatic()) { string cmd = (string) "env -i" + " REDIRECT_STATUS=" + cli_encode("CGI") + " DOCUMENT_ROOT=" + cli_encode(getWebRoot(host)) + " " + req.cgiExport() + (req.isExistingField("Content-Length") ? " CONTENT_LENGTH=" + cli_encode(req.getField( "Content-Length")) : "") + (req.isExistingField("Content-Type") ? " CONTENT_TYPE=" + cli_encode( req.getField("Content-Type")) : "") + ((socket->isSecured()) ? " HTTPS=on" : "") + " PATH_INFO=" + cli_encode(path.getFilePathInfo()) + " PATH_TRANSLATED=" + cli_encode(path.getAbsolutePath()) + " QUERY_STRING=" + cli_encode(path.getQuery()) + " REMOTE_ADDR=" + cli_encode(socket->getPeerAddress()->toString()) + " REMOTE_HOST=" + cli_encode(info->host) + " REMOTE_PORT=" + cli_encode(to_string(socket->getPeerPort())) + " REQUEST_METHOD=" + cli_encode(req.getMethod()) + " REQUEST_URI=" + cli_encode(req.getPath()) + " SCRIPT_FILENAME=" + cli_encode(path.getFilePath()) + " SCRIPT_NAME=" + cli_encode(path.getRelativePath()) + " SERVER_ADMIN=" + cli_encode("lorenz.stechauner@gmail.com") + " SERVER_NAME=" + cli_encode(host) + " SERVER_PORT=" + cli_encode(to_string(socket->getSocketPort())) + " SERVER_SOFTWARE=" + cli_encode("Necronda 3.0") + " SERVER_PROTOCOL=" + cli_encode("HTTP/1.1") + " GATEWAY_INTERFACE=" + cli_encode("CGI/1.1") + " /usr/bin/php-cgi"; pipes = procopen(cmd.c_str()); childpid = pipes.pid; long len = req.isExistingField("Content-Length") ? strtol(req.getField("Content-Length").c_str(), nullptr, 10) : ((req.getMethod() == "POST" || req.getMethod() == "PUT") ? -1 : 0); socket->receive(pipes.stdin, len); t = new thread(php_error_handler, prefix, pipes.stderr); string line; while (!(line = read_line(pipes.stdout)).empty()) { pos = line.find(':'); string index = line.substr(0, pos); string data = line.substr(pos + 1, line.length() - pos); while (index[0] == ' ') index.erase(index.begin() + 0); while (index[index.length() - 1] == ' ') index.erase(index.end() - 1); while (data[0] == ' ') data.erase(data.begin() + 0); while (data[data.length() - 1] == ' ') data.erase(data.end() - 1); if (index == "Status") { statuscode = (int) strtol(data.substr(0, 3).c_str(), nullptr, 10); } else { if (index == "Location" && statuscode == 0) { statuscode = 303; } else if (index == "Content-Type") { type = data; } req.setField(index, data); } } websocket = statuscode == 101 && req.isExistingResponseField("Connection") && req.getResponseField("Connection") == "upgrade"; fclose(file); file = pipes.stdout; if (websocket) { req.respond(statuscode); goto respond; } else { fclose(pipes.stdin); int c = fgetc(pipes.stdout); if (c == -1) { // No Data -> Error req.respond((statuscode == 0) ? 500 : statuscode); goto respond; } else { ungetc(c, pipes.stdout); } } } statuscode = (statuscode == 0) ? 200 : statuscode; compress = (type.find("text/") == 0 || (type.find("application/") == 0 && type.find("+xml") != string::npos) || type == "application/json" || type == "application/javascript") && req.isExistingField("Accept-Encoding") && req.getField("Accept-Encoding").find("deflate") != string::npos; if (compress) { req.setField("Accept-Ranges", "none"); } if (compress && req.isExistingField("Range")) { req.respond(416); } else 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(statuscode, file, compress); } fclose(file); if (childpid > 0) { waitpid(childpid, nullptr, 0); } respond: HttpStatusCode status = req.getStatusCode(); int code = status.code; string color; string comment; if ((code >= 200 && code < 300) || code == 304 || code == 101) { color = "\x1B[1;32m"; // Success (Cached, Switching Protocols): Green } else if (code >= 100 && code < 200) { color = "\x1B[1;93m"; // Continue: Yellow } else if (code >= 300 && code < 400) { color = "\x1B[1;93m"; // Redirect: Yellow comment = " -> " + (req.isExistingResponseField("Location") ? req.getResponseField("Location") : ""); } else if (code >= 400 && code < 500) { color = "\x1B[1;31m"; // Client Error: Red //comment = " -> " + req.getPath(); } else if (code >= 500 & code < 600) { color = "\x1B[1;31m"; // Server Error: Red //comment = " -> " + req.getPath(); } string msg = color + to_string(status.code) + " " + status.message + comment + " (" + formatTime(req.getDuration()) + ")\x1B[0m"; log(prefix, msg); if (!host.empty()) { log_to_file(prefix, msg, host); } if (websocket) { websocket_handler(socket, &pipes); } } catch (char *msg) { HttpStatusCode status = req.getStatusCode(); log(prefix, to_string(status.code) + " " + status.message + " (" + formatTime(req.getDuration()) + ")"); try { if (strncmp(msg, "timeout", strlen(msg)) == 0) { log(prefix, "Timeout!"); req.setField("Connection", "close"); req.respond(408); error = true; } else if (strncmp(msg, "Invalid path", strlen(msg)) == 0) { log(prefix, "Timeout!"); req.setField("Connection", "close"); req.respond(400); } else { log(prefix, (string) "Unable to receive from socket: " + msg); error = true; } } catch (char *msg2) { } } return !error; } /** * Handles HTTP clients * @param socket The socket * @param id The client ID */ void client_handler(Socket *socket, long id, bool ssl) { const char *prefix; char const *col1; char const *col2 = "\x1B[0m"; IpAddressInfo info = get_ip_address_info(socket->getPeerAddress()); auto os = get_os_info(socket->getFd()); { auto group = (int) (id % 6); if (group == 0) { col1 = "\x1B[0;31m"; // Red } else if (group == 1) { col1 = "\x1B[0;32m"; // Green } else if (group == 2) { col1 = "\x1B[0;34m"; // Blue } else if (group == 3) { col1 = "\x1B[0;33m"; // Yellow } else if (group == 4) { col1 = "\x1B[0;35m"; // Magenta } else { col1 = "\x1B[0;36m"; // Cyan } string *a = new string("[" + socket->getSocketAddress()->toString() + "][" + to_string(socket->getSocketPort()) + "]" + col1 + "[" + info.host + "][" + to_string(socket->getPeerPort()) + "]" + col2 + " "); prefix = a->c_str(); } log(prefix, "Connection established"); log(prefix, string("Host: ") + info.host + " (" + socket->getPeerAddress()->toString() + ")"); log(prefix, string("OS: ") + os); log(prefix, string("Location: ") + info.cc + "/" + info.country + ", " + info.prov + "/" + info.provname + ", " + info.city); log(prefix, string("Local Date: ") + info.localdate + " (" + info.timezone + ")"); bool err = false; try { socket->setReceiveTimeout(3600000); socket->setSendTimeout(36000000); } catch (char *msg) { log(prefix, (string) "Unable to set timeout on socket: " + msg); err = true; } try { 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"); socket->sslHandshake("/cert/necronda.net/privkey.pem", "/cert/necronda.net/fullchain.pem"); } } catch (char *msg) { log(prefix, (string) "Unable to perform handshake: " + msg); err = true; } long reqnum = 0; if (!err) { while (connection_handler(prefix, col1, col2, socket, id, ++reqnum, &info)); reqnum--; } log(prefix, "Connection terminated (#:" + to_string(reqnum) + ", R: " + formatSize(socket->getBytesReceived()) + ", S: " + formatSize(socket->getBytesSent()) + ", T: " + formatTime(socket->getDuration()) + ")"); socket->close(); }