]> gitweb.ps.run Git - chirp/blob - src/main.zig
update db
[chirp] / src / main.zig
1 const std = @import("std");
2 const lmdb = @import("lmdb");
3
4 // db {{{
5
6 const Prng = struct {
7     prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0),
8
9     pub fn gen_id(self: *Prng, dbi: anytype) Id {
10         var id = self.prng.next();
11
12         while (dbi.has(id)) {
13             id = self.prng.next();
14         }
15
16         return id;
17     }
18 };
19 var prng = Prng{};
20
21 // }}}
22
23 // http stuff {{{
24
25 pub fn redirect(req: *std.http.Server.Request, location: []const u8) !void {
26     try req.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "Location", .value = location }} });
27 }
28
29 pub fn get_body(req: *std.http.Server.Request) []const u8 {
30     return req.server.read_buffer[req.head_end .. req.head_end + (req.head.content_length orelse 0)];
31 }
32
33 pub fn get_value(req: *std.http.Server.Request, name: []const u8) ?[]const u8 {
34     const body = get_body(req);
35     if (std.mem.indexOf(u8, body, name)) |name_index| {
36         if (std.mem.indexOfScalarPos(u8, body, name_index, '=')) |eql_index| {
37             if (std.mem.indexOfScalarPos(u8, body, name_index, '&')) |amp_index| {
38                 return body[eql_index + 1 .. amp_index];
39             }
40
41             return body[eql_index + 1 .. body.len];
42         }
43     }
44     return null;
45 }
46
47 pub fn get_cookie(req: *std.http.Server.Request, name: []const u8) ?CookieValue {
48     var header_it = req.iterateHeaders();
49     while (header_it.next()) |header| {
50         if (std.mem.eql(u8, header.name, "Cookie")) {
51             if (std.mem.indexOf(u8, header.value, name)) |name_index| {
52                 if (std.mem.indexOfScalarPos(u8, header.value, name_index, '=')) |eql_index| {
53                     if (std.mem.indexOfPos(u8, header.value, name_index, "; ")) |semi_index| {
54                         return CookieValue.fromSlice(header.value[eql_index + 1 .. semi_index]) catch null;
55                     }
56
57                     return CookieValue.fromSlice(header.value[eql_index + 1 .. header.value.len]) catch null;
58                 }
59             }
60         }
61     }
62     return null;
63 }
64
65 // }}}
66
67 // content {{{
68
69 const User = struct {
70     // TODO: choose sizes
71     username: Username,
72     password_hash: PasswordHash,
73 };
74
75 const Id = u64;
76 const Username = std.BoundedArray(u8, 16);
77 const PasswordHash = std.BoundedArray(u8, 128);
78 const SessionToken = u64;
79 const CookieValue = std.BoundedArray(u8, 128);
80
81 pub fn hash_password(password: []const u8) !PasswordHash {
82     var hash_buffer = try PasswordHash.init(128);
83
84     // TODO: choose buffer size
85     // TODO: dont allocate on stack, maybe zero memory?
86     var buffer: [1024 * 10]u8 = undefined;
87     var alloc = std.heap.FixedBufferAllocator.init(&buffer);
88
89     // TODO: choose limits
90     const result = try std.crypto.pwhash.argon2.strHash(password, .{
91         .allocator = alloc.allocator(),
92         .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
93     }, hash_buffer.slice());
94
95     try hash_buffer.resize(result.len);
96
97     return hash_buffer;
98 }
99
100 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
101     var buffer: [1024 * 10]u8 = undefined;
102     var alloc = std.heap.FixedBufferAllocator.init(&buffer);
103
104     if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
105         .allocator = alloc.allocator(),
106     })) {
107         return true;
108     } else |err| {
109         std.debug.print("verify error: {}\n", .{err});
110         return false;
111     }
112 }
113
114 pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !void {
115     const username_array = try Username.fromSlice(username);
116
117     var user_id: Id = undefined;
118
119     if (env.txn()) |*txn| {
120         defer txn.commit();
121         if (txn.dbi("users", Id, User)) |users| {
122             user_id = prng.gen_id(users);
123             users.put(user_id, User{
124                 .username = username_array,
125                 .password_hash = try hash_password(password),
126             });
127         }
128     }
129
130     if (env.txn()) |txn| {
131         defer txn.commit();
132         if (txn.dbi("user_ids", @TypeOf(username_array.buffer), Id)) |user_ids| {
133             user_ids.put(username_array.buffer, user_id);
134         }
135     }
136
137     env.sync();
138 }
139
140 pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) ?SessionToken {
141     const username_array = Username.fromSlice(username) catch return null;
142
143     var user_id_maybe: ?Id = null;
144     var user_maybe: ?User = null;
145
146     if (env.txn()) |txn| {
147         defer txn.abort();
148         if (txn.dbi("user_ids", @TypeOf(username_array.buffer), Id)) |user_ids| {
149             user_id_maybe = user_ids.get(username_array.buffer);
150             std.debug.print("id: {?}\n", .{user_id_maybe});
151         }
152     }
153
154     if (user_id_maybe) |user_id| {
155         if (env.txn()) |txn| {
156             defer txn.abort();
157             if (txn.dbi("users", Id, User)) |users| {
158                 user_maybe = users.get(user_id_maybe.?);
159             }
160         }
161
162         if (user_maybe) |user| {
163             if (verify_password(password, user.password_hash)) {
164                 if (env.txn()) |txn| {
165                     defer txn.commit();
166                     if (txn.dbi("sessions", Id, Id)) |sessions| {
167                         const session_token = prng.gen_id(sessions);
168                         sessions.put(session_token, user_id);
169                         return session_token;
170                     }
171                 }
172             }
173         }
174     }
175
176     return null;
177 }
178
179 fn logout_user(env: *lmdb.Env, session_token: SessionToken) void {
180     if (env.txn()) |txn| {
181         defer txn.commit();
182         if (txn.dbi("sessions", Id, Id)) |sessions| {
183             sessions.del(session_token);
184         }
185     }
186 }
187
188 fn get_session_user(env: *lmdb.Env, session_token: SessionToken) ?User {
189     var user_id_maybe: ?Id = null;
190
191     if (env.txn()) |txn| {
192         defer txn.abort();
193         if (txn.dbi("sessions", Id, Id)) |sessions| {
194             user_id_maybe = sessions.get(session_token);
195         }
196     }
197
198     if (user_id_maybe) |user_id| {
199         if (env.txn()) |txn| {
200             defer txn.abort();
201             if (txn.dbi("users", Id, User)) |users| {
202                 return users.get(user_id);
203             }
204         }
205     }
206
207     return null;
208 }
209
210 // }}}
211
212 fn list_users(env: *lmdb.Env) void {
213     if (env.txn()) |txn| {
214         defer txn.abort();
215         if (txn.dbi("users", Id, User)) |users| {
216             var cursor = users.cursor();
217
218             var key: Id = undefined;
219             var user_maybe = cursor.get(&key, .First);
220
221             while (user_maybe) |user| {
222                 std.debug.print("[{}] {s}\n", .{ key, user.username.constSlice() });
223
224                 user_maybe = cursor.get(&key, .Next);
225             }
226         }
227     }
228 }
229
230 pub fn main() !void {
231     // server
232     const address = try std.net.Address.resolveIp("::", 8080);
233
234     var server = try address.listen(.{
235         .reuse_address = true,
236     });
237     defer server.deinit();
238
239     // lmdb
240     var env = lmdb.Env.open("db", 1024 * 1024 * 10);
241     defer env.close();
242
243     list_users(&env);
244
245     accept: while (true) {
246         const conn = try server.accept();
247
248         std.debug.print("new connection: {}\n", .{conn});
249
250         var read_buffer: [1024]u8 = undefined;
251         var http_server = std.http.Server.init(conn, &read_buffer);
252
253         while (http_server.state == .ready) {
254             var req = http_server.receiveHead() catch continue;
255
256             std.debug.print("[{}]: {s}\n", .{ req.head.method, req.head.target });
257
258             var logged_in: ?struct {
259                 user: User,
260                 session_token: SessionToken,
261             } = null;
262
263             if (get_cookie(&req, "session_token")) |session_token_str| {
264                 const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str.constSlice(), 10);
265                 if (get_session_user(&env, session_token)) |user| {
266                     logged_in = .{
267                         .user = user,
268                         .session_token = session_token,
269                     };
270                 }
271                 // TODO: delete session token
272                 // TODO: add changeable headers (set, delete cookies)
273             }
274
275             // html
276             if (req.head.method == .GET) {
277                 if (std.mem.eql(u8, req.head.target, "/register")) {
278                     try req.respond(
279                         \\<form action="/register" method="post">
280                         \\<input type="text" name="username" />
281                         \\<input type="password" name="password" />
282                         \\<input type="submit" value="Register" />
283                         \\</form>
284                     , .{});
285                 } else if (std.mem.eql(u8, req.head.target, "/login")) {
286                     try req.respond(
287                         \\<form action="/login" method="post">
288                         \\<input type="text" name="username" />
289                         \\<input type="password" name="password" />
290                         \\<input type="submit" value="Login" />
291                         \\</form>
292                     , .{});
293                 } else {
294                     if (logged_in) |login| {
295                         var response_buffer = try std.BoundedArray(u8, 1024).init(0);
296                         try std.fmt.format(response_buffer.writer(),
297                             \\<a href="/user/{s}">Home</a>
298                             \\<form action="/logout" method="post"><input type="submit" value="Logout" /></form>
299                             \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
300                         , .{login.user.username.constSlice()});
301                         try req.respond(response_buffer.constSlice(), .{});
302                     } else {
303                         try req.respond(
304                             \\<a href="/register">Register</a>
305                             \\<a href="/login">Login</a>
306                             \\<form action="/quit" method="post"><input type="submit" value="Quit" /></form>
307                         , .{});
308                     }
309                 }
310             }
311             // api
312             else {
313                 if (std.mem.eql(u8, req.head.target, "/register")) {
314                     // TODO: handle args not supplied
315                     const username = get_value(&req, "username").?;
316                     const password = get_value(&req, "password").?;
317
318                     std.debug.print("New user: {s} {s}\n", .{ username, password });
319                     try register_user(&env, username, password);
320
321                     try redirect(&req, "/login");
322                 } else if (std.mem.eql(u8, req.head.target, "/login")) {
323                     // TODO: handle args not supplied
324                     const username = get_value(&req, "username").?;
325                     const password = get_value(&req, "password").?;
326
327                     std.debug.print("New login: {s} {s}\n", .{ username, password });
328                     if (login_user(&env, username, password)) |session_token| {
329                         var redirect_buffer = try std.BoundedArray(u8, 128).init(0);
330                         try std.fmt.format(redirect_buffer.writer(), "/user/{s}", .{username});
331
332                         var cookie_buffer = try std.BoundedArray(u8, 128).init(0);
333                         try std.fmt.format(cookie_buffer.writer(), "session_token={}; Secure; HttpOnly", .{session_token});
334
335                         try req.respond("", .{
336                             .status = .see_other,
337                             .extra_headers = &.{
338                                 .{ .name = "Location", .value = redirect_buffer.constSlice() },
339                                 .{ .name = "Set-Cookie", .value = cookie_buffer.constSlice() },
340                             },
341                         });
342                     } else {
343                         try redirect(&req, "/login");
344                     }
345                 } else if (std.mem.eql(u8, req.head.target, "/logout")) {
346                     if (logged_in) |login| {
347                         logout_user(&env, login.session_token);
348                         try req.respond("", .{
349                             .status = .see_other,
350                             .extra_headers = &.{
351                                 .{ .name = "Location", .value = "/" },
352                                 .{ .name = "Set-Cookie", .value = "session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT" },
353                             },
354                         });
355                     }
356                 } else if (std.mem.eql(u8, req.head.target, "/quit")) {
357                     try redirect(&req, "/");
358                     break :accept;
359                 } else {
360                     try req.respond(
361                         \\<p>POST</p>
362                     , .{});
363                 }
364             }
365         }
366     }
367 }