test.py 35 KB

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