Compare commits
4 Commits
d0dd2507d4
...
8d36300f27
Author | SHA1 | Date | |
---|---|---|---|
8d36300f27 | |||
a01c01522c | |||
8cc3059744 | |||
a3967b8652 |
|
@ -227,7 +227,7 @@ pub fn Request(comptime action: anytype) type {
|
||||||
// look at the return type
|
// look at the return type
|
||||||
var isJson: bool = undefined;
|
var isJson: bool = undefined;
|
||||||
for (response.headers) |h| {
|
for (response.headers) |h| {
|
||||||
if (std.mem.eql(u8, "Content-Type", h.name)) {
|
if (std.ascii.eqlIgnoreCase("Content-Type", h.name)) {
|
||||||
if (std.mem.startsWith(u8, h.value, "application/json")) {
|
if (std.mem.startsWith(u8, h.value, "application/json")) {
|
||||||
isJson = true;
|
isJson = true;
|
||||||
} else if (std.mem.startsWith(u8, h.value, "application/x-amz-json-1.0")) {
|
} else if (std.mem.startsWith(u8, h.value, "application/x-amz-json-1.0")) {
|
||||||
|
|
|
@ -120,7 +120,6 @@ pub const AwsHttp = struct {
|
||||||
// End CreateRequest. This should return a struct with a deinit function that can do
|
// End CreateRequest. This should return a struct with a deinit function that can do
|
||||||
// destroys, etc
|
// destroys, etc
|
||||||
|
|
||||||
// TODO: Add headers
|
|
||||||
try zfetch.init(); // This only does anything on Windows. Not sure how performant it is to do this on every request
|
try zfetch.init(); // This only does anything on Windows. Not sure how performant it is to do this on every request
|
||||||
defer zfetch.deinit();
|
defer zfetch.deinit();
|
||||||
var headers = zfetch.Headers.init(self.allocator);
|
var headers = zfetch.Headers.init(self.allocator);
|
||||||
|
@ -128,41 +127,54 @@ pub const AwsHttp = struct {
|
||||||
for (request.headers) |header|
|
for (request.headers) |header|
|
||||||
try headers.appendValue(header.name, header.value);
|
try headers.appendValue(header.name, header.value);
|
||||||
try addHeaders(self.allocator, &headers, endpoint.host, request.body, request.content_type, request.headers);
|
try addHeaders(self.allocator, &headers, endpoint.host, request.body, request.content_type, request.headers);
|
||||||
|
log.debug("All Request Headers:", .{});
|
||||||
|
for (headers.list.items) |h|
|
||||||
|
log.debug("\t{s}: {s}", .{ h.name, h.value });
|
||||||
|
|
||||||
if (signing_config) |opts| try signing.signRequest(self.allocator, request, opts);
|
if (signing_config) |opts| try signing.signRequest(self.allocator, request, opts);
|
||||||
|
|
||||||
// TODO: make req
|
|
||||||
|
|
||||||
// TODO: Construct URL with endpoint and request info
|
// TODO: Construct URL with endpoint and request info
|
||||||
var req = try zfetch.Request.init(self.allocator, "https://www.lerch.org", null);
|
var req = try zfetch.Request.init(self.allocator, "https://httpbin.org/post", null);
|
||||||
|
defer req.deinit();
|
||||||
|
|
||||||
// TODO: http method as requested
|
const method = std.meta.stringToEnum(zfetch.Method, request.method).?;
|
||||||
// TODO: payload
|
try req.do(method, headers, if (request.body.len == 0) null else request.body);
|
||||||
try req.do(.GET, headers, null);
|
|
||||||
|
|
||||||
// TODO: Timeout - is this now above us?
|
// TODO: Timeout - is this now above us?
|
||||||
log.debug("request_complete. Response code {d}: {s}", .{ req.status.code, req.status.reason });
|
log.debug("request_complete. Response code {d}: {s}", .{ req.status.code, req.status.reason });
|
||||||
log.debug("headers:", .{});
|
log.debug("Response headers:", .{});
|
||||||
var resp_headers = try std.ArrayList(Header).initCapacity(self.allocator, req.headers.list.items.len);
|
var resp_headers = try std.ArrayList(Header).initCapacity(self.allocator, req.headers.list.items.len);
|
||||||
|
defer resp_headers.deinit();
|
||||||
|
var content_length: usize = 0;
|
||||||
for (req.headers.list.items) |h| {
|
for (req.headers.list.items) |h| {
|
||||||
log.debug(" {s}: {s}", .{ h.name, h.value });
|
log.debug(" {s}: {s}", .{ h.name, h.value });
|
||||||
resp_headers.appendAssumeCapacity(.{ .name = h.name, .value = h.value });
|
resp_headers.appendAssumeCapacity(.{
|
||||||
|
.name = try (self.allocator.dupe(u8, h.name)),
|
||||||
|
.value = try (self.allocator.dupe(u8, h.value)),
|
||||||
|
});
|
||||||
|
if (content_length == 0 and std.ascii.eqlIgnoreCase("content-length", h.name))
|
||||||
|
content_length = std.fmt.parseInt(usize, h.value, 10) catch 0;
|
||||||
}
|
}
|
||||||
const reader = req.reader();
|
const reader = req.reader();
|
||||||
// TODO: Get content length and use that to allocate the buffer
|
// TODO: Get content length and use that to allocate the buffer
|
||||||
|
// Content length can be missing, and why would we trust it anyway
|
||||||
var buf: [65535]u8 = undefined;
|
var buf: [65535]u8 = undefined;
|
||||||
|
var resp_payload = try std.ArrayList(u8).initCapacity(self.allocator, content_length);
|
||||||
|
defer resp_payload.deinit();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const read = try reader.read(&buf);
|
const read = try reader.read(&buf);
|
||||||
|
try resp_payload.appendSlice(buf[0..read]);
|
||||||
if (read == 0) break;
|
if (read == 0) break;
|
||||||
}
|
}
|
||||||
log.debug("raw response body:\n{s}", .{buf});
|
log.debug("raw response body:\n{s}", .{resp_payload.items});
|
||||||
|
|
||||||
// Headers would need to be allocated/copied into HttpResult similar
|
// Headers would need to be allocated/copied into HttpResult similar
|
||||||
// to RequestContext, so we'll leave this as a later excercise
|
// to RequestContext, so we'll leave this as a later excercise
|
||||||
// if it becomes necessary
|
// if it becomes necessary
|
||||||
const rc = HttpResult{
|
const rc = HttpResult{
|
||||||
.response_code = req.status.code,
|
.response_code = req.status.code,
|
||||||
.body = "change me", // TODO: work this all out
|
.body = resp_payload.toOwnedSlice(),
|
||||||
.headers = resp_headers.toOwnedSlice(),
|
.headers = resp_headers.toOwnedSlice(),
|
||||||
.allocator = self.allocator,
|
.allocator = self.allocator,
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub const Config = struct {
|
||||||
// In the CRT, this is only used if the body has been precalculated. We don't have
|
// In the CRT, this is only used if the body has been precalculated. We don't have
|
||||||
// this use case, and we'll ignore
|
// this use case, and we'll ignore
|
||||||
// .signed_body_value = c.aws_byte_cursor_from_c_str(""),
|
// .signed_body_value = c.aws_byte_cursor_from_c_str(""),
|
||||||
signed_body_header: enum { sha256, none } = .sha256, // https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L131
|
signed_body_header: SignatureType = .sha256, // https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/include/aws/auth/signing_config.h#L131
|
||||||
|
|
||||||
// This is more complex in the CRT. We'll just take the creds. Someone
|
// This is more complex in the CRT. We'll just take the creds. Someone
|
||||||
// else can use a provider and get them in advance
|
// else can use a provider and get them in advance
|
||||||
|
@ -61,8 +61,11 @@ pub const Config = struct {
|
||||||
// string, equal to the value specified here. If this value is zero or if header signing is being used then
|
// 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.
|
// this parameter has no effect.
|
||||||
expiration_in_seconds: u64 = 0,
|
expiration_in_seconds: u64 = 0,
|
||||||
|
|
||||||
|
flags: ConfigFlags = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const SignatureType = enum { sha256, none };
|
||||||
pub const SigningError = error{
|
pub const SigningError = error{
|
||||||
NotImplemented,
|
NotImplemented,
|
||||||
};
|
};
|
||||||
|
@ -75,6 +78,510 @@ pub fn signRequest(allocator: std.mem.Allocator, http_request: base.Request, con
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validateConfig(config: Config) SigningError!void {
|
fn validateConfig(config: Config) SigningError!void {
|
||||||
_ = config;
|
if (config.signature_type != .headers or
|
||||||
return SigningError.NotImplemented;
|
config.signed_body_header != .sha256 or
|
||||||
|
config.expiration_in_seconds != 0 or
|
||||||
|
config.algorithm != .v4 or
|
||||||
|
!config.flags.omit_session_token or
|
||||||
|
!config.flags.should_normalize_uri_path or
|
||||||
|
!config.flags.use_double_uri_encode)
|
||||||
|
return SigningError.NotImplemented;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createCanonicalRequest(allocator: std.mem.Allocator, request: base.Request, config: Config) ![]const u8 {
|
||||||
|
// 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
|
||||||
|
const canonical_method = canonicalRequestMethod(request.method);
|
||||||
|
const canonical_url = try canonicalUri(allocator, request.path, config.flags.use_double_uri_encode);
|
||||||
|
defer allocator.free(canonical_url);
|
||||||
|
const canonical_query = try canonicalQueryString(allocator, request.path);
|
||||||
|
defer allocator.free(canonical_query);
|
||||||
|
const canonical_headers = try canonicalHeaders(allocator, request.headers);
|
||||||
|
defer allocator.free(canonical_headers.str);
|
||||||
|
defer allocator.free(canonical_headers.signed_headers);
|
||||||
|
const payload_hash = try hashPayload(allocator, request.body, config.signed_body_header);
|
||||||
|
defer allocator.free(payload_hash);
|
||||||
|
return try std.fmt.allocPrint(allocator, fmt, .{
|
||||||
|
canonical_method,
|
||||||
|
canonical_url,
|
||||||
|
canonical_query,
|
||||||
|
canonical_headers.str,
|
||||||
|
canonical_headers.signed_headers,
|
||||||
|
payload_hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!double_encode)
|
||||||
|
return error.S3NotImplemented;
|
||||||
|
if (path.len == 0 or path[0] == '?' or path[0] == '#')
|
||||||
|
return try allocator.dupe(u8, "/");
|
||||||
|
const encoded_once = try encodeUri(allocator, path);
|
||||||
|
if (!double_encode)
|
||||||
|
return encoded_once[0 .. std.mem.lastIndexOf(u8, encoded_once, "?") orelse encoded_once.len];
|
||||||
|
defer allocator.free(encoded_once);
|
||||||
|
const encoded_twice = try encodeUri(allocator, encoded_once);
|
||||||
|
return encoded_twice[0 .. std.mem.lastIndexOf(u8, encoded_twice, "?") orelse encoded_twice.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
if (should_encode and std.ascii.isAlNum(c))
|
||||||
|
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 encodeUri(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
|
||||||
|
const reserved_characters = ";,/?:@&=+$#";
|
||||||
|
const unreserved_marks = "-_.!~*'()";
|
||||||
|
var encoded = try std.ArrayList(u8).initCapacity(allocator, path.len);
|
||||||
|
defer encoded.deinit();
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (should_encode and std.ascii.isAlNum(c))
|
||||||
|
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);
|
||||||
|
std.sort.sort([]const u8, sort_me.items, {}, lessThanBinary);
|
||||||
|
|
||||||
|
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;
|
||||||
|
var first_equals = std.mem.indexOf(u8, i, "=");
|
||||||
|
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 {
|
||||||
|
var buffer = try allocator.alloc(u8, std.mem.replacementSize(u8, haystack, needle, replacement_value));
|
||||||
|
_ = 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,
|
||||||
|
};
|
||||||
|
fn canonicalHeaders(allocator: std.mem.Allocator, headers: []base.Header) !CanonicalHeaders {
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
var dest = try allocator.alloc(base.Header, headers.len);
|
||||||
|
defer {
|
||||||
|
for (dest) |h| {
|
||||||
|
allocator.free(h.name);
|
||||||
|
allocator.free(h.value);
|
||||||
|
}
|
||||||
|
allocator.free(dest);
|
||||||
|
}
|
||||||
|
var total_len: usize = 0;
|
||||||
|
var total_name_len: usize = 0;
|
||||||
|
for (headers) |h, i| {
|
||||||
|
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);
|
||||||
|
dest[i] = .{
|
||||||
|
.name = try std.ascii.allocLowerString(allocator, h.name),
|
||||||
|
.value = try std.fmt.allocPrint(allocator, "{s}", .{value}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std.sort.sort(base.Header, dest, {}, lessThan);
|
||||||
|
|
||||||
|
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;
|
||||||
|
for (dest) |h| {
|
||||||
|
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{
|
||||||
|
.str = dest_str.toOwnedSlice(),
|
||||||
|
.signed_headers = signed_headers.toOwnedSlice(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonicalHeaderValue(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {
|
||||||
|
var started = false;
|
||||||
|
var in_quote = false;
|
||||||
|
var start: usize = 0;
|
||||||
|
const rc = try allocator.alloc(u8, value.len);
|
||||||
|
var rc_inx: usize = 0;
|
||||||
|
for (value) |c, i| {
|
||||||
|
if (!started and !std.ascii.isSpace(c)) {
|
||||||
|
started = true;
|
||||||
|
start = i;
|
||||||
|
}
|
||||||
|
if (started) {
|
||||||
|
if (!in_quote and i > 0 and std.ascii.isSpace(c) and std.ascii.isSpace(value[i - 1]))
|
||||||
|
continue;
|
||||||
|
// if (c == '"') in_quote = !in_quote;
|
||||||
|
rc[rc_inx] = c;
|
||||||
|
rc_inx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim end
|
||||||
|
while (std.ascii.isSpace(rc[rc_inx - 1]))
|
||||||
|
rc_inx -= 1;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hashPayload(allocator: std.mem.Allocator, payload: []const u8, sig_type: SignatureType) ![]const u8 {
|
||||||
|
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";
|
||||||
|
|
||||||
|
// {
|
||||||
|
// // TODO: Remove block
|
||||||
|
// 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" });
|
||||||
|
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
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
// {
|
||||||
|
// // TODO: Remove block
|
||||||
|
// std.testing.log_level = .debug;
|
||||||
|
// _ = try std.io.getStdErr().write("\n");
|
||||||
|
// }
|
||||||
|
const actual = try canonicalHeaders(allocator, headers.items);
|
||||||
|
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();
|
||||||
|
try headers.append(.{ .name = "Content-Type", .value = "application/x-www-form-urlencoded" });
|
||||||
|
try headers.append(.{ .name = "Content-Length", .value = "43" });
|
||||||
|
try headers.append(.{ .name = "User-Agent", .value = "zig-aws 1.0, Powered by the AWS Common Runtime." });
|
||||||
|
try headers.append(.{ .name = "Host", .value = "sts.us-west-2.amazonaws.com" });
|
||||||
|
try headers.append(.{ .name = "Accept", .value = "application/json" });
|
||||||
|
const req = base.Request{
|
||||||
|
.path = "/",
|
||||||
|
.query = "",
|
||||||
|
.body = "Action=GetCallerIdentity&Version=2011-06-15",
|
||||||
|
.method = "POST",
|
||||||
|
.content_type = "application/json",
|
||||||
|
.headers = headers.items,
|
||||||
|
};
|
||||||
|
{
|
||||||
|
// TODO: Remove block
|
||||||
|
std.testing.log_level = .debug;
|
||||||
|
_ = try std.io.getStdErr().write("\n");
|
||||||
|
}
|
||||||
|
const request = try createCanonicalRequest(allocator, req, .{
|
||||||
|
.region = "us-west-2", // us-east-1
|
||||||
|
.service = "sts", // service
|
||||||
|
.credentials = .{
|
||||||
|
.access_key = "AKIDEXAMPLE",
|
||||||
|
.secret_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
|
.session_token = null,
|
||||||
|
},
|
||||||
|
.signing_time = 1642187728, // Should be 2015-08-30T12:36:00Z. Does zig stdlib have date parsing?
|
||||||
|
});
|
||||||
|
defer allocator.free(request);
|
||||||
|
std.log.debug("canonical request:\n{s}", .{request});
|
||||||
|
try std.testing.expect(request.len > 0); // TODO: improvify this
|
||||||
|
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
try headers.append(.{ .name = "Content-Type", .value = "application/x-www-form-urlencoded" });
|
||||||
|
try headers.append(.{ .name = "Content-Length", .value = "43" });
|
||||||
|
try headers.append(.{ .name = "User-Agent", .value = "zig-aws 1.0, Powered by the AWS Common Runtime." });
|
||||||
|
try headers.append(.{ .name = "Host", .value = "sts.us-west-2.amazonaws.com" });
|
||||||
|
try headers.append(.{ .name = "Accept", .value = "application/json" });
|
||||||
|
const req = base.Request{
|
||||||
|
.path = "/",
|
||||||
|
.query = "",
|
||||||
|
.body = "Action=GetCallerIdentity&Version=2011-06-15",
|
||||||
|
.method = "POST",
|
||||||
|
.content_type = "application/json",
|
||||||
|
.headers = headers.items,
|
||||||
|
};
|
||||||
|
{
|
||||||
|
// TODO: Remove block
|
||||||
|
std.testing.log_level = .debug;
|
||||||
|
_ = try std.io.getStdErr().write("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// we could look at sigv4 signing tests at:
|
||||||
|
// https://github.com/awslabs/aws-c-auth/blob/ace1311f8ef6ea890b26dd376031bed2721648eb/tests/sigv4_signing_tests.c#L1478
|
||||||
|
//
|
||||||
|
// for valid signatures. TODO: Get literally anything working first
|
||||||
|
try signRequest(allocator, req, .{
|
||||||
|
.region = "us-west-2", // us-east-1
|
||||||
|
.service = "sts", // service
|
||||||
|
.credentials = .{
|
||||||
|
.access_key = "AKIDEXAMPLE",
|
||||||
|
.secret_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
|
.session_token = null,
|
||||||
|
},
|
||||||
|
.signing_time = 1642187728, // Should be 2015-08-30T12:36:00Z. Does zig stdlib have date parsing?
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
79
src/date.zig
Normal file
79
src/date.zig
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// From https://gist.github.com/WoodyAtHome/3ef50b17f0fa2860ac52b97af12f8d15
|
||||||
|
// Translated from German. We don't need any local time for this use case, and conversion
|
||||||
|
// really requires the TZ DB.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const DateTime = struct { day: u8, month: u8, year: u16, hour: u8, minute: u8, second: u8 };
|
||||||
|
|
||||||
|
pub fn timestamp2DateTime(timestamp: i64) DateTime {
|
||||||
|
|
||||||
|
// aus https://de.wikipedia.org/wiki/Unixzeit
|
||||||
|
const unixtime = @intCast(u64, timestamp);
|
||||||
|
const SECONDS_PER_DAY = 86400; //* 24* 60 * 60 */
|
||||||
|
const DAYS_PER_YEAR = 365; //* Normal year (no leap year) */
|
||||||
|
const DAYS_IN_4_YEARS = 1461; //* 4*365 + 1 */
|
||||||
|
const DAYS_IN_100_YEARS = 36524; //* 100*365 + 25 - 1 */
|
||||||
|
const DAYS_IN_400_YEARS = 146097; //* 400*365 + 100 - 4 + 1 */
|
||||||
|
const DAY_NUMBER_ADJUSTED_1970_01_01 = 719468; //* Day number relates to March 1st */
|
||||||
|
|
||||||
|
var dayN: u64 = DAY_NUMBER_ADJUSTED_1970_01_01 + unixtime / SECONDS_PER_DAY;
|
||||||
|
var seconds_since_midnight: u64 = unixtime % SECONDS_PER_DAY;
|
||||||
|
var temp: u64 = 0;
|
||||||
|
|
||||||
|
// Leap year rules for Gregorian Calendars
|
||||||
|
// Any year divisible by 100 is not a leap year unless also divisible by 400
|
||||||
|
temp = 4 * (dayN + DAYS_IN_100_YEARS + 1) / DAYS_IN_400_YEARS - 1;
|
||||||
|
var year = @intCast(u16, 100 * temp);
|
||||||
|
dayN -= DAYS_IN_100_YEARS * temp + temp / 4;
|
||||||
|
|
||||||
|
// For Julian calendars, each year divisible by 4 is a leap year
|
||||||
|
temp = 4 * (dayN + DAYS_PER_YEAR + 1) / DAYS_IN_4_YEARS - 1;
|
||||||
|
year += @intCast(u16, temp);
|
||||||
|
dayN -= DAYS_PER_YEAR * temp + temp / 4;
|
||||||
|
|
||||||
|
// TagN calculates the days of the year in relation to March 1
|
||||||
|
var month = @intCast(u8, (5 * dayN + 2) / 153);
|
||||||
|
var day = @intCast(u8, dayN - (@intCast(u64, month) * 153 + 2) / 5 + 1);
|
||||||
|
// 153 = 31+30+31+30+31 Days for the 5 months from March through July
|
||||||
|
// 153 = 31+30+31+30+31 Days for the 5 months from August through December
|
||||||
|
// 31+28 Days for January and February (see below)
|
||||||
|
// +2: Rounding adjustment
|
||||||
|
// +1: The first day in March is March 1st (not March 0)
|
||||||
|
|
||||||
|
month += 3; // Convert from the day that starts on March 1st, to a human year */
|
||||||
|
if (month > 12) { // months 13 and 14 become 1 (January) und 2 (February) of the next year
|
||||||
|
month -= 12;
|
||||||
|
year += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hours = @intCast(u8, seconds_since_midnight / 3600);
|
||||||
|
var minutes = @intCast(u8, seconds_since_midnight % 3600 / 60);
|
||||||
|
var seconds = @intCast(u8, seconds_since_midnight % 60);
|
||||||
|
|
||||||
|
return DateTime{ .day = day, .month = month, .year = year, .hour = hours, .minute = minutes, .second = seconds };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn printDateTime(dt: DateTime) void {
|
||||||
|
std.log.debug("{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0<2}Z", .{
|
||||||
|
dt.year,
|
||||||
|
dt.month,
|
||||||
|
dt.day,
|
||||||
|
dt.hour,
|
||||||
|
dt.minute,
|
||||||
|
dt.second,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn printNowUtc() void {
|
||||||
|
printDateTime(timestamp2DateTime(std.time.timestamp()));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "GMT and localtime" {
|
||||||
|
std.testing.log_level = .debug;
|
||||||
|
std.log.debug("\n", .{});
|
||||||
|
printDateTime(timestamp2DateTime(std.time.timestamp()));
|
||||||
|
try std.testing.expectEqual(DateTime{ .year = 2020, .month = 8, .day = 28, .hour = 9, .minute = 32, .second = 27 }, timestamp2DateTime(1598607147));
|
||||||
|
|
||||||
|
try std.testing.expectEqual(DateTime{ .year = 2020, .month = 11, .day = 1, .hour = 5, .minute = 6, .second = 7 }, timestamp2DateTime(1604207167));
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user