1 // http-client.uc - HTTP/1.1 client with optional HTTPS support 2 // Demonstrates FFI with network sockets, DNS resolution, and OpenSSL TLS 3 // Note: This demo shows the structure but network calls may fail due to network issues 4 5 import * as ffi from 'ffi'; 6 7 // ============================================================================ 8 // Load OpenSSL if available 9 // ============================================================================ 10 11 let SSL = null; 12 13 try { 14 // Load libssl with automatic function wrapping using cdefs 15 SSL = ffi.dlopen('ssl', false, ` 16 typedef struct ssl_ctx_st SSL_CTX; 17 typedef struct ssl_st SSL; 18 19 SSL_CTX *SSL_CTX_new(void *method); 20 void SSL_CTX_free(SSL_CTX *ctx); 21 int SSL_CTX_set_verify(SSL_CTX *ctx, int mode, void *callback); 22 int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *cafile, const char *capath); 23 24 SSL *SSL_new(SSL_CTX *ctx); 25 void SSL_free(SSL *ssl); 26 int SSL_set_fd(SSL *ssl, int fd); 27 int SSL_connect(SSL *ssl); 28 int SSL_read(SSL *ssl, void *buf, int num); 29 int SSL_write(SSL *ssl, const void *buf, int num); 30 int SSL_shutdown(SSL *ssl); 31 long SSL_get_verify_result(SSL *ssl); 32 const char *SSL_get_error(SSL *ssl, int ret); 33 long SSL_ctrl(SSL *ssl, int cmd, long larg, void *parg); 34 35 void *TLS_client_method(void); 36 void OPENSSL_init_ssl(long opts, const void *settings); 37 `); 38 print("OpenSSL loaded successfully\n"); 39 } catch (e) { 40 print("Warning: Could not load OpenSSL - HTTPS not available\n"); 41 SSL = null; 42 } 43 44 // ============================================================================ 45 // Load libc with socket functions using dlopen cdefs 46 // ============================================================================ 47 48 let libc = ffi.dlopen(null, false, ` 49 typedef int socklen_t; 50 typedef unsigned short sa_family_t; 51 typedef unsigned short in_port_t; 52 53 struct in_addr { 54 unsigned int s_addr; 55 }; 56 57 struct sockaddr_in { 58 sa_family_t sin_family; 59 in_port_t sin_port; 60 struct in_addr sin_addr; 61 char sin_zero[8]; 62 }; 63 64 struct addrinfo { 65 int ai_flags; 66 int ai_family; 67 int ai_socktype; 68 int ai_protocol; 69 socklen_t ai_addrlen; 70 struct sockaddr_in *ai_addr; 71 char *ai_canonname; 72 struct addrinfo *ai_next; 73 }; 74 75 int socket(int domain, int type, int protocol); 76 int connect(int sockfd, struct sockaddr_in *addr, socklen_t addrlen); 77 ssize_t send(int sockfd, const char *buf, size_t len, int flags); 78 ssize_t recv(int sockfd, char *buf, size_t len, int flags); 79 int close(int fd); 80 int shutdown(int fd, int how); 81 int getaddrinfo(const char *node, const char *service, struct addrinfo *hints, struct addrinfo **result); 82 void freeaddrinfo(struct addrinfo *ai); 83 `); 84 85 // Constants 86 const AF_INET = 2; 87 const SOCK_STREAM = 1; 88 const IPPROTO_TCP = 6; 89 const SHUT_RDWR = 2; 90 91 // OpenSSL constants 92 const SSL_VERIFY_NONE = 0; 93 const SSL_VERIFY_PEER = 1; 94 95 // ============================================================================ 96 // HTTPResponse_create - Factory for HTTP response objects 97 // 98 // Creates an HTTPResponse object with standard property access and helper 99 // methods for inspecting the response. This object wraps raw HTTP response 100 // data into a usable interface. 101 // 102 // Parameters: 103 // status - HTTP status code (e.g. 200, 404, 500) 104 // statusText - Human-readable status text (e.g. "OK", "Not Found") 105 // headers - Object mapping header names to values (default: {}) 106 // body - Response body as a string 107 // 108 // Methods available on the returned object: 109 // ok() - Returns true if status is in 2xx range 110 // clientError() - Returns true if status is in 4xx range 111 // serverError() - Returns true if status is 5xx or above 112 // getHeader(n) - Returns header value, case-insensitive key lookup 113 // json() - Attempts to parse body as JSON, returns null on failure 114 // 115 // Usage: 116 // let resp = HTTPResponse_create(200, "OK", {"Content-Type": "text/html"}, "<html>") 117 // if (resp.ok()) { 118 // print("Headers Content-Type: ", resp.getHeader("content-type"), "\n"); 119 // } 120 // ============================================================================ 121 122 function HTTPResponse_create(status, statusText, headers, body) { 123 return proto({ 124 status: status, 125 statusText: statusText, 126 headers: headers || {}, 127 body: body || '' 128 }, { 129 ok: function() { 130 return this.status >= 200 && this.status < 300; 131 }, 132 133 clientError: function() { 134 return this.status >= 400 && this.status < 500; 135 }, 136 137 serverError: function() { 138 return this.status >= 500; 139 }, 140 141 getHeader: function(name) { 142 let lowerName = name.toLowerCase(); 143 for (let key in this.headers) { 144 if (key.toLowerCase() === lowerName) 145 return this.headers[key]; 146 } 147 return null; 148 }, 149 150 json: function() { 151 try { 152 return json(this.body); 153 } catch (e) { 154 return null; 155 } 156 } 157 }); 158 } 159 160 // ============================================================================ 161 // SSLConnection_create - Wrapper for SSL/TLS connections 162 // 163 // Establishes a TLS session over an existing TCP socket using OpenSSL. 164 // Sets up certificate verification mode, configures SNI hostname support, 165 // and performs the SSL handshake. The returned object wraps the SSL state 166 // and provides transparent read/write methods that can be used in place 167 // of raw socket I/O. 168 // 169 // Parameters: 170 // sock - TCP socket file descriptor returned by libc.socket() 171 // host - Server hostname string (used for SNI extension) 172 // 173 // Methods available on the returned object: 174 // read(buf, len) - Read up to len bytes through SSL (like SSL_read) 175 // write(buf, len) - Write up to len bytes through SSL (like SSL_write) 176 // shutdown() - Send SSL close alert and free SSL/CTX resources 177 // close() - Shutdown SSL and close the underlying socket 178 // 179 // Usage (within HTTPClient_create after socket connect): 180 // let conn = SSLConnection_create(sock, "example.com"); 181 // conn.write(request, len); // sends encrypted data 182 // conn.read(buf, 4096); // reads decrypted data 183 // conn.close(); // cleanup 184 // ============================================================================ 185 186 function SSLConnection_create(sock, host) { 187 if (!SSL) 188 die("Error: OpenSSL not available - HTTPS not supported\n"); 189 190 // Get TLS method and create context 191 let tls_method = SSL.TLS_client_method(); 192 let ssl_ctx = SSL.SSL_CTX_new(tls_method); 193 194 if (!ssl_ctx) 195 die("Error: Failed to create SSL context\n"); 196 197 // Set verify mode (disable verification for simplicity - not recommended for production) 198 SSL.SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE, null); 199 200 // Create SSL structure 201 let ssl = SSL.SSL_new(ssl_ctx); 202 if (!ssl) { 203 SSL.SSL_CTX_free(ssl_ctx); 204 die("Error: Failed to create SSL structure\n"); 205 } 206 207 // Set the socket file descriptor 208 if (SSL.SSL_set_fd(ssl, sock) !== 1) { 209 SSL.SSL_free(ssl); 210 SSL.SSL_CTX_free(ssl_ctx); 211 die("Error: Failed to set SSL fd\n"); 212 } 213 214 // Set SNI hostname (Server Name Indication) 215 // SSL_CTRL_SET_TLSEXT_HOSTNAME = 55 216 const SSL_CTRL_SET_TLSEXT_HOSTNAME = 55; 217 SSL.SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, 0, host); 218 219 // Perform SSL handshake 220 let ret = SSL.SSL_connect(ssl); 221 if (ret !== 1) { 222 let err = SSL.SSL_get_error(ssl, ret); 223 SSL.SSL_free(ssl); 224 SSL.SSL_CTX_free(ssl_ctx); 225 die("Error: SSL handshake failed: " + ffi.string(err) + "\n"); 226 } 227 228 return { 229 ssl: ssl, 230 ssl_ctx: ssl_ctx, 231 sock: sock, 232 233 read: function(buf, len) { 234 return SSL.SSL_read(this.ssl, buf, len); 235 }, 236 237 write: function(buf, len) { 238 return SSL.SSL_write(this.ssl, buf, len); 239 }, 240 241 shutdown: function() { 242 SSL.SSL_shutdown(this.ssl); 243 SSL.SSL_free(this.ssl); 244 SSL.SSL_CTX_free(this.ssl_ctx); 245 }, 246 247 close: function() { 248 this.shutdown(); 249 libc.close(this.sock); 250 } 251 }; 252 } 253 254 // ============================================================================ 255 // HTTPClient_create - Low-level HTTP/1.1 client object 256 // 257 // Creates a raw HTTP client with manual request construction. Manages socket 258 // lifecycle including DNS resolution via getaddrinfo, TCP connection, and 259 // optional SSL/TLS upgrade. This is the foundational client used by 260 // httpGet() but provides full control over request details. 261 // 262 // The returned object is a closure over the socket and SSL state, providing 263 // methods to construct and send HTTP requests. All responses are parsed into 264 // HTTPResponse objects. 265 // 266 // Parameters: 267 // host - Server hostname (e.g. "example.com") 268 // port - Server port number, defaults to 80 if omitted 269 // useSSL - If true and OpenSSL is available, upgrades to HTTPS 270 // 271 // Methods available on the returned object: 272 // get(path, headers) - Send GET request to path with optional extra headers 273 // post(path, body, headers) - Send POST request with body (auto-sets Content-Type 274 // and Content-Length headers if not provided) 275 // request(method, path, body, headers) - Generic method for any HTTP verb 276 // close() - Shut down socket (and SSL if active) 277 // 278 // Usage: 279 // let client = HTTPClient_create("httpbin.org", 80); 280 // let resp = client.get("/get"); 281 // print("Status: ", resp.status, "\nBody: ", resp.body, "\n"); 282 // client.close(); 283 // 284 // let client = HTTPClient_create("example.com", 443, true); 285 // let resp = client.post("/api/data", '{"key":"value"}'); 286 // client.close(); 287 // ============================================================================ 288 289 function HTTPClient_create(host, port, useSSL) { 290 port = port || 80; 291 useSSL = useSSL || false; 292 let sock = libc.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 293 294 if (sock < 0) 295 die("Error: Failed to create socket\n"); 296 297 // Resolve hostname using getaddrinfo 298 let hints = { 299 ai_family: AF_INET, 300 ai_socktype: SOCK_STREAM, 301 ai_protocol: IPPROTO_TCP, 302 ai_flags: 0 303 }; 304 305 let resPtr = ffi.ctype('struct addrinfo *', null); 306 let rc = libc.getaddrinfo(host, '' + port, hints, resPtr.ptr()); 307 if (rc !== 0) { 308 libc.close(sock); 309 die("Error: Failed to resolve " + host + ":" + port + "\n"); 310 } 311 312 // Use index() to get pointer without auto-conversion 313 let sockaddr = resPtr.index('ai_addr'); 314 let ai_addrlen = resPtr.get('ai_addrlen'); 315 rc = libc.connect(sock, sockaddr, ai_addrlen); 316 317 libc.freeaddrinfo(resPtr); 318 319 if (rc < 0) { 320 libc.close(sock); 321 die("Error: Failed to connect to " + host + ":" + port + "\n"); 322 } 323 324 // Upgrade to SSL if requested 325 let conn = null; 326 if (useSSL) { 327 conn = SSLConnection_create(sock, host); 328 } 329 330 return { 331 host: host, 332 port: port, 333 socket: sock, 334 ssl: conn, 335 useSSL: useSSL, 336 337 get: function(path, headers) { 338 headers = headers || {}; 339 return this.request("GET", path, null, headers); 340 }, 341 342 post: function(path, body, headers) { 343 headers = headers || {}; 344 headers["Content-Type"] = headers["Content-Type"] || "application/x-www-form-urlencoded"; 345 headers["Content-Length"] = '' + length(body); 346 return this.request("POST", path, body, headers); 347 }, 348 349 request: function(method, path, body, headers) { 350 // Build request 351 let request = method + " " + path + " HTTP/1.1\r\n"; 352 request = request + "Host: " + this.host + "\r\n"; 353 request = request + "Connection: close\r\n"; 354 355 for (let key in headers) { 356 request = request + key + ": " + headers[key] + "\r\n"; 357 } 358 359 if (body) 360 request = request + body + "\r\n"; 361 362 request = request + "\r\n"; 363 364 // Send request 365 let sent; 366 if (this.ssl) { 367 sent = this.ssl.write(request, length(request)); 368 } else { 369 sent = libc.send(this.socket, request, length(request), 0); 370 } 371 372 if (sent < 0) 373 die("Error: Failed to send request\n"); 374 375 // Receive response 376 let chunks = []; 377 let chunkSize = 4096; 378 379 while (true) { 380 let chunk = ffi.ctype('char[' + chunkSize + ']'); 381 let n; 382 if (this.ssl) { 383 n = this.ssl.read(chunk.ptr(), chunkSize); 384 } else { 385 n = libc.recv(this.socket, chunk.ptr(), chunkSize, 0); 386 } 387 388 if (n < 0) { 389 let err = ffi.errno(); 390 die("Error: read failed: " + err + "\n"); 391 } 392 393 if (n === 0) 394 break; 395 396 // Use slice() to convert received bytes to string 397 push(chunks, chunk.slice(0, n)); 398 } 399 400 if (length(chunks) === 0) 401 die("Error: No data received\n"); 402 403 // Parse response 404 let data = join('', chunks); 405 406 // Parse status: "HTTP/1.1 200 OK" 407 let status = 0; 408 let statusText = "OK"; 409 410 let m = match(data, /^HTTP\/1\.[01] (\d\d\d) (.+)\r\n/); 411 if (m) { 412 status = int(m[1]); 413 statusText = m[2]; 414 } 415 416 // Find body after \r\n\r\n 417 let crlfcrlf = index(data, "\r\n\r\n"); 418 let responseBody = ''; 419 if (crlfcrlf >= 0) { 420 responseBody = substr(data, crlfcrlf + 4); 421 } 422 423 let responseHeaders = { "Content-Type": "application/json" }; 424 425 return HTTPResponse_create(status, statusText, responseHeaders, responseBody); 426 }, 427 428 close: function() { 429 if (this.ssl) { 430 this.ssl.close(); 431 } else if (this.socket !== null) { 432 libc.shutdown(this.socket, SHUT_RDWR); 433 libc.close(this.socket); 434 this.socket = null; 435 } 436 } 437 }; 438 } 439 440 // ============================================================================ 441 // Convenience Functions 442 // ============================================================================ 443 444 // ============================================================================= 445 // parseUrl - URL string parser 446 // 447 // Splits a URL string into its component parts: scheme, host, port, and path. 448 // Handles standard http/https schemes with optional port specification and path. 449 // 450 // Parameters: 451 // url - Complete URL string (e.g. "https://example.com:8443/api/v1?query") 452 // 453 // Returns object with properties: 454 // host - Server hostname without port 455 // port - Port number (443 for https, 80 for http, or extracted from URL) 456 // path - URL path starting with '/' (defaults to '/' if no path) 457 // scheme - URL scheme as a string ("http" or "https") 458 // 459 // Usage: 460 // let parts = parseUrl("https://api.example.com:8443/users/list"); 461 // // returns: { host: "api.example.com", port: 8443, path: "/users/list", scheme: "https" } 462 // let client = HTTPClient_create(parts.host, parts.port, parts.scheme === "https"); 463 // let resp = client.get(parts.path); 464 // ============================================================================ 465 466 function parseUrl(url) { 467 let scheme = "http"; 468 469 // Check for http:// or https:// prefix 470 let prefix = index(url, "://"); 471 if (prefix >= 0) { 472 let beforePrefix = substr(url, 0, prefix); 473 if (beforePrefix === "https") { 474 scheme = "https"; 475 } 476 url = substr(url, prefix + 3); 477 } 478 479 let path = '/'; 480 let slashIdx = index(url, '/'); 481 if (slashIdx >= 0) { 482 path = substr(url, slashIdx); 483 url = substr(url, 0, slashIdx); 484 } 485 486 let host = url; 487 let port = scheme === "https" ? 443 : 80; 488 let colonIdx = index(url, ':'); 489 if (colonIdx >= 0) { 490 host = substr(url, 0, colonIdx); 491 port = int(substr(url, colonIdx + 1)); 492 } 493 494 return { host: host, path: path, port: port, scheme: scheme }; 495 } 496 497 function httpGet(url) { 498 // Quick one-shot HTTP GET using the full HTTPClient pipeline. 499 // Parses the URL, establishes connection (with optional SSL), sends 500 // the request, receives the response, and cleans up the connection. 501 let urlParts = parseUrl(url); 502 let client = HTTPClient_create(urlParts.host, urlParts.port, urlParts.scheme === "https"); 503 let response = client.get(urlParts.path); 504 client.close(); 505 return response; 506 } 507 508 // ============================================================================ 509 // Main 510 // ============================================================================ 511 512 if (length(ARGV) < 1) { 513 print("Usage: ucode ", SCRIPT_NAME, " <url>\n"); 514 print("Example: ucode ", SCRIPT_NAME, " http://httpbin.org/get\n"); 515 print("Example: ucode ", SCRIPT_NAME, " https://httpbin.org/get\n"); 516 exit(1); 517 } 518 519 let url = ARGV[0]; 520 let urlParts = parseUrl(url); 521 522 print("GET ", url, "\n"); 523 524 try { 525 let client = HTTPClient_create(urlParts.host, urlParts.port, urlParts.scheme === "https"); 526 let response = client.get(urlParts.path); 527 client.close(); 528 529 print("\n--- Response ---\n"); 530 print(response.body); 531 } catch (e) { 532 print("Error: ", e, "\n"); 533 exit(1); 534 }
This page was automatically generated by LXR 0.3.1. • OpenWrt