From 0b2f4de9c1db5ec1059f6cd9b8c44e07f3af0b51 Mon Sep 17 00:00:00 2001 From: patrick-scho Date: Fri, 1 Nov 2024 22:42:30 +0100 Subject: [PATCH] add files --- .gitmodules | 3 + build.zig | 41 +++++ env.sh | 5 + lmdb | 1 + run.sh | 2 + src/main.zig | 416 +++++++++++++++++++++++++++++++++++++++++++++++++++ todo.md | 18 +++ 7 files changed, 486 insertions(+) create mode 100644 .gitmodules create mode 100644 build.zig create mode 100755 env.sh create mode 160000 lmdb create mode 100755 run.sh create mode 100644 src/main.zig create mode 100644 todo.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f17bff6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lmdb"] + path = lmdb + url = https://github.com/LMDB/lmdb diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..7a13429 --- /dev/null +++ b/build.zig @@ -0,0 +1,41 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "lmdb", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .cwd_relative = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const lmdb_mod = b.createModule(.{ + .root_source_file = .{ .cwd_relative = "../ziglmdb/src/lmdb.zig" }, + }); + lmdb_mod.addIncludePath(.{ .cwd_relative = "./lmdb/libraries/liblmdb" }); + lmdb_mod.addCSourceFiles(.{ .files = &.{ + "./lmdb/libraries/liblmdb/midl.c", + "./lmdb/libraries/liblmdb/mdb.c", + } }); + exe.root_module.addImport("lmdb", lmdb_mod); + + exe.linkLibC(); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/env.sh b/env.sh new file mode 100755 index 0000000..c1671b9 --- /dev/null +++ b/env.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +run kitty +run app localhost:8080 +hx . + diff --git a/lmdb b/lmdb new file mode 160000 index 0000000..da9aeda --- /dev/null +++ b/lmdb @@ -0,0 +1 @@ +Subproject commit da9aeda08c3ff710a0d47d61a079f5a905b0a10a diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..7525245 --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +while zig build run; do sleep 1; done diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..1871dcc --- /dev/null +++ b/src/main.zig @@ -0,0 +1,416 @@ +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

+ , .{}); + } + } + } + } +} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..ca69d6a --- /dev/null +++ b/todo.md @@ -0,0 +1,18 @@ +## Todo +- lmdb + - check + - del + - interface: dbi +- generate ids +- Account + - Register + - Login + - Logout + - Change username + - Change password +- Home +- Post +- Comment +- Like +- Repost +- Timeline -- 2.50.1