1#!/usr/bin/env python 2# 3# Copyright 2018 Espressif Systems (Shanghai) PTE LTD 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17# Utility for testing the web server. Test cases: 18# Assume the device supports 'n' simultaneous open sockets 19# 20# HTTP Server Tests 21# 22# 0. Firmware Settings: 23# - Create a dormant thread whose sole job is to call httpd_stop() when instructed 24# - Measure the following before httpd_start() is called: 25# - current free memory 26# - current free sockets 27# - Measure the same whenever httpd_stop is called 28# - Register maximum possible URI handlers: should be successful 29# - Register one more URI handler: should fail 30# - Deregister on URI handler: should be successful 31# - Register on more URI handler: should succeed 32# - Register separate handlers for /hello, /hello/type_html. Also 33# ensure that /hello/type_html is registered BEFORE /hello. (tests 34# that largest matching URI is picked properly) 35# - Create URI handler /adder. Make sure it uses a custom free_ctx 36# structure to free it up 37 38# 1. Using Standard Python HTTP Client 39# - simple GET on /hello (returns Hello World. Ensures that basic 40# firmware tests are complete, or returns error) 41# - POST on /hello (should fail) 42# - PUT on /hello (should fail) 43# - simple POST on /echo (returns whatever the POST data) 44# - simple PUT on /echo (returns whatever the PUT data) 45# - GET on /echo (should fail) 46# - simple GET on /hello/type_html (returns Content type as text/html) 47# - simple GET on /hello/status_500 (returns HTTP status 500) 48# - simple GET on /false_uri (returns HTTP status 404) 49# - largest matching URI handler is picked is already verified because 50# of /hello and /hello/type_html tests 51# 52# 53# 2. Session Tests 54# - Sessions + Pipelining basics: 55# - Create max supported sessions 56# - On session i, 57# - send 3 back-to-back POST requests with data i on /adder 58# - read back 3 responses. They should be i, 2i and 3i 59# - Tests that 60# - pipelining works 61# - per-session context is maintained for all supported 62# sessions 63# - Close all sessions 64# 65# - Cleanup leftover data: Tests that the web server properly cleans 66# up leftover data 67# - Create a session 68# - POST on /leftover_data with 52 bytes of data (data includes 69# \r\n)(the handler only 70# reads first 10 bytes and returns them, leaving the rest of the 71# bytes unread) 72# - GET on /hello (should return 'Hello World') 73# - POST on /false_uri with 52 bytes of data (data includes \r\n) 74# (should return HTTP 404) 75# - GET on /hello (should return 'Hello World') 76# 77# - Test HTTPd Asynchronous response 78# - Create a session 79# - GET on /async_data 80# - returns 'Hello World!' as a response 81# - the handler schedules an async response, which generates a second 82# response 'Hello Double World!' 83# 84# - Spillover test 85# - Create max supported sessions with the web server 86# - GET /hello on all the sessions (should return Hello World) 87# - Create one more session, this should fail 88# - GET /hello on all the sessions (should return Hello World) 89# 90# - Timeout test 91# - Create a session and only Send 'GE' on the same (simulates a 92# client that left the network halfway through a request) 93# - Wait for recv-wait-timeout 94# - Server should automatically close the socket 95 96 97# ############ TODO TESTS ############# 98 99# 3. Stress Tests 100# 101# - httperf 102# - Run the following httperf command: 103# httperf --server=10.31.130.126 --wsess=8,50,0.5 --rate 8 --burst-length 2 104# 105# - The above implies that the test suite will open 106# - 8 simultaneous connections with the server 107# - the rate of opening the sessions will be 8 per sec. So in our 108# case, a new connection will be opened every 0.2 seconds for 1 second 109# - The burst length 2 indicates that 2 requests will be sent 110# simultaneously on the same connection in a single go 111# - 0.5 seconds is the time between sending out 2 bursts 112# - 50 is the total number of requests that will be sent out 113# 114# - So in the above example, the test suite will open 8 115# connections, each separated by 0.2 seconds. On each connection 116# it will send 2 requests in a single burst. The bursts on a 117# single connection will be separated by 0.5 seconds. A total of 118# 25 bursts (25 x 2 = 50) will be sent out 119 120# 4. Leak Tests 121# - Simple Leak test 122# - Simple GET on /hello/restart (returns success, stop web server, measures leaks, restarts webserver) 123# - Simple GET on /hello/restart_results (returns the leak results) 124# - Leak test with open sockets 125# - Open 8 sessions 126# - Simple GET on /hello/restart (returns success, stop web server, 127# measures leaks, restarts webserver) 128# - All sockets should get closed 129# - Simple GET on /hello/restart_results (returns the leak results) 130 131 132from __future__ import division, print_function 133 134import argparse 135import http.client 136import random 137import socket 138import string 139import sys 140import threading 141import time 142from builtins import object, range, str 143 144try: 145 import Utility 146except ImportError: 147 import os 148 149 # This environment variable is expected on the host machine 150 # > export TEST_FW_PATH=~/esp/esp-idf/tools/tiny-test-fw 151 test_fw_path = os.getenv('TEST_FW_PATH') 152 if test_fw_path and test_fw_path not in sys.path: 153 sys.path.insert(0, test_fw_path) 154 155 import Utility 156 157_verbose_ = False 158 159 160class Session(object): 161 def __init__(self, addr, port, timeout=15): 162 self.client = socket.create_connection((addr, int(port)), timeout=timeout) 163 self.target = addr 164 self.status = 0 165 self.encoding = '' 166 self.content_type = '' 167 self.content_len = 0 168 169 def send_err_check(self, request, data=None): 170 rval = True 171 try: 172 self.client.sendall(request.encode()) 173 if data: 174 self.client.sendall(data.encode()) 175 except socket.error as err: 176 self.client.close() 177 Utility.console_log('Socket Error in send :', err) 178 rval = False 179 return rval 180 181 def send_get(self, path, headers=None): 182 request = 'GET ' + path + ' HTTP/1.1\r\nHost: ' + self.target 183 if headers: 184 for field, value in headers.items(): 185 request += '\r\n' + field + ': ' + value 186 request += '\r\n\r\n' 187 return self.send_err_check(request) 188 189 def send_put(self, path, data, headers=None): 190 request = 'PUT ' + path + ' HTTP/1.1\r\nHost: ' + self.target 191 if headers: 192 for field, value in headers.items(): 193 request += '\r\n' + field + ': ' + value 194 request += '\r\nContent-Length: ' + str(len(data)) + '\r\n\r\n' 195 return self.send_err_check(request, data) 196 197 def send_post(self, path, data, headers=None): 198 request = 'POST ' + path + ' HTTP/1.1\r\nHost: ' + self.target 199 if headers: 200 for field, value in headers.items(): 201 request += '\r\n' + field + ': ' + value 202 request += '\r\nContent-Length: ' + str(len(data)) + '\r\n\r\n' 203 return self.send_err_check(request, data) 204 205 def read_resp_hdrs(self): 206 try: 207 state = 'nothing' 208 resp_read = '' 209 while True: 210 char = self.client.recv(1).decode() 211 if char == '\r' and state == 'nothing': 212 state = 'first_cr' 213 elif char == '\n' and state == 'first_cr': 214 state = 'first_lf' 215 elif char == '\r' and state == 'first_lf': 216 state = 'second_cr' 217 elif char == '\n' and state == 'second_cr': 218 state = 'second_lf' 219 else: 220 state = 'nothing' 221 resp_read += char 222 if state == 'second_lf': 223 break 224 # Handle first line 225 line_hdrs = resp_read.splitlines() 226 line_comp = line_hdrs[0].split() 227 self.status = line_comp[1] 228 del line_hdrs[0] 229 self.encoding = '' 230 self.content_type = '' 231 headers = dict() 232 # Process other headers 233 for h in range(len(line_hdrs)): 234 line_comp = line_hdrs[h].split(':') 235 if line_comp[0] == 'Content-Length': 236 self.content_len = int(line_comp[1]) 237 if line_comp[0] == 'Content-Type': 238 self.content_type = line_comp[1].lstrip() 239 if line_comp[0] == 'Transfer-Encoding': 240 self.encoding = line_comp[1].lstrip() 241 if len(line_comp) == 2: 242 headers[line_comp[0]] = line_comp[1].lstrip() 243 return headers 244 except socket.error as err: 245 self.client.close() 246 Utility.console_log('Socket Error in recv :', err) 247 return None 248 249 def read_resp_data(self): 250 try: 251 read_data = '' 252 if self.encoding != 'chunked': 253 while len(read_data) != self.content_len: 254 read_data += self.client.recv(self.content_len).decode() 255 else: 256 chunk_data_buf = '' 257 while (True): 258 # Read one character into temp buffer 259 read_ch = self.client.recv(1) 260 # Check CRLF 261 if (read_ch == '\r'): 262 read_ch = self.client.recv(1).decode() 263 if (read_ch == '\n'): 264 # If CRLF decode length of chunk 265 chunk_len = int(chunk_data_buf, 16) 266 # Keep adding to contents 267 self.content_len += chunk_len 268 rem_len = chunk_len 269 while (rem_len): 270 new_data = self.client.recv(rem_len) 271 read_data += new_data 272 rem_len -= len(new_data) 273 chunk_data_buf = '' 274 # Fetch remaining CRLF 275 if self.client.recv(2) != '\r\n': 276 # Error in packet 277 Utility.console_log('Error in chunked data') 278 return None 279 if not chunk_len: 280 # If last chunk 281 break 282 continue 283 chunk_data_buf += '\r' 284 # If not CRLF continue appending 285 # character to chunked data buffer 286 chunk_data_buf += read_ch 287 return read_data 288 except socket.error as err: 289 self.client.close() 290 Utility.console_log('Socket Error in recv :', err) 291 return None 292 293 def close(self): 294 self.client.close() 295 296 297def test_val(text, expected, received): 298 if expected != received: 299 Utility.console_log(' Fail!') 300 Utility.console_log(' [reason] ' + text + ':') 301 Utility.console_log(' expected: ' + str(expected)) 302 Utility.console_log(' received: ' + str(received)) 303 return False 304 return True 305 306 307class adder_thread (threading.Thread): 308 def __init__(self, id, dut, port): 309 threading.Thread.__init__(self) 310 self.id = id 311 self.dut = dut 312 self.depth = 3 313 self.session = Session(dut, port) 314 315 def run(self): 316 self.response = [] 317 318 # Pipeline 3 requests 319 if (_verbose_): 320 Utility.console_log(' Thread: Using adder start ' + str(self.id)) 321 322 for _ in range(self.depth): 323 self.session.send_post('/adder', str(self.id)) 324 time.sleep(2) 325 326 for _ in range(self.depth): 327 self.session.read_resp_hdrs() 328 self.response.append(self.session.read_resp_data()) 329 330 def adder_result(self): 331 if len(self.response) != self.depth: 332 Utility.console_log('Error : missing response packets') 333 return False 334 for i in range(len(self.response)): 335 if not test_val('Thread' + str(self.id) + ' response[' + str(i) + ']', 336 str(self.id * (i + 1)), str(self.response[i])): 337 return False 338 return True 339 340 def close(self): 341 self.session.close() 342 343 344def get_hello(dut, port): 345 # GET /hello should return 'Hello World!' 346 Utility.console_log("[test] GET /hello returns 'Hello World!' =>", end=' ') 347 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 348 conn.request('GET', '/hello') 349 resp = conn.getresponse() 350 if not test_val('status_code', 200, resp.status): 351 conn.close() 352 return False 353 if not test_val('data', 'Hello World!', resp.read().decode()): 354 conn.close() 355 return False 356 if not test_val('data', 'text/html', resp.getheader('Content-Type')): 357 conn.close() 358 return False 359 Utility.console_log('Success') 360 conn.close() 361 return True 362 363 364def put_hello(dut, port): 365 # PUT /hello returns 405' 366 Utility.console_log('[test] PUT /hello returns 405 =>', end=' ') 367 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 368 conn.request('PUT', '/hello', 'Hello') 369 resp = conn.getresponse() 370 if not test_val('status_code', 405, resp.status): 371 conn.close() 372 return False 373 Utility.console_log('Success') 374 conn.close() 375 return True 376 377 378def post_hello(dut, port): 379 # POST /hello returns 405' 380 Utility.console_log('[test] POST /hello returns 405 =>', end=' ') 381 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 382 conn.request('POST', '/hello', 'Hello') 383 resp = conn.getresponse() 384 if not test_val('status_code', 405, resp.status): 385 conn.close() 386 return False 387 Utility.console_log('Success') 388 conn.close() 389 return True 390 391 392def post_echo(dut, port): 393 # POST /echo echoes data' 394 Utility.console_log('[test] POST /echo echoes data =>', end=' ') 395 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 396 conn.request('POST', '/echo', 'Hello') 397 resp = conn.getresponse() 398 if not test_val('status_code', 200, resp.status): 399 conn.close() 400 return False 401 if not test_val('data', 'Hello', resp.read().decode()): 402 conn.close() 403 return False 404 Utility.console_log('Success') 405 conn.close() 406 return True 407 408 409def put_echo(dut, port): 410 # PUT /echo echoes data' 411 Utility.console_log('[test] PUT /echo echoes data =>', end=' ') 412 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 413 conn.request('PUT', '/echo', 'Hello') 414 resp = conn.getresponse() 415 if not test_val('status_code', 200, resp.status): 416 conn.close() 417 return False 418 if not test_val('data', 'Hello', resp.read().decode()): 419 conn.close() 420 return False 421 Utility.console_log('Success') 422 conn.close() 423 return True 424 425 426def get_echo(dut, port): 427 # GET /echo returns 404' 428 Utility.console_log('[test] GET /echo returns 405 =>', end=' ') 429 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 430 conn.request('GET', '/echo') 431 resp = conn.getresponse() 432 if not test_val('status_code', 405, resp.status): 433 conn.close() 434 return False 435 Utility.console_log('Success') 436 conn.close() 437 return True 438 439 440def get_test_headers(dut, port): 441 # GET /test_header returns data of Header2' 442 Utility.console_log('[test] GET /test_header =>', end=' ') 443 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 444 custom_header = {'Header1': 'Value1', 'Header3': 'Value3'} 445 header2_values = ['', ' ', 'Value2', ' Value2', 'Value2 ', ' Value2 '] 446 for val in header2_values: 447 custom_header['Header2'] = val 448 conn.request('GET', '/test_header', headers=custom_header) 449 resp = conn.getresponse() 450 if not test_val('status_code', 200, resp.status): 451 conn.close() 452 return False 453 hdr_val_start_idx = val.find('Value2') 454 if hdr_val_start_idx == -1: 455 if not test_val('header: Header2', '', resp.read().decode()): 456 conn.close() 457 return False 458 else: 459 if not test_val('header: Header2', val[hdr_val_start_idx:], resp.read().decode()): 460 conn.close() 461 return False 462 resp.read() 463 Utility.console_log('Success') 464 conn.close() 465 return True 466 467 468def get_hello_type(dut, port): 469 # GET /hello/type_html returns text/html as Content-Type' 470 Utility.console_log('[test] GET /hello/type_html has Content-Type of text/html =>', end=' ') 471 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 472 conn.request('GET', '/hello/type_html') 473 resp = conn.getresponse() 474 if not test_val('status_code', 200, resp.status): 475 conn.close() 476 return False 477 if not test_val('data', 'Hello World!', resp.read().decode()): 478 conn.close() 479 return False 480 if not test_val('data', 'text/html', resp.getheader('Content-Type')): 481 conn.close() 482 return False 483 Utility.console_log('Success') 484 conn.close() 485 return True 486 487 488def get_hello_status(dut, port): 489 # GET /hello/status_500 returns status 500' 490 Utility.console_log('[test] GET /hello/status_500 returns status 500 =>', end=' ') 491 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 492 conn.request('GET', '/hello/status_500') 493 resp = conn.getresponse() 494 if not test_val('status_code', 500, resp.status): 495 conn.close() 496 return False 497 Utility.console_log('Success') 498 conn.close() 499 return True 500 501 502def get_false_uri(dut, port): 503 # GET /false_uri returns status 404' 504 Utility.console_log('[test] GET /false_uri returns status 404 =>', end=' ') 505 conn = http.client.HTTPConnection(dut, int(port), timeout=15) 506 conn.request('GET', '/false_uri') 507 resp = conn.getresponse() 508 if not test_val('status_code', 404, resp.status): 509 conn.close() 510 return False 511 Utility.console_log('Success') 512 conn.close() 513 return True 514 515 516def parallel_sessions_adder(dut, port, max_sessions): 517 # POSTs on /adder in parallel sessions 518 Utility.console_log('[test] POST {pipelined} on /adder in ' + str(max_sessions) + ' sessions =>', end=' ') 519 t = [] 520 # Create all sessions 521 for i in range(max_sessions): 522 t.append(adder_thread(i, dut, port)) 523 524 for i in range(len(t)): 525 t[i].start() 526 527 for i in range(len(t)): 528 t[i].join() 529 530 res = True 531 for i in range(len(t)): 532 if not test_val('Thread' + str(i) + ' Failed', t[i].adder_result(), True): 533 res = False 534 t[i].close() 535 if (res): 536 Utility.console_log('Success') 537 return res 538 539 540def async_response_test(dut, port): 541 # Test that an asynchronous work is executed in the HTTPD's context 542 # This is tested by reading two responses over the same session 543 Utility.console_log('[test] Test HTTPD Work Queue (Async response) =>', end=' ') 544 s = Session(dut, port) 545 546 s.send_get('/async_data') 547 s.read_resp_hdrs() 548 if not test_val('First Response', 'Hello World!', s.read_resp_data()): 549 s.close() 550 return False 551 s.read_resp_hdrs() 552 if not test_val('Second Response', 'Hello Double World!', s.read_resp_data()): 553 s.close() 554 return False 555 s.close() 556 Utility.console_log('Success') 557 return True 558 559 560def leftover_data_test(dut, port): 561 # Leftover data in POST is purged (valid and invalid URIs) 562 Utility.console_log('[test] Leftover data in POST is purged (valid and invalid URIs) =>', end=' ') 563 s = http.client.HTTPConnection(dut + ':' + port, timeout=15) 564 565 s.request('POST', url='/leftover_data', body='abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz') 566 resp = s.getresponse() 567 if not test_val('Partial data', 'abcdefghij', resp.read().decode()): 568 s.close() 569 return False 570 571 s.request('GET', url='/hello') 572 resp = s.getresponse() 573 if not test_val('Hello World Data', 'Hello World!', resp.read().decode()): 574 s.close() 575 return False 576 577 s.request('POST', url='/false_uri', body='abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz') 578 resp = s.getresponse() 579 if not test_val('False URI Status', str(404), str(resp.status)): 580 s.close() 581 return False 582 # socket would have been closed by server due to error 583 s.close() 584 585 s = http.client.HTTPConnection(dut + ':' + port, timeout=15) 586 s.request('GET', url='/hello') 587 resp = s.getresponse() 588 if not test_val('Hello World Data', 'Hello World!', resp.read().decode()): 589 s.close() 590 return False 591 592 s.close() 593 Utility.console_log('Success') 594 return True 595 596 597def spillover_session(dut, port, max_sess): 598 # Session max_sess_sessions + 1 is rejected 599 Utility.console_log('[test] Session max_sess_sessions (' + str(max_sess) + ') + 1 is rejected =>', end=' ') 600 s = [] 601 _verbose_ = True 602 for i in range(max_sess + 1): 603 if (_verbose_): 604 Utility.console_log('Executing ' + str(i)) 605 try: 606 a = http.client.HTTPConnection(dut + ':' + port, timeout=15) 607 a.request('GET', url='/hello') 608 resp = a.getresponse() 609 if not test_val('Connection ' + str(i), 'Hello World!', resp.read().decode()): 610 a.close() 611 break 612 s.append(a) 613 except Exception: 614 if (_verbose_): 615 Utility.console_log('Connection ' + str(i) + ' rejected') 616 a.close() 617 break 618 619 # Close open connections 620 for a in s: 621 a.close() 622 623 # Check if number of connections is equal to max_sess 624 Utility.console_log(['Fail','Success'][len(s) == max_sess]) 625 return (len(s) == max_sess) 626 627 628def recv_timeout_test(dut, port): 629 Utility.console_log('[test] Timeout occurs if partial packet sent =>', end=' ') 630 s = Session(dut, port) 631 s.client.sendall(b'GE') 632 s.read_resp_hdrs() 633 resp = s.read_resp_data() 634 if not test_val('Request Timeout', 'Server closed this connection', resp): 635 s.close() 636 return False 637 s.close() 638 Utility.console_log('Success') 639 return True 640 641 642def packet_size_limit_test(dut, port, test_size): 643 Utility.console_log('[test] send size limit test =>', end=' ') 644 retry = 5 645 while (retry): 646 retry -= 1 647 Utility.console_log('data size = ', test_size) 648 s = http.client.HTTPConnection(dut + ':' + port, timeout=15) 649 random_data = ''.join(string.printable[random.randint(0,len(string.printable)) - 1] for _ in list(range(test_size))) 650 path = '/echo' 651 s.request('POST', url=path, body=random_data) 652 resp = s.getresponse() 653 if not test_val('Error', '200', str(resp.status)): 654 if test_val('Error', '500', str(resp.status)): 655 Utility.console_log('Data too large to be allocated') 656 test_size = test_size // 10 657 else: 658 Utility.console_log('Unexpected error') 659 s.close() 660 Utility.console_log('Retry...') 661 continue 662 resp = resp.read().decode() 663 result = (resp == random_data) 664 if not result: 665 test_val('Data size', str(len(random_data)), str(len(resp))) 666 s.close() 667 Utility.console_log('Retry...') 668 continue 669 s.close() 670 Utility.console_log('Success') 671 return True 672 Utility.console_log('Failed') 673 return False 674 675 676def arbitrary_termination_test(dut, port): 677 Utility.console_log('[test] Arbitrary termination test =>', end=' ') 678 cases = [ 679 { 680 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nCustom: SomeValue\r\n\r\n', 681 'code': '200', 682 'header': 'SomeValue' 683 }, 684 { 685 'request': 'POST /echo HTTP/1.1\nHost: ' + dut + '\r\nCustom: SomeValue\r\n\r\n', 686 'code': '200', 687 'header': 'SomeValue' 688 }, 689 { 690 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\nCustom: SomeValue\r\n\r\n', 691 'code': '200', 692 'header': 'SomeValue' 693 }, 694 { 695 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nCustom: SomeValue\n\r\n', 696 'code': '200', 697 'header': 'SomeValue' 698 }, 699 { 700 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nCustom: SomeValue\r\n\n', 701 'code': '200', 702 'header': 'SomeValue' 703 }, 704 { 705 'request': 'POST /echo HTTP/1.1\nHost: ' + dut + '\nCustom: SomeValue\n\n', 706 'code': '200', 707 'header': 'SomeValue' 708 }, 709 { 710 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nContent-Length: 5\n\r\nABCDE', 711 'code': '200', 712 'body': 'ABCDE' 713 }, 714 { 715 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nContent-Length: 5\r\n\nABCDE', 716 'code': '200', 717 'body': 'ABCDE' 718 }, 719 { 720 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nContent-Length: 5\n\nABCDE', 721 'code': '200', 722 'body': 'ABCDE' 723 }, 724 { 725 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nContent-Length: 5\n\n\rABCD', 726 'code': '200', 727 'body': '\rABCD' 728 }, 729 { 730 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\r\nCustom: SomeValue\r\r\n\r\r\n', 731 'code': '400' 732 }, 733 { 734 'request': 'POST /echo HTTP/1.1\r\r\nHost: ' + dut + '\r\n\r\n', 735 'code': '400' 736 }, 737 { 738 'request': 'POST /echo HTTP/1.1\r\n\rHost: ' + dut + '\r\n\r\n', 739 'code': '400' 740 }, 741 { 742 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\rCustom: SomeValue\r\n', 743 'code': '400' 744 }, 745 { 746 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nCustom: Some\rValue\r\n', 747 'code': '400' 748 }, 749 { 750 'request': 'POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nCustom- SomeValue\r\n\r\n', 751 'code': '400' 752 } 753 ] 754 for case in cases: 755 s = Session(dut, port) 756 s.client.sendall((case['request']).encode()) 757 resp_hdrs = s.read_resp_hdrs() 758 resp_body = s.read_resp_data() 759 s.close() 760 if not test_val('Response Code', case['code'], s.status): 761 return False 762 if 'header' in case.keys(): 763 resp_hdr_val = None 764 if 'Custom' in resp_hdrs.keys(): 765 resp_hdr_val = resp_hdrs['Custom'] 766 if not test_val('Response Header', case['header'], resp_hdr_val): 767 return False 768 if 'body' in case.keys(): 769 if not test_val('Response Body', case['body'], resp_body): 770 return False 771 Utility.console_log('Success') 772 return True 773 774 775def code_500_server_error_test(dut, port): 776 Utility.console_log('[test] 500 Server Error test =>', end=' ') 777 s = Session(dut, port) 778 # Sending a very large content length will cause malloc to fail 779 content_len = 2**30 780 s.client.sendall(('POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nContent-Length: ' + str(content_len) + '\r\n\r\nABCD').encode()) 781 s.read_resp_hdrs() 782 s.read_resp_data() 783 if not test_val('Server Error', '500', s.status): 784 s.close() 785 return False 786 s.close() 787 Utility.console_log('Success') 788 return True 789 790 791def code_501_method_not_impl(dut, port): 792 Utility.console_log('[test] 501 Method Not Implemented =>', end=' ') 793 s = Session(dut, port) 794 path = '/hello' 795 s.client.sendall(('ABC ' + path + ' HTTP/1.1\r\nHost: ' + dut + '\r\n\r\n').encode()) 796 s.read_resp_hdrs() 797 s.read_resp_data() 798 # Presently server sends back 400 Bad Request 799 # if not test_val("Server Error", "501", s.status): 800 # s.close() 801 # return False 802 if not test_val('Server Error', '400', s.status): 803 s.close() 804 return False 805 s.close() 806 Utility.console_log('Success') 807 return True 808 809 810def code_505_version_not_supported(dut, port): 811 Utility.console_log('[test] 505 Version Not Supported =>', end=' ') 812 s = Session(dut, port) 813 path = '/hello' 814 s.client.sendall(('GET ' + path + ' HTTP/2.0\r\nHost: ' + dut + '\r\n\r\n').encode()) 815 s.read_resp_hdrs() 816 s.read_resp_data() 817 if not test_val('Server Error', '505', s.status): 818 s.close() 819 return False 820 s.close() 821 Utility.console_log('Success') 822 return True 823 824 825def code_400_bad_request(dut, port): 826 Utility.console_log('[test] 400 Bad Request =>', end=' ') 827 s = Session(dut, port) 828 path = '/hello' 829 s.client.sendall(('XYZ ' + path + ' HTTP/1.1\r\nHost: ' + dut + '\r\n\r\n').encode()) 830 s.read_resp_hdrs() 831 s.read_resp_data() 832 if not test_val('Client Error', '400', s.status): 833 s.close() 834 return False 835 s.close() 836 Utility.console_log('Success') 837 return True 838 839 840def code_404_not_found(dut, port): 841 Utility.console_log('[test] 404 Not Found =>', end=' ') 842 s = Session(dut, port) 843 path = '/dummy' 844 s.client.sendall(('GET ' + path + ' HTTP/1.1\r\nHost: ' + dut + '\r\n\r\n').encode()) 845 s.read_resp_hdrs() 846 s.read_resp_data() 847 if not test_val('Client Error', '404', s.status): 848 s.close() 849 return False 850 s.close() 851 Utility.console_log('Success') 852 return True 853 854 855def code_405_method_not_allowed(dut, port): 856 Utility.console_log('[test] 405 Method Not Allowed =>', end=' ') 857 s = Session(dut, port) 858 path = '/hello' 859 s.client.sendall(('POST ' + path + ' HTTP/1.1\r\nHost: ' + dut + '\r\n\r\n').encode()) 860 s.read_resp_hdrs() 861 s.read_resp_data() 862 if not test_val('Client Error', '405', s.status): 863 s.close() 864 return False 865 s.close() 866 Utility.console_log('Success') 867 return True 868 869 870def code_408_req_timeout(dut, port): 871 Utility.console_log('[test] 408 Request Timeout =>', end=' ') 872 s = Session(dut, port) 873 s.client.sendall(('POST /echo HTTP/1.1\r\nHost: ' + dut + '\r\nContent-Length: 10\r\n\r\nABCD').encode()) 874 s.read_resp_hdrs() 875 s.read_resp_data() 876 if not test_val('Client Error', '408', s.status): 877 s.close() 878 return False 879 s.close() 880 Utility.console_log('Success') 881 return True 882 883 884def code_411_length_required(dut, port): 885 Utility.console_log('[test] 411 Length Required =>', end=' ') 886 s = Session(dut, port) 887 path = '/echo' 888 s.client.sendall(('POST ' + path + ' HTTP/1.1\r\nHost: ' + dut + '\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n').encode()) 889 s.read_resp_hdrs() 890 s.read_resp_data() 891 # Presently server sends back 400 Bad Request 892 # if not test_val("Client Error", "411", s.status): 893 # s.close() 894 # return False 895 if not test_val('Client Error', '400', s.status): 896 s.close() 897 return False 898 s.close() 899 Utility.console_log('Success') 900 return True 901 902 903def send_getx_uri_len(dut, port, length): 904 s = Session(dut, port) 905 method = 'GET ' 906 version = ' HTTP/1.1\r\n' 907 path = '/' + 'x' * (length - len(method) - len(version) - len('/')) 908 s.client.sendall(method.encode()) 909 time.sleep(1) 910 s.client.sendall(path.encode()) 911 time.sleep(1) 912 s.client.sendall((version + 'Host: ' + dut + '\r\n\r\n').encode()) 913 s.read_resp_hdrs() 914 s.read_resp_data() 915 s.close() 916 return s.status 917 918 919def code_414_uri_too_long(dut, port, max_uri_len): 920 Utility.console_log('[test] 414 URI Too Long =>', end=' ') 921 status = send_getx_uri_len(dut, port, max_uri_len) 922 if not test_val('Client Error', '404', status): 923 return False 924 status = send_getx_uri_len(dut, port, max_uri_len + 1) 925 if not test_val('Client Error', '414', status): 926 return False 927 Utility.console_log('Success') 928 return True 929 930 931def send_postx_hdr_len(dut, port, length): 932 s = Session(dut, port) 933 path = '/echo' 934 host = 'Host: ' + dut 935 custom_hdr_field = '\r\nCustom: ' 936 custom_hdr_val = 'x' * (length - len(host) - len(custom_hdr_field) - len('\r\n\r\n') + len('0')) 937 request = ('POST ' + path + ' HTTP/1.1\r\n' + host + custom_hdr_field + custom_hdr_val + '\r\n\r\n').encode() 938 s.client.sendall(request[:length // 2]) 939 time.sleep(1) 940 s.client.sendall(request[length // 2:]) 941 hdr = s.read_resp_hdrs() 942 resp = s.read_resp_data() 943 s.close() 944 if hdr and ('Custom' in hdr): 945 return (hdr['Custom'] == custom_hdr_val), resp 946 return False, s.status 947 948 949def code_431_hdr_too_long(dut, port, max_hdr_len): 950 Utility.console_log('[test] 431 Header Too Long =>', end=' ') 951 res, status = send_postx_hdr_len(dut, port, max_hdr_len) 952 if not res: 953 return False 954 res, status = send_postx_hdr_len(dut, port, max_hdr_len + 1) 955 if not test_val('Client Error', '431', status): 956 return False 957 Utility.console_log('Success') 958 return True 959 960 961def test_upgrade_not_supported(dut, port): 962 Utility.console_log('[test] Upgrade Not Supported =>', end=' ') 963 s = Session(dut, port) 964 # path = "/hello" 965 s.client.sendall(('OPTIONS * HTTP/1.1\r\nHost:' + dut + '\r\nUpgrade: TLS/1.0\r\nConnection: Upgrade\r\n\r\n').encode()) 966 s.read_resp_hdrs() 967 s.read_resp_data() 968 if not test_val('Client Error', '400', s.status): 969 s.close() 970 return False 971 s.close() 972 Utility.console_log('Success') 973 return True 974 975 976if __name__ == '__main__': 977 # Execution begins here... 978 # Configuration 979 # Max number of threads/sessions 980 max_sessions = 7 981 max_uri_len = 512 982 max_hdr_len = 512 983 984 parser = argparse.ArgumentParser(description='Run HTTPD Test') 985 parser.add_argument('-4','--ipv4', help='IPv4 address') 986 parser.add_argument('-6','--ipv6', help='IPv6 address') 987 parser.add_argument('-p','--port', help='Port') 988 args = vars(parser.parse_args()) 989 990 dut4 = args['ipv4'] 991 dut6 = args['ipv6'] 992 port = args['port'] 993 dut = dut4 994 995 _verbose_ = True 996 997 Utility.console_log('### Basic HTTP Client Tests') 998 get_hello(dut, port) 999 post_hello(dut, port) 1000 put_hello(dut, port) 1001 post_echo(dut, port) 1002 get_echo(dut, port) 1003 put_echo(dut, port) 1004 get_hello_type(dut, port) 1005 get_hello_status(dut, port) 1006 get_false_uri(dut, port) 1007 get_test_headers(dut, port) 1008 1009 Utility.console_log('### Error code tests') 1010 code_500_server_error_test(dut, port) 1011 code_501_method_not_impl(dut, port) 1012 code_505_version_not_supported(dut, port) 1013 code_400_bad_request(dut, port) 1014 code_404_not_found(dut, port) 1015 code_405_method_not_allowed(dut, port) 1016 code_408_req_timeout(dut, port) 1017 code_414_uri_too_long(dut, port, max_uri_len) 1018 code_431_hdr_too_long(dut, port, max_hdr_len) 1019 test_upgrade_not_supported(dut, port) 1020 1021 # Not supported yet (Error on chunked request) 1022 # code_411_length_required(dut, port) 1023 1024 Utility.console_log('### Sessions and Context Tests') 1025 parallel_sessions_adder(dut, port, max_sessions) 1026 leftover_data_test(dut, port) 1027 async_response_test(dut, port) 1028 spillover_session(dut, port, max_sessions) 1029 recv_timeout_test(dut, port) 1030 packet_size_limit_test(dut, port, 50 * 1024) 1031 arbitrary_termination_test(dut, port) 1032 get_hello(dut, port) 1033 1034 sys.exit() 1035