Compare commits

...

5 Commits

Author SHA1 Message Date
bd3605e387
support request data in query protocol calls
This should complete the query protocol calls. However, there are likely
gaps in implementation for the transformation of request parameters
to url encoded body data
2021-06-14 16:12:55 -07:00
9a0908bc63
adjust build.zig for zig 0.9.0 >= c5d412268 2021-06-12 13:40:23 -07:00
fe7e37b71a
rough out protocol plan 2021-06-12 13:39:26 -07:00
01aa8c8d1a
add http error handling 2021-06-12 13:38:50 -07:00
78478ab470
update Dockerfile for 0.8.0 2021-06-12 09:33:58 -07:00
6 changed files with 195 additions and 25 deletions

View File

@ -105,6 +105,6 @@ RUN tar -czf aws-c-auth-clang.tgz /usr/local/*
FROM alpine:3.13 as final
COPY --from=auth /aws-c-auth-clang.tgz /
ADD https://ziglang.org/download/0.7.1/zig-linux-x86_64-0.7.1.tar.xz /
ADD https://ziglang.org/download/0.8.0/zig-linux-x86_64-0.8.0.tar.xz /
RUN tar -xzf /aws-c-auth-clang.tgz && mkdir /src && tar -C /usr/local -xf zig-linux* && \
ln -s /usr/local/zig-linux*/zig /usr/local/bin/zig

View File

@ -30,21 +30,13 @@ pub fn build(b: *Builder) void {
exe.linkSystemLibrary("c");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.override_dest_dir = .{ .Custom = ".." };
// TODO: Figure out -static
// Neither of these two work
// exe.addCompileFlags([][]const u8{
// "-static",
// "--strip",
// });
//
// To compile on stock 0.8.0, comment this line of code, or use the Makefile
// See https://github.com/ziglang/zig/pull/8248
//
// On a musl-based x86_64 system, this pre-compiled zig can be used:
// https://github.com/elerch/zig/releases/download/0.8.0/zig-0.8.0-static-support-musl-libz.tgz
exe.is_static = true;
// This line works as of c5d412268
// Earliest nightly is 05b5e49bc on 2021-06-12
// https://ziglang.org/builds/zig-linux-x86_64-0.9.0-dev.113+05b5e49bc.tar.xz
// exe.override_dest_dir = .{ .Custom = ".." };
exe.override_dest_dir = .{ .custom = ".." };
exe.linkage = .static;
exe.strip = true;
exe.install();

View File

@ -2,6 +2,8 @@ const std = @import("std");
const awshttp = @import("awshttp.zig");
const json = @import("json.zig");
const url = @import("url.zig");
const case = @import("case.zig");
const servicemodel = @import("servicemodel.zig");
const log = std.log.scoped(.aws);
@ -44,16 +46,52 @@ pub const Aws = struct {
const service = meta_info.service;
const action = meta_info.action;
const R = Response(request);
const FullR = FullResponse(request);
log.debug("service endpoint {s}", .{service.endpoint_prefix});
log.debug("service sigv4 name {s}", .{service.sigv4_name});
log.debug("version {s}", .{service.version});
log.debug("action {s}", .{action.action_name});
const response = try self.aws_http.callApi(
log.debug("call: prefix {s}, sigv4 {s}, version {s}, action {s}", .{
service.endpoint_prefix,
service.sigv4_name,
service.version,
action.action_name,
});
log.debug("proto: {s}", .{service.aws_protocol});
switch (service.aws_protocol) {
.query => return self.callQuery(request, service, action, options),
.ec2_query => @compileError("EC2 Query protocol not yet supported"),
.rest_json_1 => @compileError("REST Json 1 protocol not yet supported"),
.json_1_0 => @compileError("Json 1.0 protocol not yet supported"),
.json_1_1 => @compileError("Json 1.1 protocol not yet supported"),
.rest_xml => @compileError("REST XML protocol not yet supported"),
}
}
// Call using query protocol. This is documented as an XML protocol, but
// throwing a JSON accept header seems to work
fn callQuery(self: Self, comptime request: anytype, service: anytype, action: anytype, options: Options) !FullResponse(request) {
var buffer = std.ArrayList(u8).init(self.allocator);
defer buffer.deinit();
const writer = buffer.writer();
const transformer = struct {
allocator: *std.mem.Allocator,
const This = @This();
pub fn transform(this: This, name: []const u8) ![]const u8 {
return try case.snakeToPascal(this.allocator, name);
}
pub fn transform_deinit(this: This, name: []const u8) void {
this.allocator.free(name);
}
}{ .allocator = self.allocator };
try url.encode(request, writer, .{ .field_name_transformer = transformer });
const continuation = if (buffer.items.len > 0) "&" else "";
const body = try std.fmt.allocPrint(self.allocator, "Action={s}&Version={s}{s}{s}\n", .{ action.action_name, service.version, continuation, buffer.items });
defer self.allocator.free(body);
const FullR = FullResponse(request);
const response = try self.aws_http.callApi(
service.endpoint_prefix,
body,
.{
.region = options.region,
.dualstack = options.dualstack,
@ -61,6 +99,11 @@ pub const Aws = struct {
},
);
defer response.deinit();
if (response.response_code != 200) {
log.err("call failed! return status: {d}", .{response.response_code});
log.err("Request:\n |{s}\nResponse:\n |{s}", .{ body, response.body });
return error.HttpFailure;
}
// TODO: Check status code for badness
var stream = json.TokenStream.init(response.body);

View File

@ -232,12 +232,11 @@ pub const AwsHttp = struct {
/// It will calculate the appropriate endpoint and action parameters for the
/// service called, and will set up the signing options. The return
/// value is simply a raw HttpResult
pub fn callApi(self: Self, service: []const u8, version: []const u8, action: []const u8, options: Options) !HttpResult {
pub fn callApi(self: Self, service: []const u8, body: []const u8, options: Options) !HttpResult {
const endpoint = try regionSubDomain(self.allocator, service, options.region, options.dualstack);
defer endpoint.deinit();
const body = try std.fmt.allocPrint(self.allocator, "Action={s}&Version={s}\n", .{ action, version });
defer self.allocator.free(body);
httplog.debug("Calling {s}.{s}, endpoint {s}", .{ service, action, endpoint.uri });
httplog.debug("Calling endpoint {s}", .{endpoint.uri});
httplog.debug("Body\n====\n{s}\n====", .{body});
const signing_options: SigningOptions = .{
.region = options.region,
.service = if (options.sigv4_service_name) |name| name else service,

40
src/case.zig Normal file
View File

@ -0,0 +1,40 @@
const std = @import("std");
const expectEqualStrings = std.testing.expectEqualStrings;
pub fn snakeToCamel(allocator: *std.mem.Allocator, name: []const u8) ![]u8 {
var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator();
var target_inx: u64 = 0;
var previous_ascii: u8 = 0;
const rc = try allocator.alloc(u8, name.len); // This is slightly overkill, will need <= number of input chars
while (utf8_name.nextCodepoint()) |cp| {
if (cp > 0xff) return error.UnicodeNotSupported;
const ascii_char = @truncate(u8, cp);
if (ascii_char != '_') {
if (previous_ascii == '_' and ascii_char >= 'a' and ascii_char <= 'z') {
const uppercase_char = ascii_char - ('a' - 'A');
rc[target_inx] = uppercase_char;
} else {
rc[target_inx] = ascii_char;
}
target_inx = target_inx + 1;
}
previous_ascii = ascii_char;
}
rc[target_inx] = 0; // add zero sentinel
return rc[0..target_inx];
}
pub fn snakeToPascal(allocator: *std.mem.Allocator, name: []const u8) ![]u8 {
const rc = try snakeToCamel(allocator, name);
if (rc[0] >= 'a' and rc[0] <= 'z') {
const uppercase_char = rc[0] - ('a' - 'A');
rc[0] = uppercase_char;
}
return rc;
}
test "converts from snake to camelCase" {
const allocator = std.testing.allocator;
const camel = try snakeToCamel(allocator, "access_key_id");
defer allocator.free(camel);
try expectEqualStrings("accessKeyId", camel);
}

96
src/url.zig Normal file
View File

@ -0,0 +1,96 @@
const std = @import("std");
pub fn encode(obj: anytype, writer: anytype, options: anytype) !void {
try encodeStruct("", obj, writer, options);
}
fn encodeStruct(parent: []const u8, obj: anytype, writer: anytype, options: anytype) !void {
var first = true;
inline for (@typeInfo(@TypeOf(obj)).Struct.fields) |field| {
const field_name = if (@hasField(@TypeOf(options), "field_name_transformer")) try options.field_name_transformer.transform(field.name) else field.name;
defer {
if (@hasField(@TypeOf(options), "field_name_transformer"))
options.field_name_transformer.transform_deinit(field_name);
}
if (!first) _ = try writer.write("&");
switch (@typeInfo(field.field_type)) {
.Struct => {
try encodeStruct(field_name ++ ".", @field(obj, field.name), writer);
},
else => try writer.print("{s}{s}={s}", .{ parent, field_name, @field(obj, field.name) }),
}
first = false;
}
}
fn testencode(expected: []const u8, value: anytype, options: anytype) !void {
const ValidationWriter = struct {
const Self = @This();
pub const Writer = std.io.Writer(*Self, Error, write);
pub const Error = error{
TooMuchData,
DifferentData,
};
expected_remaining: []const u8,
fn init(exp: []const u8) Self {
return .{ .expected_remaining = exp };
}
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
fn write(self: *Self, bytes: []const u8) Error!usize {
// std.debug.print("{s}", .{bytes});
if (self.expected_remaining.len < bytes.len) {
std.debug.warn(
\\====== expected this output: =========
\\{s}
\\======== instead found this: =========
\\{s}
\\======================================
, .{
self.expected_remaining,
bytes,
});
return error.TooMuchData;
}
if (!std.mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) {
std.debug.warn(
\\====== expected this output: =========
\\{s}
\\======== instead found this: =========
\\{s}
\\======================================
, .{
self.expected_remaining[0..bytes.len],
bytes,
});
return error.DifferentData;
}
self.expected_remaining = self.expected_remaining[bytes.len..];
return bytes.len;
}
};
var vos = ValidationWriter.init(expected);
try encode(value, vos.writer(), options);
if (vos.expected_remaining.len > 0) return error.NotEnoughData;
}
test "can url encode an object" {
try testencode(
"Action=GetCallerIdentity&Version=2021-01-01",
.{ .Action = "GetCallerIdentity", .Version = "2021-01-01" },
.{},
);
}
test "can url encode a complex object" {
try testencode(
"Action=GetCallerIdentity&Version=2021-01-01&complex.innermember=foo",
.{ .Action = "GetCallerIdentity", .Version = "2021-01-01", .complex = .{ .innermember = "foo" } },
.{},
);
}