| 1 | // vtest flaky: true |
| 2 | // vtest retry: 3 |
| 3 | module main |
| 4 | |
| 5 | import net |
| 6 | import net.html |
| 7 | import net.http |
| 8 | import net.mbedtls |
| 9 | import sync.pool |
| 10 | import time |
| 11 | |
| 12 | const concurrent_large_response_requests = 6 |
| 13 | const concurrent_large_response_workers = 3 |
| 14 | const concurrent_large_response_links = 2_000 |
| 15 | const concurrent_large_response_chunk_size = 8_192 |
| 16 | const concurrent_large_response_test_cert = '-----BEGIN CERTIFICATE-----\nMIIEOTCCAyECFG64Q2g46jZb3kRbDOJWX/BwjSp6MA0GCSqGSIb3DQEBCwUAMEUx\nCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl\ncm5ldCBXaWRnaXRzIFB0eSBMdGQwIBcNMjMwODAyMTcyOTQyWhgPMjA1MDEyMTcx\nNzI5NDJaMGsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYD\nVQQHDAtMb3MgQW5nZWxlczEdMBsGA1UECgwUQ2F0YWx5c3QgRGV2ZWxvcG1lbnQx\nEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC\nggIBALqAI4fqUi+QBVWcsXglouLdOML5+w0+1hSR1KdO0Q5XPdQAs/yYWJ+KUkDw\nG++rfy9DUPq7FNRBVurXQkcAtn6gXdllGUSjwUiDo/N4mMOyS/2sufBuaeww7jVi\nrppH+zwP1tUnjRd6khl6bi1Ian9VSzr3Iy9CkXIg1GU4CPXkOydLeoQfepXxWoK1\nOUNwT3VKC/stAfY3j/NIIeiJYkyuRGFCkxn/BUjN+AsXiTugRcYKEFHdIPkOuCXp\nYbhf+lLsczpxCs3rdZG9b/N6mEDCzXTmeHkmsjdPTf+1k5DZZvKzVBBrgdxCgBb7\n5RwjF5v9WmnIc33wWgfJC6FaUzj9NYxYUbPHD+jTz0rJB/jj4u/xJlM/e5NRmXdW\n70pOMKXtWjRSolLOFIPKLY1qs3KMTAZxKKWPDDF7WlMJxMRt7nnnks5yw43Nog4C\njDLk1ZgETnPpLgo3jbmJdIv+OHKTJrBlVvDq7VTyixCoS5G8KoOmyQJhaXG6NwE2\niVhH5JIKgzgCfetfDsnjxqJ/qtrFXPa8FF2TsomD0NK/GZmIcs+9OeVB75Jn5uhF\nfLHScpiTbuu5w3P/LI/MqihLRB6RRNnRzPH8fIg5bYC9b770ta/8GcFRuYE8t+UR\nGtqXJoIKixbDlqV54kal8FQzYzhETf9+NM6Kb/lKEfG/pslvAgMBAAEwDQYJKoZI\nhvcNAQELBQADggEBALI3uNiNO0QE1brA3QYFK+d9ZroB72NrJ0UNkzYHDg2Fc6xg\n4aVVfaxY08+TmKc0JlMOW+pUxeCW/+UBSngdQiR9EE9xm0k0XIrAsy9RXxRvEtPu\nM1VI2h7ayp1Y2BrnQinevTSgtqLRyS1VbOFRl1FiyVvinw2I0KsDdAMNevAPXcOa\nQ8pUgUq6f56DkhocQaj+hxD/uV8HryNxuoSXnPhvfTN3z4YRGzsaWevJ9EYJliOM\n+XugcqfFJ+W7/QCEcAHCL+Bw6OydG5NFORr3p57PXjjcL/uKmxPBrWg2Bz6uT4uR\nMhj0zttiFHLAt9jGfyk6W57UNUja1e1ggftJJhs=\n-----END CERTIFICATE-----\n' |
| 17 | const concurrent_large_response_test_key = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n' |
| 18 | |
| 19 | struct ConcurrentLargeResponseContext { |
| 20 | expected_body string |
| 21 | expected_link_count int |
| 22 | } |
| 23 | |
| 24 | struct ConcurrentLargeResponseResult { |
| 25 | status_code int |
| 26 | body_matches bool |
| 27 | link_count int |
| 28 | err string |
| 29 | } |
| 30 | |
| 31 | fn test_https_large_bodies_can_be_processed_concurrently() { |
| 32 | // Watchdog thread to prevent the test from hanging indefinitely on CI. |
| 33 | spawn fn () { |
| 34 | time.sleep(120 * time.second) |
| 35 | eprintln('mbedtls concurrent large response test: timeout reached, exiting.') |
| 36 | exit(1) |
| 37 | }() |
| 38 | |
| 39 | mut port_listener := net.listen_tcp(.ip, '127.0.0.1:0')! |
| 40 | port := port_listener.addr()!.port()! |
| 41 | port_listener.close()! |
| 42 | |
| 43 | mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', mbedtls.SSLConnectConfig{ |
| 44 | cert: concurrent_large_response_test_cert |
| 45 | cert_key: concurrent_large_response_test_key |
| 46 | validate: false |
| 47 | in_memory_verification: true |
| 48 | })! |
| 49 | server := spawn serve_large_html_responses(mut listener, concurrent_large_response_body(), |
| 50 | concurrent_large_response_requests) |
| 51 | |
| 52 | mut urls := []string{cap: concurrent_large_response_requests} |
| 53 | for i in 0 .. concurrent_large_response_requests { |
| 54 | urls << 'https://127.0.0.1:${port}/?req=${i}' |
| 55 | } |
| 56 | expected_body := concurrent_large_response_body() |
| 57 | mut pp := pool.new_pool_processor(callback: concurrent_large_response_worker) |
| 58 | pp.set_max_jobs(concurrent_large_response_workers) |
| 59 | pp.set_shared_context(&ConcurrentLargeResponseContext{ |
| 60 | expected_body: expected_body |
| 61 | expected_link_count: concurrent_large_response_links |
| 62 | }) |
| 63 | pp.work_on_items(urls) |
| 64 | server.wait() |
| 65 | |
| 66 | for result in pp.get_results_ref[ConcurrentLargeResponseResult]() { |
| 67 | assert result.err == '' |
| 68 | assert result.status_code == 200 |
| 69 | assert result.body_matches |
| 70 | assert result.link_count == concurrent_large_response_links |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | fn test_https_request_timeout_closes_the_connection() { |
| 75 | mut port_listener := net.listen_tcp(.ip, '127.0.0.1:0')! |
| 76 | port := port_listener.addr()!.port()! |
| 77 | port_listener.close()! |
| 78 | |
| 79 | mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', mbedtls.SSLConnectConfig{ |
| 80 | cert: concurrent_large_response_test_cert |
| 81 | cert_key: concurrent_large_response_test_key |
| 82 | validate: false |
| 83 | in_memory_verification: true |
| 84 | })! |
| 85 | server := spawn serve_incomplete_https_response(mut listener) |
| 86 | |
| 87 | mut req := http.new_request(.get, 'https://127.0.0.1:${port}', '') |
| 88 | req.read_timeout = 250 * time.millisecond |
| 89 | req.validate = false |
| 90 | req.do() or { |
| 91 | assert server.wait() |
| 92 | return |
| 93 | } |
| 94 | panic('expected the HTTPS request to time out') |
| 95 | } |
| 96 | |
| 97 | fn test_https_chunked_body_is_returned_by_http_get_text() { |
| 98 | mut port_listener := net.listen_tcp(.ip, '127.0.0.1:0')! |
| 99 | port := port_listener.addr()!.port()! |
| 100 | port_listener.close()! |
| 101 | |
| 102 | mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', mbedtls.SSLConnectConfig{ |
| 103 | cert: concurrent_large_response_test_cert |
| 104 | cert_key: concurrent_large_response_test_key |
| 105 | validate: false |
| 106 | in_memory_verification: true |
| 107 | })! |
| 108 | expected_body := concurrent_large_response_body() |
| 109 | server := spawn serve_chunked_html_response(mut listener, expected_body) |
| 110 | |
| 111 | body := http.get_text('https://127.0.0.1:${port}/chunked') |
| 112 | server.wait() |
| 113 | |
| 114 | assert body == expected_body |
| 115 | } |
| 116 | |
| 117 | fn concurrent_large_response_body() string { |
| 118 | return '<html><body>' + |
| 119 | '<article><a href="/item">payload</a></article>'.repeat(concurrent_large_response_links) + |
| 120 | '</body></html>' |
| 121 | } |
| 122 | |
| 123 | fn serve_incomplete_https_response(mut listener mbedtls.SSLListener) bool { |
| 124 | defer { |
| 125 | listener.shutdown() or {} |
| 126 | } |
| 127 | mut conn := listener.accept() or { panic(err) } |
| 128 | defer { |
| 129 | conn.shutdown() or {} |
| 130 | } |
| 131 | mut request_buf := []u8{len: 2048} |
| 132 | _ = conn.read(mut request_buf) or { return false } |
| 133 | conn.write_string('HTTP/1.1 200 OK\r\nContent-Length: 2\r\n') or { return false } |
| 134 | conn.set_read_timeout(time.second) |
| 135 | mut drain_buf := []u8{len: 128} |
| 136 | _ = conn.read(mut drain_buf) or { return err.code() != net.err_timed_out_code } |
| 137 | return false |
| 138 | } |
| 139 | |
| 140 | fn serve_large_html_responses(mut listener mbedtls.SSLListener, body string, request_count int) { |
| 141 | defer { |
| 142 | listener.shutdown() or {} |
| 143 | } |
| 144 | mut handlers := []thread{cap: request_count} |
| 145 | for _ in 0 .. request_count { |
| 146 | mut conn := listener.accept() or { panic(err) } |
| 147 | handlers << spawn write_large_html_response(mut conn, body) |
| 148 | } |
| 149 | handlers.wait() |
| 150 | } |
| 151 | |
| 152 | fn serve_chunked_html_response(mut listener mbedtls.SSLListener, body string) { |
| 153 | defer { |
| 154 | listener.shutdown() or {} |
| 155 | } |
| 156 | mut conn := listener.accept() or { panic(err) } |
| 157 | write_chunked_html_response(mut conn, body) |
| 158 | } |
| 159 | |
| 160 | fn write_large_html_response(mut conn mbedtls.SSLConn, body string) { |
| 161 | defer { |
| 162 | conn.shutdown() or {} |
| 163 | } |
| 164 | mut request_buf := []u8{len: 2048} |
| 165 | _ = conn.read(mut request_buf) or { return } |
| 166 | header := 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: ${body.len}\r\nConnection: close\r\n\r\n' |
| 167 | conn.write_string(header) or { return } |
| 168 | mut start := 0 |
| 169 | for start < body.len { |
| 170 | end := if start + concurrent_large_response_chunk_size > body.len { |
| 171 | body.len |
| 172 | } else { |
| 173 | start + concurrent_large_response_chunk_size |
| 174 | } |
| 175 | conn.write_string(body[start..end]) or { return } |
| 176 | start = end |
| 177 | time.sleep(1 * time.millisecond) |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | fn write_chunked_html_response(mut conn mbedtls.SSLConn, body string) { |
| 182 | defer { |
| 183 | conn.shutdown() or {} |
| 184 | } |
| 185 | mut request_buf := []u8{len: 2048} |
| 186 | _ = conn.read(mut request_buf) or { return } |
| 187 | header := 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n' |
| 188 | conn.write_string(header) or { return } |
| 189 | mut start := 0 |
| 190 | for start < body.len { |
| 191 | end := if start + concurrent_large_response_chunk_size > body.len { |
| 192 | body.len |
| 193 | } else { |
| 194 | start + concurrent_large_response_chunk_size |
| 195 | } |
| 196 | chunk := body[start..end] |
| 197 | conn.write_string('${chunk.len:x}\r\n') or { return } |
| 198 | conn.write_string(chunk) or { return } |
| 199 | conn.write_string('\r\n') or { return } |
| 200 | start = end |
| 201 | time.sleep(1 * time.millisecond) |
| 202 | } |
| 203 | conn.write_string('0\r\n\r\n') or { return } |
| 204 | } |
| 205 | |
| 206 | fn concurrent_large_response_worker(mut pp pool.PoolProcessor, idx int, _wid int) &ConcurrentLargeResponseResult { |
| 207 | url := pp.get_item[string](idx) |
| 208 | ctx := unsafe { &ConcurrentLargeResponseContext(pp.get_shared_context()) } |
| 209 | resp := http.fetch( |
| 210 | method: .get |
| 211 | url: url |
| 212 | validate: false |
| 213 | ) or { return &ConcurrentLargeResponseResult{ |
| 214 | err: '${err}' |
| 215 | } } |
| 216 | doc := html.parse(resp.body) |
| 217 | return &ConcurrentLargeResponseResult{ |
| 218 | status_code: resp.status_code |
| 219 | body_matches: resp.body == ctx.expected_body |
| 220 | link_count: doc.get_tags(name: 'a').len |
| 221 | } |
| 222 | } |
| 223 | |