const std = @import("std"); const lmdb = @import("lmdb"); // db {{{ const Db = struct { const Self = @This(); env: ?*lmdb.MDB_env = undefined, txn: ?*lmdb.MDB_txn = undefined, dbi: lmdb.MDB_dbi = undefined, prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0), pub fn gen_id(self: *Self) Id { var id = self.prng.next(); while (self.has(id)) { id = self.prng.next(); } return id; } pub fn open(self: *Self, name: [*c]const u8) void { _ = lmdb.mdb_env_create(&self.env); _ = lmdb.mdb_env_set_maxdbs(self.env, 10); _ = lmdb.mdb_env_set_mapsize(self.env, 1024 * 1024 * 1); _ = lmdb.mdb_env_open(self.env, name, lmdb.MDB_WRITEMAP, 0o664); // _ = lmdb.mdb_env_open(self.env, name, lmdb.MDB_NOSYNC | lmdb.MDB_WRITEMAP, 0o664); } pub fn close(self: *Self) void { lmdb.mdb_env_close(self.env); } pub fn begin(self: *Self, name: [*c]const u8) void { switch (lmdb.mdb_txn_begin(self.env, null, 0, &self.txn)) { 0 => {}, else => |err| { std.debug.print("txn err: {}\n", .{err}); }, } // TODO: lmdb.MDB_INTEGERKEY? _ = lmdb.mdb_dbi_open(self.txn, name, lmdb.MDB_CREATE, &self.dbi); } pub fn commit(self: *Self) void { switch (lmdb.mdb_txn_commit(self.txn)) { 0 => {}, lmdb.MDB_MAP_FULL => { std.debug.print("resize\n", .{}); _ = lmdb.mdb_env_set_mapsize(self.env, 0); }, else => |err| { std.debug.print("commit err: {}\n", .{err}); }, } // TODO: necessary? lmdb.mdb_dbi_close(self.env, self.dbi); } pub fn sync(self: *Self) void { switch (lmdb.mdb_env_sync(self.env, 1)) { 0 => {}, else => |err| { std.debug.print("sync err: {}\n", .{err}); }, } } pub fn put(self: *Self, key: anytype, value: anytype) void { lmdb.put(self.txn, self.dbi, key, value); } pub fn get(self: *Self, key: anytype, comptime T: type) ?T { return lmdb.get(self.txn, self.dbi, key, T); } pub fn del(self: *Self, key: anytype) void { lmdb.del(self.txn, self.dbi, key); } pub fn has(self: *Self, key: anytype) bool { return lmdb.has(self.txn, self.dbi, key); } }; // }}} // http stuff {{{ pub fn redirect(req: *std.http.Server.Request, location: []const u8) !void { try req.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "Location", .value = location }} }); } pub fn get_body(req: *std.http.Server.Request) []const u8 { return req.server.read_buffer[req.head_end .. req.head_end + (req.head.content_length orelse 0)]; } pub fn get_value(req: *std.http.Server.Request, name: []const u8) ?[]const u8 { const body = get_body(req); if (std.mem.indexOf(u8, body, name)) |name_index| { if (std.mem.indexOfScalarPos(u8, body, name_index, '=')) |eql_index| { if (std.mem.indexOfScalarPos(u8, body, name_index, '&')) |amp_index| { return body[eql_index + 1 .. amp_index]; } return body[eql_index + 1 .. body.len]; } } return null; } pub fn get_cookie(req: *std.http.Server.Request, name: []const u8) ?CookieValue { var header_it = req.iterateHeaders(); while (header_it.next()) |header| { if (std.mem.eql(u8, header.name, "Cookie")) { if (std.mem.indexOf(u8, header.value, name)) |name_index| { if (std.mem.indexOfScalarPos(u8, header.value, name_index, '=')) |eql_index| { if (std.mem.indexOfPos(u8, header.value, name_index, "; ")) |semi_index| { return CookieValue.fromSlice(header.value[eql_index + 1 .. semi_index]) catch null; } return CookieValue.fromSlice(header.value[eql_index + 1 .. header.value.len]) catch null; } } } } return null; } // }}} // content {{{ const User = struct { // TODO: choose sizes username: Username, password_hash: PasswordHash, }; const Id = u64; const Username = std.BoundedArray(u8, 16); const PasswordHash = std.BoundedArray(u8, 128); const SessionToken = u64; 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(db: *Db, username: []const u8, password: []const u8) !void { const username_array = try Username.fromSlice(username); db.begin("users"); const user_id = db.gen_id(); db.put(user_id, User{ .username = username_array, .password_hash = try hash_password(password), }); db.commit(); db.begin("ids"); db.put(username_array.buffer, user_id); db.commit(); std.debug.print("id: {}\n", .{user_id}); db.sync(); } pub fn login_user(db: *Db, username: []const u8, password: []const u8) ?SessionToken { const username_array = Username.fromSlice(username) catch return null; db.begin("ids"); const user_id = db.get(username_array.buffer, Id) orelse return null; std.debug.print("id: {}\n", .{user_id}); // TODO: maybe no commit? db.commit(); db.begin("users"); const user = db.get(user_id, User) orelse return null; db.commit(); if (verify_password(password, user.password_hash)) { db.begin("sessions"); const session_token = db.gen_id(); db.put(session_token, user_id); db.commit(); db.sync(); return session_token; } else { return null; } } fn logout_user(db: *Db, session_token: SessionToken) void { db.begin("sessions"); db.del(session_token); db.commit(); } fn get_session_user(db: *Db, session_token: SessionToken) ?User { db.begin("sessions"); const user_id = db.get(session_token, Id) orelse return null; db.commit(); db.begin("users"); const user = db.get(user_id, User) orelse return null; db.commit(); return user; } // }}} fn list_users(db: *Db) void { _ = lmdb.mdb_txn_begin(db.env, null, 0, &db.txn); _ = lmdb.mdb_dbi_open(db.txn, "users", lmdb.MDB_CREATE, &db.dbi); var cursor: ?*lmdb.MDB_cursor = undefined; _ = lmdb.mdb_cursor_open(db.txn, db.dbi, &cursor); var key: lmdb.MDB_val = undefined; var val: lmdb.MDB_val = undefined; var result = lmdb.mdb_cursor_get(cursor, &key, &val, lmdb.MDB_FIRST); while (result != lmdb.MDB_NOTFOUND) { const user_id = @as(*align(1) Id, @ptrCast(key.mv_data.?)).*; const user = @as(*align(1) User, @ptrCast(val.mv_data.?)).*; std.debug.print("[{}] {s}\n", .{ user_id, user.username.constSlice() }); result = lmdb.mdb_cursor_get(cursor, &key, &val, lmdb.MDB_NEXT); } _ = lmdb.mdb_cursor_close(cursor); _ = lmdb.mdb_dbi_close(db.env, db.dbi); _ = lmdb.mdb_txn_commit(db.txn); } pub fn main() !void { // server const address = try std.net.Address.resolveIp("::", 8080); var server = try address.listen(.{ .reuse_address = true, }); defer server.deinit(); // lmdb var db = Db{}; db.open("./db"); defer db.close(); list_users(&db); accept: while (true) { const conn = try server.accept(); std.debug.print("new connection: {}\n", .{conn}); var read_buffer: [1024]u8 = undefined; var http_server = std.http.Server.init(conn, &read_buffer); while (http_server.state == .ready) { var req = http_server.receiveHead() catch continue; std.debug.print("[{}]: {s}\n", .{ req.head.method, req.head.target }); var logged_in: ?struct { user: User, session_token: SessionToken, } = null; if (get_cookie(&req, "session_token")) |session_token_str| { const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str.constSlice(), 10); if (get_session_user(&db, session_token)) |user| { logged_in = .{ .user = user, .session_token = session_token, }; } // TODO: delete session token // TODO: add changeable headers (set, delete cookies) } // html if (req.head.method == .GET) { if (std.mem.eql(u8, req.head.target, "/register")) { try req.respond( \\
\\ \\ \\ \\
, .{}); } else if (std.mem.eql(u8, req.head.target, "/login")) { try req.respond( \\
\\ \\ \\ \\
, .{}); } else { if (logged_in) |login| { var response_buffer = try std.BoundedArray(u8, 1024).init(0); try std.fmt.format(response_buffer.writer(), \\Home \\
\\
, .{login.user.username.constSlice()}); try req.respond(response_buffer.constSlice(), .{}); } else { try req.respond( \\Register \\Login \\
, .{}); } } } // api else { if (std.mem.eql(u8, req.head.target, "/register")) { // TODO: handle args not supplied const username = get_value(&req, "username").?; const password = get_value(&req, "password").?; std.debug.print("New user: {s} {s}\n", .{ username, password }); try register_user(&db, username, password); try redirect(&req, "/login"); } else if (std.mem.eql(u8, req.head.target, "/login")) { // TODO: handle args not supplied const username = get_value(&req, "username").?; const password = get_value(&req, "password").?; std.debug.print("New login: {s} {s}\n", .{ username, password }); if (login_user(&db, username, password)) |session_token| { var redirect_buffer = try std.BoundedArray(u8, 128).init(0); try std.fmt.format(redirect_buffer.writer(), "/user/{s}", .{username}); var cookie_buffer = try std.BoundedArray(u8, 128).init(0); try std.fmt.format(cookie_buffer.writer(), "session_token={}; Secure; HttpOnly", .{session_token}); try req.respond("", .{ .status = .see_other, .extra_headers = &.{ .{ .name = "Location", .value = redirect_buffer.constSlice() }, .{ .name = "Set-Cookie", .value = cookie_buffer.constSlice() }, }, }); } else { try redirect(&req, "/login"); } } else if (std.mem.eql(u8, req.head.target, "/logout")) { if (logged_in) |login| { logout_user(&db, login.session_token); try req.respond("", .{ .status = .see_other, .extra_headers = &.{ .{ .name = "Location", .value = "/" }, .{ .name = "Set-Cookie", .value = "session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT" }, }, }); } } else if (std.mem.eql(u8, req.head.target, "/quit")) { try redirect(&req, "/"); break :accept; } else { try req.respond( \\

POST

, .{}); } } } } }