2022-01-12 17:18:16 +00:00
|
|
|
const std = @import("std");
|
|
|
|
const base = @import("aws_http_base.zig");
|
|
|
|
const auth = @import("aws_authentication.zig");
|
2022-01-19 18:54:23 +00:00
|
|
|
const date = @import("date.zig");
|
2022-01-12 17:18:16 +00:00
|
|
|
|
|
|
|
const log = std.log.scoped(.aws_signing);
|
|
|
|
|
2022-05-25 21:24:56 +00:00
|
|
|
// TODO: Remove this?! This is an aws_signing, so we should know a thing
|
|
|
|
// or two about aws. So perhaps the right level of abstraction here
|
|
|
|
// is to have our service signing idiosyncracies dealt with in this
|
|
|
|
// code base. Pretty much all these flags are specific to use with S3
|
|
|
|
// except omit_session_token, which will likely apply to serveral services,
|
|
|
|
// just not sure which one yet. I'll leave this here, commented for now
|
|
|
|
// in case we need to revisit the decision
|
|
|
|
//
|
2022-01-12 17:18:16 +00:00
|
|
|
// see https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L186-L207
|
2022-05-25 21:24:56 +00:00
|
|
|
// pub const ConfigFlags = packed struct {
|
|
|
|
// // We assume the uri will be encoded once in preparation for transmission. Certain services
|
|
|
|
// // do not decode before checking signature, requiring us to actually double-encode the uri in the canonical
|
|
|
|
// // request in order to pass a signature check.
|
|
|
|
//
|
|
|
|
// use_double_uri_encode: bool = true,
|
|
|
|
//
|
|
|
|
// // Controls whether or not the uri paths should be normalized when building the canonical request
|
|
|
|
// should_normalize_uri_path: bool = true,
|
|
|
|
//
|
|
|
|
// // Controls whether "X-Amz-Security-Token" is omitted from the canonical request.
|
|
|
|
// // "X-Amz-Security-Token" is added during signing, as a header or
|
|
|
|
// // query param, when credentials have a session token.
|
|
|
|
// // If false (the default), this parameter is included in the canonical request.
|
|
|
|
// // If true, this parameter is still added, but omitted from the canonical request.
|
|
|
|
// omit_session_token: bool = true,
|
|
|
|
// };
|
2023-09-14 21:06:35 +00:00
|
|
|
pub const Credentials = auth.Credentials;
|
2022-01-12 17:18:16 +00:00
|
|
|
|
|
|
|
pub const Config = struct {
|
|
|
|
// These two should be all you need to set most of the time
|
|
|
|
service: []const u8,
|
2023-09-14 21:06:35 +00:00
|
|
|
credentials: Credentials,
|
2022-01-12 17:18:16 +00:00
|
|
|
|
|
|
|
region: []const u8 = "aws-global",
|
|
|
|
// https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L38
|
|
|
|
algorithm: enum { v4, v4a } = .v4,
|
|
|
|
// https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L24
|
|
|
|
// config_type: ?? // CRT only has one value. We'll ignore for now
|
2022-01-19 18:54:23 +00:00
|
|
|
|
2022-01-12 17:18:16 +00:00
|
|
|
// https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L49
|
|
|
|
signature_type: enum {
|
|
|
|
headers, // we only support this
|
|
|
|
query_params,
|
|
|
|
request_chunk,
|
|
|
|
request_event, // not implemented by CRT
|
|
|
|
canonical_request_headers,
|
|
|
|
canonical_request_query_params,
|
|
|
|
} = .headers,
|
2022-01-19 18:54:23 +00:00
|
|
|
|
|
|
|
/// Used for testing. If null, will use current time
|
|
|
|
signing_time: ?i64 = null,
|
2022-01-12 17:18:16 +00:00
|
|
|
|
|
|
|
// In the CRT, should_sign_header is a function to allow header filtering.
|
|
|
|
// The _ud would be a anyopaque user defined data for the function to use
|
|
|
|
// .should_sign_header = null,
|
|
|
|
// .should_sign_header_ud = null,
|
|
|
|
|
|
|
|
// In the CRT, this is only used if the body has been precalculated. We don't have
|
|
|
|
// this use case, and we'll ignore
|
|
|
|
// .signed_body_value = c.aws_byte_cursor_from_c_str(""),
|
2022-01-18 01:28:43 +00:00
|
|
|
signed_body_header: SignatureType = .sha256, // https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L131
|
2022-01-12 17:18:16 +00:00
|
|
|
|
|
|
|
// This is more complex in the CRT. We'll just take the creds. Someone
|
|
|
|
// else can use a provider and get them in advance
|
|
|
|
// https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L225-L251
|
|
|
|
// If non-zero and the signing transform is query param, then signing will add X-Amz-Expires to the query
|
|
|
|
// string, equal to the value specified here. If this value is zero or if header signing is being used then
|
|
|
|
// this parameter has no effect.
|
|
|
|
expiration_in_seconds: u64 = 0,
|
2022-01-18 01:28:43 +00:00
|
|
|
|
2022-05-25 21:24:56 +00:00
|
|
|
// flags: ConfigFlags = .{},
|
2022-01-12 17:18:16 +00:00
|
|
|
};
|
|
|
|
|
2022-01-18 01:28:43 +00:00
|
|
|
pub const SignatureType = enum { sha256, none };
|
2022-01-12 17:18:16 +00:00
|
|
|
pub const SigningError = error{
|
|
|
|
NotImplemented,
|
2022-01-19 18:54:23 +00:00
|
|
|
S3NotImplemented,
|
2022-01-12 17:18:16 +00:00
|
|
|
|
2022-01-19 18:54:23 +00:00
|
|
|
// There are a number of forbidden headers that the signing process
|
|
|
|
// basically "owns". For clarity, and because zig does not have a way
|
|
|
|
// to provide an error message
|
|
|
|
//
|
|
|
|
/// Used if the request headers already includes X-Amz-Date
|
|
|
|
/// If a specific date is required, use a specific signing_time in config
|
|
|
|
XAmzDateHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes Authorization
|
|
|
|
AuthorizationHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-content-sha256
|
|
|
|
XAmzContentSha256HeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-signature
|
|
|
|
XAmzSignatureHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-algorithm
|
|
|
|
XAmzAlgorithmHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-credential
|
|
|
|
XAmzCredentialHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-signedheaders
|
|
|
|
XAmzSignedHeadersHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-security-token
|
|
|
|
XAmzSecurityTokenHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-expires
|
|
|
|
XAmzExpiresHeaderInRequest,
|
|
|
|
/// Used if the request headers already includes x-amz-region-set
|
|
|
|
XAmzRegionSetHeaderInRequest,
|
|
|
|
} || std.fmt.AllocPrintError;
|
|
|
|
|
2022-01-20 02:58:53 +00:00
|
|
|
const forbidden_headers = .{
|
|
|
|
.{ .name = "x-amz-content-sha256", .err = SigningError.XAmzContentSha256HeaderInRequest },
|
|
|
|
.{ .name = "Authorization", .err = SigningError.AuthorizationHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-Signature", .err = SigningError.XAmzSignatureHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-Algorithm", .err = SigningError.XAmzAlgorithmHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-Credential", .err = SigningError.XAmzCredentialHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-Date", .err = SigningError.XAmzDateHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-SignedHeaders", .err = SigningError.XAmzSignedHeadersHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-Security-Token", .err = SigningError.XAmzSecurityTokenHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-Expires", .err = SigningError.XAmzExpiresHeaderInRequest },
|
|
|
|
.{ .name = "X-Amz-Region-Set", .err = SigningError.XAmzRegionSetHeaderInRequest },
|
|
|
|
};
|
|
|
|
|
|
|
|
const skipped_headers = .{
|
|
|
|
"x-amzn-trace-id",
|
|
|
|
"User-Agent",
|
|
|
|
"connection",
|
|
|
|
"sec-websocket-key",
|
|
|
|
"sec-websocket-protocol",
|
|
|
|
"sec-websocket-version",
|
|
|
|
"upgrade",
|
|
|
|
};
|
|
|
|
|
2022-01-19 18:54:23 +00:00
|
|
|
/// Signs a request. Only header signing is currently supported. Note that
|
|
|
|
/// This adds two headers to the request, which will need to be freed by the
|
|
|
|
/// caller. Use freeSignedRequest with the same parameters to free
|
2022-01-20 03:12:50 +00:00
|
|
|
pub fn signRequest(allocator: std.mem.Allocator, request: base.Request, config: Config) SigningError!base.Request {
|
2022-01-12 17:18:16 +00:00
|
|
|
try validateConfig(config);
|
2022-01-19 18:54:23 +00:00
|
|
|
for (request.headers) |h| {
|
|
|
|
inline for (forbidden_headers) |f| {
|
|
|
|
if (std.ascii.eqlIgnoreCase(h.name, f.name))
|
|
|
|
return f.err;
|
|
|
|
}
|
|
|
|
}
|
2022-01-20 03:12:50 +00:00
|
|
|
var rc = request;
|
2022-01-19 18:54:23 +00:00
|
|
|
|
|
|
|
const signing_time = config.signing_time orelse std.time.timestamp();
|
|
|
|
|
|
|
|
const signed_date = date.timestampToDateTime(signing_time);
|
|
|
|
|
|
|
|
const signing_iso8601 = try std.fmt.allocPrint(
|
|
|
|
allocator,
|
2022-01-20 20:58:56 +00:00
|
|
|
"{:0>4}{:0>2}{:0>2}T{:0>2}{:0>2}{:0>2}Z",
|
2022-01-19 18:54:23 +00:00
|
|
|
.{
|
|
|
|
signed_date.year,
|
|
|
|
signed_date.month,
|
|
|
|
signed_date.day,
|
|
|
|
signed_date.hour,
|
|
|
|
signed_date.minute,
|
|
|
|
signed_date.second,
|
|
|
|
},
|
|
|
|
);
|
2022-01-20 03:12:50 +00:00
|
|
|
errdefer freeSignedRequest(allocator, &rc, config);
|
2022-01-19 18:54:23 +00:00
|
|
|
|
2022-05-25 21:24:56 +00:00
|
|
|
var additional_header_count: u3 = 3;
|
2022-01-21 03:42:55 +00:00
|
|
|
if (config.credentials.session_token != null)
|
|
|
|
additional_header_count += 1;
|
|
|
|
const newheaders = try allocator.alloc(base.Header, rc.headers.len + additional_header_count);
|
2022-01-20 02:58:53 +00:00
|
|
|
errdefer allocator.free(newheaders);
|
2022-01-20 03:12:50 +00:00
|
|
|
const oldheaders = rc.headers;
|
2022-01-21 03:42:55 +00:00
|
|
|
if (config.credentials.session_token) |t| {
|
2022-05-25 21:24:56 +00:00
|
|
|
newheaders[newheaders.len - 4] = base.Header{
|
2022-01-21 03:42:55 +00:00
|
|
|
.name = "X-Amz-Security-Token",
|
|
|
|
.value = try allocator.dupe(u8, t),
|
|
|
|
};
|
|
|
|
}
|
2022-01-20 03:12:50 +00:00
|
|
|
errdefer freeSignedRequest(allocator, &rc, config);
|
2022-01-20 02:58:53 +00:00
|
|
|
std.mem.copy(base.Header, newheaders, oldheaders);
|
2022-05-25 21:24:56 +00:00
|
|
|
newheaders[newheaders.len - 3] = base.Header{
|
2022-01-19 18:54:23 +00:00
|
|
|
.name = "X-Amz-Date",
|
|
|
|
.value = signing_iso8601,
|
|
|
|
};
|
2022-05-25 21:24:56 +00:00
|
|
|
// From the AWS nitro enclaves SDK, it appears that there is no reason
|
|
|
|
// to avoid *ALWAYS* adding the x-amz-content-sha256 header
|
|
|
|
// https://github.com/aws/aws-nitro-enclaves-sdk-c/blob/9ecb83d07fe953636e3c0b861d6dac0a15d00f82/source/rest.c#L464
|
|
|
|
const payload_hash = try hash(allocator, request.body, config.signed_body_header);
|
|
|
|
// This will be freed in freeSignedRequest
|
|
|
|
// defer allocator.free(payload_hash);
|
|
|
|
newheaders[newheaders.len - 2] = base.Header{
|
|
|
|
.name = "x-amz-content-sha256",
|
|
|
|
.value = payload_hash,
|
|
|
|
};
|
|
|
|
|
2022-01-20 03:12:50 +00:00
|
|
|
rc.headers = newheaders[0 .. newheaders.len - 1];
|
2022-01-12 17:18:16 +00:00
|
|
|
log.debug("Signing with access key: {s}", .{config.credentials.access_key});
|
2022-05-25 21:24:56 +00:00
|
|
|
const canonical_request = try createCanonicalRequest(allocator, rc, payload_hash, config);
|
2022-01-19 18:54:23 +00:00
|
|
|
defer {
|
|
|
|
allocator.free(canonical_request.arr);
|
|
|
|
allocator.free(canonical_request.hash);
|
|
|
|
allocator.free(canonical_request.headers.str);
|
|
|
|
allocator.free(canonical_request.headers.signed_headers);
|
|
|
|
}
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("Canonical request:\n{s}", .{canonical_request.arr});
|
|
|
|
log.debug("Canonical request hash: {s}", .{canonical_request.hash});
|
2022-01-19 18:54:23 +00:00
|
|
|
const scope = try std.fmt.allocPrint(
|
|
|
|
allocator,
|
|
|
|
"{:0>4}{:0>2}{:0>2}/{s}/{s}/aws4_request",
|
|
|
|
.{
|
|
|
|
signed_date.year,
|
|
|
|
signed_date.month,
|
|
|
|
signed_date.day,
|
|
|
|
config.region,
|
|
|
|
config.service,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
defer allocator.free(scope);
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("Scope: {s}", .{scope});
|
2022-01-19 18:54:23 +00:00
|
|
|
|
|
|
|
//Algorithm + \n +
|
|
|
|
//RequestDateTime + \n +
|
|
|
|
//CredentialScope + \n +
|
|
|
|
//HashedCanonicalRequest
|
|
|
|
const string_to_sign_fmt =
|
|
|
|
\\AWS4-HMAC-SHA256
|
|
|
|
\\{s}
|
|
|
|
\\{s}
|
|
|
|
\\{s}
|
|
|
|
;
|
|
|
|
const string_to_sign = try std.fmt.allocPrint(
|
|
|
|
allocator,
|
|
|
|
string_to_sign_fmt,
|
|
|
|
.{
|
|
|
|
signing_iso8601,
|
|
|
|
scope,
|
|
|
|
canonical_request.hash,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
defer allocator.free(string_to_sign);
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("String to sign:\n{s}", .{string_to_sign});
|
2022-01-19 18:54:23 +00:00
|
|
|
|
|
|
|
const signing_key = try getSigningKey(allocator, scope[0..8], config);
|
2022-01-20 02:58:53 +00:00
|
|
|
defer allocator.free(signing_key);
|
2022-01-19 18:54:23 +00:00
|
|
|
|
2022-01-20 02:58:53 +00:00
|
|
|
const signature = try hmac(allocator, signing_key, string_to_sign);
|
|
|
|
defer allocator.free(signature);
|
|
|
|
newheaders[newheaders.len - 1] = base.Header{
|
2022-01-19 18:54:23 +00:00
|
|
|
.name = "Authorization",
|
|
|
|
.value = try std.fmt.allocPrint(
|
|
|
|
allocator,
|
|
|
|
"AWS4-HMAC-SHA256 Credential={s}/{s}, SignedHeaders={s}, Signature={s}",
|
|
|
|
.{
|
|
|
|
config.credentials.access_key,
|
|
|
|
scope,
|
|
|
|
canonical_request.headers.signed_headers,
|
2022-01-20 02:58:53 +00:00
|
|
|
std.fmt.fmtSliceHexLower(signature),
|
2022-01-19 18:54:23 +00:00
|
|
|
},
|
|
|
|
),
|
|
|
|
};
|
2022-01-20 03:12:50 +00:00
|
|
|
rc.headers = newheaders;
|
|
|
|
return rc;
|
2022-01-12 17:18:16 +00:00
|
|
|
}
|
|
|
|
|
2022-01-20 02:58:53 +00:00
|
|
|
/// Frees allocated resources for the request, including the headers array
|
2022-01-19 18:54:23 +00:00
|
|
|
pub fn freeSignedRequest(allocator: std.mem.Allocator, request: *base.Request, config: Config) void {
|
|
|
|
validateConfig(config) catch |e| {
|
|
|
|
log.err("Signing validation failed during signature free: {}", .{e});
|
|
|
|
if (@errorReturnTrace()) |trace| {
|
|
|
|
std.debug.dumpStackTrace(trace.*);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
for (request.headers) |h| {
|
2022-01-21 03:42:55 +00:00
|
|
|
if (std.ascii.eqlIgnoreCase(h.name, "X-Amz-Date") or
|
|
|
|
std.ascii.eqlIgnoreCase(h.name, "Authorization") or
|
2022-05-25 21:24:56 +00:00
|
|
|
std.ascii.eqlIgnoreCase(h.name, "X-Amz-Security-Token") or
|
|
|
|
std.ascii.eqlIgnoreCase(h.name, "x-amz-content-sha256"))
|
2022-01-19 18:54:23 +00:00
|
|
|
allocator.free(h.value);
|
|
|
|
}
|
2022-01-20 02:58:53 +00:00
|
|
|
|
|
|
|
allocator.free(request.headers);
|
2022-01-19 18:54:23 +00:00
|
|
|
}
|
|
|
|
|
2023-09-14 21:06:35 +00:00
|
|
|
pub const credentialsFn = *const fn ([]const u8) ?Credentials;
|
2023-10-22 17:25:07 +00:00
|
|
|
|
|
|
|
pub fn verifyServerRequest(allocator: std.mem.Allocator, request: std.http.Server.Request, request_body_reader: anytype, credentials_fn: credentialsFn) !bool {
|
|
|
|
const unverified_request = UnverifiedRequest{
|
|
|
|
.headers = request.headers,
|
|
|
|
.target = request.target,
|
|
|
|
.method = request.method,
|
|
|
|
};
|
|
|
|
return verify(allocator, unverified_request, request_body_reader, credentials_fn);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const UnverifiedRequest = struct {
|
|
|
|
headers: std.http.Headers,
|
|
|
|
target: []const u8,
|
|
|
|
method: std.http.Method,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub fn verify(allocator: std.mem.Allocator, request: UnverifiedRequest, request_body_reader: anytype, credentials_fn: credentialsFn) !bool {
|
2023-09-09 04:45:36 +00:00
|
|
|
// Authorization: AWS4-HMAC-SHA256 Credential=ACCESS/20230908/us-west-2/s3/aws4_request, SignedHeaders=accept;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class, Signature=fcc43ce73a34c9bd1ddf17e8a435f46a859812822f944f9eeb2aabcd64b03523
|
2023-10-25 07:00:11 +00:00
|
|
|
const auth_header_or_null = request.headers.getFirstValue("Authorization");
|
|
|
|
const auth_header = if (auth_header_or_null) |a| a else return error.AuthorizationHeaderMissing;
|
2023-09-09 04:45:36 +00:00
|
|
|
if (!std.mem.startsWith(u8, auth_header, "AWS4-HMAC-SHA256")) return error.UnsupportedAuthorizationType;
|
|
|
|
var credential: ?[]const u8 = null;
|
|
|
|
var signed_headers: ?[]const u8 = null;
|
|
|
|
var signature: ?[]const u8 = null;
|
|
|
|
var split_iterator = std.mem.splitSequence(u8, auth_header, " ");
|
|
|
|
while (split_iterator.next()) |auth_part| {
|
|
|
|
// NOTE: auth_part likely to end with ,
|
|
|
|
if (std.ascii.startsWithIgnoreCase(auth_part, "Credential=")) {
|
|
|
|
credential = std.mem.trim(u8, auth_part["Credential=".len..], ",");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (std.ascii.startsWithIgnoreCase(auth_part, "SignedHeaders=")) {
|
|
|
|
signed_headers = std.mem.trim(u8, auth_part["SignedHeaders=".len..], ",");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (std.ascii.startsWithIgnoreCase(auth_part, "Signature=")) {
|
|
|
|
signature = std.mem.trim(u8, auth_part["Signature=".len..], ",");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (credential == null) return error.AuthorizationHeaderMissingCredential;
|
|
|
|
if (signed_headers == null) return error.AuthorizationHeaderMissingSignedHeaders;
|
|
|
|
if (signature == null) return error.AuthorizationHeaderMissingSignature;
|
|
|
|
return verifyParsedAuthorization(
|
|
|
|
allocator,
|
|
|
|
request,
|
|
|
|
request_body_reader,
|
|
|
|
credential.?,
|
|
|
|
signed_headers.?,
|
|
|
|
signature.?,
|
|
|
|
credentials_fn,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn verifyParsedAuthorization(
|
|
|
|
allocator: std.mem.Allocator,
|
2023-10-22 17:25:07 +00:00
|
|
|
request: UnverifiedRequest,
|
2023-09-09 04:45:36 +00:00
|
|
|
request_body_reader: anytype,
|
|
|
|
credential: []const u8,
|
|
|
|
signed_headers: []const u8,
|
|
|
|
signature: []const u8,
|
2023-09-14 21:06:35 +00:00
|
|
|
credentials_fn: credentialsFn,
|
2023-09-09 04:45:36 +00:00
|
|
|
) !bool {
|
|
|
|
// AWS4-HMAC-SHA256
|
|
|
|
// Credential=ACCESS/20230908/us-west-2/s3/aws4_request
|
|
|
|
// SignedHeaders=accept;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class
|
|
|
|
// Signature=fcc43ce73a34c9bd1ddf17e8a435f46a859812822f944f9eeb2aabcd64b03523
|
|
|
|
var credential_iterator = std.mem.split(u8, credential, "/");
|
|
|
|
const access_key = credential_iterator.next().?;
|
|
|
|
const credentials = credentials_fn(access_key) orelse return error.CredentialsNotFound;
|
|
|
|
// TODO: https://stackoverflow.com/questions/29276609/aws-authentication-requires-a-valid-date-or-x-amz-date-header-curl
|
|
|
|
// For now I want to see this test pass
|
|
|
|
const normalized_iso_date = request.headers.getFirstValue("x-amz-date") orelse
|
|
|
|
request.headers.getFirstValue("Date").?;
|
|
|
|
log.debug("Got date: {s}", .{normalized_iso_date});
|
|
|
|
_ = credential_iterator.next().?; // skip the date...I don't think we need this
|
|
|
|
const region = credential_iterator.next().?;
|
|
|
|
const service = credential_iterator.next().?;
|
|
|
|
const aws4_request = credential_iterator.next().?;
|
|
|
|
if (!std.mem.eql(u8, aws4_request, "aws4_request")) return error.UnexpectedCredentialValue;
|
2024-01-18 12:43:45 +00:00
|
|
|
const config = Config{
|
2023-09-09 04:45:36 +00:00
|
|
|
.service = service,
|
|
|
|
.credentials = credentials,
|
|
|
|
.region = region,
|
|
|
|
.algorithm = .v4,
|
|
|
|
.signature_type = .headers,
|
|
|
|
.signed_body_header = .sha256,
|
|
|
|
.expiration_in_seconds = 0,
|
|
|
|
.signing_time = try date.dateTimeToTimestamp(try date.parseIso8601ToDateTime(normalized_iso_date)),
|
|
|
|
};
|
|
|
|
|
|
|
|
var headers = try allocator.alloc(base.Header, std.mem.count(u8, signed_headers, ";") + 1);
|
|
|
|
defer allocator.free(headers);
|
|
|
|
var signed_headers_iterator = std.mem.splitSequence(u8, signed_headers, ";");
|
|
|
|
var inx: usize = 0;
|
|
|
|
while (signed_headers_iterator.next()) |signed_header| {
|
|
|
|
var is_forbidden = false;
|
|
|
|
inline for (forbidden_headers) |forbidden| {
|
|
|
|
if (std.ascii.eqlIgnoreCase(forbidden.name, signed_header)) {
|
|
|
|
is_forbidden = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (is_forbidden) continue;
|
|
|
|
headers[inx] = .{
|
|
|
|
.name = signed_header,
|
|
|
|
.value = request.headers.getFirstValue(signed_header).?,
|
|
|
|
};
|
|
|
|
inx += 1;
|
|
|
|
}
|
|
|
|
var target_iterator = std.mem.splitSequence(u8, request.target, "?");
|
|
|
|
var signed_request = base.Request{
|
|
|
|
.path = target_iterator.first(),
|
|
|
|
.headers = headers[0..inx],
|
|
|
|
.method = @tagName(request.method),
|
|
|
|
.content_type = request.headers.getFirstValue("content-type").?,
|
|
|
|
};
|
|
|
|
signed_request.query = request.target[signed_request.path.len..]; // TODO: should this be +1? query here would include '?'
|
|
|
|
signed_request.body = try request_body_reader.readAllAlloc(allocator, std.math.maxInt(usize));
|
|
|
|
defer allocator.free(signed_request.body);
|
|
|
|
signed_request = try signRequest(allocator, signed_request, config);
|
|
|
|
defer freeSignedRequest(allocator, &signed_request, config);
|
|
|
|
return verifySignedRequest(signed_request, signature);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn verifySignedRequest(signed_request: base.Request, signature: []const u8) !bool {
|
|
|
|
// We're not doing a lot of error checking here...we are all in control of this code
|
|
|
|
const auth_header = blk: {
|
|
|
|
for (signed_request.headers) |header| {
|
|
|
|
if (std.mem.eql(u8, header.name, "Authorization"))
|
|
|
|
break :blk header.value;
|
|
|
|
}
|
|
|
|
break :blk null;
|
|
|
|
};
|
|
|
|
var split_iterator = std.mem.splitSequence(u8, auth_header.?, " ");
|
|
|
|
const calculated_signature = blk: {
|
|
|
|
while (split_iterator.next()) |auth_part| {
|
|
|
|
if (std.ascii.startsWithIgnoreCase(auth_part, "Signature=")) {
|
|
|
|
break :blk std.mem.trim(u8, auth_part["Signature=".len..], ",");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break :blk null;
|
|
|
|
};
|
|
|
|
return std.mem.eql(u8, signature, calculated_signature.?);
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:54:23 +00:00
|
|
|
fn getSigningKey(allocator: std.mem.Allocator, signing_date: []const u8, config: Config) ![]const u8 {
|
|
|
|
// TODO: This is designed for lots of caching. We need to work that out
|
|
|
|
// kSecret = your secret access key
|
|
|
|
// kDate = HMAC("AWS4" + kSecret, Date)
|
|
|
|
// kRegion = HMAC(kDate, Region)
|
|
|
|
// kService = HMAC(kRegion, Service)
|
|
|
|
// kSigning = HMAC(kService, "aws4_request")
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug(
|
|
|
|
\\signing key params:
|
|
|
|
\\ key: (you wish)
|
|
|
|
\\ date: {s}
|
|
|
|
\\ region: {s}
|
|
|
|
\\ service: {s}
|
|
|
|
, .{ signing_date, config.region, config.service });
|
2024-01-18 12:43:45 +00:00
|
|
|
const secret = try std.fmt.allocPrint(allocator, "AWS4{s}", .{config.credentials.secret_key});
|
2022-01-19 18:54:23 +00:00
|
|
|
defer {
|
2022-01-31 17:01:01 +00:00
|
|
|
// secureZero avoids compiler optimizations that may say
|
|
|
|
// "WTF are you doing this thing? Looks like nothing to me. It's silly and we will remove it"
|
|
|
|
std.crypto.utils.secureZero(u8, secret); // zero our copy of secret
|
2022-01-19 18:54:23 +00:00
|
|
|
allocator.free(secret);
|
|
|
|
}
|
2022-01-20 02:58:53 +00:00
|
|
|
// log.debug("secret: {s}", .{secret});
|
|
|
|
const k_date = try hmac(allocator, secret, signing_date);
|
|
|
|
defer allocator.free(k_date);
|
|
|
|
const k_region = try hmac(allocator, k_date, config.region);
|
|
|
|
defer allocator.free(k_region);
|
|
|
|
const k_service = try hmac(allocator, k_region, config.service);
|
|
|
|
defer allocator.free(k_service);
|
|
|
|
const k_signing = try hmac(allocator, k_service, "aws4_request");
|
2022-01-19 18:54:23 +00:00
|
|
|
return k_signing;
|
|
|
|
}
|
2022-01-12 17:18:16 +00:00
|
|
|
fn validateConfig(config: Config) SigningError!void {
|
2022-01-18 01:28:43 +00:00
|
|
|
if (config.signature_type != .headers or
|
|
|
|
config.signed_body_header != .sha256 or
|
|
|
|
config.expiration_in_seconds != 0 or
|
2022-05-25 21:24:56 +00:00
|
|
|
config.algorithm != .v4)
|
2022-01-18 01:28:43 +00:00
|
|
|
return SigningError.NotImplemented;
|
|
|
|
}
|
|
|
|
|
2022-01-20 02:58:53 +00:00
|
|
|
fn hmac(allocator: std.mem.Allocator, key: []const u8, data: []const u8) ![]const u8 {
|
2022-01-19 18:54:23 +00:00
|
|
|
var out: [std.crypto.auth.hmac.sha2.HmacSha256.mac_length]u8 = undefined;
|
2022-01-20 02:58:53 +00:00
|
|
|
std.crypto.auth.hmac.sha2.HmacSha256.create(out[0..], data, key);
|
|
|
|
return try allocator.dupe(u8, out[0..]);
|
2022-01-19 18:54:23 +00:00
|
|
|
}
|
|
|
|
const Hashed = struct {
|
|
|
|
arr: []const u8,
|
|
|
|
hash: []const u8,
|
|
|
|
headers: CanonicalHeaders,
|
|
|
|
};
|
|
|
|
|
2022-05-25 21:24:56 +00:00
|
|
|
fn createCanonicalRequest(allocator: std.mem.Allocator, request: base.Request, payload_hash: []const u8, config: Config) !Hashed {
|
2022-01-18 01:28:43 +00:00
|
|
|
// CanonicalRequest =
|
|
|
|
// HTTPRequestMethod + '\n' +
|
|
|
|
// CanonicalURI + '\n' +
|
|
|
|
// CanonicalQueryString + '\n' +
|
|
|
|
// CanonicalHeaders + '\n' +
|
|
|
|
// SignedHeaders + '\n' +
|
|
|
|
// HexEncode(Hash(RequestPayload))
|
|
|
|
const fmt =
|
|
|
|
\\{s}
|
|
|
|
\\{s}
|
|
|
|
\\{s}
|
|
|
|
\\{s}
|
|
|
|
\\{s}
|
|
|
|
\\{s}
|
|
|
|
;
|
|
|
|
|
|
|
|
// TODO: This is all better as a writer - less allocations/copying
|
2023-08-05 20:29:23 +00:00
|
|
|
const canonical_method = try canonicalRequestMethod(request.method);
|
2022-06-01 01:47:59 +00:00
|
|
|
// Let's not mess around here...s3 is the oddball
|
|
|
|
const double_encode = !std.mem.eql(u8, config.service, "s3");
|
|
|
|
const canonical_url = try canonicalUri(allocator, request.path, double_encode);
|
2022-01-18 01:28:43 +00:00
|
|
|
defer allocator.free(canonical_url);
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("final uri: {s}", .{canonical_url});
|
2022-01-20 15:32:26 +00:00
|
|
|
const canonical_query = try canonicalQueryString(allocator, request.query);
|
2022-01-18 01:28:43 +00:00
|
|
|
defer allocator.free(canonical_query);
|
2022-01-20 15:32:26 +00:00
|
|
|
log.debug("canonical query: {s}", .{canonical_query});
|
2022-05-25 21:24:56 +00:00
|
|
|
const canonical_headers = try canonicalHeaders(allocator, request.headers, config.service);
|
2022-01-19 18:54:23 +00:00
|
|
|
|
|
|
|
const canonical_request = try std.fmt.allocPrint(allocator, fmt, .{
|
2022-01-18 01:28:43 +00:00
|
|
|
canonical_method,
|
|
|
|
canonical_url,
|
|
|
|
canonical_query,
|
|
|
|
canonical_headers.str,
|
|
|
|
canonical_headers.signed_headers,
|
|
|
|
payload_hash,
|
|
|
|
});
|
2022-01-19 18:54:23 +00:00
|
|
|
errdefer allocator.free(canonical_request);
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("Canonical_request (just calculated):\n{s}", .{canonical_request});
|
2022-01-19 18:54:23 +00:00
|
|
|
const hashed = try hash(allocator, canonical_request, config.signed_body_header);
|
|
|
|
return Hashed{
|
|
|
|
.arr = canonical_request,
|
|
|
|
.hash = hashed,
|
|
|
|
.headers = canonical_headers,
|
|
|
|
};
|
2022-01-18 01:28:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn canonicalRequestMethod(method: []const u8) ![]const u8 {
|
|
|
|
return method; // We assume it's good
|
|
|
|
}
|
|
|
|
|
|
|
|
fn canonicalUri(allocator: std.mem.Allocator, path: []const u8, double_encode: bool) ![]const u8 {
|
|
|
|
// Add the canonical URI parameter, followed by a newline character. The
|
|
|
|
// canonical URI is the URI-encoded version of the absolute path component
|
|
|
|
// of the URI, which is everything in the URI from the HTTP host to the
|
|
|
|
// question mark character ("?") that begins the query string parameters (if any).
|
|
|
|
//
|
|
|
|
// Normalize URI paths according to RFC 3986. Remove redundant and relative
|
|
|
|
// path components. Each path segment must be URI-encoded twice
|
|
|
|
// (except for Amazon S3 which only gets URI-encoded once).
|
|
|
|
//
|
|
|
|
// Note: In exception to this, you do not normalize URI paths for requests
|
|
|
|
// to Amazon S3. For example, if you have a bucket with an object
|
|
|
|
// named my-object//example//photo.user, use that path. Normalizing
|
|
|
|
// the path to my-object/example/photo.user will cause the request to
|
|
|
|
// fail. For more information, see Task 1: Create a Canonical Request in
|
|
|
|
// the Amazon Simple Storage Service API Reference.
|
|
|
|
//
|
|
|
|
// If the absolute path is empty, use a forward slash (/)
|
|
|
|
//
|
|
|
|
// For now, we will "Remove redundant and relative path components". This
|
|
|
|
// doesn't apply to S3 anyway, and we'll make it the callers's problem
|
|
|
|
if (path.len == 0 or path[0] == '?' or path[0] == '#')
|
|
|
|
return try allocator.dupe(u8, "/");
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("encoding path: {s}", .{path});
|
2023-08-27 16:13:41 +00:00
|
|
|
var encoded_once = try encodeUri(allocator, path);
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("encoded path (1): {s}", .{encoded_once});
|
2023-08-30 20:49:56 +00:00
|
|
|
if (!double_encode or std.mem.indexOf(u8, path, "%") != null) { // TODO: Is the indexOf condition universally true?
|
2023-08-27 16:13:41 +00:00
|
|
|
if (std.mem.lastIndexOf(u8, encoded_once, "?")) |i| {
|
|
|
|
_ = allocator.resize(encoded_once, i);
|
|
|
|
return encoded_once[0..i];
|
|
|
|
}
|
|
|
|
return encoded_once;
|
|
|
|
}
|
2022-01-18 01:28:43 +00:00
|
|
|
defer allocator.free(encoded_once);
|
2023-08-27 16:13:41 +00:00
|
|
|
var encoded_twice = try encodeUri(allocator, encoded_once);
|
2022-01-20 02:58:53 +00:00
|
|
|
log.debug("encoded path (2): {s}", .{encoded_twice});
|
2023-08-27 16:13:41 +00:00
|
|
|
if (std.mem.lastIndexOf(u8, encoded_twice, "?")) |i| {
|
|
|
|
_ = allocator.resize(encoded_twice, i);
|
|
|
|
return encoded_twice[0..i];
|
|
|
|
}
|
|
|
|
return encoded_twice;
|
2022-01-18 01:28:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn encodeParamPart(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
|
|
|
|
const unreserved_marks = "-_.!~*'()";
|
|
|
|
var encoded = try std.ArrayList(u8).initCapacity(allocator, path.len);
|
|
|
|
defer encoded.deinit();
|
|
|
|
for (path) |c| {
|
|
|
|
var should_encode = true;
|
|
|
|
for (unreserved_marks) |r|
|
|
|
|
if (r == c) {
|
|
|
|
should_encode = false;
|
|
|
|
break;
|
|
|
|
};
|
2023-08-05 19:41:04 +00:00
|
|
|
if (should_encode and std.ascii.isAlphanumeric(c))
|
2022-01-18 01:28:43 +00:00
|
|
|
should_encode = false;
|
|
|
|
|
|
|
|
if (!should_encode) {
|
|
|
|
try encoded.append(c);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Whatever remains, encode it
|
|
|
|
try encoded.append('%');
|
|
|
|
const hex = try std.fmt.allocPrint(allocator, "{s}", .{std.fmt.fmtSliceHexUpper(&[_]u8{c})});
|
|
|
|
defer allocator.free(hex);
|
|
|
|
try encoded.appendSlice(hex);
|
|
|
|
}
|
|
|
|
return encoded.toOwnedSlice();
|
|
|
|
}
|
2023-08-29 22:21:47 +00:00
|
|
|
|
|
|
|
// URI encode every byte except the unreserved characters:
|
|
|
|
// 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'.
|
|
|
|
//
|
|
|
|
// The space character is a reserved character and must be encoded as "%20"
|
|
|
|
// (and not as "+").
|
|
|
|
//
|
|
|
|
// Each URI encoded byte is formed by a '%' and the two-digit hexadecimal value of the byte.
|
|
|
|
//
|
|
|
|
// Letters in the hexadecimal value must be uppercase, for example "%1A".
|
|
|
|
//
|
|
|
|
// Encode the forward slash character, '/', everywhere except in the object key
|
|
|
|
// name. For example, if the object key name is photos/Jan/sample.jpg, the
|
|
|
|
// forward slash in the key name is not encoded.
|
|
|
|
|
2023-08-27 16:13:41 +00:00
|
|
|
fn encodeUri(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
|
2022-01-18 01:28:43 +00:00
|
|
|
const reserved_characters = ";,/?:@&=+$#";
|
|
|
|
const unreserved_marks = "-_.!~*'()";
|
|
|
|
var encoded = try std.ArrayList(u8).initCapacity(allocator, path.len);
|
|
|
|
defer encoded.deinit();
|
2023-08-29 22:21:47 +00:00
|
|
|
// if (std.mem.startsWith(u8, path, "/2017-03-31/tags/arn")) {
|
|
|
|
// try encoded.appendSlice("/2017-03-31/tags/arn%25253Aaws%25253Alambda%25253Aus-west-2%25253A550620852718%25253Afunction%25253Aawsome-lambda-LambdaStackawsomeLambda");
|
|
|
|
// return encoded.toOwnedSlice();
|
|
|
|
// }
|
2022-01-18 01:28:43 +00:00
|
|
|
for (path) |c| {
|
|
|
|
var should_encode = true;
|
|
|
|
for (reserved_characters) |r|
|
|
|
|
if (r == c) {
|
|
|
|
should_encode = false;
|
|
|
|
break;
|
|
|
|
};
|
|
|
|
if (should_encode) {
|
|
|
|
for (unreserved_marks) |r|
|
|
|
|
if (r == c) {
|
|
|
|
should_encode = false;
|
|
|
|
break;
|
|
|
|
};
|
|
|
|
}
|
2023-08-05 19:41:04 +00:00
|
|
|
if (should_encode and std.ascii.isAlphanumeric(c))
|
2022-01-18 01:28:43 +00:00
|
|
|
should_encode = false;
|
|
|
|
|
|
|
|
if (!should_encode) {
|
|
|
|
try encoded.append(c);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Whatever remains, encode it
|
|
|
|
try encoded.append('%');
|
|
|
|
const hex = try std.fmt.allocPrint(allocator, "{s}", .{std.fmt.fmtSliceHexUpper(&[_]u8{c})});
|
|
|
|
defer allocator.free(hex);
|
|
|
|
try encoded.appendSlice(hex);
|
|
|
|
}
|
|
|
|
return encoded.toOwnedSlice();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn canonicalQueryString(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
|
|
|
|
// To construct the canonical query string, complete the following steps:
|
|
|
|
//
|
|
|
|
// Sort the parameter names by character code point in ascending order.
|
|
|
|
// Parameters with duplicate names should be sorted by value. For example,
|
|
|
|
// a parameter name that begins with the uppercase letter F precedes a
|
|
|
|
// parameter name that begins with a lowercase letter b.
|
|
|
|
//
|
|
|
|
// URI-encode each parameter name and value according to the following rules:
|
|
|
|
//
|
|
|
|
// Do not URI-encode any of the unreserved characters that RFC 3986
|
|
|
|
// defines: A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ), and tilde ( ~ ).
|
|
|
|
//
|
|
|
|
// Percent-encode all other characters with %XY, where X and Y are
|
|
|
|
// hexadecimal characters (0-9 and uppercase A-F). For example, the
|
|
|
|
// space character must be encoded as %20 (not using '+', as some
|
|
|
|
// encoding schemes do) and extended UTF-8 characters must be in the
|
|
|
|
// form %XY%ZA%BC.
|
|
|
|
//
|
|
|
|
// Double-encode any equals ( = ) characters in parameter values.
|
|
|
|
//
|
|
|
|
// Build the canonical query string by starting with the first parameter
|
|
|
|
// name in the sorted list.
|
|
|
|
//
|
|
|
|
// For each parameter, append the URI-encoded parameter name, followed by
|
|
|
|
// the equals sign character (=), followed by the URI-encoded parameter
|
|
|
|
// value. Use an empty string for parameters that have no value.
|
|
|
|
//
|
|
|
|
// Append the ampersand character (&) after each parameter value, except
|
|
|
|
// for the last value in the list.
|
|
|
|
//
|
|
|
|
// One option for the query API is to put all request parameters in the query
|
|
|
|
// string. For example, you can do this for Amazon S3 to create a presigned
|
|
|
|
// URL. In that case, the canonical query string must include not only
|
|
|
|
// parameters for the request, but also the parameters used as part of the
|
|
|
|
// signing process—the hashing algorithm, credential scope, date, and signed
|
|
|
|
// headers parameters.
|
|
|
|
//
|
|
|
|
// The following example shows a query string that includes authentication
|
|
|
|
// information. The example is formatted with line breaks for readability, but
|
|
|
|
// the canonical query string must be one continuous line of text in your code.
|
|
|
|
const first_question = std.mem.indexOf(u8, path, "?");
|
|
|
|
if (first_question == null)
|
|
|
|
return try allocator.dupe(u8, "");
|
|
|
|
|
|
|
|
// We have a query string
|
|
|
|
const query = path[first_question.? + 1 ..];
|
|
|
|
|
|
|
|
// Split this by component
|
|
|
|
var portions = std.mem.split(u8, query, "&");
|
|
|
|
var sort_me = std.ArrayList([]const u8).init(allocator);
|
|
|
|
defer sort_me.deinit();
|
|
|
|
while (portions.next()) |item|
|
|
|
|
try sort_me.append(item);
|
2023-08-05 19:41:04 +00:00
|
|
|
std.sort.pdq([]const u8, sort_me.items, {}, lessThanBinary);
|
2022-01-18 01:28:43 +00:00
|
|
|
|
|
|
|
var normalized = try std.ArrayList(u8).initCapacity(allocator, path.len);
|
|
|
|
defer normalized.deinit();
|
|
|
|
var first = true;
|
|
|
|
for (sort_me.items) |i| {
|
|
|
|
if (!first) try normalized.append('&');
|
|
|
|
first = false;
|
2024-01-18 12:43:45 +00:00
|
|
|
const first_equals = std.mem.indexOf(u8, i, "=");
|
2022-01-18 01:28:43 +00:00
|
|
|
if (first_equals == null) {
|
|
|
|
// Rare. This is "foo="
|
|
|
|
const normed_item = try encodeUri(allocator, i);
|
|
|
|
defer allocator.free(normed_item);
|
|
|
|
try normalized.appendSlice(i); // This should be encoded
|
|
|
|
try normalized.append('=');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// normal key=value stuff
|
|
|
|
const key = try encodeParamPart(allocator, i[0..first_equals.?]);
|
|
|
|
defer allocator.free(key);
|
|
|
|
|
|
|
|
const value = try encodeParamPart(allocator, i[first_equals.? + 1 ..]);
|
|
|
|
defer allocator.free(value);
|
|
|
|
// Double-encode any = in the value. But not anything else?
|
|
|
|
const weird_equals_in_value_thing = try replace(allocator, value, "%3D", "%253D");
|
|
|
|
defer allocator.free(weird_equals_in_value_thing);
|
|
|
|
try normalized.appendSlice(key);
|
|
|
|
try normalized.append('=');
|
|
|
|
try normalized.appendSlice(weird_equals_in_value_thing);
|
|
|
|
}
|
|
|
|
|
|
|
|
return normalized.toOwnedSlice();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn replace(allocator: std.mem.Allocator, haystack: []const u8, needle: []const u8, replacement_value: []const u8) ![]const u8 {
|
2024-01-18 12:43:45 +00:00
|
|
|
const buffer = try allocator.alloc(u8, std.mem.replacementSize(u8, haystack, needle, replacement_value));
|
2022-01-18 01:28:43 +00:00
|
|
|
_ = std.mem.replace(u8, haystack, needle, replacement_value, buffer);
|
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn lessThanBinary(context: void, lhs: []const u8, rhs: []const u8) bool {
|
|
|
|
_ = context;
|
|
|
|
return std.mem.lessThan(u8, lhs, rhs);
|
|
|
|
}
|
|
|
|
const CanonicalHeaders = struct {
|
|
|
|
str: []const u8,
|
|
|
|
signed_headers: []const u8,
|
|
|
|
};
|
2022-05-25 21:24:56 +00:00
|
|
|
fn canonicalHeaders(allocator: std.mem.Allocator, headers: []base.Header, service: []const u8) !CanonicalHeaders {
|
2022-01-18 01:28:43 +00:00
|
|
|
//
|
|
|
|
// Doc example. Original:
|
|
|
|
//
|
|
|
|
// Host:iam.amazonaws.com\n
|
|
|
|
// Content-Type:application/x-www-form-urlencoded; charset=utf-8\n
|
|
|
|
// My-header1: a b c \n
|
|
|
|
// X-Amz-Date:20150830T123600Z\n
|
|
|
|
// My-Header2: "a b c" \n
|
|
|
|
//
|
|
|
|
// Canonical form:
|
|
|
|
// content-type:application/x-www-form-urlencoded; charset=utf-8\n
|
|
|
|
// host:iam.amazonaws.com\n
|
|
|
|
// my-header1:a b c\n
|
|
|
|
// my-header2:"a b c"\n
|
|
|
|
// x-amz-date:20150830T123600Z\n
|
2022-01-19 18:54:23 +00:00
|
|
|
var dest = try std.ArrayList(base.Header).initCapacity(allocator, headers.len);
|
2022-01-18 01:28:43 +00:00
|
|
|
defer {
|
2022-01-19 18:54:23 +00:00
|
|
|
for (dest.items) |h| {
|
2022-01-18 01:28:43 +00:00
|
|
|
allocator.free(h.name);
|
|
|
|
allocator.free(h.value);
|
|
|
|
}
|
2022-01-19 18:54:23 +00:00
|
|
|
dest.deinit();
|
2022-01-18 01:28:43 +00:00
|
|
|
}
|
|
|
|
var total_len: usize = 0;
|
|
|
|
var total_name_len: usize = 0;
|
2022-01-19 18:54:23 +00:00
|
|
|
for (headers) |h| {
|
|
|
|
var skip = false;
|
|
|
|
inline for (skipped_headers) |s| {
|
|
|
|
if (std.ascii.eqlIgnoreCase(s, h.name)) {
|
|
|
|
skip = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-01-21 03:42:55 +00:00
|
|
|
// Well, this is fun (https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html):
|
|
|
|
//
|
|
|
|
// When you add the X-Amz-Security-Token parameter to the query string,
|
|
|
|
// some services require that you include this parameter in the
|
|
|
|
// canonical (signed) request. For other services, you add this
|
|
|
|
// parameter at the end, after you calculate the signature. For
|
|
|
|
// details, see the API reference documentation for that service.
|
2022-05-25 21:24:56 +00:00
|
|
|
if (!std.mem.eql(u8, service, "s3") and std.ascii.eqlIgnoreCase(h.name, "X-Amz-Security-Token")) {
|
2022-01-21 03:42:55 +00:00
|
|
|
skip = true;
|
|
|
|
}
|
2022-01-19 18:54:23 +00:00
|
|
|
if (skip) continue;
|
|
|
|
|
2022-01-18 01:28:43 +00:00
|
|
|
total_len += (h.name.len + h.value.len + 2);
|
|
|
|
total_name_len += (h.name.len + 1);
|
|
|
|
const value = try canonicalHeaderValue(allocator, h.value);
|
|
|
|
defer allocator.free(value);
|
2022-01-19 18:54:23 +00:00
|
|
|
const n = try std.ascii.allocLowerString(allocator, h.name);
|
|
|
|
const v = try std.fmt.allocPrint(allocator, "{s}", .{value});
|
|
|
|
try dest.append(.{ .name = n, .value = v });
|
2022-01-18 01:28:43 +00:00
|
|
|
}
|
|
|
|
|
2023-08-05 19:41:04 +00:00
|
|
|
std.sort.pdq(base.Header, dest.items, {}, lessThan);
|
2022-01-18 01:28:43 +00:00
|
|
|
|
|
|
|
var dest_str = try std.ArrayList(u8).initCapacity(allocator, total_len);
|
|
|
|
defer dest_str.deinit();
|
|
|
|
var signed_headers = try std.ArrayList(u8).initCapacity(allocator, total_name_len);
|
|
|
|
defer signed_headers.deinit();
|
|
|
|
var first = true;
|
2022-01-19 18:54:23 +00:00
|
|
|
for (dest.items) |h| {
|
2022-01-18 01:28:43 +00:00
|
|
|
dest_str.appendSliceAssumeCapacity(h.name);
|
|
|
|
dest_str.appendAssumeCapacity(':');
|
|
|
|
dest_str.appendSliceAssumeCapacity(h.value);
|
|
|
|
dest_str.appendAssumeCapacity('\n');
|
|
|
|
|
|
|
|
if (!first) signed_headers.appendAssumeCapacity(';');
|
|
|
|
first = false;
|
|
|
|
signed_headers.appendSliceAssumeCapacity(h.name);
|
|
|
|
}
|
|
|
|
return CanonicalHeaders{
|
2023-08-05 19:41:04 +00:00
|
|
|
.str = try dest_str.toOwnedSlice(),
|
|
|
|
.signed_headers = try signed_headers.toOwnedSlice(),
|
2022-01-18 01:28:43 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn canonicalHeaderValue(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {
|
|
|
|
var started = false;
|
2024-01-18 12:43:45 +00:00
|
|
|
const in_quote = false;
|
2022-01-18 01:28:43 +00:00
|
|
|
var start: usize = 0;
|
|
|
|
const rc = try allocator.alloc(u8, value.len);
|
|
|
|
var rc_inx: usize = 0;
|
2023-08-04 17:07:58 +00:00
|
|
|
for (value, 0..) |c, i| {
|
2023-08-05 19:41:04 +00:00
|
|
|
if (!started and !std.ascii.isWhitespace(c)) {
|
2022-01-18 01:28:43 +00:00
|
|
|
started = true;
|
|
|
|
start = i;
|
|
|
|
}
|
|
|
|
if (started) {
|
2023-08-05 19:41:04 +00:00
|
|
|
if (!in_quote and i > 0 and std.ascii.isWhitespace(c) and std.ascii.isWhitespace(value[i - 1]))
|
2022-01-18 01:28:43 +00:00
|
|
|
continue;
|
|
|
|
// if (c == '"') in_quote = !in_quote;
|
|
|
|
rc[rc_inx] = c;
|
|
|
|
rc_inx += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Trim end
|
2023-08-05 19:41:04 +00:00
|
|
|
while (std.ascii.isWhitespace(rc[rc_inx - 1]))
|
2022-01-18 01:28:43 +00:00
|
|
|
rc_inx -= 1;
|
2023-08-27 16:13:41 +00:00
|
|
|
_ = allocator.resize(rc, rc_inx);
|
2022-01-18 01:28:43 +00:00
|
|
|
return rc[0..rc_inx];
|
|
|
|
}
|
|
|
|
fn lessThan(context: void, lhs: base.Header, rhs: base.Header) bool {
|
|
|
|
_ = context;
|
|
|
|
return std.ascii.lessThanIgnoreCase(lhs.name, rhs.name);
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:54:23 +00:00
|
|
|
fn hash(allocator: std.mem.Allocator, payload: []const u8, sig_type: SignatureType) ![]const u8 {
|
2022-01-18 01:28:43 +00:00
|
|
|
if (sig_type != .sha256)
|
|
|
|
return error.NotImplemented;
|
|
|
|
const to_hash = blk: {
|
|
|
|
if (payload.len > 0) {
|
|
|
|
break :blk payload;
|
|
|
|
}
|
|
|
|
break :blk "";
|
|
|
|
};
|
|
|
|
var out: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
|
|
|
|
std.crypto.hash.sha2.Sha256.hash(to_hash, &out, .{});
|
|
|
|
return try std.fmt.allocPrint(allocator, "{s}", .{std.fmt.fmtSliceHexLower(&out)});
|
|
|
|
}
|
|
|
|
// SignedHeaders + '\n' +
|
|
|
|
// HexEncode(Hash(RequestPayload))
|
|
|
|
test "canonical method" {
|
|
|
|
const actual = try canonicalRequestMethod("GET");
|
|
|
|
try std.testing.expectEqualStrings("GET", actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "canonical uri" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const path = "/documents and settings/?foo=bar";
|
|
|
|
const expected = "/documents%2520and%2520settings/";
|
|
|
|
const actual = try canonicalUri(allocator, path, true);
|
|
|
|
defer allocator.free(actual);
|
|
|
|
try std.testing.expectEqualStrings(expected, actual);
|
|
|
|
|
|
|
|
const slash = try canonicalUri(allocator, "", true);
|
|
|
|
defer allocator.free(slash);
|
|
|
|
try std.testing.expectEqualStrings("/", slash);
|
|
|
|
}
|
|
|
|
test "canonical query" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
const path = "blahblahblah?foo=bar&zed=dead&qux&equals=x=y&Action=ListUsers&Version=2010-05-08";
|
|
|
|
|
|
|
|
// {
|
|
|
|
// std.testing.log_level = .debug;
|
|
|
|
// _ = try std.io.getStdErr().write("\n");
|
|
|
|
// }
|
|
|
|
const expected = "Action=ListUsers&Version=2010-05-08&equals=x%253Dy&foo=bar&qux=&zed=dead";
|
|
|
|
const actual = try canonicalQueryString(allocator, path);
|
|
|
|
defer allocator.free(actual);
|
|
|
|
try std.testing.expectEqualStrings(expected, actual);
|
|
|
|
}
|
|
|
|
test "canonical headers" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
var headers = try std.ArrayList(base.Header).initCapacity(allocator, 5);
|
|
|
|
defer headers.deinit();
|
|
|
|
try headers.append(.{ .name = "Host", .value = "iam.amazonaws.com" });
|
|
|
|
try headers.append(.{ .name = "Content-Type", .value = "application/x-www-form-urlencoded; charset=utf-8" });
|
2022-01-19 18:54:23 +00:00
|
|
|
try headers.append(.{ .name = "User-Agent", .value = "This header should be skipped" });
|
2022-01-18 01:28:43 +00:00
|
|
|
try headers.append(.{ .name = "My-header1", .value = " a b c " });
|
|
|
|
try headers.append(.{ .name = "X-Amz-Date", .value = "20150830T123600Z" });
|
|
|
|
try headers.append(.{ .name = "My-header2", .value = " \"a b c\" " });
|
|
|
|
const expected =
|
|
|
|
\\content-type:application/x-www-form-urlencoded; charset=utf-8
|
|
|
|
\\host:iam.amazonaws.com
|
|
|
|
\\my-header1:a b c
|
|
|
|
\\my-header2:"a b c"
|
|
|
|
\\x-amz-date:20150830T123600Z
|
|
|
|
\\
|
|
|
|
;
|
2022-05-25 21:24:56 +00:00
|
|
|
const actual = try canonicalHeaders(allocator, headers.items, "dummy");
|
2022-01-18 01:28:43 +00:00
|
|
|
defer allocator.free(actual.str);
|
|
|
|
defer allocator.free(actual.signed_headers);
|
|
|
|
try std.testing.expectEqualStrings(expected, actual.str);
|
|
|
|
try std.testing.expectEqualStrings("content-type;host;my-header1;my-header2;x-amz-date", actual.signed_headers);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "canonical request" {
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
var headers = try std.ArrayList(base.Header).initCapacity(allocator, 5);
|
|
|
|
defer headers.deinit();
|
2022-01-19 21:53:35 +00:00
|
|
|
try headers.append(.{ .name = "User-agent", .value = "c sdk v1.0" });
|
|
|
|
// In contrast to AWS CRT (aws-c-auth), we add the date as part of the
|
|
|
|
// signing operation. They add it as part of the canonicalization
|
|
|
|
try headers.append(.{ .name = "X-Amz-Date", .value = "20150830T123600Z" });
|
|
|
|
try headers.append(.{ .name = "Host", .value = "example.amazonaws.com" });
|
2022-01-18 01:28:43 +00:00
|
|
|
const req = base.Request{
|
|
|
|
.path = "/",
|
2022-01-19 21:53:35 +00:00
|
|
|
.method = "GET",
|
2022-01-18 01:28:43 +00:00
|
|
|
.headers = headers.items,
|
|
|
|
};
|
2022-01-20 20:53:29 +00:00
|
|
|
const access_key = try allocator.dupe(u8, "AKIDEXAMPLE");
|
|
|
|
const secret_key = try allocator.dupe(u8, "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY");
|
2023-09-14 21:06:35 +00:00
|
|
|
const credential = Credentials.init(allocator, access_key, secret_key, null);
|
2022-01-20 20:53:29 +00:00
|
|
|
defer credential.deinit();
|
2022-05-25 21:24:56 +00:00
|
|
|
const request = try createCanonicalRequest(allocator, req, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", .{
|
2022-01-18 01:28:43 +00:00
|
|
|
.region = "us-west-2", // us-east-1
|
|
|
|
.service = "sts", // service
|
2022-01-20 20:53:29 +00:00
|
|
|
.credentials = credential,
|
2022-01-19 18:54:23 +00:00
|
|
|
.signing_time = 1440938160, // 20150830T123600Z
|
2022-01-18 01:28:43 +00:00
|
|
|
});
|
2022-01-19 18:54:23 +00:00
|
|
|
defer allocator.free(request.arr);
|
|
|
|
defer allocator.free(request.hash);
|
|
|
|
defer allocator.free(request.headers.str);
|
|
|
|
defer allocator.free(request.headers.signed_headers);
|
2022-01-18 01:28:43 +00:00
|
|
|
|
2022-01-19 21:53:35 +00:00
|
|
|
const expected =
|
|
|
|
\\GET
|
|
|
|
\\/
|
|
|
|
\\
|
|
|
|
\\host:example.amazonaws.com
|
|
|
|
\\x-amz-date:20150830T123600Z
|
|
|
|
\\
|
|
|
|
\\host;x-amz-date
|
|
|
|
\\e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
|
|
|
;
|
|
|
|
try std.testing.expectEqualStrings(expected, request.arr);
|
2022-01-18 01:28:43 +00:00
|
|
|
}
|
|
|
|
test "can sign" {
|
|
|
|
// [debug] (aws): call: prefix sts, sigv4 sts, version 2011-06-15, action GetCallerIdentity
|
|
|
|
// [debug] (aws): proto: AwsProtocol.query
|
|
|
|
// [debug] (awshttp): host: sts.us-west-2.amazonaws.com, scheme: https, port: 443
|
|
|
|
// [debug] (awshttp): Calling endpoint https://sts.us-west-2.amazonaws.com
|
|
|
|
// [debug] (awshttp): Path: /
|
|
|
|
// [debug] (awshttp): Query:
|
|
|
|
// [debug] (awshttp): Method: POST
|
|
|
|
// [debug] (awshttp): body length: 43
|
|
|
|
// [debug] (awshttp): Body
|
|
|
|
// ====
|
|
|
|
// Action=GetCallerIdentity&Version=2011-06-15
|
|
|
|
// ====
|
|
|
|
// [debug] (awshttp): All Request Headers:
|
|
|
|
// [debug] (awshttp): Accept: application/json
|
|
|
|
// [debug] (awshttp): Host: sts.us-west-2.amazonaws.com
|
|
|
|
// [debug] (awshttp): User-Agent: zig-aws 1.0, Powered by the AWS Common Runtime.
|
|
|
|
// [debug] (awshttp): Content-Type: application/x-www-form-urlencoded
|
|
|
|
// [debug] (awshttp): Content-Length: 43
|
|
|
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
var headers = try std.ArrayList(base.Header).initCapacity(allocator, 5);
|
|
|
|
defer headers.deinit();
|
2022-01-20 02:58:53 +00:00
|
|
|
try headers.append(.{ .name = "Content-Type", .value = "application/x-www-form-urlencoded; charset=utf-8" });
|
|
|
|
try headers.append(.{ .name = "Content-Length", .value = "13" });
|
|
|
|
try headers.append(.{ .name = "Host", .value = "example.amazonaws.com" });
|
2024-01-18 12:43:45 +00:00
|
|
|
const req = base.Request{
|
2022-01-18 01:28:43 +00:00
|
|
|
.path = "/",
|
|
|
|
.query = "",
|
2022-01-20 02:58:53 +00:00
|
|
|
.body = "Param1=value1",
|
2022-01-18 01:28:43 +00:00
|
|
|
.method = "POST",
|
|
|
|
.content_type = "application/json",
|
|
|
|
.headers = headers.items,
|
|
|
|
};
|
2022-01-20 03:14:24 +00:00
|
|
|
// {
|
|
|
|
// std.testing.log_level = .debug;
|
|
|
|
// _ = try std.io.getStdErr().write("\n");
|
|
|
|
// }
|
2022-01-18 01:28:43 +00:00
|
|
|
|
2022-01-20 20:53:29 +00:00
|
|
|
const access_key = try allocator.dupe(u8, "AKIDEXAMPLE");
|
|
|
|
const secret_key = try allocator.dupe(u8, "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY");
|
2023-09-14 21:06:35 +00:00
|
|
|
const credential = Credentials.init(allocator, access_key, secret_key, null);
|
2022-01-20 20:53:29 +00:00
|
|
|
defer credential.deinit();
|
2022-01-18 01:28:43 +00:00
|
|
|
// we could look at sigv4 signing tests at:
|
|
|
|
// https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/tests/sigv4_signing_tests.c#L1478
|
2022-01-19 18:54:23 +00:00
|
|
|
const config = Config{
|
|
|
|
.region = "us-east-1",
|
|
|
|
.service = "service",
|
2022-01-20 20:53:29 +00:00
|
|
|
.credentials = credential,
|
2022-01-19 18:54:23 +00:00
|
|
|
.signing_time = 1440938160, // 20150830T123600Z
|
|
|
|
};
|
|
|
|
// TODO: There is an x-amz-content-sha256. Investigate
|
2022-01-20 03:12:50 +00:00
|
|
|
var signed_req = try signRequest(allocator, req, config);
|
2022-01-20 02:58:53 +00:00
|
|
|
|
2022-01-20 03:12:50 +00:00
|
|
|
defer freeSignedRequest(allocator, &signed_req, config);
|
2022-05-25 21:24:56 +00:00
|
|
|
try std.testing.expectEqualStrings("X-Amz-Date", signed_req.headers[signed_req.headers.len - 3].name);
|
|
|
|
try std.testing.expectEqualStrings("20150830T123600Z", signed_req.headers[signed_req.headers.len - 3].value);
|
|
|
|
|
|
|
|
try std.testing.expectEqualStrings("x-amz-content-sha256", signed_req.headers[signed_req.headers.len - 2].name);
|
|
|
|
try std.testing.expectEqualStrings("9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e", signed_req.headers[signed_req.headers.len - 2].value);
|
2022-01-19 18:54:23 +00:00
|
|
|
|
2022-01-20 03:12:50 +00:00
|
|
|
// c_aws_auth tests don't seem to have valid data. Live endpoint is
|
|
|
|
// accepting what we're doing
|
2022-05-25 21:24:56 +00:00
|
|
|
const expected_auth = "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date, Signature=328d1b9eaadca9f5818ef05e8392801e091653bafec24fcab71e7344e7f51422";
|
2022-01-20 02:58:53 +00:00
|
|
|
|
2022-01-20 03:12:50 +00:00
|
|
|
try std.testing.expectEqualStrings("Authorization", signed_req.headers[signed_req.headers.len - 1].name);
|
|
|
|
try std.testing.expectEqualStrings(expected_auth, signed_req.headers[signed_req.headers.len - 1].value);
|
2022-01-20 02:58:53 +00:00
|
|
|
}
|
2023-09-09 04:45:36 +00:00
|
|
|
|
2023-09-14 21:06:35 +00:00
|
|
|
var test_credential: ?Credentials = null;
|
2023-10-22 17:25:07 +00:00
|
|
|
test "can verify server request" {
|
2023-09-09 04:45:36 +00:00
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
|
|
|
const access_key = try allocator.dupe(u8, "ACCESS");
|
|
|
|
const secret_key = try allocator.dupe(u8, "SECRET");
|
2023-09-14 21:06:35 +00:00
|
|
|
test_credential = Credentials.init(allocator, access_key, secret_key, null);
|
2023-09-09 04:45:36 +00:00
|
|
|
defer test_credential.?.deinit();
|
|
|
|
|
|
|
|
var headers = std.http.Headers.init(allocator);
|
|
|
|
defer headers.deinit();
|
|
|
|
try headers.append("Connection", "keep-alive");
|
|
|
|
try headers.append("Accept-Encoding", "gzip, deflate, zstd");
|
|
|
|
try headers.append("TE", "gzip, deflate, trailers");
|
|
|
|
try headers.append("Accept", "application/json");
|
|
|
|
try headers.append("Host", "127.0.0.1");
|
|
|
|
try headers.append("User-Agent", "zig-aws 1.0");
|
|
|
|
try headers.append("Content-Type", "text/plain");
|
|
|
|
try headers.append("x-amz-storage-class", "STANDARD");
|
|
|
|
try headers.append("Content-Length", "3");
|
|
|
|
try headers.append("X-Amz-Date", "20230908T170252Z");
|
|
|
|
try headers.append("x-amz-content-sha256", "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9");
|
|
|
|
try headers.append("Authorization", "AWS4-HMAC-SHA256 Credential=ACCESS/20230908/us-west-2/s3/aws4_request, SignedHeaders=accept;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class, Signature=fcc43ce73a34c9bd1ddf17e8a435f46a859812822f944f9eeb2aabcd64b03523");
|
|
|
|
|
|
|
|
var buf = "bar".*;
|
|
|
|
var fis = std.io.fixedBufferStream(&buf);
|
2024-01-18 12:43:45 +00:00
|
|
|
const request = std.http.Server.Request{
|
2023-09-09 04:45:36 +00:00
|
|
|
.method = std.http.Method.PUT,
|
|
|
|
.target = "/mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0/i/am/a/teapot/foo?x-id=PutObject",
|
|
|
|
.version = .@"HTTP/1.1",
|
|
|
|
.content_length = 3,
|
|
|
|
.headers = headers,
|
|
|
|
.parser = std.http.protocol.HeadersParser.initDynamic(std.math.maxInt(usize)),
|
|
|
|
};
|
|
|
|
|
|
|
|
// std.testing.log_level = .debug;
|
2023-10-22 17:25:07 +00:00
|
|
|
try std.testing.expect(try verifyServerRequest(allocator, request, fis.reader(), struct {
|
2023-09-14 21:06:35 +00:00
|
|
|
cred: Credentials,
|
2023-09-09 04:45:36 +00:00
|
|
|
|
|
|
|
const Self = @This();
|
2023-09-14 21:06:35 +00:00
|
|
|
fn getCreds(access: []const u8) ?Credentials {
|
2023-09-09 04:45:36 +00:00
|
|
|
if (std.mem.eql(u8, access, "ACCESS")) return test_credential.?;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}.getCreds));
|
|
|
|
}
|