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