Homework 1 - HTTP




Introduction


In this assignment you will build the foundation of the server that you will develop throughout the semester. All homework assignments will continue to add features to the same web app. A front end of the app is provided for you, which you can clone using the link below.

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

This front end is designed to send requests to your server for most of the homework objectives throughout the semester and your goal will be to implement the corresponding endpoints on the back end. This front end is offered to make it easier to complete, and test, your server. You are also free to modify this front end in any way you'd like, or design and build your own, as long as the features for each objective are clear and accessible when visiting your front end.


Deviations


In addition to modifying the front end, or building your own, you are free to deviate from the specific details of each objective as long as you still implement the features for each objective. This offers you flexibility to implement your app the way you'd like, while still learning the concepts required for each objective. There is no limit to the amount you are allowed to deviate as long as the concepts are still covered.

If you choose to deviate from the stated objectives, you must justify how this deviation still covers the concepts required for the objective.


Objective 1: Hosting Static Files


Goals:

  • Host all the static files needed to load your homepage
  • Utilize separate modules/structs/files to organization your server code

Concepts Covered:

  • Project organization
  • HTTP Requests
  • HTTP Responses
  • Routing Requests
  • MIME Types
  • UTF-8
  • 404 Not Found
  • Rendering HTML Templates

If this objective, you will build the foundation of your server by hosting all the static files needed to load your homepage. In the provided front end, this involves hosting every file in the public directory with the proper MIME type, and rending 2 HTML templates.

Project Organization

You are required to have some thoughtful organization of your server code. As with all objectives, you may deviate from the structure below as long as your architecture is clear and maintainable. If you deviate from this structure and it is not clear, it will be more difficult for the course staff to help you if you need assistance with your code and you may not be able to get the help you need later in the semester (In addition to losing credit for this objective).


The Request Struct

Create a struct that will parse HTTP requests and set appropriate fields. This struct should take a raw request as bytes and set fields containing the request information. These fields must include:

  • method
  • path
  • headers
  • body
In later objectives, you may want to add more fields. For example, on the next objective you may find it useful to add cookies to your request parsing.

Tips:

  • Headers may contain the character ':'
  • The body of a request may contain the sequence "\r\n\r\n"
  • The body may contain data that is not text. If you treat the body as text, you will have to update this struct in HW3 when you handle image and video uploads
  • HTTP headers are case-insensitive so "Content-Length" and "content-length" should be treated the same

The Response Struct

Create a struct that will build HTTP responses with methods to populate the data of the response. This struct must include ways to:

  • Set the status code and message
  • Add headers
  • Add the body of the response as either text, JSON, or binary
  • Convert the object into bytes to send over the TCP connection
  • Set the Content-Length header to the correct value
  • Always set the X-Content-Type-Options: nosniff header
As with the Request struct, you may way to add more functionality to this struct in later objectives, including cookies.

Tips:

  • Choosing thoughtful defaults can save you time. For example, if the status code and message are never set, default to 200 OK since it will be the most common status for your responses
  • When adding cookie functionality for the next objective, note that you must handle multiple Set-Cookie headers in the same response. By handling cookies as a special case instead of as a raw header, it can be easier to add this feature
  • When sending JSON, the Content-Type header must be set to "application/json"

The Router Struct

Create a struct that will route different requests to the appropriate function. This struct must include ways to:

  • Add routes to the router containing the method, path, and function to call for that route
  • Route requests that do not have to match the exact path for the route (e.g. Host all files in the public directory for any path that starts with "/public")
  • When a request is read over the TCP connection, route the requests to the appropriate function using the routes that have been added to the router
  • Send a 404 Not Found response if no matching route is found for a request

Tips:

  • Your method that routes requests can take a Request object using your request struct
  • The method and path must both match for a route to be considered a match
  • If multiple routes match the request, you must decide how to decide which route to use. The typical way to do this is to choose the route that was added first. Then to route a request, iterate through all routes in the order in which they were added until you find a match. Of no match is found, send your 404

Hosting Static Files

This is the first task where you'll start adding functionality to your server (The three structs you've written will make it easier for you develop your server throughout the semester). 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" and send it in the body of a response. You must use the three support structs you've created to accomplish this.

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").


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 server and visit "http://localhost:<your_port_number>" in your browser to see the provided front end.

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 struct 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.


Objective 2: Guest Chat


Goals:

  • Add a chat feature to your server that does not require authentication
  • Ensure each message posted by the same user has the same username
  • Use a PostgreSQL database to store chat messages
  • Implement ways for users to create, update, and delete their own messages

Concepts Covered:

  • POST/PATCH/DELETE requests
  • Cookies
  • PostgreSQL

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 a database to complete these end points. All functionality must persist through a server restart, and you must use a PostgreSQL database.

To test your app with a database, you can either install and run PostgreSQL on your device, or setup Docker to run a PostgreSQL container (Which is the next objective so you have to do this anyway).


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 your choosing. A simple text response of "message sent" is fine

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.


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.

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. This cookie must be set when they send their first message

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 a package to generate ids and tokens if you'd like.


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}`

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.


1 - Hosting static files 2 - Guest chat + API + Cookies 3 - Docker 4 - Keep-Alive 5 - Caching

Objective 3: Docker and docker-compose


Goals:

  • Setup Docker and docker-compose to compile and run your server with a single "docker compose up" command
  • Run your server and database in separate containers

Concepts Covered:

  • Docker
  • docker-compose
  • Multiple Containers for a Single Application

Setup your app to run in a docker container such that running "docker compose up" from the root directory of your project will start your server and database in separate containers with your app available on localhost port 8080. To accomplish this, you will need to write a Dockerfile that will both compile and run your server code. You will also need a docker-compose.yml file that will start your server and database in separate containers.

Tips:

  • While debugging, you should run "docker compose up --build --force-recreate --renew-anon-volumes" to ensure that the containers are fully rebuilt. If you do not see your new changes, this is usually the issue
  • The "--renew-anon-volumes" option will delete everything in your database container and start fresh. You may want to remove this option once you have your database working properly and add it again if you want to wipe your data. If you are using named volumes, it is assumed that you know how to use them properly
  • You must implement a way for your app to wait for the database to be ready for connections before connecting. There are many different solutions to this with the modern solution being a health check setup in docker compose
  • If you are running Windows, install WSL2 and run Docker from Ubuntu (Note: Windows will make your life as a developer more difficult. Consider switching to Linux if you are frustrated by the issues)

Objective 4: Keep-Alive


Goals:

  • Implement Keep-Alive support for your server
  • Reuse TCP connections for multiple requests

Concepts Covered:

  • Keep-Alive
  • Concurrent connections
  • Efficient TCP connection management

Implement Keep-Alive support for your server such that a client is able to send multiple requests, and receive multiple responses, over the same TCP connection.

You must also gracefully handle any connections that have been closed by the client. Keeping goroutines running on a closed connection is not acceptable.

Tips:

  • Since this objective does not add any visible features to your app, you will have to think about how you will test your implementation

Objective 5: Caching


Goals:

  • Add caching to your server to improve performance
  • Use both ETag and timeouts to cache responses

Concepts Covered:

  • HTTP Caching
  • ETag
  • Timeouts (Max-Age or Last-Modified)

The goal for this objective is to set the appropriate headers to add caching support to your server. Note that you will not cache anything, but you are setting headers that will inform the browser how to cache your responses.

You must implement both ETag and timeouts to cache responses. For timeouts, you may choose to use max-age or Last-Modified. If a request is received for content that should be cached, you should return a 304 Not Modified response.

You must cache all your static content which any request that starts with "/public". You may cache more than just static content, but it is not required.

Tips:

  • When using ETags, when the content of the file changes the ETag must also change. For testing, you should change the content of the file and then request it again to ensure the ETag changes
  • You should use a very short timeout (e.g. 10 seconds). This will make testing much faster and simpler

Submission


Submit all files for your server to Autolab in a .zip file

Complete the homework submission form after submitting your zip file to Autolab: Homework Submission Form

If you do not both submit your zip file to Autolab and complete the homework submission form before the due date, your submission will not be graded. Do not be one of the students who will get a 0 for not submitting the form after completing the assignment.

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 --build --force-recreate", then navigate to localhost:8080 in your browser and test all of your features. This simulates exactly what the TAs will do during grading. If you notice that your database is still populated from your prior tests, you may also want to run "docker volume prune" to clear your database and all other volumes on your machine.

If you ignore this recommendation, don't be surprised when you do not earn credit even if your code functions properly on your laptop.


Grading


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

3 (Complete) Clearly correct
2 (Complete) Mostly correct, but with some minor issues
1 (Incomplete) Not all features outlined in this document are functional, but an attempt was made to complete the objective
0 (Incomplete) No attempt to complete the objective or violation of the assignment (Ex. Using an HTTP library)
0 (Security Risk) If any security risk is found while testing, all objectives will be scored 0 unless a proper security essay is submitted

Note that for your final grade there is no difference between a 2 and 3, or a 0 and a 1.

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



Security Essay


If a security risk is found in your submission, you will be assigned a 0 for all 5 objectives. However, you will still have an opportunity to earn credit for the objectives 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

If your submission contains multiple security risks, you may have to write multiple essays to recover your homework grade

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.