1 const std = @import("std");
2 const lmdb = @import("lmdb");
9 env: ?*lmdb.MDB_env = undefined,
10 txn: ?*lmdb.MDB_txn = undefined,
11 dbi: lmdb.MDB_dbi = undefined,
12 prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0),
14 pub fn gen_id(self: *Self) Id {
15 var id = self.prng.next();
17 while (self.has(id)) {
18 id = self.prng.next();
24 pub fn open(self: *Self, name: [*c]const u8) void {
25 _ = lmdb.mdb_env_create(&self.env);
26 _ = lmdb.mdb_env_set_maxdbs(self.env, 10);
27 _ = lmdb.mdb_env_set_mapsize(self.env, 1024 * 1024 * 1);
28 _ = lmdb.mdb_env_open(self.env, name, lmdb.MDB_WRITEMAP, 0o664);
29 // _ = lmdb.mdb_env_open(self.env, name, lmdb.MDB_NOSYNC | lmdb.MDB_WRITEMAP, 0o664);
32 pub fn close(self: *Self) void {
33 lmdb.mdb_env_close(self.env);
36 pub fn begin(self: *Self, name: [*c]const u8) void {
37 switch (lmdb.mdb_txn_begin(self.env, null, 0, &self.txn)) {
40 std.debug.print("txn err: {}\n", .{err});
44 // TODO: lmdb.MDB_INTEGERKEY?
45 _ = lmdb.mdb_dbi_open(self.txn, name, lmdb.MDB_CREATE, &self.dbi);
48 pub fn commit(self: *Self) void {
49 switch (lmdb.mdb_txn_commit(self.txn)) {
51 lmdb.MDB_MAP_FULL => {
52 std.debug.print("resize\n", .{});
53 _ = lmdb.mdb_env_set_mapsize(self.env, 0);
56 std.debug.print("commit err: {}\n", .{err});
61 lmdb.mdb_dbi_close(self.env, self.dbi);
64 pub fn sync(self: *Self) void {
65 switch (lmdb.mdb_env_sync(self.env, 1)) {
68 std.debug.print("sync err: {}\n", .{err});
73 pub fn put(self: *Self, key: anytype, value: anytype) void {
74 lmdb.put(self.txn, self.dbi, key, value);
77 pub fn get(self: *Self, key: anytype, comptime T: type) ?T {
78 return lmdb.get(self.txn, self.dbi, key, T);
81 pub fn del(self: *Self, key: anytype) void {
82 lmdb.del(self.txn, self.dbi, key);
85 pub fn has(self: *Self, key: anytype) bool {
86 return lmdb.has(self.txn, self.dbi, key);
94 pub fn redirect(req: *std.http.Server.Request, location: []const u8) !void {
95 try req.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "Location", .value = location }} });
98 pub fn get_body(req: *std.http.Server.Request) []const u8 {
99 return req.server.read_buffer[req.head_end .. req.head_end + (req.head.content_length orelse 0)];
102 pub fn get_value(req: *std.http.Server.Request, name: []const u8) ?[]const u8 {
103 const body = get_body(req);
104 if (std.mem.indexOf(u8, body, name)) |name_index| {
105 if (std.mem.indexOfScalarPos(u8, body, name_index, '=')) |eql_index| {
106 if (std.mem.indexOfScalarPos(u8, body, name_index, '&')) |amp_index| {
107 return body[eql_index + 1 .. amp_index];
110 return body[eql_index + 1 .. body.len];
116 pub fn get_cookie(req: *std.http.Server.Request, name: []const u8) ?CookieValue {
117 var header_it = req.iterateHeaders();
118 while (header_it.next()) |header| {
119 if (std.mem.eql(u8, header.name, "Cookie")) {
120 if (std.mem.indexOf(u8, header.value, name)) |name_index| {
121 if (std.mem.indexOfScalarPos(u8, header.value, name_index, '=')) |eql_index| {
122 if (std.mem.indexOfPos(u8, header.value, name_index, "; ")) |semi_index| {
123 return CookieValue.fromSlice(header.value[eql_index + 1 .. semi_index]) catch null;
126 return CookieValue.fromSlice(header.value[eql_index + 1 .. header.value.len]) catch null;
138 const User = struct {
139 // TODO: choose sizes
141 password_hash: PasswordHash,
145 const Username = std.BoundedArray(u8, 16);
146 const PasswordHash = std.BoundedArray(u8, 128);
147 const SessionToken = u64;
148 const CookieValue = std.BoundedArray(u8, 128);
150 pub fn hash_password(password: []const u8) !PasswordHash {
151 var hash_buffer = try PasswordHash.init(128);
153 // TODO: choose buffer size
154 // TODO: dont allocate on stack, maybe zero memory?
155 var buffer: [1024 * 10]u8 = undefined;
156 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
158 // TODO: choose limits
159 const result = try std.crypto.pwhash.argon2.strHash(password, .{
160 .allocator = alloc.allocator(),
161 .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
162 }, hash_buffer.slice());
164 try hash_buffer.resize(result.len);
169 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
170 var buffer: [1024 * 10]u8 = undefined;
171 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
173 if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
174 .allocator = alloc.allocator(),
178 std.debug.print("verify error: {}\n", .{err});
183 pub fn register_user(db: *Db, username: []const u8, password: []const u8) !void {
184 const username_array = try Username.fromSlice(username);
187 const user_id = db.gen_id();
188 db.put(user_id, User{
189 .username = username_array,
190 .password_hash = try hash_password(password),
195 db.put(username_array.buffer, user_id);
198 std.debug.print("id: {}\n", .{user_id});
203 pub fn login_user(db: *Db, username: []const u8, password: []const u8) ?SessionToken {
204 const username_array = Username.fromSlice(username) catch return null;
207 const user_id = db.get(username_array.buffer, Id) orelse return null;
208 std.debug.print("id: {}\n", .{user_id});
209 // TODO: maybe no commit?
213 const user = db.get(user_id, User) orelse return null;
216 if (verify_password(password, user.password_hash)) {
217 db.begin("sessions");
218 const session_token = db.gen_id();
219 db.put(session_token, user_id);
224 return session_token;
230 fn logout_user(db: *Db, session_token: SessionToken) void {
231 db.begin("sessions");
232 db.del(session_token);
236 fn get_session_user(db: *Db, session_token: SessionToken) ?User {
237 db.begin("sessions");
238 const user_id = db.get(session_token, Id) orelse return null;
242 const user = db.get(user_id, User) orelse return null;
250 fn list_users(db: *Db) void {
251 _ = lmdb.mdb_txn_begin(db.env, null, 0, &db.txn);
253 _ = lmdb.mdb_dbi_open(db.txn, "users", lmdb.MDB_CREATE, &db.dbi);
255 var cursor: ?*lmdb.MDB_cursor = undefined;
256 _ = lmdb.mdb_cursor_open(db.txn, db.dbi, &cursor);
258 var key: lmdb.MDB_val = undefined;
259 var val: lmdb.MDB_val = undefined;
260 var result = lmdb.mdb_cursor_get(cursor, &key, &val, lmdb.MDB_FIRST);
262 while (result != lmdb.MDB_NOTFOUND) {
263 const user_id = @as(*align(1) Id, @ptrCast(key.mv_data.?)).*;
264 const user = @as(*align(1) User, @ptrCast(val.mv_data.?)).*;
266 std.debug.print("[{}] {s}\n", .{ user_id, user.username.constSlice() });
268 result = lmdb.mdb_cursor_get(cursor, &key, &val, lmdb.MDB_NEXT);
271 _ = lmdb.mdb_cursor_close(cursor);
273 _ = lmdb.mdb_dbi_close(db.env, db.dbi);
275 _ = lmdb.mdb_txn_commit(db.txn);
278 pub fn main() !void {
280 const address = try std.net.Address.resolveIp("::", 8080);
282 var server = try address.listen(.{
283 .reuse_address = true,
285 defer server.deinit();
294 accept: while (true) {
295 const conn = try server.accept();
297 std.debug.print("new connection: {}\n", .{conn});
299 var read_buffer: [1024]u8 = undefined;
300 var http_server = std.http.Server.init(conn, &read_buffer);
302 while (http_server.state == .ready) {
303 var req = http_server.receiveHead() catch continue;
305 std.debug.print("[{}]: {s}\n", .{ req.head.method, req.head.target });
307 var logged_in: ?struct {
309 session_token: SessionToken,
312 if (get_cookie(&req, "session_token")) |session_token_str| {
313 const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str.constSlice(), 10);
314 if (get_session_user(&db, session_token)) |user| {
317 .session_token = session_token,
320 // TODO: delete session token
321 // TODO: add changeable headers (set, delete cookies)
325 if (req.head.method == .GET) {
326 if (std.mem.eql(u8, req.head.target, "/register")) {
328 \\<form action="/register" method="post">
329 \\<input type="text" name="username" />
330 \\<input type="password" name="password" />
331 \\<input type="submit" value="Register" />
334 } else if (std.mem.eql(u8, req.head.target, "/login")) {
336 \\<form action="/login" method="post">
337 \\<input type="text" name="username" />
338 \\<input type="password" name="password" />
339 \\<input type="submit" value="Login" />
343 if (logged_in) |login| {
344 var response_buffer = try std.BoundedArray(u8, 1024).init(0);
345 try std.fmt.format(response_buffer.writer(),
346 \\<a href="/user/{s}">Home</a>
347 \\<form action="/logout" method="post"><input type="submit" value="Logout" /></form>
348 \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
349 , .{login.user.username.constSlice()});
350 try req.respond(response_buffer.constSlice(), .{});
353 \\<a href="/register">Register</a>
354 \\<a href="/login">Login</a>
355 \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
362 if (std.mem.eql(u8, req.head.target, "/register")) {
363 // TODO: handle args not supplied
364 const username = get_value(&req, "username").?;
365 const password = get_value(&req, "password").?;
367 std.debug.print("New user: {s} {s}\n", .{ username, password });
368 try register_user(&db, username, password);
370 try redirect(&req, "/login");
371 } else if (std.mem.eql(u8, req.head.target, "/login")) {
372 // TODO: handle args not supplied
373 const username = get_value(&req, "username").?;
374 const password = get_value(&req, "password").?;
376 std.debug.print("New login: {s} {s}\n", .{ username, password });
377 if (login_user(&db, username, password)) |session_token| {
378 var redirect_buffer = try std.BoundedArray(u8, 128).init(0);
379 try std.fmt.format(redirect_buffer.writer(), "/user/{s}", .{username});
381 var cookie_buffer = try std.BoundedArray(u8, 128).init(0);
382 try std.fmt.format(cookie_buffer.writer(), "session_token={}; Secure; HttpOnly", .{session_token});
384 try req.respond("", .{
385 .status = .see_other,
387 .{ .name = "Location", .value = redirect_buffer.constSlice() },
388 .{ .name = "Set-Cookie", .value = cookie_buffer.constSlice() },
392 try redirect(&req, "/login");
394 } else if (std.mem.eql(u8, req.head.target, "/logout")) {
395 if (logged_in) |login| {
396 logout_user(&db, login.session_token);
397 try req.respond("", .{
398 .status = .see_other,
400 .{ .name = "Location", .value = "/" },
401 .{ .name = "Set-Cookie", .value = "session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT" },
405 } else if (std.mem.eql(u8, req.head.target, "/quit")) {
406 try redirect(&req, "/");