Skip to content

Commit 03e3f95

Browse files
Merge pull request #810 from lightpanda-io/proxy-authentication
basic/bearer proxy authentication
2 parents e721b0a + aea3426 commit 03e3f95

File tree

4 files changed

+157
-5
lines changed

4 files changed

+157
-5
lines changed

src/app.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub const App = struct {
3131
tls_verify_host: bool = true,
3232
http_proxy: ?std.Uri = null,
3333
proxy_type: ?http.ProxyType = null,
34+
proxy_auth: ?http.ProxyAuth = null,
3435
};
3536

3637
pub fn init(allocator: Allocator, config: Config) !*App {
@@ -58,6 +59,7 @@ pub const App = struct {
5859
.max_concurrent = 3,
5960
.http_proxy = config.http_proxy,
6061
.proxy_type = config.proxy_type,
62+
.proxy_auth = config.proxy_auth,
6163
.tls_verify_host = config.tls_verify_host,
6264
}),
6365
.config = config,

src/http/client.zig

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@ pub const ProxyType = enum {
4646
connect,
4747
};
4848

49+
pub const ProxyAuth = union(enum) {
50+
basic: struct { user_pass: []const u8 },
51+
bearer: struct { token: []const u8 },
52+
53+
pub fn header_value(self: ProxyAuth, allocator: Allocator) ![]const u8 {
54+
switch (self) {
55+
.basic => |*auth| {
56+
if (std.mem.indexOfScalar(u8, auth.user_pass, ':') == null) return error.InvalidProxyAuth;
57+
58+
const prefix = "Basic ";
59+
var encoder = std.base64.standard.Encoder;
60+
const size = encoder.calcSize(auth.user_pass.len);
61+
var buffer = try allocator.alloc(u8, size + prefix.len);
62+
@memcpy(buffer[0..prefix.len], prefix);
63+
_ = std.base64.standard.Encoder.encode(buffer[prefix.len..], auth.user_pass);
64+
return buffer;
65+
},
66+
.bearer => |*auth| {
67+
const prefix = "Bearer ";
68+
var buffer = try allocator.alloc(u8, auth.token.len + prefix.len);
69+
@memcpy(buffer[0..prefix.len], prefix);
70+
@memcpy(buffer[prefix.len..], auth.token);
71+
return buffer;
72+
},
73+
}
74+
}
75+
};
76+
4977
// Thread-safe. Holds our root certificate, connection pool and state pool
5078
// Used to create Requests.
5179
pub const Client = struct {
@@ -54,6 +82,7 @@ pub const Client = struct {
5482
state_pool: StatePool,
5583
http_proxy: ?Uri,
5684
proxy_type: ?ProxyType,
85+
proxy_auth: ?[]const u8, // Basic <user:pass; base64> or Bearer <token>
5786
root_ca: tls.config.CertBundle,
5887
tls_verify_host: bool = true,
5988
connection_manager: ConnectionManager,
@@ -63,6 +92,7 @@ pub const Client = struct {
6392
max_concurrent: usize = 3,
6493
http_proxy: ?std.Uri = null,
6594
proxy_type: ?ProxyType = null,
95+
proxy_auth: ?ProxyAuth = null,
6696
tls_verify_host: bool = true,
6797
max_idle_connection: usize = 10,
6898
};
@@ -71,10 +101,10 @@ pub const Client = struct {
71101
var root_ca: tls.config.CertBundle = if (builtin.is_test) .{} else try tls.config.CertBundle.fromSystem(allocator);
72102
errdefer root_ca.deinit(allocator);
73103

74-
const state_pool = try StatePool.init(allocator, opts.max_concurrent);
104+
var state_pool = try StatePool.init(allocator, opts.max_concurrent);
75105
errdefer state_pool.deinit(allocator);
76106

77-
const connection_manager = ConnectionManager.init(allocator, opts.max_idle_connection);
107+
var connection_manager = ConnectionManager.init(allocator, opts.max_idle_connection);
78108
errdefer connection_manager.deinit();
79109

80110
return .{
@@ -84,6 +114,7 @@ pub const Client = struct {
84114
.state_pool = state_pool,
85115
.http_proxy = opts.http_proxy,
86116
.proxy_type = if (opts.http_proxy == null) null else (opts.proxy_type orelse .connect),
117+
.proxy_auth = if (opts.proxy_auth) |*auth| try auth.header_value(allocator) else null,
87118
.tls_verify_host = opts.tls_verify_host,
88119
.connection_manager = connection_manager,
89120
.request_pool = std.heap.MemoryPool(Request).init(allocator),
@@ -98,6 +129,10 @@ pub const Client = struct {
98129
self.state_pool.deinit(allocator);
99130
self.connection_manager.deinit();
100131
self.request_pool.deinit();
132+
133+
if (self.proxy_auth) |auth| {
134+
allocator.free(auth);
135+
}
101136
}
102137

103138
pub fn request(self: *Client, method: Request.Method, uri: *const Uri) !*Request {
@@ -763,6 +798,13 @@ pub const Request = struct {
763798

764799
try self.headers.append(arena, .{ .name = "User-Agent", .value = "Lightpanda/1.0" });
765800
try self.headers.append(arena, .{ .name = "Accept", .value = "*/*" });
801+
802+
if (self._client.isSimpleProxy()) {
803+
if (self._client.proxy_auth) |proxy_auth| {
804+
try self.headers.append(arena, .{ .name = "Proxy-Authorization", .value = proxy_auth });
805+
}
806+
}
807+
766808
self.requestStarting();
767809
}
768810

@@ -887,7 +929,13 @@ pub const Request = struct {
887929
var writer = fbs.writer();
888930

889931
try writer.print("CONNECT {s}:{d} HTTP/1.1\r\n", .{ self._request_host, self._request_port });
890-
try writer.print("Host: {s}:{d}\r\n\r\n", .{ self._request_host, self._request_port });
932+
try writer.print("Host: {s}:{d}\r\n", .{ self._request_host, self._request_port });
933+
934+
if (self._client.proxy_auth) |proxy_auth| {
935+
try writer.print("Proxy-Authorization: {s}\r\n", .{proxy_auth});
936+
}
937+
938+
_ = try writer.write("\r\n");
891939
return buf[0..fbs.pos];
892940
}
893941

@@ -3030,15 +3078,56 @@ test "HttpClient: sync with body proxy CONNECT" {
30303078
}
30313079
try testing.expectEqual("over 9000!", try res.next());
30323080
try testing.expectEqual(201, res.header.status);
3033-
try testing.expectEqual(5, res.header.count());
3081+
try testing.expectEqual(6, res.header.count());
30343082
try testing.expectEqual("Close", res.header.get("connection"));
30353083
try testing.expectEqual("10", res.header.get("content-length"));
30363084
try testing.expectEqual("127.0.0.1", res.header.get("_host"));
30373085
try testing.expectEqual("Lightpanda/1.0", res.header.get("_user-agent"));
30383086
try testing.expectEqual("*/*", res.header.get("_accept"));
3087+
// Proxy headers
3088+
try testing.expectEqual("127.0.0.1:9582", res.header.get("__host"));
30393089
}
30403090
}
30413091

3092+
test "HttpClient: basic authentication CONNECT" {
3093+
const proxy_uri = try Uri.parse("http://127.0.0.1:9582/");
3094+
var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .basic = .{ .user_pass = "user:pass" } } });
3095+
defer client.deinit();
3096+
3097+
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo");
3098+
var req = try client.request(.GET, &uri);
3099+
defer req.deinit();
3100+
3101+
var res = try req.sendSync(.{});
3102+
3103+
try testing.expectEqual(201, res.header.status);
3104+
// Destination headers
3105+
try testing.expectEqual(null, res.header.get("_authorization"));
3106+
try testing.expectEqual(null, res.header.get("_proxy-authorization"));
3107+
// Proxy headers
3108+
try testing.expectEqual(null, res.header.get("__authorization"));
3109+
try testing.expectEqual("Basic dXNlcjpwYXNz", res.header.get("__proxy-authorization"));
3110+
}
3111+
test "HttpClient: bearer authentication CONNECT" {
3112+
const proxy_uri = try Uri.parse("http://127.0.0.1:9582/");
3113+
var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .bearer = .{ .token = "fruitsalad" } } });
3114+
defer client.deinit();
3115+
3116+
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo");
3117+
var req = try client.request(.GET, &uri);
3118+
defer req.deinit();
3119+
3120+
var res = try req.sendSync(.{});
3121+
3122+
try testing.expectEqual(201, res.header.status);
3123+
// Destination headers
3124+
try testing.expectEqual(null, res.header.get("_authorization"));
3125+
try testing.expectEqual(null, res.header.get("_proxy-authorization"));
3126+
// Proxy headers
3127+
try testing.expectEqual(null, res.header.get("__authorization"));
3128+
try testing.expectEqual("Bearer fruitsalad", res.header.get("__proxy-authorization"));
3129+
}
3130+
30423131
test "HttpClient: sync with gzip body" {
30433132
for (0..2) |i| {
30443133
var client = try testClient(.{});

src/main.zig

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ fn run(alloc: Allocator) !void {
8585
.run_mode = args.mode,
8686
.http_proxy = args.httpProxy(),
8787
.proxy_type = args.proxyType(),
88+
.proxy_auth = args.proxyAuth(),
8889
.tls_verify_host = args.tlsVerifyHost(),
8990
});
9091
defer app.deinit();
@@ -164,6 +165,13 @@ const Command = struct {
164165
};
165166
}
166167

168+
fn proxyAuth(self: *const Command) ?http.ProxyAuth {
169+
return switch (self.mode) {
170+
inline .serve, .fetch => |opts| opts.common.proxy_auth,
171+
else => unreachable,
172+
};
173+
}
174+
167175
fn logLevel(self: *const Command) ?log.Level {
168176
return switch (self.mode) {
169177
inline .serve, .fetch => |opts| opts.common.log_level,
@@ -208,6 +216,7 @@ const Command = struct {
208216
const Common = struct {
209217
http_proxy: ?std.Uri = null,
210218
proxy_type: ?http.ProxyType = null,
219+
proxy_auth: ?http.ProxyAuth = null,
211220
tls_verify_host: bool = true,
212221
log_level: ?log.Level = null,
213222
log_format: ?log.Format = null,
@@ -233,6 +242,14 @@ const Command = struct {
233242
\\ and expects the proxy to MITM the request.
234243
\\ Defaults to connect when --http_proxy is set.
235244
\\
245+
\\--proxy_bearer_token
246+
\\ The token to send for bearer authentication with the proxy
247+
\\ Proxy-Authorization: Bearer <token>
248+
\\
249+
\\--proxy_basic_auth
250+
\\ The user:password to send for basic authentication with the proxy
251+
\\ Proxy-Authorization: Basic <base64(user:password)>
252+
\\
236253
\\--log_level The log level: debug, info, warn, error or fatal.
237254
\\ Defaults to
238255
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
@@ -492,6 +509,31 @@ fn parseCommonArg(
492509
return true;
493510
}
494511

512+
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
513+
if (common.proxy_auth != null) {
514+
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_bearer_token" });
515+
return error.InvalidArgument;
516+
}
517+
const str = args.next() orelse {
518+
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
519+
return error.InvalidArgument;
520+
};
521+
common.proxy_auth = .{ .bearer = .{ .token = str } };
522+
return true;
523+
}
524+
if (std.mem.eql(u8, "--proxy_basic_auth", opt)) {
525+
if (common.proxy_auth != null) {
526+
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_basic_auth" });
527+
return error.InvalidArgument;
528+
}
529+
const str = args.next() orelse {
530+
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_basic_auth" });
531+
return error.InvalidArgument;
532+
};
533+
common.proxy_auth = .{ .basic = .{ .user_pass = str } };
534+
return true;
535+
}
536+
495537
if (std.mem.eql(u8, "--log_level", opt)) {
496538
const str = args.next() orelse {
497539
log.fatal(.app, "missing argument value", .{ .arg = "--log_level" });
@@ -606,6 +648,7 @@ fn serveHTTP(address: std.net.Address) !void {
606648
var conn = try listener.accept();
607649
defer conn.stream.close();
608650
var http_server = std.http.Server.init(conn, &read_buffer);
651+
var connect_headers: std.ArrayListUnmanaged(std.http.Header) = .{};
609652
REQUEST: while (true) {
610653
var request = http_server.receiveHead() catch |err| switch (err) {
611654
error.HttpConnectionClosing => continue :ACCEPT,
@@ -617,6 +660,16 @@ fn serveHTTP(address: std.net.Address) !void {
617660

618661
if (request.head.method == .CONNECT) {
619662
try request.respond("", .{ .status = .ok });
663+
664+
// Proxy headers and destination headers are separated in the case of a CONNECT proxy
665+
// We store the CONNECT headers, then continue with the request for the destination
666+
var it = request.iterateHeaders();
667+
while (it.next()) |hdr| {
668+
try connect_headers.append(aa, .{
669+
.name = try std.fmt.allocPrint(aa, "__{s}", .{hdr.name}),
670+
.value = try aa.dupe(u8, hdr.value),
671+
});
672+
}
620673
continue :REQUEST;
621674
}
622675

@@ -657,6 +710,11 @@ fn serveHTTP(address: std.net.Address) !void {
657710
.value = hdr.value,
658711
});
659712
}
713+
714+
if (connect_headers.items.len > 0) {
715+
try headers.appendSlice(aa, connect_headers.items);
716+
connect_headers.clearRetainingCapacity();
717+
}
660718
try headers.append(aa, .{ .name = "Connection", .value = "Close" });
661719

662720
try request.respond("over 9000!", .{

src/testing.zig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ pub fn expectEqual(expected: anytype, actual: anytype) !void {
6666
if (@typeInfo(@TypeOf(expected)) == .null) {
6767
return std.testing.expectEqual(null, actual);
6868
}
69-
return expectEqual(expected, actual.?);
69+
if (actual) |_actual| {
70+
return expectEqual(expected, _actual);
71+
}
72+
return std.testing.expectEqual(expected, null);
7073
},
7174
.@"union" => |union_info| {
7275
if (union_info.tag_type == null) {

0 commit comments

Comments
 (0)