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 fn IdType(comptime T: type) type {
11 const ti = @typeInfo(@TypeOf(T.get));
12 return ti.Fn.params[1].type.?;
15 pub fn gen_id(dbi: anytype) IdType(@TypeOf(dbi)) {
16 var id: IdType(@TypeOf(dbi)) = @enumFromInt(Prng.prng.next());
19 id = @enumFromInt(Prng.prng.next());
25 pub fn gen_str(dbi: anytype, comptime len: usize) [len]u8 {
26 var buf: [len / 2]u8 = undefined;
27 var res: [len]u8 = undefined;
29 for (0..len / 2) |i| {
30 res[i * 2 + 0] = 'a' + (buf[i] % 16);
31 res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16);
34 while (dbi.has(res)) {
36 for (0..len / 2) |i| {
37 res[i * 2 + 0] = 'a' + (buf[i] % 16);
38 res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16);
47 fn Key(types: anytype) type {
48 return std.meta.Tuple(&types);
50 fn users(txn: *const lmdb.Txn) !lmdb.Dbi(UserId, User) {
51 return try txn.dbi("users", UserId, User);
53 fn user_ids(txn: *const lmdb.Txn) !lmdb.Dbi(Username, UserId) {
54 return try txn.dbi("user_ids", Username, UserId);
56 fn sessions(txn: *const lmdb.Txn) !lmdb.Dbi(SessionToken, UserId) {
57 return try txn.dbi("sessions", SessionToken, UserId);
59 fn posts(txn: *const lmdb.Txn) !lmdb.Dbi(PostId, Post) {
60 return try txn.dbi("posts", PostId, Post);
62 fn lists(txn: *const lmdb.Txn, comptime T: type) !lmdb.Dbi(ListId, ListNode(T)) {
63 return try txn.dbi("lists", ListId, ListNode(T));
65 fn sets(txn: *const lmdb.Txn, comptime T: type) !lmdb.Dbi(Key(.{ SetId, T }), u0) {
66 return try txn.dbi("sets", Key(.{ SetId, T }), u0);
69 const ListId = enum(u64) { _ };
71 pub fn ListNode(comptime T: type) type {
79 pub fn List(comptime T: type) type {
83 first: ListId = undefined,
84 last: ListId = undefined,
87 pub const Iterator = struct {
88 dbi: lmdb.Dbi(ListId, ListNode(T)),
91 pub fn next(self: *Iterator) ?T {
92 const id = self.id_maybe orelse return null;
94 // TODO: how to handle this?
95 const ln = self.dbi.get(id) orelse return null;
96 self.id_maybe = ln.next;
100 pub fn it(self: *const Self, txn: *const lmdb.Txn) !Iterator {
101 const list = try Db.lists(txn, T);
102 return .{ .dbi = list, .id_maybe = if (self.len > 0) self.first else null };
104 pub fn append(self: *Self, txn: *const lmdb.Txn, t: T) !void {
105 const list = try Db.lists(txn, T);
106 const new_id = Prng.gen_id(list);
118 const prev_id = self.last;
119 var last = list.get(prev_id).?;
121 list.put(prev_id, last);
136 const SetId = enum(u64) { _ };
138 pub fn Set(comptime T: type) type {
140 const Self = @This();
141 const SetKey = Key(.{ SetId, T });
144 set_id: ?SetId = null,
146 pub fn init(self: *Self) void {
147 self.set_id = @enumFromInt(Prng.prng.next());
150 pub fn has(self: Self, txn: *const lmdb.Txn, t: T) !bool {
151 if (self.set_id == null) return false;
153 const set = try Db.sets(txn, T);
154 const key = SetKey{ self.set_id.?, t };
159 pub fn add(self: *Self, txn: *const lmdb.Txn, t: T) !void {
160 if (self.set_id == null) self.init();
162 const set = try Db.sets(txn, T);
163 const key = SetKey{ self.set_id.?, t };
173 pub fn del(self: *Self, txn: *const lmdb.Txn, t: T) !void {
174 if (self.set_id == null) self.init();
176 const set = try Db.sets(txn, T);
177 const key = SetKey{ self.set_id.?, t };
194 const User = struct {
195 // TODO: choose sizes
198 password_hash: PasswordHash,
199 posts: PostList = PostList{},
202 const Post = struct {
208 upvotes: UserList = UserList{},
209 downvotes: UserList = UserList{},
210 comments: PostList = PostList{},
216 const SessionTokenLen = 16;
219 const Login = struct {
222 session_token: SessionToken,
224 const UserId = enum(u64) { _ };
225 const PostId = enum(u64) { _ };
226 const Timestamp = i64;
227 const Username = std.BoundedArray(u8, 16);
228 const PasswordHash = std.BoundedArray(u8, 128);
229 const SessionToken = [SessionTokenLen]u8;
230 const CookieValue = std.BoundedArray(u8, 128);
231 const PostText = std.BoundedArray(u8, 1024);
232 const PostList = Db.List(PostId);
233 const UserList = Db.Set(UserId);
235 pub fn hash_password(password: []const u8) !PasswordHash {
236 var hash_buffer = try PasswordHash.init(128);
238 // TODO: choose buffer size
239 // TODO: dont allocate on stack, maybe zero memory?
240 var buffer: [1024 * 10]u8 = undefined;
241 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
243 // TODO: choose limits
244 const result = try std.crypto.pwhash.argon2.strHash(password, .{
245 .allocator = alloc.allocator(),
246 .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
247 }, hash_buffer.slice());
249 try hash_buffer.resize(result.len);
254 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
255 var buffer: [1024 * 10]u8 = undefined;
256 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
258 if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
259 .allocator = alloc.allocator(),
263 std.debug.print("verify error: {}\n", .{err});
268 pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool {
269 const username_array = try Username.fromSlice(username);
271 const txn = try env.txn();
277 const users = try Db.users(&txn);
278 const user_ids = try Db.user_ids(&txn);
280 if (user_ids.has(username_array)) {
283 const user_id = Prng.gen_id(users);
284 users.put(user_id, User{
286 .username = username_array,
287 .password_hash = try hash_password(password),
290 user_ids.put(username_array, user_id);
296 pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) !SessionToken {
297 const username_array = try Username.fromSlice(username);
299 const txn = try env.txn();
305 const user_ids = try Db.user_ids(&txn);
306 const user_id = user_ids.get(username_array) orelse return error.UnknownUsername;
307 std.debug.print("id: {}\n", .{user_id});
309 const users = try Db.users(&txn);
310 if (users.get(user_id)) |user| {
311 if (verify_password(password, user.password_hash)) {
312 const sessions = try Db.sessions(&txn);
313 const session_token = Prng.gen_str(sessions, SessionTokenLen);
314 sessions.put(session_token, user_id);
315 return session_token;
317 return error.IncorrectPassword;
320 return error.UserNotFound;
324 fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void {
325 const txn = try env.txn();
331 const sessions = try Db.sessions(&txn);
332 sessions.del(session_token);
335 fn post(env: *lmdb.Env, user_id: UserId, text: []const u8) !void {
336 var post_id: PostId = undefined;
338 var txn = try env.txn();
340 const posts = try Db.posts(&txn);
341 post_id = Prng.gen_id(posts);
342 posts.put(post_id, Post{
345 .time = std.time.timestamp(),
346 .text = try PostText.fromSlice(text),
355 const users = try Db.users(&txn);
356 var user = users.get(user_id) orelse return error.UserNotFound;
357 try user.posts.append(&txn, post_id);
358 users.put(user_id, user);
365 fn vote(env: *lmdb.Env, post_id: PostId, user_id: UserId, dir: enum { Up, Down }) !void {
366 const txn = try env.txn();
372 const posts = try Db.posts(&txn);
374 var p = posts.get(post_id) orelse return error.PostNotFound;
376 try p.upvotes.add(&txn, user_id);
378 try p.downvotes.add(&txn, user_id);
380 posts.put(post_id, p);
383 fn unvote(env: *lmdb.Env, post_id: PostId, user_id: UserId, dir: enum { Up, Down }) !void {
384 const txn = try env.txn();
390 const posts = try Db.posts(&txn);
392 var p = posts.get(post_id) orelse return error.PostNotFound;
394 try p.upvotes.del(&txn, user_id);
396 try p.downvotes.del(&txn, user_id);
398 posts.put(post_id, p);
401 fn get_session_user_id(env: *lmdb.Env, session_token: SessionToken) !UserId {
402 const txn = try env.txn();
405 const sessions = try Db.sessions(&txn);
407 if (sessions.get(session_token)) |user_id| {
410 return error.SessionNotFound;
414 fn get_user(env: *lmdb.Env, user_id: UserId) !?User {
415 const txn = try env.txn();
418 const users = try Db.users(&txn);
419 return users.get(user_id);
425 fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void {
426 try res.write("<form style=\"display: inline-block!important;\" action=\"", .{});
427 try res.write(fmt_action, args_action);
428 try res.write("\" method=\"post\">", .{});
430 inline for (inputs) |input| {
431 switch (@typeInfo(@TypeOf(input))) {
433 try res.write("<input ", .{});
434 try res.write(input[0], input[1]);
435 try res.write(" />", .{});
438 try res.write("<input ", .{});
439 try res.write(input, .{});
440 try res.write(" />", .{});
445 try res.write("</form>", .{});
450 fn write_header(res: *http.Response, logged_in: ?Login) !void {
451 if (logged_in) |login| {
453 \\<a href="/user/{s}">Home</a><br />
454 , .{login.user.username.constSlice()});
455 try html_form(res, "/logout", .{}, .{
456 \\type="submit" value="Logout"
458 try html_form(res, "/quit", .{}, .{
459 \\type="submit" value="Quit"
461 try res.write("<br />", .{});
462 try html_form(res, "/post", .{}, .{
463 \\type="text" name="text"
465 \\type="submit" value="Post"
469 \\<a href="/">Home</a><br />
470 \\<a href="/register">Register</a>
471 \\<a href="/login">Login</a><br />
473 try html_form(res, "/quit", .{}, .{
474 \\type="submit" value="Quit"
478 fn write_posts(res: *http.Response, txn: *const lmdb.Txn, user: User, login: ?Login) !void {
479 const posts = try Db.posts(txn);
480 var it = try user.posts.it(txn);
481 while (it.next()) |post_id| {
482 const p = posts.get(post_id) orelse break;
486 , .{p.text.constSlice()});
487 if (login != null and try p.upvotes.has(txn, login.?.user_id)) {
488 try html_form(res, "/unupvote/{}", .{@intFromEnum(post_id)}, .{
489 .{ "type=\"submit\" value=\"⬆ {}\"", .{p.upvotes.len} },
492 try html_form(res, "/upvote/{}", .{@intFromEnum(post_id)}, .{
493 .{ "type=\"submit\" value=\"⬆ {}\"", .{p.upvotes.len} },
496 if (login != null and try p.downvotes.has(txn, login.?.user_id)) {
497 try html_form(res, "/undownvote/{}", .{@intFromEnum(post_id)}, .{
498 .{ "type=\"submit\" value=\"⬇ {}\"", .{p.downvotes.len} },
501 try html_form(res, "/downvote/{}", .{@intFromEnum(post_id)}, .{
502 .{ "type=\"submit\" value=\"⬇ {}\"", .{p.downvotes.len} },
506 \\<span>💭 {}</span>
508 , .{p.comments.len});
513 fn list_users(env: lmdb.Env) !void {
514 const txn = try env.txn();
517 // const users = try Db.users(&txn);
518 const users = try txn.dbi("users", UserId, User);
519 var cursor = try users.cursor();
521 var key: UserId = undefined;
522 var user_maybe = cursor.get(&key, .First);
524 while (user_maybe) |*user| {
525 std.debug.print("[{}] {s}\n", .{ key, user.username.constSlice() });
527 user_maybe = cursor.get(&key, .Next);
530 fn list_user_ids(env: lmdb.Env) !void {
531 const txn = try env.txn();
534 const user_ids = try Db.user_ids(&txn);
535 var cursor = try user_ids.cursor();
537 var key: Username = undefined;
538 var user_id_maybe = cursor.get(&key, .First);
540 while (user_id_maybe) |user_id| {
541 std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
543 user_id_maybe = cursor.get(&key, .Next);
547 fn list_sessions(env: lmdb.Env) !void {
548 const txn = try env.txn();
551 const sessions = try Db.sessions(&txn);
552 var cursor = try sessions.cursor();
554 var key: SessionToken = undefined;
555 var user_id_maybe = cursor.get(&key, .First);
557 while (user_id_maybe) |user_id| {
558 std.debug.print("[{s}] {}\n", .{ key, user_id });
560 user_id_maybe = cursor.get(&key, .Next);
564 fn list_posts(env: lmdb.Env) !void {
565 const txn = try env.txn();
568 const posts = try Db.posts(&txn);
569 var cursor = try posts.cursor();
571 var key: PostId = undefined;
572 var post_maybe = cursor.get(&key, .First);
574 while (post_maybe) |p| {
575 std.debug.print("[{}] {s}\n", .{ key, p.text.constSlice() });
577 post_maybe = cursor.get(&key, .Next);
581 const ReqBufferSize = 4096;
582 const ResHeadBufferSize = 1024 * 16;
583 const ResBodyBufferSize = 1024 * 16;
585 pub fn main() !void {
587 var server = try http.Server.init("::", 8080);
588 defer server.deinit();
591 var env = try lmdb.Env.open("db", 1024 * 1024 * 10);
594 std.debug.print("Users:\n", .{});
596 std.debug.print("User IDs:\n", .{});
597 try list_user_ids(env);
598 std.debug.print("Sessions:\n", .{});
599 try list_sessions(env);
600 std.debug.print("Posts:\n", .{});
603 try handle_connection(&server, &env);
604 // const ThreadCount = 1;
605 // var ts: [ThreadCount]std.Thread = undefined;
607 // for (0..ThreadCount) |i| {
608 // ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
610 // for (0..ThreadCount) |i| {
614 std.debug.print("done\n", .{});
617 fn handle_connection(server: *http.Server, env: *lmdb.Env) !void {
619 var req_buffer: [ReqBufferSize]u8 = undefined;
620 var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
621 var res_body_buffer: [ResBodyBufferSize]u8 = undefined;
623 accept: while (true) {
626 while (try server.next_request(&req_buffer)) |req| {
627 // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
630 var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer);
632 // check session token
633 var logged_in: ?Login = null;
635 if (req.get_cookie("session_token")) |session_token_str| {
636 var session_token: SessionToken = undefined;
637 std.mem.copyForwards(u8, &session_token, session_token_str);
638 // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
639 // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
640 if (get_session_user_id(env, session_token)) |user_id| {
641 const txn = try env.txn();
643 const users = try Db.users(&txn);
646 .user = users.get(user_id) orelse return error.UserNotFound,
648 .session_token = session_token,
651 std.debug.print("get_session_user err: {}\n", .{err});
655 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
661 if (req.method == .GET) {
662 try write_header(&res, logged_in);
664 if (std.mem.eql(u8, req.target, "/register")) {
666 \\<form action="/register" method="post">
667 \\<input type="text" name="username" />
668 \\<input type="password" name="password" />
669 \\<input type="submit" value="Register" />
673 } else if (std.mem.eql(u8, req.target, "/login")) {
675 \\<form action="/login" method="post">
676 \\<input type="text" name="username" />
677 \\<input type="password" name="password" />
678 \\<input type="submit" value="Login" />
682 } else if (std.mem.startsWith(u8, req.target, "/user/")) {
683 const username = req.target[6..req.target.len];
685 const txn = try env.txn();
688 const user_ids = try Db.user_ids(&txn);
689 if (user_ids.get(try Username.fromSlice(username))) |user_id| {
690 const users = try Db.users(&txn);
691 const user = users.get(user_id).?;
692 try write_posts(&res, &txn, user, logged_in);
695 \\<p>User not found</pvo>
700 if (logged_in) |login| {
701 const user = (try get_user(env, login.user_id)).?;
703 const txn = try env.txn();
706 try write_posts(&res, &txn, user, logged_in);
710 try res.write("[GET] {s}", .{req.target});
717 if (std.mem.eql(u8, req.target, "/register")) {
718 // TODO: handle args not supplied
719 const username = req.get_value("username").?;
720 const password = req.get_value("password").?;
722 std.debug.print("New user: {s} {s}\n", .{ username, password });
723 if (try register_user(env, username, password)) {
724 try res.redirect("/login");
726 try res.redirect("/register");
729 } else if (std.mem.eql(u8, req.target, "/login")) {
730 // TODO: handle args not supplied
731 const username = req.get_value("username").?;
732 const password = req.get_value("password").?;
734 std.debug.print("New login: {s} {s}\n", .{ username, password });
735 if (login_user(env, username, password)) |session_token| {
736 res.status = .see_other;
739 .{ "/user/{s}", .{username} },
743 .{ "session_token={s}; Secure; HttpOnly", .{session_token} },
748 std.debug.print("login_user err: {}\n", .{err});
749 try res.redirect("/login");
752 } else if (std.mem.eql(u8, req.target, "/logout")) {
753 if (logged_in) |login| {
754 try logout_user(env, login.session_token);
758 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
761 try res.redirect("/");
764 } else if (std.mem.eql(u8, req.target, "/post")) {
765 if (logged_in) |login| {
766 const text = req.get_value("text").?;
767 try post(env, login.user_id, text);
769 try res.redirect("/");
772 } else if (std.mem.eql(u8, req.target, "/quit")) {
773 try res.redirect("/");
776 } else if (std.mem.startsWith(u8, req.target, "/upvote/")) {
777 const login = logged_in orelse return error.NotLoggedIn;
779 const post_id_str = req.target[8..req.target.len];
780 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
782 try vote(env, post_id, login.user_id, .Up);
783 try unvote(env, post_id, login.user_id, .Down);
785 if (req.get_header("Referer")) |ref| {
786 try res.redirect(ref);
789 } else if (std.mem.startsWith(u8, req.target, "/downvote/")) {
790 const login = logged_in orelse return error.NotLoggedIn;
792 const post_id_str = req.target[10..req.target.len];
793 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
795 try vote(env, post_id, login.user_id, .Down);
796 try unvote(env, post_id, login.user_id, .Up);
798 if (req.get_header("Referer")) |ref| {
799 try res.redirect(ref);
802 } else if (std.mem.startsWith(u8, req.target, "/unupvote/")) {
803 const login = logged_in orelse return error.NotLoggedIn;
805 const post_id_str = req.target[10..req.target.len];
806 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
808 try unvote(env, post_id, login.user_id, .Up);
810 if (req.get_header("Referer")) |ref| {
811 try res.redirect(ref);
814 } else if (std.mem.startsWith(u8, req.target, "/undownvote/")) {
815 const login = logged_in orelse return error.NotLoggedIn;
817 const post_id_str = req.target[12..req.target.len];
818 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
820 try unvote(env, post_id, login.user_id, .Down);
822 if (req.get_header("Referer")) |ref| {
823 try res.redirect(ref);
830 try res.write("<p>[POST] {s}</p>", .{req.target});