From 43dbbe35cf1d0d9f73dafdf43fe33c3d8076a85d Mon Sep 17 00:00:00 2001 From: cwshugg Date: Tue, 26 Apr 2022 14:36:46 -0400 Subject: [PATCH 1/5] minor bugfix in error handling for video streaming test, and a message printed when '-t' can't find a specified test --- tests/server_unit_test_pserv.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/server_unit_test_pserv.py b/tests/server_unit_test_pserv.py index 0d2620d..854b31e 100755 --- a/tests/server_unit_test_pserv.py +++ b/tests/server_unit_test_pserv.py @@ -2262,7 +2262,7 @@ class VideoStreaming(Doc_Print_Test_Case): # make sure the correct value is given ("bytes") if "bytes" not in accept_ranges: raise AssertionError("Server responded with an unexpected Accept-Ranges values. " - "Expected: %s, received: %s" % (accept_ranges_expect, response.headers[header])) + "Expected: %s, received: %s" % (accept_ranges_expect, response.headers["Accept-Ranges"])) # if no occurrences were found, throw an error if occurrences == 0: @@ -2762,6 +2762,12 @@ process. if individual_test is not None: single_test_suite = unittest.TestSuite() testclass = findtest(individual_test) + + # make sure the class was found + if testclass == None: + print("Couldn't find a test with the name '%s'" % individual_test) + sys.exit(1) + if testclass == Authentication: killserver(server) server.wait() From c4c35394f45ecb33bea6540ce8c13f3eedd06050 Mon Sep 17 00:00:00 2001 From: cwshugg Date: Wed, 27 Apr 2022 18:45:44 -0400 Subject: [PATCH 2/5] fixed various testing issues with 'test_video_range_request', 'test_accept_ranges_header', and added a new test to ensure students are sending the correct Content-Type in responses to /api/login requests ('test_login_content_type') --- tests/server_unit_test_pserv.py | 99 +++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/tests/server_unit_test_pserv.py b/tests/server_unit_test_pserv.py index 854b31e..a0be263 100755 --- a/tests/server_unit_test_pserv.py +++ b/tests/server_unit_test_pserv.py @@ -1375,6 +1375,17 @@ class Access_Control(Doc_Print_Test_Case): # Close the HTTP connection self.session.close() + # =============================== Helpers ================================ # + # Does a lower-case search for headers within a response's headers. If + # found, the first ocurrence is returned (the header's value is returned). + def find_header(self, response, name): + for header in response.headers: + if header.lower() == name.lower(): + return response.headers[header] + return None + + + # ================================ Tests ================================= # def test_access_control_private_valid_token(self): """ Test Name: test_access_control_private_valid_token Number Connections: N/A @@ -1690,6 +1701,56 @@ class Access_Control(Doc_Print_Test_Case): self.assertEqual(response.status_code, requests.codes.not_found, "Server did not respond with 404 when it should have, possible IDOR?") + def test_login_content_type(self): + """ Test Name: test_login_content_type + Number Connections: N/A + Procedure: Checks to ensure the Content-Type header is being sent in + responses to GETs and POSTs to /api/login (both with AND + without Cookie headers). A failure here means either: + - 'Content-Type' is not a part of some or all of your /api/login responses, OR + - The value of your 'Content-Type' header is not what it should be. + """ + # inner helper function that takes a response and checks for the correct + # content-type header + def check_content_type(response): + # search for the content-type header and ensure we see "application/json" + content_type = self.find_header(response, "Content-Type") + content_expect = "application/json" + if content_type == None: + raise AssertionError("Server didn't respond with the Content-Type header when sent a GET request to /api/login") + if content_type.lower() != content_expect: + raise AssertionError("Server didn't respond with the correct Content-Type value when sent a GET request to /api/login. " + "Expected: '%s', received: '%s'" % (content_expect, content_type)) + + # first, we'll build the /api/login url + login_url = "http://%s:%s/api/login" % (self.hostname, self.port) + + # TEST 1: send a simple GET /api/login with NO COOKIE + try: + response = self.session.get(login_url, timeout=2) + check_content_type(response) + except requests.exceptions.RequestException: + raise AssertionError("The server did not respond within 2s") + + # TEST 2: try sending a POST /api/login with the correct credentials + try: + response = self.session.post(login_url, + json={'username': self.username, 'password': self.password}, + timeout=2) + check_content_type(response) + # Ensure that the user is authenticated + self.assertEqual(response.status_code, requests.codes.ok, "Authentication failed.") + except requests.exceptions.RequestException: + raise AssertionError("The server did not respond within 2s") + + # TEST 3: send one more GET /api/login with the cookie we just received + try: + response = self.session.get(login_url, timeout=2) + check_content_type(response) + except requests.exceptions.RequestException: + raise AssertionError("The server did not respond within 2s") + + class Fallback(Doc_Print_Test_Case): """ Test cases for HTML 5 fallback, using good requests that expect a @@ -1933,7 +1994,7 @@ class Authentication(Doc_Print_Test_Case): pool = ThreadPool(30) pool.map(test_expiry_authentication, range(30)) pool.terminate() - + def test_jwt_claims_json(self): """ Test Name: test_jwt_claims_json Number Connections: N/A @@ -2252,7 +2313,7 @@ class VideoStreaming(Doc_Print_Test_Case): # make sure the correct status code was received if response.status_code != requests.codes.ok: raise AssertionError("Server responded with %d instead of 200 OK when requested with %s" % - response.status_code, resource) + (response.status_code, resource)) # search the header dictionary (lowercase comparison) for Accept-Ranges accept_ranges_expect = "bytes" @@ -2266,7 +2327,8 @@ class VideoStreaming(Doc_Print_Test_Case): # if no occurrences were found, throw an error if occurrences == 0: - raise AssertionError("Failed to find the Accept-Ranges header in the server's responses.") + raise AssertionError("Failed to find the Accept-Ranges header in the server's responses. " + "Your server must send 'Accept-Ranges: bytes' in its HTTP responses when serving static files.") def test_video_get(self): """ Test Name: test_video_get @@ -2329,7 +2391,7 @@ class VideoStreaming(Doc_Print_Test_Case): vidsize = os.path.getsize(self.vids[0]) url = "http://%s:%s/%s" % (self.hostname, self.port, vid) # set up a few range request values to test with the video - ranges = [[0, 1], [0, 100], [300, 500], [1000, -1], [-1, 1000]] + ranges = [[0, 1], [0, 100], [300, 500], [1000, -1]]#, [-1, 1000]] # iterate across each range array to test each one for rg in ranges: @@ -2347,21 +2409,22 @@ class VideoStreaming(Doc_Print_Test_Case): prepared_req.url = url response = self.session.send(prepared_req, timeout=2) except requests.exceptions.RequestException: - raise AssertionError("The server did not respond within 2s") + raise AssertionError("The server did not respond within 2s\nRange request sent: '%s'" % rgheader) # make sure the correct status code was received if response.status_code != requests.codes.partial_content: - raise AssertionError("Server responded with %d instead of 206 PARTIAL CONTENT when range-requested with a valid video" % - response.status_code) + raise AssertionError("Server responded with %d instead of 206 PARTIAL CONTENT when range-requested with a valid video" + "\nRange request sent: '%s'" % (response.status_code, rgheader)) # check for the content-type header content_type = self.find_header(response, "Content-Type") content_expect = "video/mp4" if content_type == None: - raise AssertionError("Server didn't respond with the Content-Type header when requested with a valid video") + raise AssertionError("Server didn't respond with the Content-Type header when requested with a valid video" + "\nRange request sent: '%s'" % rgheader) if content_type.lower() != content_expect: raise AssertionError("Server didn't respond with the correct Content-Type value when requested with a valid video. " - "Expected: %s, received: %s" % (content_expect, content_type)) + "Expected: %s, received: %s\nRange request sent: '%s'" % (content_expect, content_type, rgheader)) # check for the content-length header and make sure it's the correct # value based on the current range value we're trying @@ -2372,27 +2435,29 @@ class VideoStreaming(Doc_Print_Test_Case): elif rg[1] == -1: content_length_expect = vidsize - rg[0] if content_length == None: - raise AssertionError("Server didn't respond with the Content-Length header when requested with a valid video") + raise AssertionError("Server didn't respond with the Content-Length header when requested with a valid video" + "\nRange request sent: '%s'" % rgheader) if content_length != str(content_length_expect): raise AssertionError("Server didn't respond with the correct Content-Length value when requested with a valid video. " - "Expected: %s, received: %s" % (content_length_expect, content_length)) + "Expected: %s, received: %s\nRange request sent: '%s'" % (content_length_expect, content_length, rgheader)) # check for the Content-Range header and make sure it's the correct # value content_range = self.find_header(response, "Content-Range") byte_start = rg[0] if rg[0] != -1 else vidsize - rg[1] - content_range_expect = "bytes %d-%d/%d" % (byte_start, byte_start + content_length_expect, vidsize) + content_range_expect = "bytes %d-%d/%d" % (byte_start, byte_start + content_length_expect - 1, vidsize) if content_range == None: - raise AssertionError("Server didn't respond with the Content-Range header when requested with a valid video") - if content_type.lower() != content_expect: + raise AssertionError("Server didn't respond with the Content-Range header when requested with a valid video" + "\nRange request sent: '%s'" % rgheader) + if content_range.lower() != content_range_expect: raise AssertionError("Server didn't respond with the correct Content-Range value when requested with a valid video. " - "Expected: '%s', received: '%s'" % (content_range_expect, content_range)) + "Expected: '%s', received: '%s'\nRange request sent: '%s'" % (content_range_expect, content_range, rgheader)) # finally, we'll compare the actual bytes that were received. They # must match the exact bytes found in the original file if not self.compare_file_bytes(self.vids[0], response, byte_start, content_length_expect): - raise AssertionError("Server didn't send the correct bytes. Should have been bytes %d-%d" % - (byte_start, byte_start + content_length_expect - 1)) + raise AssertionError("Server didn't send the correct bytes. Should have been bytes %d-%d" + "\nRange request sent: '%s'" % (byte_start, byte_start + content_length_expect - 1, rgheader)) ############################################################################### From 21327b0f76462c20a6d962e1b942590a0d66eebf Mon Sep 17 00:00:00 2001 From: cwshugg Date: Wed, 27 Apr 2022 21:18:03 -0400 Subject: [PATCH 3/5] tweaked feedback for test_two_connections, test_four_connections, and test_eight_connections, to ensure students know they require HTTP/1.1 persistent connection support --- tests/server_unit_test_pserv.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/server_unit_test_pserv.py b/tests/server_unit_test_pserv.py index a0be263..bd0700b 100755 --- a/tests/server_unit_test_pserv.py +++ b/tests/server_unit_test_pserv.py @@ -95,7 +95,9 @@ def run_connection_check_empty_login(http_conn, hostname): server_response = http_conn.getresponse() # Check the response status code - assert server_response.status == OK, "Server failed to respond" + assert server_response.status == OK, "Server failed to respond. " + "This test will fail until persistent connections are implemented (i.e. HTTP/1.1 support). " + "We recommend you implement this before moving forward." # Check the data included in the server's response assert check_empty_login_respnse(server_response.read().decode('utf-8')), \ @@ -1196,7 +1198,8 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case): """ Test Name: test_two_connections\n\ Number Connections: 2 \n\ Procedure: Run 2 connections simultaneously for simple GET requests:\n\ - GET /api/login HTTP/1.1 + GET /api/login HTTP/1.1 + NOTE: this test requires HTTP/1.1 persistent connection support. """ # Append two connections to the list @@ -1220,10 +1223,11 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case): def test_four_connections(self): - """ Test Name: test_two_connections\n\ + """ Test Name: test_four_connections\n\ Number Connections: 4 \n\ Procedure: Run 4 connections simultaneously for simple GET requests:\n\ - GET /api/login HTTP/1.1 + GET /api/login HTTP/1.1 + NOTE: this test requires HTTP/1.1 persistent connection support. """ # Append four connections to the list @@ -1243,10 +1247,11 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case): run_connection_check_empty_login(http_conn, self.hostname) def test_eight_connections(self): - """ Test Name: test_two_connections\n\ + """ Test Name: test_eight_connections\n\ Number Connections: 8 \n\ Procedure: Run 8 connections simultaneously for simple GET requests:\n\ - GET /api/login HTTP/1.1 + GET /api/login HTTP/1.1 + NOTE: this test requires HTTP/1.1 persistent connection support. """ # Append eight connections to the list From 721256a343ae50f19e44e316ba09da7a18c0e266 Mon Sep 17 00:00:00 2001 From: cwshugg Date: Wed, 27 Apr 2022 21:41:42 -0400 Subject: [PATCH 4/5] added onto JSON-claim-checking test (for /api/login) in order to verify servers correctly send JSON claims on a request to GET /api/login with a valid cookie. Also, modified the range-request-checking function to search for the Accept-Range header ONLY in requests to static files --- tests/server_unit_test_pserv.py | 45 +++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/tests/server_unit_test_pserv.py b/tests/server_unit_test_pserv.py index bd0700b..2806fff 100755 --- a/tests/server_unit_test_pserv.py +++ b/tests/server_unit_test_pserv.py @@ -95,8 +95,8 @@ def run_connection_check_empty_login(http_conn, hostname): server_response = http_conn.getresponse() # Check the response status code - assert server_response.status == OK, "Server failed to respond. " - "This test will fail until persistent connections are implemented (i.e. HTTP/1.1 support). " + assert server_response.status == OK, "Server failed to respond. " \ + "This test will fail until persistent connections are implemented (i.e. HTTP/1.1 support). " \ "We recommend you implement this before moving forward." # Check the data included in the server's response @@ -2012,6 +2012,7 @@ class Authentication(Doc_Print_Test_Case): self.sessions.append(requests.Session()) for i in range(30): + # ----------------------- Login JSON Check ----------------------- # # Login using the default credentials try: response = self.sessions[i].post('http://%s:%s/api/login' % (self.hostname, self.port), @@ -2027,19 +2028,13 @@ class Authentication(Doc_Print_Test_Case): # Convert the response to JSON data = response.json() - # Verify that the JWT contains 'iat' + # ensure all expected fields are present assert 'iat' in data, "Could not find the claim 'iat' in the JSON object." - - # Verify that the JWT contains 'iat' assert 'exp' in data, "Could not find the claim 'exp' in the JSON object." - - # Verify that the JWT contains 'sub' assert 'sub' in data, "Could not find the claim 'sub' in the JSON object." - # Verify that the 'iat' claim to is a valid date from self.current_year + # verify that the two timestamps are valid dates assert datetime.fromtimestamp(data['iat']).year == self.current_year, "'iat' returned is not a valid date" - - # Verify that the 'exp' claim to is a valid date from self.current_year assert datetime.fromtimestamp(data['exp']).year == self.current_year, "'exp' returned is not a valid date" # Verify that the subject claim to is set to the right username @@ -2048,6 +2043,34 @@ class Authentication(Doc_Print_Test_Case): except ValueError: raise AssertionError('The login API did not return a valid JSON object') + # --------------------- Login GET JSON Check --------------------- # + # send a GET request to retrieve the same claims as above + try: + response = self.sessions[i].get('http://%s:%s/api/login' % (self.hostname, self.port), + timeout=2) + except requests.exceptions.RequestException: + raise AssertionError("The server did not respond within 2s") + + try: + # Convert the response to JSON + data = response.json() + + # ensure all expected fields are present + assert 'iat' in data, "Could not find the claim 'iat' in the JSON object." + assert 'exp' in data, "Could not find the claim 'exp' in the JSON object." + assert 'sub' in data, "Could not find the claim 'sub' in the JSON object." + + # Verify that the two timestamps are valid dates from self.current_year + assert datetime.fromtimestamp(data['iat']).year == self.current_year, "'iat' returned is not a valid date" + assert datetime.fromtimestamp(data['exp']).year == self.current_year, "'exp' returned is not a valid date" + + # Verify that the subject claim to is set to the right username + assert data['sub'] == self.username, "The subject claim 'sub' should be set to %s" % self.username + + except ValueError: + raise AssertionError('The login GET API did not return a valid JSON object') + + # Sleep for a short duration before testing again time.sleep(random.random() / 10.0) @@ -2299,7 +2322,7 @@ class VideoStreaming(Doc_Print_Test_Case): """ # build a collection of URLs to try url_prefix = "http://%s:%s" % (self.hostname, self.port) - resources = ["/", "/index.html", "/public/index.html", "/api/login", "/api/video", "/v1.mp4"] + resources = ["/index.html", "/public/index.html", "/v1.mp4"] # do the following for each URL occurrences = 0 From a5525414c8f156d6744ce6049d295979a4b1887c Mon Sep 17 00:00:00 2001 From: cwshugg Date: Thu, 28 Apr 2022 11:52:38 -0400 Subject: [PATCH 5/5] small fix for a test's feedback --- tests/server_unit_test_pserv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/server_unit_test_pserv.py b/tests/server_unit_test_pserv.py index 2806fff..9eaa5b6 100755 --- a/tests/server_unit_test_pserv.py +++ b/tests/server_unit_test_pserv.py @@ -1722,9 +1722,9 @@ class Access_Control(Doc_Print_Test_Case): content_type = self.find_header(response, "Content-Type") content_expect = "application/json" if content_type == None: - raise AssertionError("Server didn't respond with the Content-Type header when sent a GET request to /api/login") + raise AssertionError("Server didn't respond with the Content-Type header when sent a request to /api/login") if content_type.lower() != content_expect: - raise AssertionError("Server didn't respond with the correct Content-Type value when sent a GET request to /api/login. " + raise AssertionError("Server didn't respond with the correct Content-Type value when sent a request to /api/login. " "Expected: '%s', received: '%s'" % (content_expect, content_type)) # first, we'll build the /api/login url