const std = @import("std"); const lmdb = @import("lmdb"); const http = @import("http"); // db {{{ const Prng = struct { var prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0); pub fn gen_id(dbi: anytype) Id { var id = Prng.prng.next(); while (dbi.has(id)) { id = Prng.prng.next(); } return id; } pub fn gen_str(dbi: anytype, comptime len: usize) [len]u8 { var buf: [len / 2]u8 = undefined; var res: [len]u8 = undefined; Prng.prng.fill(&buf); for (0..len / 2) |i| { res[i * 2 + 0] = 'a' + (buf[i] % 16); res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16); } while (dbi.has(res)) { Prng.prng.fill(&buf); for (0..len / 2) |i| { res[i * 2 + 0] = 'a' + (buf[i] % 16); res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16); } } return res; } }; const Db = struct { fn users(txn: *const lmdb.Txn) !lmdb.Dbi(Id, User) { return try txn.dbi("users", Id, User); } fn user_ids(txn: *const lmdb.Txn) !lmdb.Dbi(Username, Id) { return try txn.dbi("user_ids", Username, Id); } fn sessions(txn: *const lmdb.Txn) !lmdb.Dbi(SessionToken, Id) { return try txn.dbi("sessions", SessionToken, Id); } }; // }}} // content {{{ const User = struct { // TODO: choose sizes username: Username, password_hash: PasswordHash, }; const SessionTokenLen = 16; const Id = u64; const Username = std.BoundedArray(u8, 16); const PasswordHash = std.BoundedArray(u8, 128); const SessionToken = [SessionTokenLen]u8; const CookieValue = std.BoundedArray(u8, 128); pub fn hash_password(password: []const u8) !PasswordHash { var hash_buffer = try PasswordHash.init(128); // TODO: choose buffer size // TODO: dont allocate on stack, maybe zero memory? var buffer: [1024 * 10]u8 = undefined; var alloc = std.heap.FixedBufferAllocator.init(&buffer); // TODO: choose limits const result = try std.crypto.pwhash.argon2.strHash(password, .{ .allocator = alloc.allocator(), .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024), }, hash_buffer.slice()); try hash_buffer.resize(result.len); return hash_buffer; } pub fn verify_password(password: []const u8, hash: PasswordHash) bool { var buffer: [1024 * 10]u8 = undefined; var alloc = std.heap.FixedBufferAllocator.init(&buffer); if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{ .allocator = alloc.allocator(), })) { return true; } else |err| { std.debug.print("verify error: {}\n", .{err}); return false; } } pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool { const username_array = try Username.fromSlice(username); const txn = try env.txn(); defer { txn.commit(); env.sync(); } const users = try Db.users(&txn); const user_ids = try Db.user_ids(&txn); if (user_ids.has(username_array)) { return false; } else { const user_id = Prng.gen_id(users); users.put(user_id, User{ .username = username_array, .password_hash = try hash_password(password), }); user_ids.put(username_array, user_id); return true; } } pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) !SessionToken { const username_array = try Username.fromSlice(username); const txn = try env.txn(); defer { txn.commit(); env.sync(); } const user_ids = try Db.user_ids(&txn); const user_id = user_ids.get(username_array) orelse return error.UnknownUsername; std.debug.print("id: {}\n", .{user_id}); const users = try Db.users(&txn); if (users.get(user_id)) |user| { if (verify_password(password, user.password_hash)) { const sessions = try Db.sessions(&txn); const session_token = Prng.gen_str(sessions, SessionTokenLen); sessions.put(session_token, user_id); return session_token; } else { return error.IncorrectPassword; } } else { return error.UserNotFound; } } fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void { const txn = try env.txn(); defer { txn.commit(); env.sync(); } const sessions = try Db.sessions(&txn); sessions.del(session_token); } fn get_session_user(env: *lmdb.Env, session_token: SessionToken) !User { const txn = try env.txn(); defer txn.abort(); const sessions = try Db.sessions(&txn); const users = try Db.users(&txn); if (sessions.get(session_token)) |user_id| { return users.get(user_id) orelse error.UnknownUser; } else { return error.SessionNotFound; } } // }}} fn list_users(env: *lmdb.Env) !void { const txn = try env.txn(); defer txn.abort(); // const users = try Db.users(&txn); const users = try txn.dbi("users", Id, User); var cursor = try users.cursor(); var key: Id = undefined; var user_maybe = cursor.get(&key, .First); while (user_maybe) |*user| { std.debug.print("[{}] {s}\n", .{ key, user.username.constSlice() }); user_maybe = cursor.get(&key, .Next); } } fn list_user_ids(env: *lmdb.Env) !void { const txn = try env.txn(); defer txn.abort(); const user_ids = try Db.user_ids(&txn); var cursor = try user_ids.cursor(); var key: Username = undefined; var user_id_maybe = cursor.get(&key, .First); while (user_id_maybe) |user_id| { std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id }); user_id_maybe = cursor.get(&key, .Next); } } fn list_sessions(env: *lmdb.Env) !void { const txn = try env.txn(); defer txn.abort(); const sessions = try Db.sessions(&txn); var cursor = try sessions.cursor(); var key: SessionToken = undefined; var user_id_maybe = cursor.get(&key, .First); while (user_id_maybe) |user_id| { std.debug.print("[{s}] {}\n", .{ key, user_id }); user_id_maybe = cursor.get(&key, .Next); } } const ReqBufferSize = 4096; const ResHeadBufferSize = 4096; const ResBodyBufferSize = 4096; pub fn main() !void { // server var server = try http.Server.init("::", 8080); defer server.deinit(); // lmdb var env = lmdb.Env.open("db", 1024 * 100); defer env.close(); std.debug.print("Users:\n", .{}); try list_users(&env); std.debug.print("User IDs:\n", .{}); try list_user_ids(&env); std.debug.print("Sessions:\n", .{}); try list_sessions(&env); try handle_connection(&server, &env); // const ThreadCount = 1; // var ts: [ThreadCount]std.Thread = undefined; // for (0..ThreadCount) |i| { // ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env }); // } // for (0..ThreadCount) |i| { // ts[i].join(); // } std.debug.print("done\n", .{}); } fn handle_connection(server: *http.Server, env: *lmdb.Env) !void { // TODO: static? var req_buffer: [ReqBufferSize]u8 = undefined; var res_head_buffer: [ResHeadBufferSize]u8 = undefined; var res_body_buffer: [ResBodyBufferSize]u8 = undefined; accept: while (true) { server.wait(); while (try server.next_request(&req_buffer)) |req| { // std.debug.print("[{}]: {s}\n", .{ req.method, req.target }); // reponse var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer); // check session token var logged_in: ?struct { user: User, session_token: SessionToken, } = null; if (req.get_cookie("session_token")) |session_token_str| { var session_token: SessionToken = undefined; std.mem.copyForwards(u8, &session_token, session_token_str); // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10); // const session_token = std.mem.bytesToValue(SessionToken, session_token_str); if (get_session_user(env, session_token)) |user| { logged_in = .{ .user = user, .session_token = session_token, }; } else |err| { std.debug.print("get_session_user err: {}\n", .{err}); try res.add_header( "Set-Cookie", .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"}, ); } } // html if (req.method == .GET) { if (std.mem.eql(u8, req.target, "/register")) { try res.write( \\
, .{}); try res.send(); } else if (std.mem.eql(u8, req.target, "/login")) { try res.write( \\ , .{}); try res.send(); } else { if (logged_in) |login| { try res.write( \\Home \\ \\ , .{login.user.username.constSlice()}); try res.send(); } else { try res.write( \\Register \\Login \\ , .{}); try res.send(); } } } // api else { if (std.mem.eql(u8, req.target, "/register")) { // TODO: handle args not supplied const username = req.get_value("username").?; const password = req.get_value("password").?; std.debug.print("New user: {s} {s}\n", .{ username, password }); if (try register_user(env, username, password)) { try res.redirect("/login"); } else { try res.redirect("/register"); } try res.send(); } else if (std.mem.eql(u8, req.target, "/login")) { // TODO: handle args not supplied const username = req.get_value("username").?; const password = req.get_value("password").?; std.debug.print("New login: {s} {s}\n", .{ username, password }); if (login_user(env, username, password)) |session_token| { res.status = .see_other; try res.add_header( "Location", .{ "/user/{s}", .{username} }, ); try res.add_header( "Set-Cookie", .{ "session_token={s}; Secure; HttpOnly", .{session_token} }, ); try res.send(); } else |err| { std.debug.print("login_user err: {}\n", .{err}); try res.redirect("/login"); try res.send(); } } else if (std.mem.eql(u8, req.target, "/logout")) { if (logged_in) |login| { try logout_user(env, login.session_token); try res.add_header( "Set-Cookie", .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"}, ); try res.redirect("/"); try res.send(); } } else if (std.mem.eql(u8, req.target, "/quit")) { try res.redirect("/"); try res.send(); break :accept; } else { // try req.respond( // \\POST
// , .{}); try res.write("{s}
", .{req.target}); try res.send(); } } } } }