UNIXworkcode

1 /* 2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 3 * 4 * Copyright 2026 Olaf Wintermann. All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 * POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 30 #include "httpclient.h" 31 32 #include "../util/socket.h" 33 #include "../daemon/event.h" 34 #include "../proxy/httpclient.h" 35 36 #include <cx/buffer.h> 37 #include <cx/string.h> 38 39 #include <pthread.h> 40 41 static EVHandler *test_eventhandler; 42 static pthread_mutex_t test_mutex; 43 static pthread_cond_t test_cond; 44 static volatile int test_finished; 45 46 void http_client_tests_init(void) { 47 pthread_mutex_init(&test_mutex, NULL); 48 pthread_cond_init(&test_cond, NULL); 49 50 EventHandlerConfig config; 51 config.isdefault = 0; 52 config.name = cx_str(NULL); 53 config.nthreads = 1; 54 test_eventhandler = evhandler_create(&config); 55 } 56 57 void http_client_tests_cleanup(void) { 58 ev_instance_shutdown(test_eventhandler->instances[0]); 59 } 60 61 static void test_response_finished(HttpClient *client, void *userdata) { 62 pthread_mutex_lock(&test_mutex); 63 pthread_cond_signal(&test_cond); 64 pthread_mutex_unlock(&test_mutex); 65 http_client_free(client); 66 test_finished = 1; 67 } 68 69 static ssize_t test_response_body_write(HttpClient *client, void *buf, size_t nbytes, void *userdata) { 70 char *str = buf; 71 return cxBufferWrite(str, 1, nbytes, userdata); 72 } 73 74 static cxstring request_body_str; 75 static int request_body_pos = 0; 76 static size_t request_body_max_read = 16; 77 78 ssize_t test_request_body_read(HttpClient *client, void *buf, size_t nbytes, void *userdata) { 79 if(nbytes > request_body_max_read) { 80 nbytes = request_body_max_read; 81 } 82 size_t available = request_body_str.length - request_body_pos; 83 if(nbytes > available) { 84 nbytes = available; 85 } 86 if(nbytes > 0) { 87 memcpy(buf, request_body_str.ptr + request_body_pos, nbytes); 88 request_body_pos += nbytes; 89 } 90 return nbytes; 91 } 92 93 CX_TEST_SUBROUTINE(test_httpclient, cxstring request_body, int chunked_transfer, cxstring *response_blocks, size_t num_blocks, CxBuffer *out) { 94 HttpClient *client = http_client_new(test_eventhandler->instances[0]); 95 client->response_body_write = test_response_body_write; 96 client->response_body_write_userdata = out; 97 client->response_finished = test_response_finished; 98 99 int fds[2]; 100 util_socketpair(fds); 101 util_socket_setnonblock(fds[0], 1); 102 103 http_client_set_socket(client, fds[0]); 104 http_client_set_method(client, "GET"); 105 http_client_set_uri(client, "/testx01"); 106 107 request_body_str = request_body; 108 request_body_pos = 0; 109 if(request_body.length > 0) { 110 client->request_body_read = test_request_body_read; 111 if(chunked_transfer) { 112 http_client_enable_chunked_transfer_encoding(client); 113 } else { 114 http_client_set_content_length(client, request_body.length); 115 } 116 } 117 118 test_finished = 0; 119 int ret = http_client_start(client); 120 CX_TEST_ASSERT(ret == 0); 121 122 while(client->stage == 0) { 123 char buf[4096]; 124 if(read(fds[1], buf, 4096) <= 0) { 125 break; 126 } 127 } 128 129 for(int i=0;i<num_blocks;i++) { 130 size_t response_pos = 0; 131 cxstring response = response_blocks[i]; 132 while(response_pos < response.length) { 133 size_t nbytes = response.length - response_pos; 134 const char *str = response.ptr + response_pos; 135 ssize_t w = write(fds[1], str, nbytes); 136 CX_TEST_ASSERT(w >= 0); 137 response_pos += w; 138 } 139 } 140 141 pthread_mutex_lock(&test_mutex); 142 if(test_finished == 0) { 143 pthread_cond_wait(&test_cond, &test_mutex); 144 } 145 pthread_mutex_unlock(&test_mutex); 146 147 close(fds[1]); 148 } 149 150 CX_TEST(test_http_client_simple_get1) { 151 CX_TEST_DO { 152 cxstring response = cx_str( 153 "HTTP/1.1 200 OK\r\n" 154 "Content-length: 13\r\n" 155 "\r\n" 156 "Hello World!\n"); 157 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 158 159 CX_TEST_CALL_SUBROUTINE(test_httpclient, cx_str(NULL), FALSE, &response, 1, out); 160 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello World!\n")); 161 162 cxBufferFree(out); 163 } 164 } 165 166 CX_TEST(test_http_client_simple_get_line_io) { 167 CX_TEST_DO { 168 cxstring response[8]; 169 response[0] = cx_str("HTTP/1.1 200 OK\r\n"); 170 response[1] = cx_str("Content-length: 13\r\n"); 171 response[2] = cx_str("\r\n"); 172 response[3] = cx_str("Hello World!\n"); 173 174 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 175 176 CX_TEST_CALL_SUBROUTINE(test_httpclient, cx_str(NULL), FALSE, response, 4, out); 177 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello World!\n")); 178 179 cxBufferFree(out); 180 } 181 } 182 183 CX_TEST(test_http_client_simple_get_small_blocksize) { 184 CX_TEST_DO { 185 cxstring response[16]; 186 response[0] = cx_str("HTTP/1.1"); 187 response[1] = cx_str(" 2"); 188 response[2] = cx_str("00 "); 189 response[3] = cx_str(" O"); 190 response[4] = cx_str("K\r\nContent-"); 191 response[5] = cx_str("length:"); 192 response[6] = cx_str(" 26"); 193 response[7] = cx_str("\r"); 194 response[8] = cx_str("\nHost"); 195 response[9] = cx_str(": localhost\r\n\r\nHello"); 196 response[10] = cx_str(" World!\n"); 197 response[11] = cx_str("He"); 198 response[12] = cx_str("llo Wor"); 199 response[13] = cx_str("l"); 200 response[14] = cx_str("d!"); 201 response[15] = cx_str("\n"); 202 203 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 204 205 CX_TEST_CALL_SUBROUTINE(test_httpclient, cx_str(NULL), FALSE, response, 16, out); 206 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello World!\nHello World!\n")); 207 208 cxBufferFree(out); 209 } 210 } 211 212 CX_TEST(test_http_client_simple_get_1b_blocksize) { 213 CX_TEST_DO { 214 cxstring response_str = cx_str( 215 "HTTP/1.1 200 OK\r\n" 216 "Content-length: 13\r\n" 217 "\r\n" 218 "Hello World!\n"); 219 cxstring *response = calloc(response_str.length, sizeof(cxstring)); 220 for(int i=0;i<response_str.length;i++) { 221 response[i] = cx_strn(response_str.ptr+i, 1); 222 } 223 224 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 225 226 CX_TEST_CALL_SUBROUTINE(test_httpclient, cx_str(NULL), FALSE, response, response_str.length, out); 227 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello World!\n")); 228 229 cxBufferFree(out); 230 free(response); 231 } 232 } 233 234 CX_TEST(test_http_client_post_ctlen) { 235 CX_TEST_DO { 236 cxstring response_str = cx_str( 237 "HTTP/1.1 200 OK\r\n" 238 "Content-length: 21\r\n" 239 "\r\n" 240 "Hello World!---post1\n"); 241 cxstring *response = calloc(response_str.length, sizeof(cxstring)); 242 for(int i=0;i<response_str.length;i++) { 243 response[i] = cx_strn(response_str.ptr+i, 1); 244 } 245 246 cxstring request_body = cx_str("test request body, needs more than one read (len > 16)\n"); 247 248 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 249 250 CX_TEST_CALL_SUBROUTINE(test_httpclient, request_body, FALSE, response, response_str.length, out); 251 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello World!---post1\n")); 252 253 cxBufferFree(out); 254 free(response); 255 } 256 } 257 258 CX_TEST(test_http_client_post_chunked) { 259 CX_TEST_DO { 260 cxstring response_str = cx_str( 261 "HTTP/1.1 200 OK\r\n" 262 "Content-length: 21\r\n" 263 "\r\n" 264 "Hello World!---post2\n"); 265 cxstring *response = calloc(response_str.length, sizeof(cxstring)); 266 for(int i=0;i<response_str.length;i++) { 267 response[i] = cx_strn(response_str.ptr+i, 1); 268 } 269 270 cxstring request_body = cx_str("test request body, needs more than one read (len > 16)\n"); 271 272 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 273 274 CX_TEST_CALL_SUBROUTINE(test_httpclient, request_body, TRUE, response, response_str.length, out); 275 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello World!---post2\n")); 276 277 cxBufferFree(out); 278 free(response); 279 } 280 } 281 282 CX_TEST(test_http_client_post_ctlen_large) { 283 CX_TEST_DO { 284 cxstring response = cx_str( 285 "HTTP/1.1 200 OK\r\n" 286 "Content-length: 21\r\n" 287 "\r\n" 288 "Hello World!---post3\n"); 289 290 size_t ctlen = 1024*1024*64; 291 char *req_body = malloc(ctlen); 292 cxstring request_body = cx_strn(req_body, ctlen); 293 294 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 295 296 request_body_max_read = 4096; 297 CX_TEST_CALL_SUBROUTINE(test_httpclient, request_body, FALSE, &response, 1, out); 298 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello World!---post3\n")); 299 300 cxBufferFree(out); 301 free(req_body); 302 } 303 } 304 305 CX_TEST(test_http_client_get_incorrect_ctlen) { 306 CX_TEST_DO { 307 cxstring response = cx_str( 308 "HTTP/1.1 200 OK\r\n" 309 "Content-length: 5\r\n" 310 "\r\n" 311 "Hello World!\n"); 312 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 313 314 CX_TEST_CALL_SUBROUTINE(test_httpclient, cx_str(NULL), FALSE, &response, 1, out); 315 CX_TEST_ASSERT(!cx_strcmp(cx_strn(out->space, out->size), "Hello")); 316 317 cxBufferFree(out); 318 } 319 } 320 321 CX_TEST(test_http_client_broken_response) { 322 CX_TEST_DO { 323 cxstring response = cx_str( 324 "broken response\r\n" 325 "Content-length: 5\r\n" 326 "\r\n" 327 "Hello World!\n"); 328 CxBuffer *out = cxBufferCreate(NULL, NULL, 256, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); 329 330 CX_TEST_CALL_SUBROUTINE(test_httpclient, cx_str(NULL), FALSE, &response, 1, out); 331 CX_TEST_ASSERT(out->size == 0); 332 cxBufferFree(out); 333 } 334 } 335