1 const std = @import("std");
2 const lmdb = @import("lmdb");
3 const db = @import("db");
4 const http = @import("http");
9 fn users(txn: lmdb.Txn) !db.Db(UserId, User) {
10 return try db.Db(UserId, User).init(txn, "users");
12 fn user_ids(txn: lmdb.Txn) !db.Db(Username, UserId) {
13 return try db.Db(Username, UserId).init(txn, "user_ids");
15 fn sessions(txn: lmdb.Txn) !db.Db(SessionToken, UserId) {
16 return try db.Db(SessionToken, UserId).init(txn, "sessions");
18 fn posts(txn: lmdb.Txn) !db.Db(PostId, Post) {
19 return try db.Db(PostId, Post).init(txn, "posts");
31 password_hash: PasswordHash,
51 const Kind = enum { Up, Down };
58 const Login = struct {
61 session_token: SessionToken,
63 const UserId = enum(u64) { _ };
64 const PostId = enum(u64) { _ };
65 const Timestamp = i64;
66 const Username = std.BoundedArray(u8, 16);
67 const PasswordHash = std.BoundedArray(u8, 128);
68 const SessionToken = u64;
69 const CookieValue = std.BoundedArray(u8, 128);
70 const PostText = std.BoundedArray(u8, 1024);
71 const PostList = db.SetList(PostId, void);
72 const UserList = db.SetList(UserId, User);
73 const VoteList = db.SetList(UserId, Vote);
75 const Chirp = struct {
76 pub fn hash_password(password: []const u8) !PasswordHash {
77 var hash_buffer = try PasswordHash.init(128);
79 // TODO: choose buffer size
80 // TODO: dont allocate on stack, maybe zero memory?
81 var buffer: [1024 * 10]u8 = undefined;
82 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
84 // TODO: choose limits
85 const result = try std.crypto.pwhash.argon2.strHash(password, .{
86 .allocator = alloc.allocator(),
87 .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
88 }, hash_buffer.slice());
90 try hash_buffer.resize(result.len);
95 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
96 var buffer: [1024 * 10]u8 = undefined;
97 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
99 if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
100 .allocator = alloc.allocator(),
104 std.debug.print("verify error: {}\n", .{err});
109 pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool {
110 const username_array = try Username.fromSlice(username);
112 const txn = try env.txn();
113 defer txn.commit() catch {};
115 const users = try Db.users(txn);
116 const user_ids = try Db.user_ids(txn);
118 if (try user_ids.has(username_array)) {
121 const user_id = try db.Prng.gen(users.dbi, UserId);
122 const posts = try Db.posts(txn);
124 try users.put(user_id, User{
126 .name = username_array,
127 .password_hash = try hash_password(password),
128 .posts = try PostList.init(posts.dbi),
131 try user_ids.put(username_array, user_id);
139 username: []const u8,
140 password: []const u8,
142 const username_array = try Username.fromSlice(username);
144 const txn = try env.txn();
145 defer txn.commit() catch {};
147 const user_ids = try Db.user_ids(txn);
148 const user_id = try user_ids.get(username_array);
149 std.debug.print("user logging in, id: {}\n", .{user_id});
151 const users = try Db.users(txn);
152 const user = try users.get(user_id);
154 if (verify_password(password, user.password_hash)) {
155 const sessions = try Db.sessions(txn);
156 const session_token = try db.Prng.gen(sessions.dbi, SessionToken);
157 try sessions.put(session_token, user_id);
158 return session_token;
160 return error.IncorrectPassword;
164 fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void {
165 const txn = try env.txn();
166 defer txn.commit() catch {};
168 const sessions = try Db.sessions(txn);
169 try sessions.del(session_token);
172 fn post(env: *lmdb.Env, user_id: UserId, text: []const u8) !void {
173 var post_id: PostId = undefined;
175 // TODO: do this in one commit
177 var txn: lmdb.Txn = undefined;
181 defer txn.commit() catch {};
183 const posts = try Db.posts(txn);
184 post_id = try db.Prng.gen(posts.dbi, PostId);
185 const votes = try txn.dbi("votes");
186 try posts.put(post_id, Post{
189 .time = std.time.timestamp(),
190 .votes = try VoteList.init(votes),
191 .comments = try PostList.init(posts.dbi),
192 .text = try PostText.fromSlice(text),
197 // append to user's posts
199 defer txn.commit() catch {};
201 const users = try Db.users(txn);
202 var user = try users.get(user_id);
204 const posts = try Db.posts(txn);
205 var posts_view = try user.posts.open(posts.dbi);
206 try posts_view.append(post_id, {});
210 fn vote(env: *lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void {
211 const txn = try env.txn();
212 defer txn.commit() catch {};
214 const posts = try Db.posts(txn);
215 const votes = try txn.dbi("votes");
217 var p = try posts.get(post_id);
218 var votes_view = try p.votes.open(votes);
220 if (try votes_view.has(user_id)) {
221 const old_vote = try votes_view.get(user_id);
223 if (old_vote.kind == kind) {
226 try votes_view.del(user_id);
228 if (old_vote.kind == .Up) {
233 try posts.put(post_id, p);
236 try votes_view.append(user_id, Vote{
238 .time = std.time.timestamp(),
246 try posts.put(post_id, p);
249 fn unvote(env: *lmdb.Env, post_id: PostId, user_id: UserId) !void {
250 const txn = try env.txn();
251 defer txn.commit() catch {};
253 const posts = try Db.posts(txn);
254 const votes = try txn.dbi("votes");
256 var p = try posts.get(post_id);
257 var votes_view = try p.votes.open(votes);
259 if (try votes_view.has(user_id)) {
260 const v = try votes_view.get(user_id);
267 try posts.put(post_id, p);
269 try votes_view.del(user_id);
273 fn get_session_user_id(env: *lmdb.Env, session_token: SessionToken) !UserId {
274 const txn = try env.txn();
277 const sessions = try Db.sessions(txn);
279 return try sessions.get(session_token);
282 fn get_user(env: *lmdb.Env, user_id: UserId) !User {
283 const txn = try env.txn();
286 const users = try Db.users(txn);
287 return try users.get(user_id);
294 fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void {
295 try res.write("<form style=\"display: inline-block!important;\" action=\"", .{});
296 try res.write(fmt_action, args_action);
297 try res.write("\" method=\"post\">", .{});
299 inline for (inputs) |input| {
300 switch (@typeInfo(@TypeOf(input))) {
302 try res.write("<input ", .{});
303 try res.write(input[0], input[1]);
304 try res.write(" />", .{});
307 try res.write("<input ", .{});
308 try res.write(input, .{});
309 try res.write(" />", .{});
314 try res.write("</form>", .{});
319 fn write_header(res: *http.Response, logged_in: ?Login) !void {
320 if (logged_in) |login| {
322 \\<a href="/user/{s}">Home</a><br />
323 , .{login.user.name.constSlice()});
324 try html_form(res, "/logout", .{}, .{
325 \\type="submit" value="Logout"
327 try html_form(res, "/quit", .{}, .{
328 \\type="submit" value="Quit"
330 try res.write("<br />", .{});
331 try html_form(res, "/post", .{}, .{
332 \\type="text" name="text"
334 \\type="submit" value="Post"
338 \\<a href="/">Home</a><br />
339 \\<a href="/register">Register</a>
340 \\<a href="/login">Login</a><br />
342 try html_form(res, "/quit", .{}, .{
343 \\type="submit" value="Quit"
347 fn write_posts(res: *http.Response, txn: lmdb.Txn, user: User, login: ?Login) !void {
348 const votes_dbi = try txn.dbi("votes");
349 const posts = try Db.posts(txn);
350 const posts_view = try user.posts.open(posts.dbi);
352 var it = posts_view.iterator();
353 while (it.next()) |kv| {
354 const post_id = kv.key;
355 const post = try posts.get(post_id);
360 , .{post.text.constSlice()});
362 const votes_view = try post.votes.open(votes_dbi);
363 const comments_view = try post.comments.open(posts.dbi);
365 var has_voted: ?Vote.Kind = null;
367 if (login != null and try votes_view.has(login.?.user_id)) {
368 const vote = try votes_view.get(login.?.user_id);
370 has_voted = vote.kind;
373 if (has_voted != null and has_voted.? == .Up) {
374 try html_form(res, "/unupvote/{}", .{@intFromEnum(post_id)}, .{
375 .{ "type=\"submit\" value=\"⬆ {}\"", .{post.upvotes} },
378 try html_form(res, "/upvote/{}", .{@intFromEnum(post_id)}, .{
379 .{ "type=\"submit\" value=\"⬆ {}\"", .{post.upvotes} },
382 if (has_voted != null and has_voted.? == .Down) {
383 try html_form(res, "/undownvote/{}", .{@intFromEnum(post_id)}, .{
384 .{ "type=\"submit\" value=\"⬇ {}\"", .{post.downvotes} },
387 try html_form(res, "/downvote/{}", .{@intFromEnum(post_id)}, .{
388 .{ "type=\"submit\" value=\"⬇ {}\"", .{post.downvotes} },
392 \\<span>💭 {}</span>
394 , .{comments_view.len()});
399 fn list_users(env: lmdb.Env) !void {
400 const txn = try env.txn();
403 const users = try Db.users(txn);
404 var it = try users.iterator();
406 while (it.next()) |kv| {
409 std.debug.print("[{}] {s}\n", .{ key, user.name.constSlice() });
412 fn list_user_ids(env: lmdb.Env) !void {
413 const txn = try env.txn();
416 const user_ids = try Db.user_ids(txn);
417 var it = try user_ids.iterator();
419 while (it.next()) |kv| {
421 const user_id = kv.val;
422 std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
426 fn list_sessions(env: lmdb.Env) !void {
427 const txn = try env.txn();
430 const sessions = try Db.sessions(txn);
431 var it = try sessions.iterator();
433 while (it.next()) |kv| {
435 const user_id = kv.val;
436 std.debug.print("[{x}] {}\n", .{ key, user_id });
440 fn list_posts(env: lmdb.Env) !void {
441 const txn = try env.txn();
444 const posts = try Db.posts(txn);
445 var it = try posts.iterator();
447 while (it.next()) |kv| {
450 std.debug.print("[{}] {s}\n", .{ key, post.text.constSlice() });
454 const ReqBufferSize = 4096;
455 const ResHeadBufferSize = 1024 * 16;
456 const ResBodyBufferSize = 1024 * 16;
458 pub fn main() !void {
460 var server = try http.Server.init("::", 8080);
461 defer server.deinit();
464 var env = try lmdb.Env.open("db", 1024 * 1024 * 10);
467 // std.debug.print("Users:\n", .{});
468 // try list_users(env);
469 // std.debug.print("User IDs:\n", .{});
470 // try list_user_ids(env);
471 // std.debug.print("Sessions:\n", .{});
472 // try list_sessions(env);
473 // std.debug.print("Posts:\n", .{});
474 // try list_posts(env);
476 try handle_connection(&server, &env);
477 // const ThreadCount = 1;
478 // var ts: [ThreadCount]std.Thread = undefined;
480 // for (0..ThreadCount) |i| {
481 // ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
483 // for (0..ThreadCount) |i| {
487 std.debug.print("done\n", .{});
490 fn handle_connection(server: *http.Server, env: *lmdb.Env) !void {
492 var req_buffer: [ReqBufferSize]u8 = undefined;
493 var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
494 var res_body_buffer: [ResBodyBufferSize]u8 = undefined;
496 accept: while (true) {
499 while (try server.next_request(&req_buffer)) |req| {
500 // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
503 var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer);
505 // check session token
506 var logged_in: ?Login = null;
508 if (req.get_cookie("session_token")) |session_token_str| {
509 const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 16);
510 // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
511 // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
512 if (Chirp.get_session_user_id(env, session_token)) |user_id| {
513 const txn = try env.txn();
515 const users = try Db.users(txn);
518 .user = try users.get(user_id),
520 .session_token = session_token,
523 std.debug.print("get_session_user err: {}\n", .{err});
527 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
533 if (req.method == .GET) {
534 try write_header(&res, logged_in);
536 if (std.mem.eql(u8, req.target, "/register")) {
538 \\<form action="/register" method="post">
539 \\<input type="text" name="username" />
540 \\<input type="password" name="password" />
541 \\<input type="submit" value="Register" />
545 } else if (std.mem.eql(u8, req.target, "/login")) {
547 \\<form action="/login" method="post">
548 \\<input type="text" name="username" />
549 \\<input type="password" name="password" />
550 \\<input type="submit" value="Login" />
554 } else if (std.mem.startsWith(u8, req.target, "/user/")) {
555 const username = req.target[6..req.target.len];
557 const txn = try env.txn();
560 const user_ids = try Db.user_ids(txn);
561 if (user_ids.get(try Username.fromSlice(username))) |user_id| {
562 const users = try Db.users(txn);
563 const user = try users.get(user_id);
564 try write_posts(&res, txn, user, logged_in);
567 \\<p>User not found [{}]</p>
572 if (logged_in) |login| {
573 const user = try Chirp.get_user(env, login.user_id);
575 const txn = try env.txn();
578 try write_posts(&res, txn, user, logged_in);
582 try res.write("[GET] {s}", .{req.target});
589 if (std.mem.eql(u8, req.target, "/register")) {
590 // TODO: handle args not supplied
591 const username = req.get_value("username").?;
592 const password = req.get_value("password").?;
594 std.debug.print("New user: {s} {s}\n", .{ username, password });
595 if (try Chirp.register_user(env, username, password)) {
596 try res.redirect("/login");
598 try res.redirect("/register");
601 } else if (std.mem.eql(u8, req.target, "/login")) {
602 // TODO: handle args not supplied
603 const username = req.get_value("username").?;
604 const password = req.get_value("password").?;
606 std.debug.print("New login: {s} {s}\n", .{ username, password });
607 if (Chirp.login_user(env, username, password)) |session_token| {
608 res.status = .see_other;
611 .{ "/user/{s}", .{username} },
615 .{ "session_token={x}; Secure; HttpOnly", .{session_token} },
620 std.debug.print("login_user err: {}\n", .{err});
621 try res.redirect("/login");
624 } else if (std.mem.eql(u8, req.target, "/logout")) {
625 if (logged_in) |login| {
626 try Chirp.logout_user(env, login.session_token);
630 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
633 try res.redirect("/");
636 } else if (std.mem.eql(u8, req.target, "/post")) {
637 if (logged_in) |login| {
638 const text = req.get_value("text").?;
639 try Chirp.post(env, login.user_id, text);
641 try res.redirect("/");
644 } else if (std.mem.eql(u8, req.target, "/quit")) {
645 try res.redirect("/");
648 } else if (std.mem.startsWith(u8, req.target, "/upvote/")) {
649 const login = logged_in orelse return error.NotLoggedIn;
651 const post_id_str = req.target[8..req.target.len];
652 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
654 try Chirp.vote(env, post_id, login.user_id, .Up);
656 if (req.get_header("Referer")) |ref| {
657 try res.redirect(ref);
660 } else if (std.mem.startsWith(u8, req.target, "/downvote/")) {
661 const login = logged_in orelse return error.NotLoggedIn;
663 const post_id_str = req.target[10..req.target.len];
664 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
666 try Chirp.vote(env, post_id, login.user_id, .Down);
668 if (req.get_header("Referer")) |ref| {
669 try res.redirect(ref);
672 } else if (std.mem.startsWith(u8, req.target, "/unupvote/")) {
673 // TODO: maybe move to one /unvote?
674 const login = logged_in orelse return error.NotLoggedIn;
676 const post_id_str = req.target[10..req.target.len];
677 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
679 try Chirp.unvote(env, post_id, login.user_id);
681 if (req.get_header("Referer")) |ref| {
682 try res.redirect(ref);
685 } else if (std.mem.startsWith(u8, req.target, "/undownvote/")) {
686 const login = logged_in orelse return error.NotLoggedIn;
688 const post_id_str = req.target[12..req.target.len];
689 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
691 try Chirp.unvote(env, post_id, login.user_id);
693 if (req.get_header("Referer")) |ref| {
694 try res.redirect(ref);
701 try res.write("<p>[POST] {s}</p>", .{req.target});