1 const std = @import("std");
2 const lmdb = @import("lmdb");
3 const http = @import("http");
8 var prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0);
10 pub fn gen_id(dbi: anytype) Id {
11 var id = Prng.prng.next();
14 id = Prng.prng.next();
20 pub fn gen_str(dbi: anytype, comptime len: usize) [len]u8 {
21 var buf: [len / 2]u8 = undefined;
22 var res: [len]u8 = undefined;
24 for (0..len / 2) |i| {
25 res[i * 2 + 0] = 'a' + (buf[i] % 16);
26 res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16);
29 while (dbi.has(res)) {
31 for (0..len / 2) |i| {
32 res[i * 2 + 0] = 'a' + (buf[i] % 16);
33 res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16);
42 fn users(txn: *const lmdb.Txn) !lmdb.Dbi(Id, User) {
43 return try txn.dbi("users", Id, User);
45 fn user_ids(txn: *const lmdb.Txn) !lmdb.Dbi(Username, Id) {
46 return try txn.dbi("user_ids", Username, Id);
48 fn sessions(txn: *const lmdb.Txn) !lmdb.Dbi(SessionToken, Id) {
49 return try txn.dbi("sessions", SessionToken, Id);
60 password_hash: PasswordHash,
63 const SessionTokenLen = 16;
66 const Username = std.BoundedArray(u8, 16);
67 const PasswordHash = std.BoundedArray(u8, 128);
68 const SessionToken = [SessionTokenLen]u8;
69 const CookieValue = std.BoundedArray(u8, 128);
71 pub fn hash_password(password: []const u8) !PasswordHash {
72 var hash_buffer = try PasswordHash.init(128);
74 // TODO: choose buffer size
75 // TODO: dont allocate on stack, maybe zero memory?
76 var buffer: [1024 * 10]u8 = undefined;
77 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
79 // TODO: choose limits
80 const result = try std.crypto.pwhash.argon2.strHash(password, .{
81 .allocator = alloc.allocator(),
82 .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
83 }, hash_buffer.slice());
85 try hash_buffer.resize(result.len);
90 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
91 var buffer: [1024 * 10]u8 = undefined;
92 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
94 if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
95 .allocator = alloc.allocator(),
99 std.debug.print("verify error: {}\n", .{err});
104 pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool {
105 const username_array = try Username.fromSlice(username);
107 const txn = try env.txn();
113 const users = try Db.users(&txn);
114 const user_ids = try Db.user_ids(&txn);
116 if (user_ids.has(username_array)) {
119 const user_id = Prng.gen_id(users);
120 users.put(user_id, User{
121 .username = username_array,
122 .password_hash = try hash_password(password),
125 user_ids.put(username_array, user_id);
131 pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) !SessionToken {
132 const username_array = try Username.fromSlice(username);
134 const txn = try env.txn();
140 const user_ids = try Db.user_ids(&txn);
141 const user_id = user_ids.get(username_array) orelse return error.UnknownUsername;
142 std.debug.print("id: {}\n", .{user_id});
144 const users = try Db.users(&txn);
145 if (users.get(user_id)) |user| {
146 if (verify_password(password, user.password_hash)) {
147 const sessions = try Db.sessions(&txn);
148 const session_token = Prng.gen_str(sessions, SessionTokenLen);
149 sessions.put(session_token, user_id);
150 return session_token;
152 return error.IncorrectPassword;
155 return error.UserNotFound;
159 fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void {
160 const txn = try env.txn();
166 const sessions = try Db.sessions(&txn);
167 sessions.del(session_token);
170 fn get_session_user(env: *lmdb.Env, session_token: SessionToken) !User {
171 const txn = try env.txn();
174 const sessions = try Db.sessions(&txn);
175 const users = try Db.users(&txn);
177 if (sessions.get(session_token)) |user_id| {
178 return users.get(user_id) orelse error.UnknownUser;
180 return error.SessionNotFound;
186 fn list_users(env: *lmdb.Env) !void {
187 const txn = try env.txn();
190 // const users = try Db.users(&txn);
191 const users = try txn.dbi("users", Id, User);
192 var cursor = try users.cursor();
194 var key: Id = undefined;
195 var user_maybe = cursor.get(&key, .First);
197 while (user_maybe) |*user| {
198 std.debug.print("[{}] {s}\n", .{ key, user.username.constSlice() });
200 user_maybe = cursor.get(&key, .Next);
203 fn list_user_ids(env: *lmdb.Env) !void {
204 const txn = try env.txn();
207 const user_ids = try Db.user_ids(&txn);
208 var cursor = try user_ids.cursor();
210 var key: Username = undefined;
211 var user_id_maybe = cursor.get(&key, .First);
213 while (user_id_maybe) |user_id| {
214 std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
216 user_id_maybe = cursor.get(&key, .Next);
220 fn list_sessions(env: *lmdb.Env) !void {
221 const txn = try env.txn();
224 const sessions = try Db.sessions(&txn);
225 var cursor = try sessions.cursor();
227 var key: SessionToken = undefined;
228 var user_id_maybe = cursor.get(&key, .First);
230 while (user_id_maybe) |user_id| {
231 std.debug.print("[{s}] {}\n", .{ key, user_id });
233 user_id_maybe = cursor.get(&key, .Next);
237 const ReqBufferSize = 4096;
238 const ResHeadBufferSize = 4096;
239 const ResBodyBufferSize = 4096;
241 pub fn main() !void {
243 var server = try http.Server.init("::", 8080);
244 defer server.deinit();
247 var env = lmdb.Env.open("db", 1024 * 100);
250 std.debug.print("Users:\n", .{});
251 try list_users(&env);
252 std.debug.print("User IDs:\n", .{});
253 try list_user_ids(&env);
254 std.debug.print("Sessions:\n", .{});
255 try list_sessions(&env);
257 try handle_connection(&server, &env);
258 // const ThreadCount = 1;
259 // var ts: [ThreadCount]std.Thread = undefined;
261 // for (0..ThreadCount) |i| {
262 // ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
264 // for (0..ThreadCount) |i| {
268 std.debug.print("done\n", .{});
271 fn handle_connection(server: *http.Server, env: *lmdb.Env) !void {
273 var req_buffer: [ReqBufferSize]u8 = undefined;
274 var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
275 var res_body_buffer: [ResBodyBufferSize]u8 = undefined;
277 accept: while (true) {
280 while (try server.next_request(&req_buffer)) |req| {
281 // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
284 var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer);
286 // check session token
287 var logged_in: ?struct {
289 session_token: SessionToken,
292 if (req.get_cookie("session_token")) |session_token_str| {
293 var session_token: SessionToken = undefined;
294 std.mem.copyForwards(u8, &session_token, session_token_str);
295 // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
296 // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
297 if (get_session_user(env, session_token)) |user| {
300 .session_token = session_token,
303 std.debug.print("get_session_user err: {}\n", .{err});
307 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
313 if (req.method == .GET) {
314 if (std.mem.eql(u8, req.target, "/register")) {
316 \\<form action="/register" method="post">
317 \\<input type="text" name="username" />
318 \\<input type="password" name="password" />
319 \\<input type="submit" value="Register" />
323 } else if (std.mem.eql(u8, req.target, "/login")) {
325 \\<form action="/login" method="post">
326 \\<input type="text" name="username" />
327 \\<input type="password" name="password" />
328 \\<input type="submit" value="Login" />
333 if (logged_in) |login| {
335 \\<a href="/user/{s}">Home</a>
336 \\<form action="/logout" method="post"><input type="submit" value="Logout" /></form>
337 \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
338 , .{login.user.username.constSlice()});
342 \\<a href="/register">Register</a>
343 \\<a href="/login">Login</a>
344 \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
352 if (std.mem.eql(u8, req.target, "/register")) {
353 // TODO: handle args not supplied
354 const username = req.get_value("username").?;
355 const password = req.get_value("password").?;
357 std.debug.print("New user: {s} {s}\n", .{ username, password });
358 if (try register_user(env, username, password)) {
359 try res.redirect("/login");
361 try res.redirect("/register");
364 } else if (std.mem.eql(u8, req.target, "/login")) {
365 // TODO: handle args not supplied
366 const username = req.get_value("username").?;
367 const password = req.get_value("password").?;
369 std.debug.print("New login: {s} {s}\n", .{ username, password });
370 if (login_user(env, username, password)) |session_token| {
371 res.status = .see_other;
374 .{ "/user/{s}", .{username} },
378 .{ "session_token={s}; Secure; HttpOnly", .{session_token} },
383 std.debug.print("login_user err: {}\n", .{err});
384 try res.redirect("/login");
387 } else if (std.mem.eql(u8, req.target, "/logout")) {
388 if (logged_in) |login| {
389 try logout_user(env, login.session_token);
393 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
396 try res.redirect("/");
399 } else if (std.mem.eql(u8, req.target, "/quit")) {
400 try res.redirect("/");
407 try res.write("<p>{s}</p>", .{req.target});