Homework 4 - WebSockets


Jump to current week

Introduction


You will continue to add features to your app and pull from the repo to get the new front end features.

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

If you haven't already on HW3, please go to this line in your server.py: "socketserver.TCPServer.allow_reuse_address = True" and "server = socketserver.TCPServer((host, port), MyTCPHandler)". Change both of these lines from TCPServer to ThreadingTCPServer. This will make sure that your server can handle multiple requests at once, without blocking other requests.


Learning Objective - WebSockets



Helper Method: compute_accept

In a file named websockets in the util package, write a function named compute_accept that takes a WebSocket key as a parameter (As a string) and returns the correct accept (As a string) according to the WebSocket handshake.

The output must be character for character exactly what is expected. Make sure you don't have any extra characters in your output (Not even white space).

You may use libraries to compute the SHA1 hash and Base64 encoding.


Helper Method: parse_ws_frame

Add a function named parse_ws_frame to the util.websockets file.

Add a function named parse_ws_frame to the util.websockets file that takes bytes as a parameter that represents the bytes of a WebSocket frame and parses all the values of the frame. The function returns an object containing the following fields (You have some freedom in how you design the class for this object as long as it has the required fields).

  • fin_bit
    • An int with the value of the fin bit (Either 1 or 0)
  • opcode
    • An int with the value of the opcode (eg. if the op code is bx1000, this field stores 8 as an int)
  • payload_length
    • The payload length as an int. Your function must handle all 3 payload length modes
  • payload
    • The unmasked bytes of the payload

Helper Method: generate_ws_frame

Add a function named generate_ws_frame to the util.websockets file.

Add a function named generate_ws_frame to the util.websockets file that takes bytes as a parameter and returns a properly formatted WebSocket frame (As bytes) with the input bytes as its payload. Use a fin bit of 1, an op code of bx0001 for text, and no mask. You need to handle all 3 payload length modes.



WebSocket Connections

With your 3 helper methods ready, you will now add WebSocket features to your app. First, add the following HTML paths to your server:

  • "/test-websocket" - Render test-websocket.html
  • "/drawing-board" - Render drawing-board.html
  • "/direct-messaging" - Render direct-messaging.html (AO2)
  • "/video-call" - Render video-call.html (AO3)
  • "/video-call/{roomID}" - Render video-call-room.html (AO3)

The front end will initiate a WebSocket connection using the "/websocket" path. When you receive a request on this path, you should go through the WS handshake to upgrade the connection.

When the connection is established, all WS message will be in the form of JSON objects and will all contain a field named "messageType". This messageType will determine how to handle the message.



WebSocket Echo

Implement an echo feature that will be used on the Test WebSocket page. For this feature, whenever you receive a WS message of type "echo_client", read the message from the "text" field which is what the user typed and sent. In response, send a WS message back to only the sender of type "echo_server" with a field named "text" containing the exact message that was sent to your server.

For example, if your server receives a WS frame containing the payload:

{"messageType":"echo_client","text":"WebSockets are for winners"}

You will send a WS frame back to the sender containing the payload:

{"messageType":"echo_server","text":"WebSockets are for winners"}

These message can, and will during testing, be large enough to test all 3 payload length modes. This also implies that you must implement buffering for the WebSocket frames you receive. Continuation frames are not required for this objective.



Drawing Board

Implement the shared drawing board from the Drawing Board page. When a user draws, the client will send WS messages of type "drawing" to your server. When you receive a message of this type, you will:

  • Broadcast the message to every user with an active WS connection. The message contents should be unchanged, including the same messageType of "drawing". The front end will know how to display these messages
  • Store all the information of the message in your database (This includes the fields "startX", "startY", "endX", "endY", and "color")

When a user first creates a WS connection, send them the drawings that have already been made (It is ok if you always send this even when the user is not on the Drawing Board page). This should be sent in a WS message of type "init_strokes" containing a field "strokes" that is an array of all the drawing messages received by your server. This feature will allow users to see all the drawings that have been made before they loaded the page since only the new drawings are broadcast. You should now be able to refresh the page and still see all the drawings.

Example Message: {"messageType": "init_strokes", "strokes": [{"startX": 283.1, "startY": 232.7, "endX": 287.5, "endY": 152.2, "color": "#000000"}, ... ]}

Note that you will need a way to store all WS connections in a way that can be accessed from all connections to enable broadcasting.



User Lists

Send a message of type "active_users_list" containing a field named "users" with an array containing all the authenticated users who have an active WS connection. Each user is represented as an object with a field "username"

Example Message: {"messageType": "active_users_list", "users": [{"username": "admin"}, {"username": "cse312"}]}

Broadcast this message to all WS users whenever this list changes. This includes whenever you finish a WS handshake (adding a user), and whenever you see a disconnect opcode (removing a user).



Application Objective 1 - Continuation Frames and Back-to-Back Messages


Expand your WebSocket code to support the processing of multiple frames under the following two conditions:

  • Continuation Frames
    • Check the FIN bit and handle messages that are sent over multiple frames. We will test with payloads > 131000 using chrome which sends messages over this size in multiple frames
  • Back-to-Back Frames
    • Your server must also handle multiple messages sent back-to-back. If another frame is sent while you are buffering, your last read from the TCP socket may contain the beginning of the next frame. Ensure that you are properly parsing these frames even when one read from the socket contains parts of multiple frames

To complete this objective, your server must handle the case where multiple messages requiring continuation frames are sent back-to-back.



Application Objective 2 - Direct Messaging (DMs)


Implement DMs on the Direct Messaging page. To accomplish this, add the following functionality to your server:

  • When a client sends a WS message of type "get_all_users", send them a list of all users who've registered on your app
    • You'll receive a message in this format - {"messageType":"select_user","targetUser":"koby"}
    • Send the user list in this format - {"messageType": "all_users_list", "users": [{"username": "admin"}, {"username": "cse312"}]}
    • Note that you're returning all users whether they are currently using the app or not. Return ever registered user. This will populate the dropdown list for DMs
  • When a client sends a WS message of type "select_user", which means they chose this user from the DM dropdown, send them all the DM history between them and the chosen user
    • You'll receive a message in this format - {"messageType":"select_user","targetUser":"koby"}
    • Send this user a WS message of type "message_history" containing all the DMs sent between these 2 users sorted by age (Oldest first)
    • Message history format - {"messageType": "message_history", "messages": [{"messageType": "direct_message", "fromUser": "admin", "text": "hi"}, {"messageType": "direct_message", "fromUser": "koby", "text": "hey"}]}
  • Users send message DMs as WS messages of type "direct_message"
    • DM format (from client) - {"messageType":"direct_message", "targetUser":"koby", "text":"hi"}
    • When you receive a DM, store it in your database and send the message to both clients in the conversation
    • DM format (from server) - {"messageType": "direct_message", "fromUser": "admin", "text": "hey"}

Security: Do not send DMs to anyone other than the 2 users who are part of the conversation.



Application Objective 3 - Video Calls with WebRTC


Build a Zoom clone. This feature will allow users to create video call "rooms" that can be joined by other users. Each room can contain any subset of users, and they can all share audio and video in peer-to-peer connections using WebRTC.

Add a POST endpoint at the path "/api/video-calls" that will create a new video call. The body of the request will be a JSON object with a field named "name" that should be stored as the name of the room. Your app will respond with the id of the room (Generated by you) in a JSON object (eg. {"id":"67e42b3b0cb5bda054049243"}).

To enable this feature, you'll need to support the following WS messages:

  • messageType - "get_calls"
    • Sent by the client. This is a request for a list of all the calls so the Video Call page can populate all the available calls
  • messageType - "call_list"
    • Sent by your server in response to a get_calls message. This is a list of every available call in a JSON object with the fields "id" and "name"
    • Example message - {"messageType": "call_list", "calls": [{"id": "67e426f9042e96b4cfaa70f2", "name": "meeting"}, ... ]}
  • messageType - "join_call"
    • Sent by the client when they join a video call. Contains the id of the room being joined
    • Example message - {"messageType":"join_call", "callId":"67e426f9042e96b4cfaa70f2"}
  • messageType - "call_info"
    • Sent by your server when a user joins a call. Contains the name of the room
    • Example message - {"messageType":"call_info", "name":"meeting"}
  • messageType - "existing_participants"
    • Sent by your server to let the users of a room know who else is on the call. Send this to a user when they join the room in reaction to the "join_call" message. Their frontend will then send messages to start all the required webRTC connections
    • Example message - {"messageType": "existing_participants", "participants": [{"socketId": "febd4d3a-9e61-4a95-8e52-37ba64e2674c", "username": "admin"}, ... ]}
  • messageType - "user_joined"
    • Sent by your server when a user joins the room. Broadcast this to all users in the room each time a user joins
    • Example message - {"messageType": "user_joined", "socketId": "0a8e1a10-7f20-4797-bd3e-628735f91216", "username": "jesse"}
  • messageType - "offer", "answer", and "ice_candidate"
    • Sent by the client. Each of these message types contain the corresponding webRTC information to establish a connection. Each of these messages will contain a "socketId" field. You should forward the message to the user matching this socket id with a "socketId" field populated with the sender's socket id and a "username" field containing the senders username (The recipient doesn't need to know their own socket id/username, but they do need to know the sender)
  • messageType - "user_left"
    • Sent by your server to all users in a call when one of them disconnects their WS. Must contain their "socketId" in a field


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 honest attempt was made to complete the objective
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