ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
https_keys.c
Go to the documentation of this file.
1
7#include <ascii-chat/crypto/https_keys.h>
8#include <ascii-chat/common.h>
9#include <ascii-chat/asciichat_errno.h>
10#include <ascii-chat/platform/string.h>
11#include <ascii-chat/platform/util.h>
12#include <ascii-chat/platform/process.h>
13#include <ascii-chat/platform/filesystem.h>
14#include <ascii-chat/util/url.h> // For url_parse()
15#include <ascii-chat/network/http_client.h>
16#include <string.h>
17#include <stdlib.h>
18#include <stdio.h>
19#ifndef _WIN32
20#include <unistd.h>
21#endif
22#include <fcntl.h>
23
24// =============================================================================
25// Helper Functions
26// =============================================================================
27
32static asciichat_error_t https_fetch_keys(const char *url, char **response_text, size_t *response_len) {
33 if (!url || !response_text || !response_len) {
34 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
35 return ERROR_INVALID_PARAM;
36 }
37
38 // Parse URL to extract hostname and path
39 url_parts_t url_parts = {0};
40 asciichat_error_t parse_result = url_parse(url, &url_parts);
41 if (parse_result != ASCIICHAT_OK) {
42 return parse_result;
43 }
44
45 // Use https_get from http_client
46 char *response = https_get(url_parts.host, url_parts.path);
47 url_parts_destroy(&url_parts);
48
49 if (!response) {
50 SET_ERRNO(ERROR_NETWORK, "Failed to fetch from %s", url);
51 return ERROR_NETWORK;
52 }
53
54 *response_text = response;
55 *response_len = strlen(response);
56 return ASCIICHAT_OK;
57}
58
59// =============================================================================
60// URL Construction
61// =============================================================================
62
63asciichat_error_t build_github_ssh_url(const char *username, char *url_out, size_t url_size) {
64 if (!username || !url_out) {
65 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
66 return ERROR_INVALID_PARAM;
67 }
68
69 if (url_size < 64) {
70 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
71 return ERROR_INVALID_PARAM;
72 }
73
74 // Construct GitHub SSH keys URL: https://github.com/username.keys
75 int result = safe_snprintf(url_out, url_size, "https://github.com/%s.keys", username);
76 if (result < 0 || result >= (int)url_size) {
77 SET_ERRNO(ERROR_STRING, "Failed to construct GitHub SSH URL");
78 return ERROR_STRING;
79 }
80
81 return ASCIICHAT_OK;
82}
83
84asciichat_error_t build_gitlab_ssh_url(const char *username, char *url_out, size_t url_size) {
85 if (!username || !url_out) {
86 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
87 return ERROR_INVALID_PARAM;
88 }
89
90 if (url_size < 64) {
91 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
92 return ERROR_INVALID_PARAM;
93 }
94
95 // Construct GitLab SSH keys URL: https://gitlab.com/username.keys
96 int result = safe_snprintf(url_out, url_size, "https://gitlab.com/%s.keys", username);
97 if (result < 0 || result >= (int)url_size) {
98 SET_ERRNO(ERROR_STRING, "Failed to construct GitLab SSH URL");
99 return ERROR_STRING;
100 }
101
102 return ASCIICHAT_OK;
103}
104
105asciichat_error_t build_github_gpg_url(const char *username, char *url_out, size_t url_size) {
106 if (!username || !url_out) {
107 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
108 return ERROR_INVALID_PARAM;
109 }
110
111 if (url_size < 64) {
112 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
113 return ERROR_INVALID_PARAM;
114 }
115
116 // Strip .gpg suffix if user already included it (e.g., "github:zfogg.gpg")
117 char clean_username[BUFFER_SIZE_SMALL];
118 SAFE_STRNCPY(clean_username, username, sizeof(clean_username) - 1);
119 size_t len = strlen(clean_username);
120 if (len > 4 && strcmp(clean_username + len - 4, ".gpg") == 0) {
121 clean_username[len - 4] = '\0'; // Remove .gpg suffix
122 }
123
124 // Construct GitHub GPG keys URL: https://github.com/username.gpg
125 int result = safe_snprintf(url_out, url_size, "https://github.com/%s.gpg", clean_username);
126 if (result < 0 || result >= (int)url_size) {
127 SET_ERRNO(ERROR_STRING, "Failed to construct GitHub GPG URL");
128 return ERROR_STRING;
129 }
130
131 return ASCIICHAT_OK;
132}
133
134asciichat_error_t build_gitlab_gpg_url(const char *username, char *url_out, size_t url_size) {
135 if (!username || !url_out) {
136 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
137 return ERROR_INVALID_PARAM;
138 }
139
140 if (url_size < 64) {
141 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
142 return ERROR_INVALID_PARAM;
143 }
144
145 // Strip .gpg suffix if user already included it (e.g., "gitlab:zfogg.gpg")
146 char clean_username[BUFFER_SIZE_SMALL];
147 SAFE_STRNCPY(clean_username, username, sizeof(clean_username) - 1);
148 size_t len = strlen(clean_username);
149 if (len > 4 && strcmp(clean_username + len - 4, ".gpg") == 0) {
150 clean_username[len - 4] = '\0'; // Remove .gpg suffix
151 }
152
153 // Construct GitLab GPG keys URL: https://gitlab.com/username.gpg
154 int result = safe_snprintf(url_out, url_size, "https://gitlab.com/%s.gpg", clean_username);
155 if (result < 0 || result >= (int)url_size) {
156 SET_ERRNO(ERROR_STRING, "Failed to construct GitLab GPG URL");
157 return ERROR_STRING;
158 }
159
160 return ASCIICHAT_OK;
161}
162
163// =============================================================================
164// HTTPS Key Fetching Implementation
165// =============================================================================
166
167asciichat_error_t fetch_github_ssh_keys(const char *username, char ***keys_out, size_t *num_keys) {
168 if (!username || !keys_out || !num_keys) {
169 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
170 num_keys);
171 return ERROR_INVALID_PARAM;
172 }
173
174 // Build the GitHub SSH keys URL
175 char url[BUFFER_SIZE_SMALL];
176 asciichat_error_t url_result = build_github_ssh_url(username, url, sizeof(url));
177 if (url_result != ASCIICHAT_OK) {
178 return url_result;
179 }
180
181 // Fetch the keys using HTTPS
182 char *response_text = NULL;
183 size_t response_len = 0;
184 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
185 if (fetch_result != ASCIICHAT_OK) {
186 return fetch_result;
187 }
188
189 // Parse SSH keys from the response
190 asciichat_error_t parse_result =
191 parse_ssh_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
192
193 // Clean up response text
194 SAFE_FREE(response_text);
195
196 return parse_result;
197}
198
199asciichat_error_t fetch_gitlab_ssh_keys(const char *username, char ***keys_out, size_t *num_keys) {
200 if (!username || !keys_out || !num_keys) {
201 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
202 num_keys);
203 return ERROR_INVALID_PARAM;
204 }
205
206 // Build the GitLab SSH keys URL
207 char url[BUFFER_SIZE_SMALL];
208 asciichat_error_t url_result = build_gitlab_ssh_url(username, url, sizeof(url));
209 if (url_result != ASCIICHAT_OK) {
210 return url_result;
211 }
212
213 // Fetch the keys using HTTPS
214 char *response_text = NULL;
215 size_t response_len = 0;
216 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
217 if (fetch_result != ASCIICHAT_OK) {
218 return fetch_result;
219 }
220
221 // Parse SSH keys from the response
222 asciichat_error_t parse_result =
223 parse_ssh_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
224
225 // Clean up response text
226 SAFE_FREE(response_text);
227
228 return parse_result;
229}
230
231asciichat_error_t fetch_github_gpg_keys(const char *username, char ***keys_out, size_t *num_keys) {
232 if (!username || !keys_out || !num_keys) {
233 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
234 num_keys);
235 return ERROR_INVALID_PARAM;
236 }
237
238 // Build the GitHub GPG keys URL
239 char url[BUFFER_SIZE_SMALL];
240 asciichat_error_t url_result = build_github_gpg_url(username, url, sizeof(url));
241 if (url_result != ASCIICHAT_OK) {
242 return url_result;
243 }
244
245 // Fetch the keys using HTTPS
246 char *response_text = NULL;
247 size_t response_len = 0;
248 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
249 if (fetch_result != ASCIICHAT_OK) {
250 return fetch_result;
251 }
252
253 // Parse GPG keys from the response
254 asciichat_error_t parse_result =
255 parse_gpg_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
256
257 // Clean up response text
258 SAFE_FREE(response_text);
259
260 return parse_result;
261}
262
263asciichat_error_t fetch_gitlab_gpg_keys(const char *username, char ***keys_out, size_t *num_keys) {
264 if (!username || !keys_out || !num_keys) {
265 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
266 num_keys);
267 return ERROR_INVALID_PARAM;
268 }
269
270 // Build the GitLab GPG keys URL
271 char url[BUFFER_SIZE_SMALL];
272 asciichat_error_t url_result = build_gitlab_gpg_url(username, url, sizeof(url));
273 if (url_result != ASCIICHAT_OK) {
274 return url_result;
275 }
276
277 // Fetch the keys using HTTPS
278 char *response_text = NULL;
279 size_t response_len = 0;
280 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
281 if (fetch_result != ASCIICHAT_OK) {
282 return fetch_result;
283 }
284
285 // Parse GPG keys from the response
286 asciichat_error_t parse_result =
287 parse_gpg_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
288
289 // Clean up response text
290 SAFE_FREE(response_text);
291
292 return parse_result;
293}
294
295// =============================================================================
296// Key Parsing from HTTPS Responses
297// =============================================================================
298
299asciichat_error_t parse_ssh_keys_from_response(const char *response_text, size_t response_len, char ***keys_out,
300 size_t *num_keys, size_t max_keys) {
301 if (!response_text || !keys_out || !num_keys) {
302 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for SSH key parsing");
303 return ERROR_INVALID_PARAM;
304 }
305
306 *num_keys = 0;
307 *keys_out = NULL;
308
309 // Count the number of SSH keys in the response
310 size_t key_count = 0;
311 const char *line_start = response_text;
312 const char *line_end;
313
314 while ((line_end = strchr(line_start, '\n')) != NULL) {
315 size_t line_len = line_end - line_start;
316 if (line_len > 0 && line_start[0] != '\r' && line_start[0] != '\n') {
317 key_count++;
318 }
319 line_start = line_end + 1;
320 }
321
322 // Handle last line if it doesn't end with newline
323 if (line_start < response_text + response_len) {
324 key_count++;
325 }
326
327 if (key_count == 0) {
328 SET_ERRNO(ERROR_CRYPTO_KEY, "No SSH keys found in response");
329 return ERROR_CRYPTO_KEY;
330 }
331
332 if (key_count > max_keys) {
333 key_count = max_keys;
334 }
335
336 // Allocate array for key strings
337 *keys_out = SAFE_MALLOC(sizeof(char *) * key_count, char **);
338 if (!*keys_out) {
339 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SSH keys array");
340 }
341
342 // Parse each SSH key line
343 line_start = response_text;
344 size_t parsed_keys = 0;
345
346 while (parsed_keys < key_count && (line_end = strchr(line_start, '\n')) != NULL) {
347 size_t line_len = line_end - line_start;
348
349 // Skip empty lines
350 if (line_len > 0 && line_start[0] != '\r' && line_start[0] != '\n') {
351 // Allocate space for this key line
352 (*keys_out)[parsed_keys] = SAFE_MALLOC(line_len + 1, char *);
353 if (!(*keys_out)[parsed_keys]) {
354 // Cleanup previously allocated keys
355 for (size_t i = 0; i < parsed_keys; i++) {
356 SAFE_FREE((*keys_out)[i]);
357 }
358 SAFE_FREE(*keys_out);
359 *keys_out = NULL;
360 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SSH key string");
361 }
362
363 // Copy the key line
364 memcpy((*keys_out)[parsed_keys], line_start, line_len);
365 (*keys_out)[parsed_keys][line_len] = '\0';
366
367 parsed_keys++;
368 }
369
370 line_start = line_end + 1;
371 }
372
373 // Handle last line if it doesn't end with newline
374 if (parsed_keys < key_count && line_start < response_text + response_len) {
375 size_t line_len = (response_text + response_len) - line_start;
376 if (line_len > 0) {
377 (*keys_out)[parsed_keys] = SAFE_MALLOC(line_len + 1, char *);
378 if (!(*keys_out)[parsed_keys]) {
379 // Cleanup previously allocated keys
380 for (size_t i = 0; i < parsed_keys; i++) {
381 SAFE_FREE((*keys_out)[i]);
382 }
383 SAFE_FREE(*keys_out);
384 *keys_out = NULL;
385 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SSH key string");
386 }
387 memcpy((*keys_out)[parsed_keys], line_start, line_len);
388 (*keys_out)[parsed_keys][line_len] = '\0';
389 parsed_keys++;
390 }
391 }
392
393 *num_keys = parsed_keys;
394 return ASCIICHAT_OK;
395}
396
397asciichat_error_t parse_gpg_keys_from_response(const char *response_text, size_t response_len, char ***keys_out,
398 size_t *num_keys, size_t max_keys) {
399 (void)max_keys;
400 if (!response_text || !keys_out || !num_keys) {
401 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for GPG key parsing");
402 return ERROR_INVALID_PARAM;
403 }
404
405 *num_keys = 0;
406 *keys_out = NULL;
407
408 // Check if this looks like a GPG key (starts with -----BEGIN PGP)
409 if (strncmp(response_text, "-----BEGIN PGP", 14) != 0) {
410 SET_ERRNO(ERROR_CRYPTO_KEY, "Response does not contain a valid GPG key");
411 return ERROR_CRYPTO_KEY;
412 }
413
414 // Write armored block to temp file
415 char temp_file[PLATFORM_MAX_PATH_LENGTH];
416 int fd = -1;
417 if (platform_create_temp_file(temp_file, sizeof(temp_file), "asc_gpg_import", &fd) != 0) {
418 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to create temp file for GPG import");
419 }
420
421#ifdef _WIN32
422 // On Windows, platform_create_temp_file returns fd=-1, need to open separately
423 fd = platform_open(temp_file, O_WRONLY | O_BINARY);
424 if (fd < 0) {
425 platform_delete_temp_file(temp_file);
426 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open temp file for writing");
427 }
428#endif
429
430 ssize_t written = write(fd, response_text, response_len);
431 close(fd);
432
433 if (written != (ssize_t)response_len) {
434 platform_unlink(temp_file);
435 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to write GPG key to temp file");
436 }
437
438 // Import the key using gpg --import
439 char import_cmd[BUFFER_SIZE_MEDIUM];
440 safe_snprintf(import_cmd, sizeof(import_cmd), "gpg --import '%s' 2>&1", temp_file);
441 FILE *import_fp = NULL;
442 if (platform_popen(import_cmd, "r", &import_fp) != ASCIICHAT_OK || !import_fp) {
443 platform_unlink(temp_file);
444 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to run gpg --import");
445 }
446
447 char import_output[2048];
448 size_t import_len = fread(import_output, 1, sizeof(import_output) - 1, import_fp);
449 import_output[import_len] = '\0';
450 platform_pclose(&import_fp);
451 platform_unlink(temp_file);
452
453 // Extract ALL key IDs from import output (format: "gpg: key KEYID: ...")
454 // GitHub often returns multiple keys in one armored block
455 const char *key_marker = "gpg: key ";
456 char key_ids[16][17]; // Support up to 16 keys
457 size_t key_count = 0;
458
459 log_debug("GPG import output:\n%s", import_output);
460
461 char *search_pos = import_output;
462 while (key_count < 16) {
463 char *key_line = strstr(search_pos, key_marker);
464 if (!key_line)
465 break;
466
467 key_line += strlen(key_marker);
468 int i = 0;
469 while (i < 16 && key_line[i] != ':' && key_line[i] != ' ' && key_line[i] != '\n') {
470 key_ids[key_count][i] = key_line[i];
471 i++;
472 }
473 key_ids[key_count][i] = '\0';
474
475 if (i > 0) {
476 log_debug("Extracted GPG key ID #%zu: %s", key_count, key_ids[key_count]);
477 key_count++;
478 }
479 search_pos = key_line + i;
480 }
481
482 if (key_count == 0) {
483 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to extract any key IDs from GPG import output");
484 }
485
486 log_debug("Total GPG keys extracted from import: %zu", key_count);
487
488 // Allocate array for results
489 *keys_out = SAFE_MALLOC(sizeof(char *) * key_count, char **);
490 if (!*keys_out) {
491 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate GPG keys array");
492 }
493
494 // Process each key ID to get full fingerprint
495 size_t valid_keys = 0;
496 for (size_t k = 0; k < key_count; k++) {
497 // Get full fingerprint from gpg --list-keys output
498 char list_cmd[BUFFER_SIZE_SMALL];
499 safe_snprintf(list_cmd, sizeof(list_cmd),
500 "gpg --list-keys --with-colons --fingerprint '%s' " PLATFORM_SHELL_NULL_REDIRECT, key_ids[k]);
501 FILE *list_fp = NULL;
502 if (platform_popen(list_cmd, "r", &list_fp) != ASCIICHAT_OK || !list_fp) {
503 continue; // Skip this key if we can't list it
504 }
505
506 char list_output[4096];
507 size_t list_len = fread(list_output, 1, sizeof(list_output) - 1, list_fp);
508 list_output[list_len] = '\0';
509 platform_pclose(&list_fp);
510
511 // Check if it contains an Ed25519 key (algorithm 22)
512 if (!strstr(list_output, ":22:") && !strstr(list_output, "ed25519")) {
513 continue; // Skip non-Ed25519 keys
514 }
515
516 // Extract full 40-character fingerprint from "fpr:" line
517 // Format: fpr:::::::::FINGERPRINT:
518 // The fingerprint is field 10 (after 9 colons)
519 char fingerprint[41] = {0};
520 const char *fpr_marker = "\nfpr:";
521 char *fpr_line = strstr(list_output, fpr_marker);
522 if (fpr_line) {
523 fpr_line += 1; // Skip the newline
524 // Count 9 colons from the start of the line
525 int colon_count = 0;
526 while (*fpr_line && colon_count < 9) {
527 if (*fpr_line == ':')
528 colon_count++;
529 fpr_line++;
530 }
531 // Now we should be at the start of the fingerprint
532 // Extract up to 40 hex characters
533 int fpr_len = 0;
534 while (fpr_len < 40 && fpr_line[fpr_len] && fpr_line[fpr_len] != ':' && fpr_line[fpr_len] != '\n') {
535 fingerprint[fpr_len] = fpr_line[fpr_len];
536 fpr_len++;
537 }
538 fingerprint[fpr_len] = '\0';
539 }
540
541 // If fingerprint extraction failed, use the short key ID
542 if (strlen(fingerprint) == 0) {
543 log_warn("Failed to extract fingerprint for key %s, using short key ID", key_ids[k]);
544 SAFE_STRNCPY(fingerprint, key_ids[k], sizeof(fingerprint));
545 }
546
547 log_debug("Key %s -> fingerprint: %s (length: %zu)", key_ids[k], fingerprint, strlen(fingerprint));
548
549 // Allocate and store this key in gpg:KEYID format
550 size_t gpg_key_len = strlen("gpg:") + strlen(fingerprint) + 1;
551 (*keys_out)[valid_keys] = SAFE_MALLOC(gpg_key_len, char *);
552 if (!(*keys_out)[valid_keys]) {
553 // Cleanup on allocation failure
554 for (size_t cleanup = 0; cleanup < valid_keys; cleanup++) {
555 SAFE_FREE((*keys_out)[cleanup]);
556 }
557 SAFE_FREE(*keys_out);
558 *keys_out = NULL;
559 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate GPG key string");
560 }
561
562 safe_snprintf((*keys_out)[valid_keys], gpg_key_len, "gpg:%s", fingerprint);
563 log_debug("Added valid Ed25519 key #%zu: %s", valid_keys, (*keys_out)[valid_keys]);
564 valid_keys++;
565 }
566
567 if (valid_keys == 0) {
568 SAFE_FREE(*keys_out);
569 *keys_out = NULL;
570 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid Ed25519 keys found in imported GPG keys");
571 }
572
573 *num_keys = valid_keys;
574 return ASCIICHAT_OK;
575}
char * https_get(const char *hostname, const char *path)
asciichat_error_t build_github_ssh_url(const char *username, char *url_out, size_t url_size)
Definition https_keys.c:63
asciichat_error_t fetch_gitlab_gpg_keys(const char *username, char ***keys_out, size_t *num_keys)
Definition https_keys.c:263
asciichat_error_t build_github_gpg_url(const char *username, char *url_out, size_t url_size)
Definition https_keys.c:105
asciichat_error_t parse_ssh_keys_from_response(const char *response_text, size_t response_len, char ***keys_out, size_t *num_keys, size_t max_keys)
Definition https_keys.c:299
asciichat_error_t fetch_github_ssh_keys(const char *username, char ***keys_out, size_t *num_keys)
Definition https_keys.c:167
asciichat_error_t fetch_github_gpg_keys(const char *username, char ***keys_out, size_t *num_keys)
Definition https_keys.c:231
asciichat_error_t parse_gpg_keys_from_response(const char *response_text, size_t response_len, char ***keys_out, size_t *num_keys, size_t max_keys)
Definition https_keys.c:397
asciichat_error_t fetch_gitlab_ssh_keys(const char *username, char ***keys_out, size_t *num_keys)
Definition https_keys.c:199
asciichat_error_t build_gitlab_ssh_url(const char *username, char *url_out, size_t url_size)
Definition https_keys.c:84
asciichat_error_t build_gitlab_gpg_url(const char *username, char *url_out, size_t url_size)
Definition https_keys.c:134
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
void url_parts_destroy(url_parts_t *parts)
Definition url.c:281
asciichat_error_t url_parse(const char *url, url_parts_t *parts_out)
Definition url.c:166
int platform_open(const char *pathname, int flags,...)