// vim: set ts=2 sw=2 et tw=80: /* * Insidious Proxy - Claudio Maggioni * * Sources and references used to write this program: * - https://git.maggioni.xyz/maggicl/UniversalPizzoccheri (own work from * high-school), used for code snippets on sockets * - Cppreference.com and man 3, for documentation (as usual) * - http://acme.com/software/micro_proxy/, for general inspiration for the high * level implementation and code snippets for DNS queries. * * Dependemcies: * - http://www.graphicsmagick.org/project.html for imagemagick bindings in * order to rotate the image. On macos, install with 'brew install * graphicsmagick'. * * Building: * On MacOS, run 'make macos'. Otherwise use simply 'make'. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; #define MAX_CLIENTS 128 #define BUFFER 8192 void* request_thread(void*); // Do not kill process when socket connection is broken. Error is already // handled static void broken_pipe(int signo) {} int sock_conn_d; // Close socket when shutting down static void sigint(int signo) { close(sock_conn_d); exit(0); } enum orientation { LEFT = 0, FLIP = 1, RIGHT = 2 }; enum orientation alteration; bool parse_alteration(const char*, enum orientation*); int main(int argc, char** argv) { srand(time(NULL)); signal(SIGPIPE, broken_pipe); Magick::InitializeMagick(*argv); if(argc < 3) { cerr << "Give port as first arg and image_rotation as second arg" << endl; exit(255); } if (!parse_alteration(argv[2], &alteration)) { cerr << "image_rotation can only be 'flip', 'clockwise', 'counterclockwise'" "or 'random'" << endl; exit(255); } int sock_data_d; struct sockaddr_in server; if((sock_conn_d = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("server socket not created"); exit(1); } memset(&server, 0, sizeof(server)); // zero the memory server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr("0.0.0.0"); server.sin_port = htons(atoi(argv[1])); int reusePort = 1; setsockopt(sock_conn_d, SOL_SOCKET, SO_REUSEPORT, &reusePort, sizeof(reusePort)); if(::bind(sock_conn_d, (struct sockaddr*)&server, sizeof(server)) == -1) { perror("server socket not bound"); exit(2); } while(true) { if(listen(sock_conn_d, MAX_CLIENTS) != 0) { perror("cannot listen on socket"); close(sock_conn_d); exit(3); } printf("listening...\n"); struct sockaddr_in client; socklen_t client_len = sizeof(client); sock_data_d = accept(sock_conn_d, (struct sockaddr*)&client, &client_len); if(sock_data_d == -1) { perror("cannot accept client"); close(sock_conn_d); exit(4); } int* fd = (int*) malloc(sizeof(int)); if (!fd) { perror("thread creation failed"); exit(1); } *fd = sock_data_d; pthread_t req; pthread_create(&req, NULL, request_thread, (void*) fd); } } /* * Parses argv[2] in order to set alteration using the constraints given in the * assignment. 'random' sets a random alteration fixed for the entire execution * of the program. Returns true when argv[2] is valid, false if not */ bool parse_alteration(const char* arg, enum orientation* p) { if (!strcmp(arg, "counterclockwise")) { *p = LEFT; } else if (!strcmp(arg, "flip")) { *p = FLIP; } else if (!strcmp(arg, "clockwise")) { *p = RIGHT; } else if (!strcmp(arg, "random")) { *p = (enum orientation) (rand() % 3); } else { return false; } return true; } /** * Returns true if from the given file the first two chars that are read from * the current seeking point are '\r' and '\n'. Removes those two chars from the * stream if present, otherwise no character will apprar read */ bool empty_line(FILE* in) { char a = fgetc(in), b = fgetc(in); if (a == '\r' && b == '\n') { return true; } else { ungetc(b, in); ungetc(a, in); return false; } } /** * Reads from the given file HTTP header formats, and writes them to the given * std::map. Halts when the header section is terminated (first occurrence of * '\r\n\r\n'). Returns false at the first malformed headers found, true if no * are found */ bool parse_headers(FILE* in, map& headers) { while(true) { // probe first two chars to see if body will start if (empty_line(in)) { break; } string header_name; char c; while((c = fgetc(in)) != ':') { if (c == EOF || c == '\r' || c == '\n' || !isascii(c)) { return false; } else { header_name += c; } } do { c = fgetc(in); } while(isspace(c) && c != '\n' && c != '\r'); ungetc(c, in); string value = ""; do { c = fgetc(in); value += c; } while(c != '\r'); value.pop_back(); fgetc(in); // \n headers[header_name] = value; } return true; } /** * Returns true if it is customary for the given HTTP method to have a body */ bool has_body(const char* method) { return !strcmp(method, "POST") || !strcmp(method, "PUT"); } /** * Returns true if the given string contains a valid HTTP 1.1 chunk start * delimiter, false otherwise. */ bool is_chunk_start(const char* buf) { const char* i; for (i = buf; isdigit(*i); i++); if (*i != '\r') return false; i++; if (*i != '\n') return false; i++; return *i == '\0'; } /** * Returns true if the given buffer contains a valid HTTP 1.1 chunk end * delimiter at its very end, false otherwise. */ bool is_chunk_end(vector& body) { const char b = body.back(); body.pop_back(); const char a = body.back(); body.push_back(b); return a == '\r' && b == '\n'; } /** * Fetch and parse HTTP body from given file, assuming the header has already * been parsed. Parses bodies either delimited by Content-Length or endoded * using HTTP 1.1 chunds. If the body appears to not meet this criteria (by * inspecting the HTTP headers), false is returned, otherwise returns true. */ bool fetch_body(FILE* in, vector& body, const map headers, bool strip_chunked) { bool chunked; size_t length; { auto p = headers.find("Content-Length"); chunked = p == headers.end(); if (!chunked) { length = atol(p->second.c_str()); } else { auto q = headers.find("Transfer-Encoding"); if (q == headers.end() || q->second != "chunked") { return false; } } } if (!chunked) { // read Content-Length bytes uint8_t buf[BUFFER]; size_t r; for(size_t w = 0; w < length; w += r) { r = fread(buf, 1, (length - w) > BUFFER ? BUFFER : (length - w), in); if (r == -1) return false; body.insert(end(body), buf, buf + r); } } else { // This was implemented before Prof. Carzaniga said chunked encoding is not // required. I am leaving this just because it is already done. // Read chunks and search for final chunk 0\r\n\r\n char buf[BUFFER + 1]; buf[BUFFER] = '\0'; bool will_chunk_start = true; while (true) { size_t len; for (len = 0; len < BUFFER; len++) { if ((buf[len] = fgetc(in)) == '\n') { buf[len + 1] = '\0'; break; } } if (!strcmp(buf, "0\r\n") && will_chunk_start) { break; } if (will_chunk_start && is_chunk_start(buf) && strip_chunked) { continue; } body.insert(end(body), begin(buf), end(buf)); body.pop_back(); will_chunk_start = is_chunk_end(body); } char a = fgetc(in), b = fgetc(in); if (!strip_chunked) { body.insert(end(body), begin(buf), end(buf)); body.push_back(a); // \r body.push_back(b); // \n } } return true; } /** * Sends an HTTP response with status code and message given. Then proceeds to * close the given input and output files and terminates the current thread. */ void send_error(FILE* in, FILE* out, const char* protocol, const int status, const string message) { const char* msg = message.c_str(); fprintf(out, "%s %d %s\r\n", protocol, status, msg); fprintf(out, "Content-Type: text/html; charset=utf-8\r\n"); fprintf(out, "Connection: close\r\n"); cerr << status << ' ' << msg << endl; char message_buf[4096]; message_buf[4095] = '\0'; snprintf(message_buf, 4095, "\n" "\n" " \n" " \n" " %s\n" " \n" " \n" "

%s

\n" " \n" "\n", msg, msg); fprintf(out, "Content-Length: %lu\r\n\r\n", strlen(message_buf)); fprintf(out, "%s", message_buf); fclose(in); fflush(out); fclose(out); pthread_exit(NULL); } /** * Converts url in a zero-terminated string for the host name, copied in a * pre-allocated char array given as copy_in. Port is returned * as integer, -1 on error */ int find_host_port(const char* url, char* copy_in) { const char* i = url; size_t c = 0; while (*i != ':' && *i != '\0' && *i != '/') { if (c >= 256 || (!isalnum(*i) && *i != '-' && *i != '.')) return -1; *copy_in = *i; copy_in++; i++; c++; } if (c == 0) return -1; *copy_in = '\0'; i++; if (*i == '\0' || *i == '/') return 80; unsigned p; if (sscanf(i, "%5u", &p) == 1) { return p < 65536 ? p : 80; } else { return 80; } } struct forward { int in; int out; }; bool total_write(uint8_t* data, size_t n, int out) { for(size_t w = 0; w < n;) { ssize_t written = write(out, data + w, n - w); if (written == -1) { return false; } w += written; } return true; } void* forwarder_thread(void* data) { struct forward* f = (struct forward*) data; const size_t BUF_SIZE = 65536; uint8_t buffer[BUF_SIZE]; while (true) { size_t r = read(f->in, buffer, BUF_SIZE); if (r == 0) { break; } if (!total_write(buffer, r, f->out)) { #if DEBUG cerr << "Closing CONNECT forwarder" << endl; #endif return NULL; } } return NULL; } /** * Returns a socket file descriptof of a newly opened socket to the given host. */ int open_client_socket(FILE* in, FILE* out, const char* protocol, const char* host) { int port; char hostname[257]; if ((port = find_host_port(host, hostname)) == -1) { send_error(in, out, protocol, 500, "Hostname parse error"); } int socketfd = socket(AF_INET, SOCK_STREAM, 0); if (socketfd == -1) { send_error(in, out, protocol, 500, "TCP socket connection error"); } struct hostent *he = gethostbyname(hostname); if (!he) { send_error(in, out, protocol, 404, "Unknown host"); } struct sockaddr_in locale; memset (&locale, 0, sizeof(struct sockaddr_in)); locale.sin_family = AF_INET; locale.sin_addr.s_addr = htonl(INADDR_ANY); locale.sin_port = htons(0); if (::bind(socketfd, (struct sockaddr*) &locale, sizeof(locale)) == -1) { send_error(in, out, protocol, 500, "TCP socket connection error"); } struct sockaddr_in remote; memset (&remote, 0, sizeof(struct sockaddr_in)); remote.sin_family = he->h_addrtype; memmove(&remote.sin_addr, he->h_addr, he->h_length); remote.sin_port = htons((uint16_t) port); if (connect(socketfd, (struct sockaddr*) &remote, sizeof(remote)) == -1) { send_error(in, out, protocol, 500, "TCP socket connection error"); } return socketfd; } /** * Handle HTTP connect method. Opens a client socket to the host in *url (by * doing a dns query first). Then forks in another threads and starts relaying * data both from the HTTP client socket to the newly opened socket and * videversa. */ void handle_connect(FILE* in, FILE* out, const char* protocol, char* url, const int fd) { int socketfd = open_client_socket(in, out, protocol, url); struct forward from = { .in = fd, .out = socketfd }, to = { .in = socketfd, .out = fd }; fprintf(out, "%s %d %s\r\n\r\n", protocol, 200, "Connection established"); fflush(out); #if DEBUG cerr << "CONNECT: Connection established" << endl; #endif pthread_t ffrom; pthread_create(&ffrom, NULL, forwarder_thread, &from); forwarder_thread(&to); pthread_join(ffrom, NULL); } void* request_thread(void* data) { int fd = *((int*) data); FILE* in = fdopen(dup(*((int*) data)), "r"); FILE* out = fdopen(dup(*((int*) data)), "w"); free(data); while (true) { char method[10]; char url[8000]; char protocol[10]; map headers; vector body; int i; if ((i = fscanf(in, "%10s %8000s %10s\r\n", method, url, protocol)) < 3) { if (i < 0) break; send_error(in, out, protocol, 400, "Bad request line"); } #if DEBUG cerr << "METHOD: " << method << " URL: " << url << " PROTOCOL: " << protocol << endl; #endif if (!parse_headers(in, headers)) { send_error(in, out, protocol, 400, "Malformed header"); } if (strncmp(url, "http", 4) && strcmp(method, "CONNECT")) { send_error(in, out, protocol, 400, "Request line must contain absolute" " URL (this is a proxy)"); } bool has_body = fetch_body(in, body, headers, false); #if DEBUG cerr << "has_body: " << has_body << endl; cerr << "Request body parsed" << endl; #endif if (!strcmp(method, "CONNECT")) { handle_connect(in, out, protocol, url, fd); break; } else { // Delete Proxy-Connection header { auto i = headers.find("Proxy-Connection"); if (i != headers.end()) headers.erase(i); } if (strncmp(url, "http://", 7)) { send_error(in, out, protocol, 400, "Protocol in URL not supported"); } char* host = url + 7; int serverfd = open_client_socket(in, out, protocol, host); FILE* s_in = fdopen(dup(serverfd), "r"); FILE* s_out = fdopen(dup(serverfd), "w"); fprintf(s_out, "%s %s %s\r\n", method, url, protocol); for (auto i = headers.begin(); i != headers.end(); i++) { fprintf(s_out, "%s: %s\r\n", i->first.c_str(), i->second.c_str()); } fprintf(s_out, "\r\n"); if (has_body) { for (auto i = body.begin(); i < body.end(); i++) { fputc(*i, s_out); } } fflush(s_out); char protocol_r[10]; char message[8000]; unsigned code; const char* image = NULL; const char* const PNG = "PNG"; const char* const JPEG = "JPEG"; fscanf(s_in, "%10s %3u ", protocol_r, &code); fgets(message, 8000, s_in); message[strlen(message) - 2] = '\0'; #if DEBUG cerr << "Response: STATUS: " << code << " MESSAGE: " << message << endl; #endif headers.clear(); parse_headers(s_in, headers); { auto a = headers.find("Content-Type"); if (a != headers.end()) { if (a->second == "image/jpeg") image = JPEG; else if (a->second == "image/png") image = PNG; } } body.clear(); has_body = fetch_body(s_in, body, headers, true); fclose(s_in); fclose(s_out); close(serverfd); Magick::Blob output; if (has_body && image) { try { Magick::Blob my_blob(&body[0], body.size()); Magick::Image to_rotate; try { to_rotate = Magick::Image(my_blob); } catch(Magick::Warning& ignored) {} try { to_rotate.magick(image); } catch(Magick::Warning& ignored) {} try { switch (alteration) { case FLIP: to_rotate.flip(); break; case LEFT: to_rotate.rotate(-90); break; case RIGHT: to_rotate.rotate(90); break; } } catch(Magick::Warning& ignored) {} try { to_rotate.write(&output); } catch(Magick::Warning& ignored) {} fprintf(out, "%s %u %s\n", protocol_r, code, message); headers["Content-Length"] = to_string(output.length()); } catch (Magick::Exception &error) { cout << "Magick++ image conversion failed: " << error.what() << endl; send_error(in, out, protocol, 500, "Image conversion failed"); } } else if (has_body) { headers["Content-Length"] = to_string(body.size()); } else { headers["Content-Length"] = "0"; } fprintf(out, "%s %u %s\n", protocol_r, code, message); headers["Connection"] = "keep-alive"; for (auto i = headers.begin(); i != headers.end(); i++) { string header = i->first + ": " + i->second + "\r\n"; fprintf(out, "%s", header.c_str()); } fprintf(out, "\r\n"); fflush(out); if (has_body && image) { total_write((uint8_t*) output.data(), output.length(), fd); } else if (has_body) { total_write(&body[0], body.size(), fd); } fflush(out); } } cout << "closing data socket...\n"; fclose(in); fflush(out); fclose(out); close(fd); pthread_exit(NULL); }