comparison s3/s3_uploader.c @ 219:8c9bb0b0759e hg-web

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