comparison s3/s3_uploader.c @ 196:83f16548ba41

[AI] Adding s3 bucket uploader code using Seobeo.
author MrJuneJune <me@mrjunejune.com>
date Sat, 14 Feb 2026 16:08:15 -0800
parents
children 6cdee35a7ba9
comparison
equal deleted inserted replaced
195:f8f5004a920a 196:83f16548ba41
1 #include "s3_uploader.h"
2 #include "seobeo/seobeo.h"
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <time.h>
8 #include <ctype.h>
9
10 #include <openssl/hmac.h>
11 #include <openssl/sha.h>
12
13 #define S3_ARENA_SIZE (10 * ONE_MEGA_BYTE)
14 #define S3_RESULT_ARENA_SIZE (64 * 1024)
15 #define S3_SERVICE_NAME "s3"
16 #define S3_AWS4_REQUEST "aws4_request"
17 #define S3_ALGORITHM "AWS4-HMAC-SHA256"
18
19 // --- Internal Structures --- //
20
21 typedef struct {
22 char date[9]; // YYYYMMDD
23 char datetime[17]; // YYYYMMDDTHHMMSSZ
24 } S3_Timestamp;
25
26 // --- Forward Declarations --- //
27
28 static void s3__get_timestamp(S3_Timestamp *p_ts);
29 static void s3__sha256_hex(const uint8 *data, size_t len, char *out);
30 static void s3__hmac_sha256(const uint8 *key, size_t key_len,
31 const uint8 *data, size_t data_len,
32 uint8 *out);
33 static void s3__hex_encode(const uint8 *data, size_t len, char *out);
34 static char *s3__uri_encode(const char *str, Dowa_Arena *p_arena);
35 static char *s3__build_canonical_request(const char *method,
36 const char *uri,
37 const char *query,
38 const char *headers,
39 const char *signed_headers,
40 const char *payload_hash,
41 Dowa_Arena *p_arena);
42 static char *s3__build_string_to_sign(const char *datetime,
43 const char *date,
44 const char *region,
45 const char *canonical_request,
46 Dowa_Arena *p_arena);
47 static void s3__calculate_signing_key(const char *secret_key,
48 const char *date,
49 const char *region,
50 uint8 *out);
51 static char *s3__build_authorization_header(const char *access_key,
52 const char *date,
53 const char *region,
54 const char *signed_headers,
55 const uint8 *signing_key,
56 const char *string_to_sign,
57 Dowa_Arena *p_arena);
58 static uint8 *s3__load_file(const char *path, size_t *p_size);
59
60 // --- Public API Implementation --- //
61
62 S3_Result S3_Upload_File(const S3_Config *p_config,
63 const char *local_path,
64 const char *s3_key)
65 {
66 const char *content_type = S3_Guess_Content_Type(local_path);
67 return S3_Upload_File_With_Content_Type(p_config, local_path, s3_key, content_type);
68 }
69
70 S3_Result S3_Upload_File_With_Content_Type(const S3_Config *p_config,
71 const char *local_path,
72 const char *s3_key,
73 const char *content_type)
74 {
75 S3_Result result = {0};
76 result.p_arena = Dowa_Arena_Create(S3_RESULT_ARENA_SIZE);
77
78 size_t file_size = 0;
79 uint8 *file_data = s3__load_file(local_path, &file_size);
80 if (!file_data)
81 {
82 result.success = FALSE;
83 result.status_code = 0;
84 result.error_message = Dowa_String_Copy_Arena("Failed to read file", result.p_arena);
85 return result;
86 }
87
88 result = S3_Upload_Data(p_config, file_data, file_size, s3_key, content_type);
89 free(file_data);
90
91 return result;
92 }
93
94 S3_Result S3_Upload_Data(const S3_Config *p_config,
95 const uint8 *data,
96 size_t data_length,
97 const char *s3_key,
98 const char *content_type)
99 {
100 S3_Result result = {0};
101 result.p_arena = Dowa_Arena_Create(S3_RESULT_ARENA_SIZE);
102
103 if (!p_config || !data || !s3_key)
104 {
105 result.success = FALSE;
106 result.error_message = Dowa_String_Copy_Arena("Invalid parameters", result.p_arena);
107 return result;
108 }
109
110 Dowa_Arena *p_arena = Dowa_Arena_Create(S3_ARENA_SIZE);
111
112 // Get timestamp
113 S3_Timestamp ts;
114 s3__get_timestamp(&ts);
115
116 // Calculate payload hash
117 char payload_hash[65];
118 s3__sha256_hex(data, data_length, payload_hash);
119
120 // Build host
121 char *host;
122 if (p_config->endpoint)
123 {
124 host = Dowa_String_Copy_Arena((char *)p_config->endpoint, p_arena);
125 }
126 else if (p_config->use_path_style)
127 {
128 size_t host_len = strlen("s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
129 host = Dowa_Arena_Allocate(p_arena, host_len);
130 snprintf(host, host_len, "s3.%s.amazonaws.com", p_config->region);
131 }
132 else
133 {
134 size_t host_len = strlen(p_config->bucket) + strlen(".s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
135 host = Dowa_Arena_Allocate(p_arena, host_len);
136 snprintf(host, host_len, "%s.s3.%s.amazonaws.com", p_config->bucket, p_config->region);
137 }
138
139 // Build URI path
140 char *uri_path;
141 char *encoded_key = s3__uri_encode(s3_key, p_arena);
142 if (p_config->use_path_style)
143 {
144 size_t uri_len = strlen("/") + strlen(p_config->bucket) + strlen("/") + strlen(encoded_key) + 1;
145 uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
146 snprintf(uri_path, uri_len, "/%s/%s", p_config->bucket, encoded_key);
147 }
148 else
149 {
150 size_t uri_len = strlen("/") + strlen(encoded_key) + 1;
151 uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
152 snprintf(uri_path, uri_len, "/%s", encoded_key);
153 }
154
155 // Build canonical headers (must be sorted alphabetically)
156 char content_length_str[32];
157 snprintf(content_length_str, sizeof(content_length_str), "%zu", data_length);
158
159 size_t headers_len = 512 + strlen(host) + strlen(content_type) + strlen(content_length_str);
160 char *canonical_headers = Dowa_Arena_Allocate(p_arena, headers_len);
161 snprintf(canonical_headers, headers_len,
162 "content-length:%s\n"
163 "content-type:%s\n"
164 "host:%s\n"
165 "x-amz-content-sha256:%s\n"
166 "x-amz-date:%s\n",
167 content_length_str,
168 content_type,
169 host,
170 payload_hash,
171 ts.datetime);
172
173 const char *signed_headers = "content-length;content-type;host;x-amz-content-sha256;x-amz-date";
174
175 // Build canonical request
176 char *canonical_request = s3__build_canonical_request("PUT",
177 uri_path,
178 "", // No query string
179 canonical_headers,
180 signed_headers,
181 payload_hash,
182 p_arena);
183
184 // Build string to sign
185 char *string_to_sign = s3__build_string_to_sign(ts.datetime,
186 ts.date,
187 p_config->region,
188 canonical_request,
189 p_arena);
190
191 // Calculate signing key
192 uint8 signing_key[32];
193 s3__calculate_signing_key(p_config->secret_access_key,
194 ts.date,
195 p_config->region,
196 signing_key);
197
198 // Build authorization header
199 char *auth_header = s3__build_authorization_header(p_config->access_key_id,
200 ts.date,
201 p_config->region,
202 signed_headers,
203 signing_key,
204 string_to_sign,
205 p_arena);
206
207 // Build URL
208 size_t url_len = strlen("https://") + strlen(host) + strlen(uri_path) + 1;
209 char *url = Dowa_Arena_Allocate(p_arena, url_len);
210 snprintf(url, url_len, "https://%s%s", host, uri_path);
211
212 // Execute request using seobeo
213 Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(url);
214 Seobeo_Client_Request_Set_Method(p_req, "PUT");
215 Seobeo_Client_Request_Set_Body(p_req, (const char *)data, data_length);
216 Seobeo_Client_Request_Add_Header_Map(p_req, "Content-Type", content_type);
217 Seobeo_Client_Request_Add_Header_Map(p_req, "x-amz-date", ts.datetime);
218 Seobeo_Client_Request_Add_Header_Map(p_req, "x-amz-content-sha256", payload_hash);
219 Seobeo_Client_Request_Add_Header_Map(p_req, "Authorization", auth_header);
220
221 Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
222
223 if (p_resp)
224 {
225 result.status_code = p_resp->status_code;
226 if (p_resp->status_code >= 200 && p_resp->status_code < 300)
227 {
228 result.success = TRUE;
229
230 // Extract ETag from response headers
231 if (p_resp->headers)
232 {
233 char *etag = Dowa_HashMap_Get(p_resp->headers, "etag");
234 if (!etag) etag = Dowa_HashMap_Get(p_resp->headers, "ETag");
235 if (etag)
236 {
237 result.etag = Dowa_String_Copy_Arena(etag, result.p_arena);
238 }
239 }
240 }
241 else
242 {
243 result.success = FALSE;
244 if (p_resp->body && p_resp->body_length > 0)
245 {
246 result.error_message = Dowa_String_Copy_Arena(p_resp->body, result.p_arena);
247 }
248 else
249 {
250 result.error_message = Dowa_String_Copy_Arena("Upload failed", result.p_arena);
251 }
252 }
253 Seobeo_Client_Response_Destroy(p_resp);
254 }
255 else
256 {
257 result.success = FALSE;
258 result.error_message = Dowa_String_Copy_Arena("Failed to execute request", result.p_arena);
259 }
260
261 Seobeo_Client_Request_Destroy(p_req);
262 Dowa_Arena_Free(p_arena);
263
264 return result;
265 }
266
267 void S3_Result_Destroy(S3_Result *p_result)
268 {
269 if (p_result && p_result->p_arena)
270 {
271 Dowa_Arena_Free(p_result->p_arena);
272 p_result->p_arena = NULL;
273 p_result->error_message = NULL;
274 p_result->etag = NULL;
275 }
276 }
277
278 // --- Presigned URL Implementation --- //
279
280 static char *s3__uri_encode_strict(const char *str, Dowa_Arena *p_arena)
281 {
282 // Stricter encoding for query string values (no forward slash allowed)
283 if (!str) return NULL;
284
285 size_t len = strlen(str);
286 size_t max_len = len * 3 + 1;
287 char *out = Dowa_Arena_Allocate(p_arena, max_len);
288 char *p = out;
289
290 for (size_t i = 0; i < len; i++)
291 {
292 char c = str[i];
293 if ((c >= 'A' && c <= 'Z') ||
294 (c >= 'a' && c <= 'z') ||
295 (c >= '0' && c <= '9') ||
296 c == '-' || c == '_' || c == '.' || c == '~')
297 {
298 *p++ = c;
299 }
300 else
301 {
302 sprintf(p, "%%%02X", (unsigned char)c);
303 p += 3;
304 }
305 }
306 *p = '\0';
307 return out;
308 }
309
310 static S3_Presigned_URL s3__presign_url(const S3_Config *p_config,
311 const char *s3_key,
312 const char *method,
313 const char *content_type,
314 int32 expires_seconds)
315 {
316 S3_Presigned_URL result = {0};
317 result.p_arena = Dowa_Arena_Create(S3_RESULT_ARENA_SIZE);
318
319 if (!p_config || !s3_key)
320 {
321 result.success = FALSE;
322 result.error_message = Dowa_String_Copy_Arena("Invalid parameters", result.p_arena);
323 return result;
324 }
325
326 Dowa_Arena *p_arena = Dowa_Arena_Create(S3_ARENA_SIZE);
327
328 // Get timestamp
329 S3_Timestamp ts;
330 s3__get_timestamp(&ts);
331
332 // Build host
333 char *host;
334 if (p_config->endpoint)
335 {
336 host = Dowa_String_Copy_Arena((char *)p_config->endpoint, p_arena);
337 }
338 else if (p_config->use_path_style)
339 {
340 size_t host_len = strlen("s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
341 host = Dowa_Arena_Allocate(p_arena, host_len);
342 snprintf(host, host_len, "s3.%s.amazonaws.com", p_config->region);
343 }
344 else
345 {
346 size_t host_len = strlen(p_config->bucket) + strlen(".s3.") + strlen(p_config->region) + strlen(".amazonaws.com") + 1;
347 host = Dowa_Arena_Allocate(p_arena, host_len);
348 snprintf(host, host_len, "%s.s3.%s.amazonaws.com", p_config->bucket, p_config->region);
349 }
350
351 // Build URI path
352 char *uri_path;
353 char *encoded_key = s3__uri_encode(s3_key, p_arena);
354 if (p_config->use_path_style)
355 {
356 size_t uri_len = strlen("/") + strlen(p_config->bucket) + strlen("/") + strlen(encoded_key) + 1;
357 uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
358 snprintf(uri_path, uri_len, "/%s/%s", p_config->bucket, encoded_key);
359 }
360 else
361 {
362 size_t uri_len = strlen("/") + strlen(encoded_key) + 1;
363 uri_path = Dowa_Arena_Allocate(p_arena, uri_len);
364 snprintf(uri_path, uri_len, "/%s", encoded_key);
365 }
366
367 // Build credential scope
368 size_t scope_len = strlen(p_config->access_key_id) + strlen(ts.date) + strlen(p_config->region) +
369 strlen(S3_SERVICE_NAME) + strlen(S3_AWS4_REQUEST) + 10;
370 char *credential = Dowa_Arena_Allocate(p_arena, scope_len);
371 snprintf(credential, scope_len, "%s/%s/%s/%s/%s",
372 p_config->access_key_id, ts.date, p_config->region, S3_SERVICE_NAME, S3_AWS4_REQUEST);
373
374 char *encoded_credential = s3__uri_encode_strict(credential, p_arena);
375
376 // Build signed headers (host is required, content-type for PUT)
377 const char *signed_headers;
378 if (content_type && strcmp(method, "PUT") == 0)
379 {
380 signed_headers = "content-type;host";
381 }
382 else
383 {
384 signed_headers = "host";
385 }
386
387 // Build canonical query string (must be sorted alphabetically)
388 char expires_str[16];
389 snprintf(expires_str, sizeof(expires_str), "%d", expires_seconds);
390
391 size_t query_len = 1024 + strlen(encoded_credential);
392 char *canonical_query = Dowa_Arena_Allocate(p_arena, query_len);
393
394 if (content_type && strcmp(method, "PUT") == 0)
395 {
396 char *encoded_content_type = s3__uri_encode_strict(content_type, p_arena);
397 snprintf(canonical_query, query_len,
398 "X-Amz-Algorithm=%s"
399 "&X-Amz-Credential=%s"
400 "&X-Amz-Date=%s"
401 "&X-Amz-Expires=%s"
402 "&X-Amz-SignedHeaders=%s"
403 "&x-amz-content-type=%s",
404 S3_ALGORITHM,
405 encoded_credential,
406 ts.datetime,
407 expires_str,
408 signed_headers,
409 encoded_content_type);
410 }
411 else
412 {
413 snprintf(canonical_query, query_len,
414 "X-Amz-Algorithm=%s"
415 "&X-Amz-Credential=%s"
416 "&X-Amz-Date=%s"
417 "&X-Amz-Expires=%s"
418 "&X-Amz-SignedHeaders=%s",
419 S3_ALGORITHM,
420 encoded_credential,
421 ts.datetime,
422 expires_str,
423 signed_headers);
424 }
425
426 // Build canonical headers
427 size_t headers_len = 256 + strlen(host) + (content_type ? strlen(content_type) : 0);
428 char *canonical_headers = Dowa_Arena_Allocate(p_arena, headers_len);
429
430 if (content_type && strcmp(method, "PUT") == 0)
431 {
432 snprintf(canonical_headers, headers_len,
433 "content-type:%s\nhost:%s\n",
434 content_type, host);
435 }
436 else
437 {
438 snprintf(canonical_headers, headers_len, "host:%s\n", host);
439 }
440
441 // For presigned URLs, payload is UNSIGNED-PAYLOAD
442 const char *payload_hash = "UNSIGNED-PAYLOAD";
443
444 // Build canonical request
445 char *canonical_request = s3__build_canonical_request(method,
446 uri_path,
447 canonical_query,
448 canonical_headers,
449 signed_headers,
450 payload_hash,
451 p_arena);
452
453 // Build string to sign
454 char *string_to_sign = s3__build_string_to_sign(ts.datetime,
455 ts.date,
456 p_config->region,
457 canonical_request,
458 p_arena);
459
460 // Calculate signing key
461 uint8 signing_key[32];
462 s3__calculate_signing_key(p_config->secret_access_key,
463 ts.date,
464 p_config->region,
465 signing_key);
466
467 // Calculate signature
468 uint8 signature[32];
469 s3__hmac_sha256(signing_key, 32,
470 (const uint8 *)string_to_sign, strlen(string_to_sign),
471 signature);
472
473 char signature_hex[65];
474 s3__hex_encode(signature, 32, signature_hex);
475
476 // Build final presigned URL
477 size_t url_len = strlen("https://") + strlen(host) + strlen(uri_path) + 1 +
478 strlen(canonical_query) + strlen("&X-Amz-Signature=") + 64 + 1;
479 char *url = Dowa_Arena_Allocate(result.p_arena, url_len);
480 snprintf(url, url_len, "https://%s%s?%s&X-Amz-Signature=%s",
481 host, uri_path, canonical_query, signature_hex);
482
483 result.success = TRUE;
484 result.url = url;
485
486 Dowa_Arena_Free(p_arena);
487 return result;
488 }
489
490 S3_Presigned_URL S3_Presign_Put(const S3_Config *p_config,
491 const char *s3_key,
492 const char *content_type,
493 int32 expires_seconds)
494 {
495 return s3__presign_url(p_config, s3_key, "PUT", content_type, expires_seconds);
496 }
497
498 S3_Presigned_URL S3_Presign_Get(const S3_Config *p_config,
499 const char *s3_key,
500 int32 expires_seconds)
501 {
502 return s3__presign_url(p_config, s3_key, "GET", NULL, expires_seconds);
503 }
504
505 void S3_Presigned_URL_Destroy(S3_Presigned_URL *p_url)
506 {
507 if (p_url && p_url->p_arena)
508 {
509 Dowa_Arena_Free(p_url->p_arena);
510 p_url->p_arena = NULL;
511 p_url->url = NULL;
512 p_url->error_message = NULL;
513 }
514 }
515
516 const char *S3_Guess_Content_Type(const char *filename)
517 {
518 if (!filename) return "application/octet-stream";
519
520 const char *dot = strrchr(filename, '.');
521 if (!dot) return "application/octet-stream";
522
523 dot++; // Skip the dot
524
525 // Common content types
526 if (strcasecmp(dot, "html") == 0 || strcasecmp(dot, "htm") == 0)
527 return "text/html";
528 if (strcasecmp(dot, "css") == 0)
529 return "text/css";
530 if (strcasecmp(dot, "js") == 0)
531 return "application/javascript";
532 if (strcasecmp(dot, "json") == 0)
533 return "application/json";
534 if (strcasecmp(dot, "xml") == 0)
535 return "application/xml";
536 if (strcasecmp(dot, "txt") == 0)
537 return "text/plain";
538 if (strcasecmp(dot, "csv") == 0)
539 return "text/csv";
540
541 // Images
542 if (strcasecmp(dot, "png") == 0)
543 return "image/png";
544 if (strcasecmp(dot, "jpg") == 0 || strcasecmp(dot, "jpeg") == 0)
545 return "image/jpeg";
546 if (strcasecmp(dot, "gif") == 0)
547 return "image/gif";
548 if (strcasecmp(dot, "svg") == 0)
549 return "image/svg+xml";
550 if (strcasecmp(dot, "webp") == 0)
551 return "image/webp";
552 if (strcasecmp(dot, "ico") == 0)
553 return "image/x-icon";
554
555 // Audio/Video
556 if (strcasecmp(dot, "mp3") == 0)
557 return "audio/mpeg";
558 if (strcasecmp(dot, "mp4") == 0)
559 return "video/mp4";
560 if (strcasecmp(dot, "webm") == 0)
561 return "video/webm";
562 if (strcasecmp(dot, "ogg") == 0)
563 return "audio/ogg";
564 if (strcasecmp(dot, "wav") == 0)
565 return "audio/wav";
566
567 // Documents
568 if (strcasecmp(dot, "pdf") == 0)
569 return "application/pdf";
570 if (strcasecmp(dot, "zip") == 0)
571 return "application/zip";
572 if (strcasecmp(dot, "gz") == 0 || strcasecmp(dot, "gzip") == 0)
573 return "application/gzip";
574 if (strcasecmp(dot, "tar") == 0)
575 return "application/x-tar";
576
577 // Fonts
578 if (strcasecmp(dot, "woff") == 0)
579 return "font/woff";
580 if (strcasecmp(dot, "woff2") == 0)
581 return "font/woff2";
582 if (strcasecmp(dot, "ttf") == 0)
583 return "font/ttf";
584 if (strcasecmp(dot, "otf") == 0)
585 return "font/otf";
586
587 return "application/octet-stream";
588 }
589
590 // --- Internal Implementation --- //
591
592 static void s3__get_timestamp(S3_Timestamp *p_ts)
593 {
594 time_t now = time(NULL);
595 struct tm *utc = gmtime(&now);
596
597 strftime(p_ts->date, sizeof(p_ts->date), "%Y%m%d", utc);
598 strftime(p_ts->datetime, sizeof(p_ts->datetime), "%Y%m%dT%H%M%SZ", utc);
599 }
600
601 static void s3__hex_encode(const uint8 *data, size_t len, char *out)
602 {
603 static const char hex[] = "0123456789abcdef";
604 for (size_t i = 0; i < len; i++)
605 {
606 out[i * 2] = hex[(data[i] >> 4) & 0x0F];
607 out[i * 2 + 1] = hex[data[i] & 0x0F];
608 }
609 out[len * 2] = '\0';
610 }
611
612 static void s3__sha256_hex(const uint8 *data, size_t len, char *out)
613 {
614 uint8 hash[SHA256_DIGEST_LENGTH];
615 SHA256(data, len, hash);
616 s3__hex_encode(hash, SHA256_DIGEST_LENGTH, out);
617 }
618
619 static void s3__hmac_sha256(const uint8 *key, size_t key_len,
620 const uint8 *data, size_t data_len,
621 uint8 *out)
622 {
623 unsigned int out_len = 0;
624 HMAC(EVP_sha256(), key, (int)key_len, data, data_len, out, &out_len);
625 }
626
627 static char *s3__uri_encode(const char *str, Dowa_Arena *p_arena)
628 {
629 if (!str) return NULL;
630
631 size_t len = strlen(str);
632 // Worst case: every char becomes %XX (3 chars)
633 size_t max_len = len * 3 + 1;
634 char *out = Dowa_Arena_Allocate(p_arena, max_len);
635 char *p = out;
636
637 for (size_t i = 0; i < len; i++)
638 {
639 char c = str[i];
640 // Unreserved characters per RFC 3986
641 if ((c >= 'A' && c <= 'Z') ||
642 (c >= 'a' && c <= 'z') ||
643 (c >= '0' && c <= '9') ||
644 c == '-' || c == '_' || c == '.' || c == '~' || c == '/')
645 {
646 *p++ = c;
647 }
648 else
649 {
650 sprintf(p, "%%%02X", (unsigned char)c);
651 p += 3;
652 }
653 }
654 *p = '\0';
655 return out;
656 }
657
658 static char *s3__build_canonical_request(const char *method,
659 const char *uri,
660 const char *query,
661 const char *headers,
662 const char *signed_headers,
663 const char *payload_hash,
664 Dowa_Arena *p_arena)
665 {
666 size_t len = strlen(method) + strlen(uri) + strlen(query) +
667 strlen(headers) + strlen(signed_headers) + strlen(payload_hash) + 10;
668 char *out = Dowa_Arena_Allocate(p_arena, len);
669 snprintf(out, len, "%s\n%s\n%s\n%s\n%s\n%s",
670 method, uri, query, headers, signed_headers, payload_hash);
671 return out;
672 }
673
674 static char *s3__build_string_to_sign(const char *datetime,
675 const char *date,
676 const char *region,
677 const char *canonical_request,
678 Dowa_Arena *p_arena)
679 {
680 // Hash the canonical request
681 char canonical_hash[65];
682 s3__sha256_hex((const uint8 *)canonical_request, strlen(canonical_request), canonical_hash);
683
684 // Build credential scope
685 size_t scope_len = strlen(date) + strlen(region) + strlen(S3_SERVICE_NAME) + strlen(S3_AWS4_REQUEST) + 4;
686 char *scope = Dowa_Arena_Allocate(p_arena, scope_len);
687 snprintf(scope, scope_len, "%s/%s/%s/%s", date, region, S3_SERVICE_NAME, S3_AWS4_REQUEST);
688
689 // Build string to sign
690 size_t len = strlen(S3_ALGORITHM) + strlen(datetime) + strlen(scope) + strlen(canonical_hash) + 10;
691 char *out = Dowa_Arena_Allocate(p_arena, len);
692 snprintf(out, len, "%s\n%s\n%s\n%s", S3_ALGORITHM, datetime, scope, canonical_hash);
693
694 return out;
695 }
696
697 static void s3__calculate_signing_key(const char *secret_key,
698 const char *date,
699 const char *region,
700 uint8 *out)
701 {
702 // AWS4 + SecretAccessKey
703 size_t key_len = 4 + strlen(secret_key) + 1;
704 char *k_secret = malloc(key_len);
705 snprintf(k_secret, key_len, "AWS4%s", secret_key);
706
707 uint8 k_date[32];
708 uint8 k_region[32];
709 uint8 k_service[32];
710
711 s3__hmac_sha256((const uint8 *)k_secret, strlen(k_secret),
712 (const uint8 *)date, strlen(date), k_date);
713
714 s3__hmac_sha256(k_date, 32,
715 (const uint8 *)region, strlen(region), k_region);
716
717 s3__hmac_sha256(k_region, 32,
718 (const uint8 *)S3_SERVICE_NAME, strlen(S3_SERVICE_NAME), k_service);
719
720 s3__hmac_sha256(k_service, 32,
721 (const uint8 *)S3_AWS4_REQUEST, strlen(S3_AWS4_REQUEST), out);
722
723 free(k_secret);
724 }
725
726 static char *s3__build_authorization_header(const char *access_key,
727 const char *date,
728 const char *region,
729 const char *signed_headers,
730 const uint8 *signing_key,
731 const char *string_to_sign,
732 Dowa_Arena *p_arena)
733 {
734 // Calculate signature
735 uint8 signature[32];
736 s3__hmac_sha256(signing_key, 32,
737 (const uint8 *)string_to_sign, strlen(string_to_sign),
738 signature);
739
740 char signature_hex[65];
741 s3__hex_encode(signature, 32, signature_hex);
742
743 // Build credential
744 size_t cred_len = strlen(access_key) + strlen(date) + strlen(region) +
745 strlen(S3_SERVICE_NAME) + strlen(S3_AWS4_REQUEST) + 10;
746 char *credential = Dowa_Arena_Allocate(p_arena, cred_len);
747 snprintf(credential, cred_len, "%s/%s/%s/%s/%s",
748 access_key, date, region, S3_SERVICE_NAME, S3_AWS4_REQUEST);
749
750 // Build full authorization header
751 size_t auth_len = strlen(S3_ALGORITHM) + strlen(credential) +
752 strlen(signed_headers) + strlen(signature_hex) + 64;
753 char *auth = Dowa_Arena_Allocate(p_arena, auth_len);
754 snprintf(auth, auth_len,
755 "%s Credential=%s, SignedHeaders=%s, Signature=%s",
756 S3_ALGORITHM, credential, signed_headers, signature_hex);
757
758 return auth;
759 }
760
761 static uint8 *s3__load_file(const char *path, size_t *p_size)
762 {
763 FILE *f = fopen(path, "rb");
764 if (!f)
765 {
766 *p_size = 0;
767 return NULL;
768 }
769
770 fseek(f, 0, SEEK_END);
771 long size = ftell(f);
772 fseek(f, 0, SEEK_SET);
773
774 if (size <= 0)
775 {
776 fclose(f);
777 *p_size = 0;
778 return NULL;
779 }
780
781 uint8 *data = malloc((size_t)size);
782 if (!data)
783 {
784 fclose(f);
785 *p_size = 0;
786 return NULL;
787 }
788
789 size_t read = fread(data, 1, (size_t)size, f);
790 fclose(f);
791
792 if (read != (size_t)size)
793 {
794 free(data);
795 *p_size = 0;
796 return NULL;
797 }
798
799 *p_size = (size_t)size;
800 return data;
801 }