Homework 1 - HTTP


Jump to current week

Introduction


In this assignment you will build the foundation of the server that you will develop throughout the semester. All 4 homework assignments will continue to build on the same web app. The front end of the app will be provided to you, and we will release new features to the front end for each homework. You can clone the starter code for this assignment using the link below.

GitHub Repository link: https://github.com/CSE-312/CSE312-Server

Sample Deployment: https://312demo.nico.engineer/

You should never need to modify this front end. The grader in Autolab will simulate this front end code so any changes you make may affect the grading of your submissions (ie. Do not change the front end to fix your issues). Your tasks throughout the 4 homework assignments will be to build the corresponding end points on the back end that are called by the front end.


Learning Objective - Hosting Files and Guest Chat


This section outlines all the requirements for the learning objective portion of this assignment. You must complete all of these requirements. The learning objective is autograded in Autolab and you may submit as many times as you need until you earn credit.

This learning objective will guide you through building the foundation of a server that understands HTTP, communicates with a database, implements a chat feature, and utilizes cookies to track user sessions.


The Request Class

Complete the Request class in the util package of the starter code. The constructor has been set up and contains all the instance variables that should be populated by parsing the input which will be an HTTP request in bytes. Your task is to parse the bytes and set the 'body', 'method', 'path', 'http_version', 'headers', and 'cookies' instance variables. Note that the body must be set as bytes while the other values will be set as str's (headers and cookies will map str to str). More details about each instance variable:

  • body
    • All the content of the body of the request as bytes
    • Ideally, the bytes of the body should never be converted to a str. It's recommended that you look for the first b"\r\n\r\n" and treat everything after that as the body before parsing any headers, which should be parsed as string since they are ASCII-only. If you convert the body to a string, you will have to fix this in HW3 when you handle image and video uploads
  • method
    • The HTTP method from the request line
  • path
    • The path from the request line
  • http_version
    • The HTTP version from the request line. You should parse this anyway, but it will always be "HTTP/1.1" in this course
  • headers
    • The headers variable is a dictionary that must be populated with all the headers of the request where the keys in the dictionary are the names of each header and the values are the header values. You must parse all the headers of the request and add them all to this dictionary
    • Technical note: HTTP requests are allowed to contain multiple headers with the same name. This means it's not 100% correct to store headers in a dictionary, but this will serve our purposes in this course while avoiding an increase in complexity
  • cookies
    • If there is a Cookie header, all cookies must be parsed and stored in the cookies dictionary as Strings (The dictionary will map str to str). Your parser must support multiple cookies. Be sure to remove any leading/trailing white space from the cookie names and values.
    • Note: The raw Cookies header should still be in your headers dictionary even after parsing the individual cookies in the cookies dictionary.

Something to keep in mind, for the Request, Response, and Router classes, you must not access any files or call any functions that access any files. The first round tests that we run on your classes do not have access to your folder structure, and thus will throw an error if you try to open any files.


The Response Class

Create a Response class in the util package of the starter code. Unlike the Request class, there are no specific instance variables that need to be set. Instead, the spec is to write specific methods (You will need instance variables to write these methods, but you may choose their names and they won't be accessed by the grader)

In the response class, implement the following methods:

  • __init__(self)
    • The constructor must not take any parameters (Other than self). You may initialize any instance variables you'd like in this constructor in order to complete the other methods.
    • Be careful not to use any of these method names as your variable names. For example, naming an instance variable "headers" will confuse python and make it think that you're trying to call the variable when you call the method
  • set_status(self, code, text)
    • Takes an int and a str
    • returns self (This will be true for most of these methods. Returning a reference to the calling object allows you to chain together calls)
    • Sets the status code and message for the response.
    • If this method is never called, the code and message should default to "200 OK"
  • headers(self, headers)
    • Takes a dict of str to str
    • returns self
    • Adds all the key-value pairs from the input dict as headers to the response
    • If this method is called multiple times, all headers across all calls must be part of the response
    • Note: We will not test this method with multiple headers that have the same name
  • cookies(self, cookies)
    • Takes a dict of str to str
    • returns self
    • Adds all the key-value pairs from the input dict as cookies to the response. Each cookie provided must be set using the appropriate headers
    • If this method is called multiple times, all cookies across all calls must be part of the response
    • If a cookie has any directives, they will be part of its value in the input (ie. you don't have to do anything special for directives in this method)
  • bytes(self, data)
    • Takes bytes
    • returns self
    • Appends the input to the end of the body of the response
    • If this method is called multiple times, all bytes must be appended to the body. This method can be combined with the text method
  • binary
    • Takes a str
    • returns self
    • Appends the input to the end of the body of the response in binary
    • Each character in the input string should be converted into a binary string before being appended to the body
  • text(self, data)
    • Takes a str
    • returns self
    • Appends the input to the end of the body of the response as bytes
    • If this method is called multiple times, all text must be appended to the body. This method can be combined with the bytes method (ie. calling both text and bytes should result in the text and bytes from all calls appearing in the body as bytes)
  • json(self, data)
    • Takes either a dict or a list
    • returns self
    • Sets the body of the response to the input converted to json as bytes and sets the Content-Type to "application/json" while overwriting any content that was already in the body (Since appending would break the JSON format)
    • This method should only be called once. Calling it again will replace the old body
  • to_data(self)
    • Does not take any parameters
    • returns the entire response in bytes. This is the final response that will be sent to the client over the TCP socket
    • The returned bytes must be properly formatted according the HTTP protocol and must contain all headers, cookies, the status code and message, and the body of the response along with the Content-Length header
    • If the Content-Type header was never set using the headers method, it should default to "text/plain; charset=utf-8"
    • The returned bytes may contain addition headers. eg. If you want to disable MIME type sniffing for every response, this class is the place to do it

The Router Class

You'll set up a Router class that will help you organize the code of your server throughout the semester. It is recommended that you study the provided hello path from the starter code and write at least one path (eg. hosting the HTML file at "GET /") without using the router to gain a better understanding of the structure of a server before implementing your Router class.

After Homework 1 you are welcome to make changes to the design of your router, but you are required to use a router throughout all homework assignments. The organization this router provides will prove useful as your server code gains more complexity.

Write a class named Router in a file named util/router.py with the following methods and functionality:

  • __init__(self)
    • If you use a constructor, it must take no parameters. As with the Response class, you may use the constructor to initialize any instance variables you want to use
  • add_route(self, method, path, action, exact_path=False)
    • The "add_route" method takes 4 parameters and is used to specify how to route a specific request based on its method and path
      • The 4 parameters are:
        • The HTTP method (str) of the request
        • The path (str) of the request
        • A function that takes a Request object, and a TCPHandler as parameters and does not return anything. This will be the function that handles request matching the given method and path. The provided hello_path function is an example of this type of function along with sample usage of it in server.py
        • A boolean indicating if the path must match exactly or begin with the provided path. This boolean has a default value of False
      • The add_route method does not return anything
      • This method is called to add a route to the router. For example, if you have a function named "send_home_page" that takes a Request and a TCPHandler and sends an HTTP response containing the HTML of your home page using the handler that should be called on a GET request for the path "/", you would call add_route("GET", "/", send_home_page, True)
  • route_request(self, request, handler)
    • The "route_request" method takes a Request object and a TCPHandler and does not return anything
    • This method will check the method and path of the request, determine which added path should be used, and call the function associated with that path with arguments matching the parameters of this method (eg. forward the Request and TCPHandler to the matching function)
    • If there is no path matching the request, send a 404 Not Found response over the handler. You may choose the message in the body of this response
    • If multiple routes match the request, use the route that was added first with the add_route method
    • When determining if a route is a match, the method and path must both match. If the boolean (from the add_route method) was True, the path must match exactly. If the boolean was False, the path of the request must start with the path for the route for it to be considered a match (eg. add_route("GET", "/public/image/", serve_image) uses the default boolean value of False so a request of "GET /public/image/eagle.jpg" would be routed to the serve_image function


Hosting Static Files

This is the first task where you'll start adding functionality to your server (The three classes you've written will make it easier for you develop your server throughout the semester). Review the code in server.py to see how it uses your Router and Request classes. Then, add a route to your router that will host all files in the public directory.

To accomplish this task, you should add a route that will match any path starting with "/public", then extract the file path from the rest of the path and serve that files contents in the response. For example, if you receive a request for the path "/public/imgs/dog.jpg", you should read the file "/public/imgs/dog.jpg", create a Response object, call bytes on the response and give it the contents of the file, call to_data to get the final response, then send the response using the handlers send_all method.

All files must be served with the correct MIME type. You may use the file extensions to determine these MIME types. For this HW, you only need to handle the file extensions that appear in the provided public directory. (Note: .ico is an image with MIME type "image/x-icon"). There are 7 total types you need to handle.

In addition to hosting all the files in the public directory, you will add paths for each of the pages of the app. For each of these pages, you will need to render an HTML template (Don't panic, this comes down to reading 2 files and doing 1 find and replace). There is an HTML template in "public/layout/layout.html". This template contains all the structure of the app including menus, navigation, metadata, imports, etc. This template has one placeholder that is exactly the string "{{content}}". Each html file for a page will be inserted at this placeholder so the structure does not have to be copied into every file. This also makes it easier to make changes to every page on the app since only template.html has to be changed to affect all pages. To render a page, for example "index.html", you will read both the layout.html and index.html files, then replace "{{content}}" from the template with all the content of index.html to render the full page. This rendered page is what you'll send to the client when they request index.html.

Add the following special paths that require rendering HTML using layout.html as described above:

  • "/" - Render index.html
  • "/chat" - Render chat.html

At this point, you can run your app and visit "http://localhost:8080" in your browser to see the provided front end. We will add more pages in each of the homework assignments using the same template.html.

Security: The X-Content-Type-Options: nosniff header must be set on all responses (Please double/triple check this header for the exact spelling and syntax. If you are off by 1 character, the browser will not disable MIME type sniffing which renders your header useless and can be a security issue). It is recommended that you add this header in your Response class so you'll never forget it

UTF-8: Some files contain emojis that will be displayed when the page loads. These characters must display properly.

404: If a request is received for any path that should not serve content, return a 404 response with a message saying that the content was not found. This can be a plain text message.



Chat Feature

The front end contains a chat feature that will send specific requests to your server. To enable this feature, implement the following end points. Note that you will need to use both a database and cookies to complete all of these end points. All functionality must persist through a server restart, and you must use a MongoDB database. During grading, there will be a MongoDB database running on localhost port 27017.

To test your app with a database, you can either install MongoDB on your device, use the provided docker-compose.db-only.yml file with Docker, use Docker in the command line, or any other way you have to run MongoDB on localhost.


Create Chat Message (`POST /api/chats`)

Creates a new chat message. The frontend will send a POST request to your backend at the route `/api/chats`. Listed below is the format expected. The entirety of the body of the request will be a JSON string

Request (JSON): {"content": string}

Response: 200 OK with a message of exactly "Great work sending a chat message!!"

You may assume the request is properly formatted

When a message is sent, you should create a unique id for the message and store it in your database along with the author of the post. See the spec for the GET request to see the expected format of a chat message.

If the same user creates multiple posts, all posts must have the same author. This should be tracked using a cookie that is set and tracked by your server. The name of this cookie must be exactly "session" and contain a unique id used to identify this user. This cookie must be set when they send their first message (ie. do not set the cookie when they first load your homepage. Some users will only use your API and not your front end)

Since we don't have user accounts yet, you can choose random author names for users for now. The names must be different for different users, but can be randomly generated. You are allowed to use the uuid package to generate ids and tokens if you'd like.


Get Chat Messages (`GET /api/chats`)

Retrieves all chat messages. The frontend will send a GET request to your backend at the route `/api/chats`. Listed below is the format expected.

Response (JSON): {"messages": [{"author": string, "id": string, "content": string, "updated": boolean}, ...]}

The `updated` parameter that is sent back in the list of messages represents if the message has ever been updated. If it has it must be set to true. When a message is first created, this should be set to false.


Update Chat Message (`PATCH /api/chats/{id}`)

Updates an existing message with new content. The frontend will send a PATCH request to your backend at the route `/api/chats/{id}`. Listed below is the format expected.

Request (JSON): {"content": string}

Errors:

  • **403 Forbidden**: This error is for when the user lacks permission (Users can only update their own messages. Your server needs to check for this). You may choose the message for this error

After a message has been updated using this endpoint, its "updated" field must be set to true


Delete Chat Message (`DELETE /api/chats/{id}`)

Deletes an existing message from chat history. The frontend will send a DELETE request to your backend at the route `/api/chats/{id}`. Listed below is the format expected.

Errors:

  • **403 Forbidden**: This error is for when the user lacks permission (can only delete own messages). You may choose the message for this error

After a message is deleted by its author, it must never be sent by the GET endpoint again.

Security: You must escape any HTML in the users' messages. Since your users can submit any text they want, a malicious user could submit HTML tags that attack other users. You cannot allow this. You must escape any submitted HTML so it displays as plain text instead of being rendered by the browser.



Application Objective 1 - Emoji Reactions


Users can add/remove reactions to chat messages (group reactions, will display like 2 👍 1 👎)

If you click on a chat message, you will be able to choose an emoji as your reaction. You can click on the emoji to remove your reaction. To enable this feature, add/modify the following end points


Add an emoji (`PATCH /api/reaction/{messageID}`)

Updates an existing message with the new emoji. Called when a user reacts to a message. A user reacts to the same message with multiple emojis, by hitting this end point multiple times, but they cannot react to the same message with the same emoji multiple times. If you receive a request from the same user trying to react to a message with same emoji a second time, you should respond with a 403. You may choose the message for this response

The content of the request will be JSON in the format:

Request (JSON): {"emoji": "👍"}

Emojis can be treated as single character string.

Errors:

  • **403 Forbidden**: A user attempted to react with the same emoji multiple times for the same message

Remove an emoji (`DELETE /api/reaction/{messageID}`)

A user is removing an emoji reaction that they previously added.

The content of the request will be JSON in the format:

Request (JSON): {"emoji": "👍"}

Errors:

  • **403 Forbidden**: A user attempted to remove a reaction, but they don't have the given reaction for the given message

Send all reactions (`GET /api/chats`)

Modify the chats endpoint to add the emojis to all messages. Each message will have another field for "reactions" containing a list of user ids that have reacted with that emoji. You have control over the ids as long as they are unique for each user.

For example: "reactions": {"👻": ["63fc690d-ea3a-4349-ba51-0c645af40453"], "🫠": ["eda92e0a-eb7a-430b-a938-916d2102b480", "63fc690d-ea3a-4349-ba51-0c645af40453"]}



Application Objective 2 - Changing Nickname


Allow users to change their nickname and retroactively change their name in all their old messages


Add/update nickname (`PATCH /api/nickname`)

Changes the users nickname to the name in the body of the request. This will change the nickname on all messages sent by the user in the past and future.

The content of the request will be JSON in the format:

Request (JSON): {"nickname": string}

When chat messages are served, each message from this user will now contain a field "nickname" containing their chosen name.



Application Objective 3 - Profile Pictures


Use dicebear to generate profile pictures and save them for each user. These images must be consistent for each user and users will have different images. When a user first visits your app, or when they first send a message in chat, you will generate a random profile picture for them and save this image as part of their profile. Whenever they send a chat message, that message must contain their profile picture in an "imageURL" field.

Example field to be added to chat messages - "imageURL": "/public/imgs/profile-pics/eda92e0a-eb7a-430b-a938-916d2102b480.svg"

You are allowed to use the requests library when making requests to the dicebear API. If you use requests, or any external library, be sure to add it to your requirements.txt.



Submission


Submit all files for your server to Autolab in a .zip file (A .rar or .tar file is not a .zip file!)

If you used any external libraries, be sure to add them to your requirements.txt. Autolab will install all dependencies in this file, and no other dependencies, before starting your server.

It is strongly recommended that you download and test your submission after submitting. To do this, download your zip file into a new directory, unzip your zip file, enter the directory where the files were unzipped, run docker compose up, then navigate to localhost:8080 in your browser. This simulates exactly what the TAs will do during grading.



Grading


Each objective will be scored on a 0-3 scale as follows:

3 (Complete) Clearly correct. Following the testing procedure results in all expected behavior
2 (Complete) Mostly correct, but with some minor issues. Following the testing procedure does not give the exact expected results, but all features are functional
1 (Incomplete) Not all features outlined in this document are functional, but an honest attempt was made to complete the objective. Following the testing procedure gives an incorrect result, or no results at all, during any step. This includes issues running Docker or docker-compose even if the code for the objective is correct
0.3 (Incomplete) The objective would earn a 3, but a security risk was found while testing
0.2 (Incomplete) The objective would earn a 2, but a security risk was found while testing
0.1 (Incomplete) The objective would earn a 1, but a security risk was found while testing
0 (Incomplete) No attempt to complete the objective or violation of the assignment (Ex. Using an HTTP library)

Note that for your final grade there is no difference between a 2 and 3, or a 0 and a 1. The numeric score is meant to give you more feedback on your work.

3 Objective Complete
2 Objective Complete
1 Objective Not Complete
0 Objective Not Complete

Autograded objectives are graded on a pass/fail basis with grades of 3.0 or 0.0.



Security Essay


For each objective for which you earned a 0.3 or 0.2, you will still have an opportunity to earn credit for the objective by submitting an essay about the security issue you exposed. These essays must:

  • Be at least 1000 words in length
  • Explain the security issue from your submission with specific details about your code
  • Describe how you fixed the issue in your submission with specific details about the code you changed
  • Explain why this security issue is a concern and the damage that could be done if you exposed this issue in production code with live users

Any submission that does not meet all these criteria will be rejected and your objective will remain incomplete.

Due Date: Security essays are due 1-week after grades are released.

Any essay may be subject to an interview with the course staff to verify that you understand the importance of the security issue that you exposed. If an interview is required, you will be contacted by the course staff for scheduling. Decisions of whether or not an interview is required will be made at the discretion of the course staff.

When you don't have to write an essay:

  • If you never submit a security violation, you never have to write an essay for this course. Be safe. Be secure
  • If you earn a 0.1, there's no need to write an essay since you would not complete the objective anyway