Skip to content

Commit 7c9ed45

Browse files
authored
Merge pull request #14762 from truemedian/http-keepalive
std.http: add connection pooling, handle keep-alive and compressed content
2 parents 14590e9 + 524e0cd commit 7c9ed45

File tree

8 files changed

+1434
-803
lines changed

8 files changed

+1434
-803
lines changed

lib/std/Uri.zig

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,27 @@ fragment: ?[]const u8,
1616

1717
/// Applies URI encoding and replaces all reserved characters with their respective %XX code.
1818
pub fn escapeString(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {
19+
return escapeStringWithFn(allocator, input, isUnreserved);
20+
}
21+
22+
pub fn escapePath(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {
23+
return escapeStringWithFn(allocator, input, isPathChar);
24+
}
25+
26+
pub fn escapeQuery(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {
27+
return escapeStringWithFn(allocator, input, isQueryChar);
28+
}
29+
30+
pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) std.mem.Allocator.Error![]const u8 {
1931
var outsize: usize = 0;
2032
for (input) |c| {
21-
outsize += if (isUnreserved(c)) @as(usize, 1) else 3;
33+
outsize += if (keepUnescaped(c)) @as(usize, 1) else 3;
2234
}
2335
var output = try allocator.alloc(u8, outsize);
2436
var outptr: usize = 0;
2537

2638
for (input) |c| {
27-
if (isUnreserved(c)) {
39+
if (keepUnescaped(c)) {
2840
output[outptr] = c;
2941
outptr += 1;
3042
} else {
@@ -94,13 +106,14 @@ pub fn unescapeString(allocator: std.mem.Allocator, input: []const u8) error{Out
94106

95107
pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort };
96108

97-
/// Parses the URI or returns an error.
109+
/// Parses the URI or returns an error. This function is not compliant, but is required to parse
110+
/// some forms of URIs in the wild. Such as HTTP Location headers.
98111
/// The return value will contain unescaped strings pointing into the
99112
/// original `text`. Each component that is provided, will be non-`null`.
100-
pub fn parse(text: []const u8) ParseError!Uri {
113+
pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
101114
var reader = SliceReader{ .slice = text };
102115
var uri = Uri{
103-
.scheme = reader.readWhile(isSchemeChar),
116+
.scheme = "",
104117
.user = null,
105118
.password = null,
106119
.host = null,
@@ -110,14 +123,6 @@ pub fn parse(text: []const u8) ParseError!Uri {
110123
.fragment = null,
111124
};
112125

113-
// after the scheme, a ':' must appear
114-
if (reader.get()) |c| {
115-
if (c != ':')
116-
return error.UnexpectedCharacter;
117-
} else {
118-
return error.InvalidFormat;
119-
}
120-
121126
if (reader.peekPrefix("//")) { // authority part
122127
std.debug.assert(reader.get().? == '/');
123128
std.debug.assert(reader.get().? == '/');
@@ -179,6 +184,76 @@ pub fn parse(text: []const u8) ParseError!Uri {
179184
return uri;
180185
}
181186

187+
/// Parses the URI or returns an error.
188+
/// The return value will contain unescaped strings pointing into the
189+
/// original `text`. Each component that is provided, will be non-`null`.
190+
pub fn parse(text: []const u8) ParseError!Uri {
191+
var reader = SliceReader{ .slice = text };
192+
const scheme = reader.readWhile(isSchemeChar);
193+
194+
// after the scheme, a ':' must appear
195+
if (reader.get()) |c| {
196+
if (c != ':')
197+
return error.UnexpectedCharacter;
198+
} else {
199+
return error.InvalidFormat;
200+
}
201+
202+
var uri = try parseWithoutScheme(reader.readUntilEof());
203+
uri.scheme = scheme;
204+
205+
return uri;
206+
}
207+
208+
/// Resolves a URI against a base URI, conforming to RFC 3986, Section 5.
209+
/// arena owns any memory allocated by this function.
210+
pub fn resolve(Base: Uri, R: Uri, strict: bool, arena: std.mem.Allocator) !Uri {
211+
var T: Uri = undefined;
212+
213+
if (R.scheme.len > 0 and !((!strict) and (std.mem.eql(u8, R.scheme, Base.scheme)))) {
214+
T.scheme = R.scheme;
215+
T.user = R.user;
216+
T.host = R.host;
217+
T.port = R.port;
218+
T.path = try std.fs.path.resolvePosix(arena, &.{ "/", R.path });
219+
T.query = R.query;
220+
} else {
221+
if (R.host) |host| {
222+
T.user = R.user;
223+
T.host = host;
224+
T.port = R.port;
225+
T.path = R.path;
226+
T.path = try std.fs.path.resolvePosix(arena, &.{ "/", R.path });
227+
T.query = R.query;
228+
} else {
229+
if (R.path.len == 0) {
230+
T.path = Base.path;
231+
if (R.query) |query| {
232+
T.query = query;
233+
} else {
234+
T.query = Base.query;
235+
}
236+
} else {
237+
if (R.path[0] == '/') {
238+
T.path = try std.fs.path.resolvePosix(arena, &.{ "/", R.path });
239+
} else {
240+
T.path = try std.fs.path.resolvePosix(arena, &.{ "/", Base.path, R.path });
241+
}
242+
T.query = R.query;
243+
}
244+
245+
T.user = Base.user;
246+
T.host = Base.host;
247+
T.port = Base.port;
248+
}
249+
T.scheme = Base.scheme;
250+
}
251+
252+
T.fragment = R.fragment;
253+
254+
return T;
255+
}
256+
182257
const SliceReader = struct {
183258
const Self = @This();
184259

@@ -284,6 +359,14 @@ fn isPathSeparator(c: u8) bool {
284359
};
285360
}
286361

362+
fn isPathChar(c: u8) bool {
363+
return isUnreserved(c) or isSubLimit(c) or c == '/' or c == ':' or c == '@';
364+
}
365+
366+
fn isQueryChar(c: u8) bool {
367+
return isPathChar(c) or c == '?';
368+
}
369+
287370
fn isQuerySeparator(c: u8) bool {
288371
return switch (c) {
289372
'#' => true,

lib/std/crypto/tls/Client.zig

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,59 @@ pub const StreamInterface = struct {
8888
}
8989
};
9090

91+
pub fn InitError(comptime Stream: type) type {
92+
return std.mem.Allocator.Error || Stream.WriteError || Stream.ReadError || error{
93+
InsufficientEntropy,
94+
DiskQuota,
95+
LockViolation,
96+
NotOpenForWriting,
97+
TlsAlert,
98+
TlsUnexpectedMessage,
99+
TlsIllegalParameter,
100+
TlsDecryptFailure,
101+
TlsRecordOverflow,
102+
TlsBadRecordMac,
103+
CertificateFieldHasInvalidLength,
104+
CertificateHostMismatch,
105+
CertificatePublicKeyInvalid,
106+
CertificateExpired,
107+
CertificateFieldHasWrongDataType,
108+
CertificateIssuerMismatch,
109+
CertificateNotYetValid,
110+
CertificateSignatureAlgorithmMismatch,
111+
CertificateSignatureAlgorithmUnsupported,
112+
CertificateSignatureInvalid,
113+
CertificateSignatureInvalidLength,
114+
CertificateSignatureNamedCurveUnsupported,
115+
CertificateSignatureUnsupportedBitCount,
116+
TlsCertificateNotVerified,
117+
TlsBadSignatureScheme,
118+
TlsBadRsaSignatureBitCount,
119+
InvalidEncoding,
120+
IdentityElement,
121+
SignatureVerificationFailed,
122+
TlsDecryptError,
123+
TlsConnectionTruncated,
124+
TlsDecodeError,
125+
UnsupportedCertificateVersion,
126+
CertificateTimeInvalid,
127+
CertificateHasUnrecognizedObjectId,
128+
CertificateHasInvalidBitString,
129+
MessageTooLong,
130+
NegativeIntoUnsigned,
131+
TargetTooSmall,
132+
BufferTooSmall,
133+
InvalidSignature,
134+
NotSquare,
135+
NonCanonical,
136+
};
137+
}
138+
91139
/// Initiates a TLS handshake and establishes a TLSv1.3 session with `stream`, which
92140
/// must conform to `StreamInterface`.
93141
///
94142
/// `host` is only borrowed during this function call.
95-
pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !Client {
143+
pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) InitError(@TypeOf(stream))!Client {
96144
const host_len = @intCast(u16, host.len);
97145

98146
var random_buffer: [128]u8 = undefined;

lib/std/http.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,24 @@ pub const Status = enum(u10) {
248248

249249
pub const TransferEncoding = enum {
250250
chunked,
251+
// compression is intentionally omitted here, as std.http.Client stores it as content-encoding
252+
};
253+
254+
pub const ContentEncoding = enum {
251255
compress,
252256
deflate,
253257
gzip,
258+
zstd,
259+
};
260+
261+
pub const Connection = enum {
262+
keep_alive,
263+
close,
264+
};
265+
266+
pub const CustomHeader = struct {
267+
name: []const u8,
268+
value: []const u8,
254269
};
255270

256271
const std = @import("std.zig");

0 commit comments

Comments
 (0)