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