• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/ucode/examples/ffi/http-client.uc

  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