1 const std = @import("std");
2 const lmdb = @import("lmdb");
7 var prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0);
9 pub fn gen_id(dbi: anytype) Id {
10 var id = Prng.prng.next();
13 id = Prng.prng.next();
24 pub fn redirect(req: *std.http.Server.Request, location: []const u8) !void {
25 try req.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "Location", .value = location }} });
28 pub fn get_body(req: *std.http.Server.Request) []const u8 {
29 return req.server.read_buffer[req.head_end .. req.head_end + (req.head.content_length orelse 0)];
32 pub fn get_value(req: *std.http.Server.Request, name: []const u8) ?[]const u8 {
33 const body = get_body(req);
34 if (std.mem.indexOf(u8, body, name)) |name_index| {
35 if (std.mem.indexOfScalarPos(u8, body, name_index, '=')) |eql_index| {
36 if (std.mem.indexOfScalarPos(u8, body, name_index, '&')) |amp_index| {
37 return body[eql_index + 1 .. amp_index];
40 return body[eql_index + 1 .. body.len];
46 pub fn get_cookie(req: *std.http.Server.Request, name: []const u8) ?CookieValue {
47 var header_it = req.iterateHeaders();
48 while (header_it.next()) |header| {
49 if (std.mem.eql(u8, header.name, "Cookie")) {
50 if (std.mem.indexOf(u8, header.value, name)) |name_index| {
51 if (std.mem.indexOfScalarPos(u8, header.value, name_index, '=')) |eql_index| {
52 if (std.mem.indexOfPos(u8, header.value, name_index, "; ")) |semi_index| {
53 return CookieValue.fromSlice(header.value[eql_index + 1 .. semi_index]) catch null;
56 return CookieValue.fromSlice(header.value[eql_index + 1 .. header.value.len]) catch null;
71 password_hash: PasswordHash,
75 const Username = std.BoundedArray(u8, 16);
76 const PasswordHash = std.BoundedArray(u8, 128);
77 const SessionToken = u64;
78 const CookieValue = std.BoundedArray(u8, 128);
80 pub fn hash_password(password: []const u8) !PasswordHash {
81 var hash_buffer = try PasswordHash.init(128);
83 // TODO: choose buffer size
84 // TODO: dont allocate on stack, maybe zero memory?
85 var buffer: [1024 * 10]u8 = undefined;
86 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
88 // TODO: choose limits
89 const result = try std.crypto.pwhash.argon2.strHash(password, .{
90 .allocator = alloc.allocator(),
91 .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
92 }, hash_buffer.slice());
94 try hash_buffer.resize(result.len);
99 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
100 var buffer: [1024 * 10]u8 = undefined;
101 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
103 if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
104 .allocator = alloc.allocator(),
108 std.debug.print("verify error: {}\n", .{err});
113 pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !void {
114 const username_array = try Username.fromSlice(username);
116 const txn = try env.txn();
122 const users = try txn.dbi("users", Id, User);
123 const user_id = Prng.gen_id(users);
124 users.put(user_id, User{
125 .username = username_array,
126 .password_hash = try hash_password(password),
129 const user_ids = try txn.dbi("user_ids", Username, Id);
130 user_ids.put(username_array, user_id);
133 pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) !SessionToken {
134 const username_array = try Username.fromSlice(username);
136 const txn = try env.txn();
142 const user_ids = try txn.dbi("user_ids", Username, Id);
143 const user_id = user_ids.get(username_array) orelse return error.UnknownUsername;
144 std.debug.print("id: {}\n", .{user_id});
146 const users = try txn.dbi("users", Id, User);
147 if (users.get(user_id)) |user| {
148 if (verify_password(password, user.password_hash)) {
149 const sessions = try txn.dbi("sessions", Id, Id);
150 const session_token = Prng.gen_id(sessions);
151 sessions.put(session_token, user_id);
152 return session_token;
154 return error.IncorrectPassword;
157 return error.UserNotFound;
161 fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void {
162 const txn = try env.txn();
168 const sessions = try txn.dbi("sessions", Id, Id);
169 sessions.del(session_token);
172 fn get_session_user(env: *lmdb.Env, session_token: SessionToken) !User {
173 const txn = try env.txn();
176 const sessions = try txn.dbi("sessions", Id, Id);
177 const users = try txn.dbi("users", Id, User);
179 if (sessions.get(session_token)) |user_id| {
180 return users.get(user_id) orelse error.UnknownUser;
182 return error.SessionNotFound;
188 fn list_users(env: *lmdb.Env) !void {
189 const txn = try env.txn();
192 const users = try txn.dbi("users", Id, User);
193 var cursor = try users.cursor();
195 var key: Id = undefined;
196 var user_maybe = cursor.get(&key, .First);
198 while (user_maybe) |user| {
199 std.debug.print("[{}] {s}\n", .{ key, user.username.constSlice() });
201 user_maybe = cursor.get(&key, .Next);
204 fn list_user_ids(env: *lmdb.Env) !void {
205 const txn = try env.txn();
208 const user_ids = try txn.dbi("user_ids", Username, Id);
209 var cursor = try user_ids.cursor();
211 var key: Username = undefined;
212 var user_id_maybe = cursor.get(&key, .First);
214 while (user_id_maybe) |user_id| {
215 std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
217 user_id_maybe = cursor.get(&key, .Next);
221 fn list_sessions(env: *lmdb.Env) !void {
222 const txn = try env.txn();
225 const sessions = try txn.dbi("sessions", SessionToken, Id);
226 var cursor = try sessions.cursor();
228 var key: SessionToken = undefined;
229 var user_id_maybe = cursor.get(&key, .First);
231 while (user_id_maybe) |user_id| {
232 std.debug.print("[{}] {}\n", .{ key, user_id });
234 user_id_maybe = cursor.get(&key, .Next);
238 pub fn main() !void {
240 const address = try std.net.Address.resolveIp("::", 8080);
242 var server = try address.listen(.{
243 .reuse_address = true,
245 defer server.deinit();
248 var env = lmdb.Env.open("db", 1024 * 100);
251 std.debug.print("Users:\n", .{});
252 try list_users(&env);
253 std.debug.print("User IDs:\n", .{});
254 try list_user_ids(&env);
255 std.debug.print("Sessions:\n", .{});
256 try list_sessions(&env);
258 accept: while (true) {
259 const conn = try server.accept();
261 std.debug.print("new connection: {}\n", .{conn});
263 var read_buffer: [1024]u8 = undefined;
264 var http_server = std.http.Server.init(conn, &read_buffer);
266 while (http_server.state == .ready) {
267 var req = http_server.receiveHead() catch continue;
269 std.debug.print("[{}]: {s}\n", .{ req.head.method, req.head.target });
271 var logged_in: ?struct {
273 session_token: SessionToken,
276 if (get_cookie(&req, "session_token")) |session_token_str| {
277 const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str.constSlice(), 10);
278 if (get_session_user(&env, session_token)) |user| {
281 .session_token = session_token,
284 std.debug.print("get_session_user err: {}\n", .{err});
286 // TODO: delete session token
287 // TODO: add changeable headers (set, delete cookies)
291 if (req.head.method == .GET) {
292 if (std.mem.eql(u8, req.head.target, "/register")) {
294 \\<form action="/register" method="post">
295 \\<input type="text" name="username" />
296 \\<input type="password" name="password" />
297 \\<input type="submit" value="Register" />
300 } else if (std.mem.eql(u8, req.head.target, "/login")) {
302 \\<form action="/login" method="post">
303 \\<input type="text" name="username" />
304 \\<input type="password" name="password" />
305 \\<input type="submit" value="Login" />
309 if (logged_in) |login| {
310 var response_buffer = try std.BoundedArray(u8, 1024).init(0);
311 try std.fmt.format(response_buffer.writer(),
312 \\<a href="/user/{s}">Home</a>
313 \\<form action="/logout" method="post"><input type="submit" value="Logout" /></form>
314 \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
315 , .{login.user.username.constSlice()});
316 try req.respond(response_buffer.constSlice(), .{});
319 \\<a href="/register">Register</a>
320 \\<a href="/login">Login</a>
321 \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
328 if (std.mem.eql(u8, req.head.target, "/register")) {
329 // TODO: handle args not supplied
330 const username = get_value(&req, "username").?;
331 const password = get_value(&req, "password").?;
333 std.debug.print("New user: {s} {s}\n", .{ username, password });
334 try register_user(&env, username, password);
336 try redirect(&req, "/login");
337 } else if (std.mem.eql(u8, req.head.target, "/login")) {
338 // TODO: handle args not supplied
339 const username = get_value(&req, "username").?;
340 const password = get_value(&req, "password").?;
342 std.debug.print("New login: {s} {s}\n", .{ username, password });
343 if (login_user(&env, username, password)) |session_token| {
344 var redirect_buffer = try std.BoundedArray(u8, 128).init(0);
345 try std.fmt.format(redirect_buffer.writer(), "/user/{s}", .{username});
347 var cookie_buffer = try std.BoundedArray(u8, 128).init(0);
348 try std.fmt.format(cookie_buffer.writer(), "session_token={}; Secure; HttpOnly", .{session_token});
350 try req.respond("", .{
351 .status = .see_other,
353 .{ .name = "Location", .value = redirect_buffer.constSlice() },
354 .{ .name = "Set-Cookie", .value = cookie_buffer.constSlice() },
358 std.debug.print("login_user err: {}\n", .{err});
359 try redirect(&req, "/login");
361 } else if (std.mem.eql(u8, req.head.target, "/logout")) {
362 if (logged_in) |login| {
363 try logout_user(&env, login.session_token);
364 try req.respond("", .{
365 .status = .see_other,
367 .{ .name = "Location", .value = "/" },
368 .{ .name = "Set-Cookie", .value = "session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT" },
372 } else if (std.mem.eql(u8, req.head.target, "/quit")) {
373 try redirect(&req, "/");