Compare commits
5 Commits
ad54596837
...
bd3605e387
Author | SHA1 | Date | |
---|---|---|---|
bd3605e387 | |||
9a0908bc63 | |||
fe7e37b71a | |||
01aa8c8d1a | |||
78478ab470 |
|
@ -105,6 +105,6 @@ RUN tar -czf aws-c-auth-clang.tgz /usr/local/*
|
||||||
|
|
||||||
FROM alpine:3.13 as final
|
FROM alpine:3.13 as final
|
||||||
COPY --from=auth /aws-c-auth-clang.tgz /
|
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* && \
|
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
|
ln -s /usr/local/zig-linux*/zig /usr/local/bin/zig
|
||||||
|
|
20
build.zig
20
build.zig
|
@ -30,21 +30,13 @@ pub fn build(b: *Builder) void {
|
||||||
exe.linkSystemLibrary("c");
|
exe.linkSystemLibrary("c");
|
||||||
exe.setTarget(target);
|
exe.setTarget(target);
|
||||||
exe.setBuildMode(mode);
|
exe.setBuildMode(mode);
|
||||||
exe.override_dest_dir = .{ .Custom = ".." };
|
|
||||||
|
|
||||||
// TODO: Figure out -static
|
// This line works as of c5d412268
|
||||||
// Neither of these two work
|
// Earliest nightly is 05b5e49bc on 2021-06-12
|
||||||
// exe.addCompileFlags([][]const u8{
|
// https://ziglang.org/builds/zig-linux-x86_64-0.9.0-dev.113+05b5e49bc.tar.xz
|
||||||
// "-static",
|
// exe.override_dest_dir = .{ .Custom = ".." };
|
||||||
// "--strip",
|
exe.override_dest_dir = .{ .custom = ".." };
|
||||||
// });
|
exe.linkage = .static;
|
||||||
//
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
exe.strip = true;
|
exe.strip = true;
|
||||||
exe.install();
|
exe.install();
|
||||||
|
|
55
src/aws.zig
55
src/aws.zig
|
@ -2,6 +2,8 @@ const std = @import("std");
|
||||||
|
|
||||||
const awshttp = @import("awshttp.zig");
|
const awshttp = @import("awshttp.zig");
|
||||||
const json = @import("json.zig");
|
const json = @import("json.zig");
|
||||||
|
const url = @import("url.zig");
|
||||||
|
const case = @import("case.zig");
|
||||||
const servicemodel = @import("servicemodel.zig");
|
const servicemodel = @import("servicemodel.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.aws);
|
const log = std.log.scoped(.aws);
|
||||||
|
@ -44,16 +46,52 @@ pub const Aws = struct {
|
||||||
const service = meta_info.service;
|
const service = meta_info.service;
|
||||||
const action = meta_info.action;
|
const action = meta_info.action;
|
||||||
const R = Response(request);
|
const R = Response(request);
|
||||||
const FullR = FullResponse(request);
|
|
||||||
|
|
||||||
log.debug("service endpoint {s}", .{service.endpoint_prefix});
|
log.debug("call: prefix {s}, sigv4 {s}, version {s}, action {s}", .{
|
||||||
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(
|
|
||||||
service.endpoint_prefix,
|
service.endpoint_prefix,
|
||||||
|
service.sigv4_name,
|
||||||
service.version,
|
service.version,
|
||||||
action.action_name,
|
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,
|
.region = options.region,
|
||||||
.dualstack = options.dualstack,
|
.dualstack = options.dualstack,
|
||||||
|
@ -61,6 +99,11 @@ pub const Aws = struct {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
defer response.deinit();
|
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
|
// TODO: Check status code for badness
|
||||||
var stream = json.TokenStream.init(response.body);
|
var stream = json.TokenStream.init(response.body);
|
||||||
|
|
||||||
|
|
|
@ -232,12 +232,11 @@ pub const AwsHttp = struct {
|
||||||
/// It will calculate the appropriate endpoint and action parameters for the
|
/// It will calculate the appropriate endpoint and action parameters for the
|
||||||
/// service called, and will set up the signing options. The return
|
/// service called, and will set up the signing options. The return
|
||||||
/// value is simply a raw HttpResult
|
/// 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);
|
const endpoint = try regionSubDomain(self.allocator, service, options.region, options.dualstack);
|
||||||
defer endpoint.deinit();
|
defer endpoint.deinit();
|
||||||
const body = try std.fmt.allocPrint(self.allocator, "Action={s}&Version={s}\n", .{ action, version });
|
httplog.debug("Calling endpoint {s}", .{endpoint.uri});
|
||||||
defer self.allocator.free(body);
|
httplog.debug("Body\n====\n{s}\n====", .{body});
|
||||||
httplog.debug("Calling {s}.{s}, endpoint {s}", .{ service, action, endpoint.uri });
|
|
||||||
const signing_options: SigningOptions = .{
|
const signing_options: SigningOptions = .{
|
||||||
.region = options.region,
|
.region = options.region,
|
||||||
.service = if (options.sigv4_service_name) |name| name else service,
|
.service = if (options.sigv4_service_name) |name| name else service,
|
||||||
|
|
40
src/case.zig
Normal file
40
src/case.zig
Normal 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
96
src/url.zig
Normal 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" } },
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user