Skip to content

Commit de97d1b

Browse files
committed
feat: mostly migrated to frames
1 parent 8994003 commit de97d1b

File tree

16 files changed

+371
-488
lines changed

16 files changed

+371
-488
lines changed

examples/basic/main.zig

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ const Server = http.Server;
1313
const Router = http.Router;
1414
const Context = http.Context;
1515
const Route = http.Route;
16+
const Middleware = http.Middleware;
17+
18+
const Next = http.Next;
19+
const Response = http.Response;
20+
const Respond = http.Respond;
21+
22+
const CompressMiddleware = http.CompressMiddleware;
23+
const RateLimitMiddleware = http.RateLimitMiddleware;
24+
const ThreadSafeRateLimit = http.ThreadSafeRateLimit;
25+
26+
fn base_handler(_: Context, _: void) !Respond {
27+
return .{
28+
.status = .OK,
29+
.mime = http.Mime.TEXT,
30+
.body = "Hello, world!",
31+
};
32+
}
1633

1734
pub fn main() !void {
1835
const host: []const u8 = "0.0.0.0";
@@ -26,14 +43,20 @@ pub fn main() !void {
2643
// will spawn our runtimes.
2744
var t = try Tardy.init(allocator, .{
2845
.threading = .auto,
29-
.size_tasks_initial = 1024,
30-
.size_aio_reap_max = 1024,
46+
.size_tasks_initial = 128,
47+
.size_aio_reap_max = 128,
3148
});
3249
defer t.deinit();
3350

34-
//var router = try Router.init(allocator, &.{}, .{});
35-
//defer router.deinit(allocator);
36-
//router.print_route_tree();
51+
var limiter = ThreadSafeRateLimit.init(allocator);
52+
defer limiter.deinit();
53+
54+
var router = try Router.init(allocator, &.{
55+
//RateLimitMiddleware(std.time.ms_per_s, &limiter),
56+
//CompressMiddleware(.{ .gzip = .{} }),
57+
Route.init("/").get({}, base_handler).layer(),
58+
}, .{});
59+
defer router.deinit(allocator);
3760

3861
// create socket for tardy
3962
var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } });
@@ -46,23 +69,20 @@ pub fn main() !void {
4669
socket: *Socket,
4770
};
4871

49-
const params: EntryParams = .{ .router = undefined, .socket = &socket };
72+
const params: EntryParams = .{ .router = &router, .socket = &socket };
5073

5174
// This provides the entry function into the Tardy runtime. This will run
5275
// exactly once inside of each runtime (each thread gets a single runtime).
5376
try t.entry(
5477
&params,
5578
struct {
5679
fn entry(rt: *Runtime, p: *const EntryParams) !void {
57-
var server = Server.init(rt.allocator, .{});
58-
try server.serve(rt, p.socket);
80+
var server = Server.init(rt.allocator, .{
81+
.stack_size = 1024 * 1024 * 4,
82+
.socket_buffer_bytes = 1024 * 4,
83+
});
84+
try server.serve(rt, p.router, p.socket);
5985
}
6086
}.entry,
61-
{},
62-
struct {
63-
fn exit(rt: *Runtime, _: void) !void {
64-
_ = rt;
65-
}
66-
}.exit,
6787
);
6888
}

src/core/lib.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
pub const Job = @import("job.zig").Job;
22
pub const Pseudoslice = @import("pseudoslice.zig").Pseudoslice;
3+
4+
pub fn Pair(comptime A: type, comptime B: type) type {
5+
return struct { A, B };
6+
}

src/http/context.zig

Lines changed: 2 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -18,165 +18,15 @@ const MiddlewareWithData = @import("router/middleware.zig").MiddlewareWithData;
1818
const Next = @import("router/middleware.zig").Next;
1919

2020
const Runtime = @import("tardy").Runtime;
21-
const TaskFn = @import("tardy").TaskFn;
22-
2321
const Server = @import("server.zig").Server;
2422

25-
const raw_respond = @import("server.zig").raw_respond;
26-
2723
// Context is dependent on the server that gets created.
2824
pub const Context = struct {
2925
const Self = @This();
3026
allocator: std.mem.Allocator,
3127
runtime: *Runtime,
3228
/// The Request that triggered this handler.
3329
request: *const Request,
34-
/// The Response that will be returned.
35-
response: *Response,
36-
captures: []Capture,
37-
queries: *QueryMap,
38-
provision: *Provision,
39-
next: Next,
40-
triggered: bool = false,
41-
42-
pub fn to_sse(self: *Self, then: TaskFn(bool, *SSE)) !void {
43-
const sse = try self.allocator.create(SSE);
44-
sse.* = .{
45-
.context = self,
46-
.runtime = self.runtime,
47-
.allocator = self.allocator,
48-
};
49-
50-
try self.respond_headers_only(
51-
.{
52-
.status = .OK,
53-
.mime = Mime.generate(
54-
"text/event-stream",
55-
"sse",
56-
"Server-Sent Events",
57-
),
58-
},
59-
null,
60-
sse,
61-
then,
62-
);
63-
}
64-
65-
pub fn close(self: *Self) !void {
66-
self.provision.job = .close;
67-
try self.runtime.net.close(
68-
self.provision,
69-
Server.close_task,
70-
self.provision.socket,
71-
);
72-
}
73-
74-
pub fn send_then(
75-
self: *Self,
76-
data: []const u8,
77-
ctx: anytype,
78-
then: TaskFn(bool, @TypeOf(ctx)),
79-
) !void {
80-
const pslice = Pseudoslice.init(data, "", self.provision.buffer);
81-
82-
const first_chunk = try Server.prepare_send(
83-
self.runtime,
84-
self.provision,
85-
.{
86-
.other = .{
87-
.func = then,
88-
.ctx = ctx,
89-
},
90-
},
91-
pslice,
92-
);
93-
94-
try self.runtime.net.send(
95-
self.provision,
96-
Server.send_then_other_task,
97-
self.provision.socket,
98-
first_chunk,
99-
);
100-
}
101-
102-
pub fn send_then_recv(self: *Self, data: []const u8) !void {
103-
const pslice = Pseudoslice.init(data, "", self.provision.buffer);
104-
105-
const first_chunk = try Server.prepare_send(
106-
self.runtime,
107-
self.provision,
108-
.recv,
109-
pslice,
110-
);
111-
112-
try self.runtime.net.send(
113-
self.provision,
114-
Server.send_then_recv_task,
115-
self.provision.socket,
116-
first_chunk,
117-
);
118-
}
119-
120-
// This will respond with the headers only.
121-
// You will be in charge of sending the body.
122-
pub fn respond_headers_only(
123-
self: *Self,
124-
options: ResponseSetOptions,
125-
content_length: ?usize,
126-
ctx: anytype,
127-
then: TaskFn(bool, @TypeOf(ctx)),
128-
) !void {
129-
assert(!self.triggered);
130-
self.triggered = true;
131-
132-
// the body should not be set.
133-
assert(options.body == null);
134-
self.response.set(options);
135-
136-
const headers = try self.provision.response.headers_into_buffer(
137-
self.provision.buffer,
138-
content_length,
139-
);
140-
141-
try self.send_then(headers, ctx, then);
142-
}
143-
144-
pub fn respond_without_middleware(self: *Self) !void {
145-
const body = self.response.body orelse "";
146-
const headers = try self.provision.response.headers_into_buffer(
147-
self.provision.buffer,
148-
@intCast(body.len),
149-
);
150-
const pslice = Pseudoslice.init(headers, body, self.provision.buffer);
151-
152-
const first_chunk = try Server.prepare_send(
153-
self.runtime,
154-
self.provision,
155-
.recv,
156-
pslice,
157-
);
158-
159-
try self.runtime.net.send(
160-
self.provision,
161-
Server.send_then_recv_task,
162-
self.provision.socket,
163-
first_chunk,
164-
);
165-
}
166-
167-
/// This is your standard response.
168-
pub fn respond(self: *Self, options: ResponseSetOptions) !void {
169-
assert(!self.triggered);
170-
self.triggered = true;
171-
self.response.set(options);
172-
173-
// If we have a post chain, iterate through it.
174-
if (self.next.post_chain.len > 0) {
175-
self.next.stage = .post;
176-
try self.next.run();
177-
return;
178-
}
179-
180-
try self.respond_without_middleware();
181-
}
30+
/// Address for this Request/Response.
31+
address: std.net.Address,
18232
};

src/http/lib.zig

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@ pub const Status = @import("status.zig").Status;
22
pub const Method = @import("method.zig").Method;
33
pub const Request = @import("request.zig").Request;
44
pub const Response = @import("response.zig").Response;
5+
pub const Respond = @import("response.zig").Respond;
56
pub const Mime = @import("mime.zig").Mime;
67
pub const Encoding = @import("encoding.zig").Encoding;
78
pub const Date = @import("date.zig").Date;
89
pub const Headers = @import("../core/case_string_map.zig").CaseStringMap([]const u8);
910

11+
pub const Context = @import("context.zig").Context;
12+
1013
pub const Router = @import("router.zig").Router;
1114
pub const Route = @import("router/route.zig").Route;
12-
pub const Layer = @import("router/layer.zig").Layer;
15+
pub const SSE = @import("sse.zig").SSE;
1316

14-
pub const Context = @import("context.zig").Context;
17+
pub const Layer = @import("router/middleware.zig").Layer;
1518
pub const Middleware = @import("router/middleware.zig").Middleware;
1619
pub const MiddlewareFn = @import("router/middleware.zig").MiddlewareFn;
1720
pub const Next = @import("router/middleware.zig").Next;
18-
pub const SSE = @import("sse.zig").SSE;
21+
22+
pub const ThreadSafeRateLimit = @import("middlewares/lib.zig").ThreadSafeRateLimit;
23+
pub const RateLimitMiddleware = @import("middlewares/lib.zig").RateLimitMiddleware;
24+
pub const CompressMiddleware = @import("middlewares/lib.zig").CompressMiddleware;
1925

2026
pub const FsDir = @import("router/fs_dir.zig").FsDir;
2127

src/http/middlewares/lib.zig

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
const std = @import("std");
2+
3+
const Mime = @import("../mime.zig").Mime;
4+
const Respond = @import("../response.zig").Respond;
5+
6+
const Middleware = @import("../router/middleware.zig").Middleware;
7+
const Next = @import("../router/middleware.zig").Next;
8+
const Layer = @import("../router/middleware.zig").Layer;
9+
const TypedMiddlewareFn = @import("../router/middleware.zig").TypedMiddlewareFn;
10+
11+
pub const ThreadSafeRateLimit = struct {
12+
map: std.AutoHashMap(u128, i64),
13+
mutex: std.Thread.Mutex = .{},
14+
15+
pub fn init(allocator: std.mem.Allocator) ThreadSafeRateLimit {
16+
return .{
17+
.map = std.AutoHashMap(u128, i64).init(allocator),
18+
.mutex = .{},
19+
};
20+
}
21+
22+
pub fn deinit(self: *ThreadSafeRateLimit) void {
23+
self.map.deinit();
24+
}
25+
};
26+
27+
fn get_ip(addr: std.net.Address) u128 {
28+
return switch (addr.any.family) {
29+
std.posix.AF.INET => @intCast(addr.in.sa.addr),
30+
std.posix.AF.INET6 => std.mem.bytesAsValue(u128, &addr.in6.sa.addr[0]).*,
31+
else => unreachable,
32+
};
33+
}
34+
35+
// TODO: Do a proper rate-limiter. This is super crude.
36+
pub fn RateLimitMiddleware(comptime ms_per_request: usize, limiter: *ThreadSafeRateLimit) Layer {
37+
const func: TypedMiddlewareFn(*ThreadSafeRateLimit) = struct {
38+
fn rate_limit_mw(next: *Next, limit: *ThreadSafeRateLimit) !Respond {
39+
const ip = get_ip(next.context.address);
40+
const time = std.time.milliTimestamp();
41+
42+
limit.mutex.lock();
43+
const addr_entry = try limit.map.getOrPut(ip);
44+
const last_time = addr_entry.value_ptr.*;
45+
addr_entry.value_ptr.* = time;
46+
limit.mutex.unlock();
47+
48+
if (addr_entry.found_existing) {
49+
if (time - last_time >= ms_per_request) return try next.run();
50+
return Respond{
51+
.status = .@"Too Many Requests",
52+
.mime = Mime.TEXT,
53+
.body = "",
54+
};
55+
} else return try next.run();
56+
}
57+
}.rate_limit_mw;
58+
59+
return Middleware.init(limiter, func).layer();
60+
}
61+
62+
fn rate_limit_mw(next: *Next, limiter: *ThreadSafeRateLimit) !Respond {
63+
const ip = get_ip(next.context.address);
64+
const time = std.time.milliTimestamp();
65+
66+
limiter.mutex.lock();
67+
const addr_entry = try limiter.map.getOrPut(ip);
68+
const last_time = addr_entry.value_ptr.*;
69+
addr_entry.value_ptr.* = time;
70+
limiter.mutex.unlock();
71+
72+
if (addr_entry.found_existing) {
73+
if (time - last_time >= std.time.ms_per_s) return try next.run();
74+
return Respond{
75+
.status = .@"Too Many Requests",
76+
.mime = Mime.TEXT,
77+
.body = "",
78+
};
79+
} else return try next.run();
80+
}
81+
82+
// TODO: Consider using a C dependency here.
83+
// Might be nice to get some fast general compression such as zstd :)
84+
const Compression = union(enum) {
85+
gzip: std.compress.gzip.Options,
86+
};
87+
88+
pub fn CompressMiddleware(comptime compression: Compression) Layer {
89+
const func: TypedMiddlewareFn(void) = switch (compression) {
90+
.gzip => |inner| struct {
91+
fn gzip_mw(next: *Next, _: void) !Respond {
92+
var respond = try next.run();
93+
94+
var compressed = std.ArrayList(u8).init(next.context.allocator);
95+
96+
var body_stream = std.io.fixedBufferStream(respond.body);
97+
try std.compress.gzip.compress(body_stream.reader(), compressed.writer(), inner);
98+
99+
// TODO: consider having the headers be a part of the provision?
100+
// might be nice to reuse them as things go on??
101+
var header_list = std.ArrayList([2][]const u8).init(next.context.allocator);
102+
try header_list.appendSlice(respond.headers);
103+
try header_list.append(.{ "Content-Encoding", "gzip" });
104+
105+
respond.body = try compressed.toOwnedSlice();
106+
respond.headers = try header_list.toOwnedSlice();
107+
return respond;
108+
}
109+
}.gzip_mw,
110+
};
111+
112+
return Middleware.init({}, func).layer();
113+
}

0 commit comments

Comments
 (0)