]> gitweb.ps.run Git - chirp/commitdiff
add files
authorpatrick-scho <patrick.schoenberger@posteo.de>
Fri, 1 Nov 2024 21:42:30 +0000 (22:42 +0100)
committerpatrick-scho <patrick.schoenberger@posteo.de>
Fri, 1 Nov 2024 21:42:30 +0000 (22:42 +0100)
.gitmodules [new file with mode: 0644]
build.zig [new file with mode: 0644]
env.sh [new file with mode: 0755]
lmdb [new submodule]
run.sh [new file with mode: 0755]
src/main.zig [new file with mode: 0644]
todo.md [new file with mode: 0644]

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..f17bff6
--- /dev/null
@@ -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 (file)
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 (executable)
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 (submodule)
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 (executable)
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 (file)
index 0000000..1871dcc
--- /dev/null
@@ -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(
+                        \\<form action="/register" method="post">
+                        \\<input type="text" name="username" />
+                        \\<input type="password" name="password" />
+                        \\<input type="submit" value="Register" />
+                        \\</form>
+                    , .{});
+                } else if (std.mem.eql(u8, req.head.target, "/login")) {
+                    try req.respond(
+                        \\<form action="/login" method="post">
+                        \\<input type="text" name="username" />
+                        \\<input type="password" name="password" />
+                        \\<input type="submit" value="Login" />
+                        \\</form>
+                    , .{});
+                } else {
+                    if (logged_in) |login| {
+                        var response_buffer = try std.BoundedArray(u8, 1024).init(0);
+                        try std.fmt.format(response_buffer.writer(),
+                            \\<a href="/user/{s}">Home</a>
+                            \\<form action="/logout" method="post"><input type="submit" value="Logout" /></form>
+                            \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
+                        , .{login.user.username.constSlice()});
+                        try req.respond(response_buffer.constSlice(), .{});
+                    } else {
+                        try req.respond(
+                            \\<a href="/register">Register</a>
+                            \\<a href="/login">Login</a>
+                            \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
+                        , .{});
+                    }
+                }
+            }
+            // 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(
+                        \\<p>POST</p>
+                    , .{});
+                }
+            }
+        }
+    }
+}
diff --git a/todo.md b/todo.md
new file mode 100644 (file)
index 0000000..ca69d6a
--- /dev/null
+++ b/todo.md
@@ -0,0 +1,18 @@
+## Todo
+- lmdb
+  - check
+  - del
+  - interface: dbi <KEY, VAL>
+- generate ids
+- Account
+  - Register
+  - Login
+  - Logout
+  - Change username
+  - Change password
+- Home
+- Post
+- Comment
+- Like
+- Repost
+- Timeline