/* * A partial implementation of HTTP/1.0 * * This code is mainly intended as a replacement for the book's 'tiny.c' server * It provides a *partial* implementation of HTTP/1.0 which can form a basis for * the assignment. * * @author G. Back for CS 3214 Spring 2018 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "http.h" #include "hexdump.h" #include "socket.h" #include "bufio.h" #include "main.h" static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret"; // Need macros here because of the sizeof #define CRLF "\r\n" #define CR "\r" #define STARTS_WITH(field_name, header) \ (!strncasecmp(field_name, header, sizeof(header) - 1)) /* Parse HTTP request line, setting req_method, req_path, and req_version. */ static bool http_parse_request(struct http_transaction *ta) { size_t req_offset; ssize_t len = bufio_readline(ta->client->bufio, &req_offset); if (len < 2) // error, EOF, or less than 2 characters return false; char *request = bufio_offset2ptr(ta->client->bufio, req_offset); request[len-2] = '\0'; // replace LF with 0 to ensure zero-termination char *endptr; char *method = strtok_r(request, " ", &endptr); if (method == NULL) return false; if (!strcmp(method, "GET")) ta->req_method = HTTP_GET; else if (!strcmp(method, "POST")) ta->req_method = HTTP_POST; else ta->req_method = HTTP_UNKNOWN; char *req_path = strtok_r(NULL, " ", &endptr); if (req_path == NULL) return false; ta->req_path = bufio_ptr2offset(ta->client->bufio, req_path); char *http_version = strtok_r(NULL, CR, &endptr); if (http_version == NULL) // would be HTTP 0.9 return false; // record client's HTTP version in request if (!strcmp(http_version, "HTTP/1.1")) ta->req_version = HTTP_1_1; else if (!strcmp(http_version, "HTTP/1.0")) ta->req_version = HTTP_1_0; else return false; return true; } /* Process HTTP headers. */ static bool http_process_headers(struct http_transaction *ta) { ta->from = -1; ta->to = -1; for (;;) { size_t header_offset; ssize_t len = bufio_readline(ta->client->bufio, &header_offset); if (len <= 0) return false; char *header = bufio_offset2ptr(ta->client->bufio, header_offset); if (len == 2 && STARTS_WITH(header, CRLF)) // empty CRLF return true; header[len-2] = '\0'; /* Each header field consists of a name followed by a * colon (":") and the field value. Field names are * case-insensitive. The field value MAY be preceded by * any amount of LWS, though a single SP is preferred. */ char *endptr; char *field_name = strtok_r(header, ":", &endptr); if (field_name == NULL) return false; // skip white space char *field_value = endptr; while (*field_value == ' ' || *field_value == '\t') field_value++; // you may print the header like so // printf("Header: %s: %s\n", field_name, field_value); if (!strcasecmp(field_name, "Content-Length")) { ta->req_content_len = atoi(field_value); } ta->valid_token = false; /* Handle other headers here. Both field_value and field_name * are zero-terminated strings. */ // Cookie header if (!strcasecmp(field_name, "Cookie")) { // Gets the cookies. char* cookie2; char* cookie1 = strtok_r(field_value, ";", &cookie2); cookie2++; // Stores the 2nd cookie. ta->extra_cookie = cookie2; // Determines if the 1st cookie is invalid. if ((cookie1 == NULL) || (strlen(cookie1) <= 11)) { ta->resp_status = HTTP_PERMISSION_DENIED; return false; } // If not, find token. ta->token = cookie1 + 11; // + 11 gets rid of "auth_token=" heading. ta->valid_token = validate_token_exp(ta, ta->token); } // Range header if (!strcasecmp(field_name, "Range")){ // Gets token. char *endp; char *token = strtok_r(field_value, "= ", &endp); while (token != NULL && strcasecmp(token, "bytes")) { token = strtok_r(NULL, "= ", &endp); } token = strtok_r(NULL, "= ", &endp); // Determines if the token exist. if (token != NULL) { if (token[0] == '-') { ta->to = atol(token + 1); } else if (token[strlen(token) - 1] == '-') { token[strlen(token) - 1] = '\0'; ta->from = atol(token); } else { char *to; char *from = strtok_r(token, "- ", &to); ta->from = atol(from); ta->to = atol(to); } } ta->range_request = true; } } } const int MAX_HEADER_LEN = 2048; /* add a formatted header to the response buffer. */ void http_add_header(buffer_t * resp, char* key, char* fmt, ...) { va_list ap; buffer_appends(resp, key); buffer_appends(resp, ": "); va_start(ap, fmt); char *error = buffer_ensure_capacity(resp, MAX_HEADER_LEN); int len = vsnprintf(error, MAX_HEADER_LEN, fmt, ap); resp->len += len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len; va_end(ap); buffer_appends(resp, "\r\n"); } /* add a content-length header. */ static void add_content_length(buffer_t *res, size_t len) { http_add_header(res, "Content-Length", "%ld", len); } /* start the response by writing the first line of the response * to the response buffer. Used in send_response_header */ static void start_response(struct http_transaction * ta, buffer_t *res) { switch (ta->req_version) { case HTTP_1_0: buffer_appends(res, "HTTP/1.0 "); break; case HTTP_1_1: default: buffer_appends(res, "HTTP/1.1 "); break; } switch (ta->resp_status) { case HTTP_OK: buffer_appends(res, "200 OK"); break; case HTTP_PARTIAL_CONTENT: buffer_appends(res, "206 Partial Content"); break; case HTTP_BAD_REQUEST: buffer_appends(res, "400 Bad Request"); break; case HTTP_PERMISSION_DENIED: buffer_appends(res, "403 Permission Denied"); break; case HTTP_NOT_FOUND: buffer_appends(res, "404 Not Found"); break; case HTTP_METHOD_NOT_ALLOWED: buffer_appends(res, "405 Method Not Allowed"); break; case HTTP_REQUEST_TIMEOUT: buffer_appends(res, "408 Request Timeout"); break; case HTTP_REQUEST_TOO_LONG: buffer_appends(res, "414 Request Too Long"); break; case HTTP_NOT_IMPLEMENTED: buffer_appends(res, "501 Not Implemented"); break; case HTTP_SERVICE_UNAVAILABLE: buffer_appends(res, "503 Service Unavailable"); break; case HTTP_INTERNAL_ERROR: default: buffer_appends(res, "500 Internal Server Error"); break; } buffer_appends(res, CRLF); } /* Send response headers to client */ static bool send_response_header(struct http_transaction *ta) { buffer_t response; buffer_init(&response, 80); start_response(ta, &response); if (bufio_sendbuffer(ta->client->bufio, &response) == -1) return false; buffer_appends(&ta->resp_headers, CRLF); if (bufio_sendbuffer(ta->client->bufio, &ta->resp_headers) == -1) return false; buffer_delete(&response); return true; } /* Send a full response to client with the content in resp_body. */ static bool send_response(struct http_transaction *ta) { // add content-length. All other headers must have already been set. add_content_length(&ta->resp_headers, ta->resp_body.len); if (!send_response_header(ta)) return false; return bufio_sendbuffer(ta->client->bufio, &ta->resp_body) != -1; } const int MAX_ERROR_LEN = 2048; /* Send an error response. */ static bool send_error(struct http_transaction * ta, enum http_response_status status, const char *fmt, ...) { va_list ap; va_start(ap, fmt); char *error = buffer_ensure_capacity(&ta->resp_body, MAX_ERROR_LEN); int len = vsnprintf(error, MAX_ERROR_LEN, fmt, ap); ta->resp_body.len += len > MAX_ERROR_LEN ? MAX_ERROR_LEN - 1 : len; va_end(ap); ta->resp_status = status; http_add_header(&ta->resp_headers, "Content-Type", "text/plain"); return send_response(ta); } /* Send Not Found response. */ static bool send_not_found(struct http_transaction *ta) { return send_error(ta, HTTP_NOT_FOUND, "File %s not found", bufio_offset2ptr(ta->client->bufio, ta->req_path)); } /* A start at assigning an appropriate mime type. Real-world * servers use more extensive lists such as /etc/mime.types */ static const char * guess_mime_type(char *filename) { char *suffix = strrchr(filename, '.'); if (suffix == NULL) return "text/plain"; if (!strcasecmp(suffix, ".html")) return "text/html"; if (!strcasecmp(suffix, ".gif")) return "image/gif"; if (!strcasecmp(suffix, ".png")) return "image/png"; if (!strcasecmp(suffix, ".jpg")) return "image/jpeg"; if (!strcasecmp(suffix, ".js")) return "text/javascript"; if (!strcasecmp(suffix, ".mp4")) return "video/mp4"; return "text/plain"; } /* Check a token is not expired. */ bool validate_token_exp(struct http_transaction *ta, char* token) { jwt_t* cookie; // Decode token. int rc = jwt_decode(&cookie, ta->token, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE)); if (rc) return false; // Get claim (formatted grants). char* grants = jwt_get_grants_json(cookie, NULL); if (grants == NULL) return false; ta->grants = grants; // Get expiration time. json_error_t error; json_t *jgrants = json_loadb(grants, strlen(grants), 0, &error); json_int_t exp, iat; const char *sub; json_unpack(jgrants, "{s:I, s:I, s:s}", "exp", &exp, "iat", &iat, "sub", &sub); // Check expiration time. if (time(NULL) >= exp) return false; return true; } /* Handle HTTP transaction for static files. */ static bool handle_static_asset(struct http_transaction *ta, char *basedir) { char fname[PATH_MAX]; char *req_path = bufio_offset2ptr(ta->client->bufio, ta->req_path); // The code below is vulnerable to an attack. Can you see // which? Fix it to avoid indirect object reference (IDOR) attacks. snprintf(fname, sizeof fname, "%s%s", basedir, req_path); char *endptr; char *p = calloc(strlen(fname) + 1, sizeof(char)); memcpy(p, fname, strlen(fname) + 1); char *dir = strtok_r(p, "/", &endptr); dir = strtok_r(NULL, "/", &endptr); // initial ".." is okay while (dir != NULL) { if (!strcmp(dir, "..")) { return send_not_found(ta); } dir = strtok_r(NULL, "/", &endptr); } if (!strcmp(req_path, "/")) { memset(fname, 0, PATH_MAX); snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html"); } if (access(fname, R_OK)) { if (errno == EACCES) return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied."); else { if (html5_fallback) { memset(fname, 0, PATH_MAX); snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html"); } else { return send_not_found(ta); } } } // Determine file size struct stat st; int rc = stat(fname, &st); if (rc == -1) return send_error(ta, HTTP_INTERNAL_ERROR, "Could not stat file."); int filefd = open(fname, O_RDONLY); if (filefd == -1) { return send_not_found(ta); } ta->resp_status = HTTP_OK; http_add_header(&ta->resp_headers, "Content-Type", "%s", guess_mime_type(fname)); off_t from = 0, to = st.st_size - 1; if (ta->range_request) { if (ta->from >= 0) { from = ta->from; } if (ta->to >= 0) { to = ta->to; } } off_t content_length = to + 1 - from; add_content_length(&ta->resp_headers, content_length); http_add_header(&ta->resp_headers, "Accept-Ranges", "bytes"); if (ta->range_request) { ta->resp_status = HTTP_PARTIAL_CONTENT; http_add_header(&ta->resp_headers, "Content-Range", "bytes %ld-%ld/%ld", from, to, st.st_size); } bool success = send_response_header(ta); if (!success) goto out; // sendfile may send fewer bytes than requested, hence the loop while (success && from <= to) success = bufio_sendfile(ta->client->bufio, filefd, &from, to + 1 - from) > 0; out: close(filefd); return success; } /* Handles api/login POST */ static bool post_handle_login(struct http_transaction *ta) { http_add_header(&ta->resp_headers, "Content-Type", "application/json"); char* req_body = bufio_offset2ptr(ta->client->bufio, ta->req_body); // Gets the username and password. req_body[ta->req_content_len] = '\0'; json_error_t error; json_t *jgrants = json_loadb(req_body, strlen(req_body), 0, &error); const char *username; const char *password; int rc = json_unpack(jgrants, "{s:s, s:s}", "username", &username, "password", &password); if (rc == -1) { return send_error(ta, HTTP_PERMISSION_DENIED, "???"); } // Authenticates the username and password. if (!strcmp(username, "user0") && !strcmp(password, "thepassword")) { int iat = time(NULL); int exp = iat + token_expiration_time; // Create cookie. jwt_t* cookie; int rc = jwt_new(&cookie); if (rc) return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_new"); // Add grants. rc = jwt_add_grant(cookie, "sub", username); if (rc) return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant"); //time_t age = time(NULL); rc = jwt_add_grant_int(cookie, "iat", iat); if (rc) return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant"); //time_t max_age = age + 3600 * 24; rc = jwt_add_grant_int(cookie, "exp", exp); if (rc) return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant"); // Set algorithm. rc = jwt_set_alg(cookie, JWT_ALG_HS256, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE)); if (rc) return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_set_alg"); // Create token. char* token = jwt_encode_str(cookie); if (token == NULL) return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_encode_str"); // Add Set-Cookie header. http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=%s; Path=%s; Max-Age=%d; HttpOnly", token, "/", token_expiration_time); // Get claim (formatted grants). char *grants = jwt_get_grants_json(cookie, NULL); buffer_appends(&ta->resp_body, grants); // Send claim. ta->resp_status = HTTP_OK; return send_response(ta); } return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied"); } /* Handles api/login GET */ static bool get_handle_login(struct http_transaction *ta) { http_add_header(&ta->resp_headers, "Content-Type", "application/json"); // Determines if the token is invalid. if (!ta->valid_token) { buffer_appends(&ta->resp_body, "{}"); ta->resp_status = HTTP_OK; return send_response(ta); } // If not, send response. buffer_appends(&ta->resp_body, ta->grants); ta->resp_status = HTTP_OK; return send_response(ta); } /* Lists the .mp4 files in the given directory in the proper format. */ static char *list_videos(DIR* dir) { json_t *list = json_array(); struct dirent *file; while ((file = readdir(dir)) != NULL) { char *suffix = strrchr(file->d_name, '.'); if (suffix != NULL && !strcmp(suffix, ".mp4")) { // stat the file char fname[PATH_MAX]; struct stat st; snprintf(fname, sizeof(fname), "%s/%s", server_root, file->d_name); int rc = stat(fname, &st); if (rc == -1) perror("Could not list videos."); // json components json_t *entry = json_object(); json_t *size = json_integer(st.st_size); json_t *name = json_string(file->d_name); rc = json_object_set(entry, "size", size); if (rc) perror("JSON append failed."); rc = json_object_set(entry, "name", name); if (rc) perror("JSON append failed."); // add entry rc = json_array_append_new(list, entry); if (rc) perror("JSON append failed."); } } return json_dumps(list, 0); } /* Determines what the API request is. */ static int val_api_url(struct http_transaction *ta) { char *req_path = bufio_offset2ptr(ta->client->bufio, ta->req_path); if (!strcmp(req_path, "/api/login")) { return 0; } if (!strcmp(req_path, "/api/video")) { return 1; } if (!strcmp(req_path, "/api/logout")) { return 2; } return -1; } /* Handles API requests like login/logout/video. */ static bool handle_api(struct http_transaction *ta) { int val = val_api_url(ta); if (val == -1){ return send_not_found(ta); } // API login if (val == 0) { if (ta->req_method == HTTP_POST) { // Handle login post return post_handle_login(ta); } else if (ta->req_method == HTTP_GET){ // Handle login get return get_handle_login(ta); } else{ return send_error(ta, HTTP_METHOD_NOT_ALLOWED, "Unknown method.\n"); } } // API video if (val == 1) { http_add_header(&ta->resp_headers, "Accept-Ranges", "bytes"); DIR* dir = opendir(server_root); char *json = list_videos(dir); // handling fprintf(stderr, "json: %s\n", json); http_add_header(&ta->resp_headers, "Content-Type", "application/json"); // Ensures response can be sent and sends it. char *body = buffer_ensure_capacity(&ta->resp_body, MAX_HEADER_LEN); int len = snprintf(body, strlen(json) + 2, "%s\n", json); int length = len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len; ta->resp_body.len += length; ta->resp_status = HTTP_OK; return send_response(ta); } // API logout if (val == 2) { // Add Set-Cookie header. http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=; Path=/; Max-Age=0; HttpOnly"); // Send message. buffer_appends(&ta->resp_body, "{\"message\":\"logging out\"}"); ta->resp_status = HTTP_OK; return send_response(ta); } return false; } /* Set up an http client, associating it with a bufio buffer. */ void http_setup_client(struct http_client *self, struct bufio *bufio) { self->bufio = bufio; } /* Handles attempts to access private files. */ static bool handle_private(struct http_transaction *ta) { // Determines if the token from the 1st cookie is valid. if (ta->valid_token) { ta->resp_status = HTTP_OK; return handle_static_asset(ta, server_root); } // Determines if there's a 2nd cookie. if ((ta->extra_cookie == NULL) || (strlen(ta->extra_cookie) <= 11)) { return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied."); } // If so, find its token. ta->token = ta->extra_cookie + 11; // + 11 gets rid of "auth_token=" heading. ta->valid_token = validate_token_exp(ta, ta->token); // valid if (ta->valid_token) { ta->resp_status = HTTP_OK; return handle_static_asset(ta, server_root); } // invalid return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied."); } /* Handle a single HTTP transaction. Returns true on success. */ bool http_handle_transaction(struct http_client *self) { struct http_transaction ta; memset(&ta, 0, sizeof ta); ta.client = self; if (!http_parse_request(&ta)) return false; bool is_http_1_1 = false; // false if (ta.req_version == HTTP_1_1) is_http_1_1 = true; // true if (!http_process_headers(&ta)) return false; if (ta.req_content_len > 0) { int rc = bufio_read(self->bufio, ta.req_content_len, &ta.req_body); if (rc != ta.req_content_len) return false; // To see the body, use this: // char *body = bufio_offset2ptr(ta.client->bufio, ta.req_body); // hexdump(body, ta.req_content_len); } buffer_init(&ta.resp_headers, 1024); http_add_header(&ta.resp_headers, "Server", "CS3214-Personal-Server"); buffer_init(&ta.resp_body, 0); bool rc = false; char *req_path = bufio_offset2ptr(ta.client->bufio, ta.req_path); if (STARTS_WITH(req_path, "/api")) { rc = handle_api(&ta); } else if (STARTS_WITH(req_path, "/private")) { rc = handle_private(&ta); } else { rc = handle_static_asset(&ta, server_root); } buffer_delete(&ta.resp_headers); buffer_delete(&ta.resp_body); return is_http_1_1 && rc; }