Merge myid-zig project
This commit is contained in:
6
packages/myid/.gitignore
vendored
Normal file
6
packages/myid/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.zig-cache
|
||||||
|
*.sock
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite3-shm
|
||||||
|
*.sqlite3-wal
|
||||||
|
zig-out
|
||||||
63
packages/myid/build.zig
Normal file
63
packages/myid/build.zig
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const sqlite_dep = b.dependency("sqlite", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sqlite_mod = sqlite_dep.module("sqlite");
|
||||||
|
|
||||||
|
const myid_mod = b.addModule("myid", .{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "sqlite", .module = sqlite_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const myid_exe = b.addExecutable(.{
|
||||||
|
.name = "myid",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "myid", .module = myid_mod },
|
||||||
|
.{ .name = "sqlite", .module = sqlite_mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
b.installArtifact(myid_exe);
|
||||||
|
|
||||||
|
const run_step = b.step("run", "Run the app");
|
||||||
|
|
||||||
|
const run_cmd = b.addRunArtifact(myid_exe);
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mod_tests = b.addTest(.{
|
||||||
|
.root_module = myid_mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||||
|
|
||||||
|
const exe_tests = b.addTest(.{
|
||||||
|
.root_module = myid_exe.root_module,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||||
|
|
||||||
|
const test_step = b.step("test", "Run tests");
|
||||||
|
test_step.dependOn(&run_mod_tests.step);
|
||||||
|
test_step.dependOn(&run_exe_tests.step);
|
||||||
|
}
|
||||||
17
packages/myid/build.zig.zon
Normal file
17
packages/myid/build.zig.zon
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.{
|
||||||
|
.name = .myid,
|
||||||
|
.version = "0.0.0",
|
||||||
|
.dependencies = .{
|
||||||
|
.sqlite = .{
|
||||||
|
.url = "git+https://github.com/vrischmann/zig-sqlite#be8b4965b46fc1a7a819bf3cba09f370c0e9c64c",
|
||||||
|
.hash = "sqlite-3.48.0-F2R_a9GLDgAXT-c49TfkFMt6yPOMQAYfp4ig8bRNdZs4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.fingerprint = 0x4dc8d6b2af89dd8f, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.15.0-dev.919+044ccf413",
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
},
|
||||||
|
}
|
||||||
2
packages/myid/src/default.zig
Normal file
2
packages/myid/src/default.zig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub const database_path = "db.sqlite3";
|
||||||
|
pub const socket_path = "myid.sock";
|
||||||
206
packages/myid/src/http.zig
Normal file
206
packages/myid/src/http.zig
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const main = @import("main.zig");
|
||||||
|
|
||||||
|
const Parser = @import("http/Parser.zig");
|
||||||
|
|
||||||
|
threadlocal var read_buffer: [2 * 1024 * 1024]u8 = undefined;
|
||||||
|
threadlocal var write_buffer: [2 * 1024 * 1024]u8 = undefined;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.http);
|
||||||
|
|
||||||
|
const status = struct {
|
||||||
|
pub const ok = "HTTP/1.1 200 OK\r\n";
|
||||||
|
pub const created = "HTTP/1.1 201 Created\r\n";
|
||||||
|
pub const accepted = "HTTP/1.1 202 Accepted\r\n";
|
||||||
|
pub const non_authoritative_information = "HTTP/1.1 203 Non-Authoritative Information\r\n";
|
||||||
|
pub const no_content = "HTTP/1.1 204 No Content\r\n";
|
||||||
|
pub const reset_content = "HTTP/1.1 205 Reset Content\r\n";
|
||||||
|
pub const partial_content = "HTTP/1.1 206 Partial Content\r\n";
|
||||||
|
pub const multi_status = "HTTP/1.1 207 Multi-Status\r\n";
|
||||||
|
pub const already_reported = "HTTP/1.1 208 Already Reported\r\n";
|
||||||
|
|
||||||
|
pub const multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n";
|
||||||
|
pub const moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n";
|
||||||
|
pub const found = "HTTP/1.1 302 Found\r\n";
|
||||||
|
pub const see_other = "HTTP/1.1 303 See Other\r\n";
|
||||||
|
pub const not_modified = "HTTP/1.1 304 Not Modified\r\n";
|
||||||
|
pub const temporary_redirect = "HTTP/1.1 307 Temporary Redirect\r\n";
|
||||||
|
pub const permanent_redirect = "HTTP/1.1 308 Permanent Redirect\r\n";
|
||||||
|
|
||||||
|
pub const bad_request = "HTTP/1.1 400 Bad Request\r\n";
|
||||||
|
pub const unauthorized = "HTTP/1.1 401 Unauthorized\r\n";
|
||||||
|
pub const payment_required = "HTTP/1.1 402 Payment Required\r\n";
|
||||||
|
pub const forbidden = "HTTP/1.1 403 Forbidden\r\n";
|
||||||
|
pub const not_found = "HTTP/1.1 404 Not Found\r\n";
|
||||||
|
pub const method_not_allowed = "HTTP/1.1 405 Method Not Allowed\r\n";
|
||||||
|
pub const not_acceptable = "HTTP/1.1 406 Not Acceptable\r\n";
|
||||||
|
pub const proxy_authentication_required = "HTTP/1.1 407 Proxy Authentication Required\r\n";
|
||||||
|
pub const request_timeout = "HTTP/1.1 408 Request Timeout\r\n";
|
||||||
|
pub const conflict = "HTTP/1.1 409 Conflict\r\n";
|
||||||
|
pub const gone = "HTTP/1.1 410 Gone\r\n";
|
||||||
|
pub const length_required = "HTTP/1.1 411 Length Required\r\n";
|
||||||
|
pub const precondition_failed = "HTTP/1.1 412 Precondition Failed\r\n";
|
||||||
|
pub const content_too_large = "HTTP/1.1 413 Content Too Large\r\n";
|
||||||
|
pub const uri_too_long = "HTTP/1.1 414 URI Too Long\r\n";
|
||||||
|
pub const unsupported_media_type = "HTTP/1.1 415 Unsupported Media Type\r\n";
|
||||||
|
pub const range_not_satisfiable = "HTTP/1.1 416 Range Not Satisfiable\r\n";
|
||||||
|
pub const expectation_failed = "HTTP/1.1 417 Expectation Failed\r\n";
|
||||||
|
pub const im_a_teapot = "HTTP/1.1 418 I'm a teapot\r\n";
|
||||||
|
pub const misdirected_request = "HTTP/1.1 421 Misdirected Request\r\n";
|
||||||
|
pub const unprocessable_content = "HTTP/1.1 422 Unprocessable Content\r\n";
|
||||||
|
pub const locked = "HTTP/1.1 423 Locked\r\n";
|
||||||
|
pub const failed_dependency = "HTTP/1.1 424 Failed Dependency\r\n";
|
||||||
|
pub const upgrade_required = "HTTP/1.1 426 Upgrade Required\r\n";
|
||||||
|
pub const precondition_required = "HTTP/1.1 428 Precondition Required\r\n";
|
||||||
|
pub const too_many_requests = "HTTP/1.1 429 Too Many Requests\r\n";
|
||||||
|
pub const request_header_fields_too_large = "HTTP/1.1 431 Request Header Fields Too Large\r\n";
|
||||||
|
pub const unavailable_for_legal_reasons = "HTTP/1.1 451 Unavailable For Legal Reasons\r\n";
|
||||||
|
|
||||||
|
pub const internal_server_error = "HTTP/1.1 500 Internal Server Error\r\n";
|
||||||
|
pub const not_implemented = "HTTP/1.1 501 Not Implemented\r\n";
|
||||||
|
pub const bad_gateway = "HTTP/1.1 502 Bad Gateway\r\n";
|
||||||
|
pub const service_unavailable = "HTTP/1.1 503 Service Unavailable\r\n";
|
||||||
|
pub const gateway_timeout = "HTTP/1.1 504 Gateway Timeout\r\n";
|
||||||
|
pub const http_version_not_supported = "HTTP/1.1 505 HTTP Version Not Supported\r\n";
|
||||||
|
pub const variant_also_negotiates = "HTTP/1.1 506 Variant Also Negotiates\r\n";
|
||||||
|
pub const insufficient_storage = "HTTP/1.1 507 Insufficient Storage\r\n";
|
||||||
|
pub const loop_detected = "HTTP/1.1 508 Loop Detected\r\n";
|
||||||
|
pub const not_extended = "HTTP/1.1 510 Not Extended\r\n";
|
||||||
|
pub const network_authentication_required = "HTTP/1.1 511 Network Authentication Required\r\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResponseEmptyOptions = struct {
|
||||||
|
status_text: []const u8 = status.ok,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResponseOptions = struct {
|
||||||
|
status_text: []const u8 = status.ok,
|
||||||
|
media_type: []const u8 = "text/plain; charset=utf-8",
|
||||||
|
response_body: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn makeResponseEmpty(options: ResponseEmptyOptions) ![]const u8 {
|
||||||
|
var fbs = std.io.fixedBufferStream(&write_buffer);
|
||||||
|
const writer = fbs.writer();
|
||||||
|
|
||||||
|
try writer.print("{s}", .{options.status_text});
|
||||||
|
try writer.print("\r\n", .{});
|
||||||
|
|
||||||
|
return fbs.getWritten();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeResponseClose(options: ResponseEmptyOptions) ![]const u8 {
|
||||||
|
var fbs = std.io.fixedBufferStream(&write_buffer);
|
||||||
|
const writer = fbs.writer();
|
||||||
|
|
||||||
|
try writer.print("{s}", .{options.status_text});
|
||||||
|
try writer.print("Connection: close\r\n", .{});
|
||||||
|
try writer.print("\r\n", .{});
|
||||||
|
|
||||||
|
return fbs.getWritten();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeResponse(options: ResponseOptions) ![]const u8 {
|
||||||
|
var fbs = std.io.fixedBufferStream(&write_buffer);
|
||||||
|
const writer = fbs.writer();
|
||||||
|
|
||||||
|
try writer.print("{s}", .{options.status_text});
|
||||||
|
try writer.print("Content-Type: {s}\r\n", .{options.media_type});
|
||||||
|
try writer.print("Content-Length: {d}\r\n", .{options.response_body.len});
|
||||||
|
try writer.print("\r\n", .{});
|
||||||
|
try writer.print("{s}", .{options.response_body});
|
||||||
|
|
||||||
|
return fbs.getWritten();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process(conn: std.net.Server.Connection) !void {
|
||||||
|
defer conn.stream.close();
|
||||||
|
|
||||||
|
var leftover_bytes: usize = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const start = try std.time.Instant.now();
|
||||||
|
|
||||||
|
var route: Parser.Route = undefined;
|
||||||
|
|
||||||
|
var parser = Parser.init(.{
|
||||||
|
.self = &route,
|
||||||
|
.route = routeCallback,
|
||||||
|
});
|
||||||
|
var total_bytes_read: usize = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var bytes_read: usize = undefined;
|
||||||
|
var chars: []const u8 = undefined;
|
||||||
|
|
||||||
|
if (leftover_bytes > 0) {
|
||||||
|
bytes_read = leftover_bytes;
|
||||||
|
chars = read_buffer[0..leftover_bytes];
|
||||||
|
leftover_bytes = 0;
|
||||||
|
} else {
|
||||||
|
bytes_read = try conn.stream.read(read_buffer[total_bytes_read..]);
|
||||||
|
chars = read_buffer[total_bytes_read .. total_bytes_read + bytes_read];
|
||||||
|
}
|
||||||
|
|
||||||
|
total_bytes_read += bytes_read;
|
||||||
|
|
||||||
|
const res = parser.consume(chars) catch |err| switch (err) {
|
||||||
|
error.MethodNotSupported => {
|
||||||
|
const response = try makeResponseClose(.{ .status_text = status.method_not_allowed });
|
||||||
|
try conn.stream.writeAll(response);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
error.HttpVersionNotSupported => {
|
||||||
|
const response = try makeResponseClose(.{ .status_text = status.http_version_not_supported });
|
||||||
|
try conn.stream.writeAll(response);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
error.MissingLineFeed => {
|
||||||
|
const response = try makeResponseClose(.{ .status_text = status.bad_request });
|
||||||
|
try conn.stream.writeAll(response);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
error.InvalidContentLength => {
|
||||||
|
const response = try makeResponseClose(.{ .status_text = status.bad_request });
|
||||||
|
try conn.stream.writeAll(response);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (total_bytes_read >= read_buffer.len and !res.done) {
|
||||||
|
if (parser.state == .body) {
|
||||||
|
const response = try makeResponseClose(.{ .status_text = status.content_too_large });
|
||||||
|
try conn.stream.writeAll(response);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const response = try makeResponseClose(.{ .status_text = status.request_header_fields_too_large });
|
||||||
|
try conn.stream.writeAll(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.done) {
|
||||||
|
leftover_bytes = bytes_read - res.consumed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = try makeResponse(.{ .response_body = "PONG\n" });
|
||||||
|
|
||||||
|
try conn.stream.writeAll(response);
|
||||||
|
|
||||||
|
if (leftover_bytes > 0) {
|
||||||
|
@memmove(&read_buffer, read_buffer[total_bytes_read - leftover_bytes .. total_bytes_read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = try std.time.Instant.now();
|
||||||
|
const time_ns = end.since(start);
|
||||||
|
const time_us = @divFloor(time_ns, std.time.ns_per_us);
|
||||||
|
|
||||||
|
log.info("{s} {s} ({} μs)", .{ @tagName(route.method), route.pathname, time_us });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn routeCallback(self: ?*anyopaque, route: Parser.Route) void {
|
||||||
|
@as(*Parser.Route, @alignCast(@ptrCast(self))).* = route;
|
||||||
|
}
|
||||||
356
packages/myid/src/http/Parser.zig
Normal file
356
packages/myid/src/http/Parser.zig
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Parser = @This();
|
||||||
|
|
||||||
|
const Callbacks = struct {
|
||||||
|
self: ?*anyopaque = null,
|
||||||
|
|
||||||
|
route: ?*const fn (self: ?*anyopaque, route: Route) void = null,
|
||||||
|
header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null,
|
||||||
|
body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null,
|
||||||
|
|
||||||
|
pub const init: Callbacks = .{};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Error = error{
|
||||||
|
MethodNotSupported,
|
||||||
|
HttpVersionNotSupported,
|
||||||
|
MissingLineFeed,
|
||||||
|
InvalidContentLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = union(enum) {
|
||||||
|
pub fn methodComplete(method: Method) State {
|
||||||
|
return .{
|
||||||
|
.method_complete = .{
|
||||||
|
.method = method,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pathname(method: Method, p: []const u8) State {
|
||||||
|
return .{
|
||||||
|
.pathname_state = .{
|
||||||
|
.method = method,
|
||||||
|
.pathname = p,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn headerValue(name: []const u8, value: []const u8) State {
|
||||||
|
return .{
|
||||||
|
.header_value = .{
|
||||||
|
.name = name,
|
||||||
|
.value = value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init: void,
|
||||||
|
method_d: void,
|
||||||
|
method_g: void,
|
||||||
|
method_h: void,
|
||||||
|
method_p: void,
|
||||||
|
method_de: void,
|
||||||
|
method_ge: void,
|
||||||
|
method_he: void,
|
||||||
|
method_pa: void,
|
||||||
|
method_po: void,
|
||||||
|
method_pu: void,
|
||||||
|
method_del: void,
|
||||||
|
method_hea: void,
|
||||||
|
method_pat: void,
|
||||||
|
method_pos: void,
|
||||||
|
method_dele: void,
|
||||||
|
method_patc: void,
|
||||||
|
method_delet: void,
|
||||||
|
method_complete: struct { method: Method },
|
||||||
|
pathname_state: struct { method: Method, pathname: []const u8 },
|
||||||
|
pathname_complete: void,
|
||||||
|
version_h: void,
|
||||||
|
version_ht: void,
|
||||||
|
version_htt: void,
|
||||||
|
version_http: void,
|
||||||
|
@"version_http/@": void,
|
||||||
|
@"version_http/1@": void,
|
||||||
|
@"version_http/1.@": void,
|
||||||
|
version_complete: void,
|
||||||
|
start_line_end: void,
|
||||||
|
header_name_start: void,
|
||||||
|
header_name: []const u8,
|
||||||
|
header_value: struct { name: []const u8, value: []const u8 },
|
||||||
|
header_line_end: void,
|
||||||
|
headers_end: void,
|
||||||
|
body: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConsumeResult = struct {
|
||||||
|
consumed: usize,
|
||||||
|
done: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConsumeCharResult = enum {
|
||||||
|
not_done,
|
||||||
|
done,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Method = enum {
|
||||||
|
DELETE,
|
||||||
|
GET,
|
||||||
|
HEAD,
|
||||||
|
PATCH,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Route = struct {
|
||||||
|
method: Method,
|
||||||
|
pathname: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
callbacks: Callbacks,
|
||||||
|
state: State,
|
||||||
|
current_header_is_content_length: bool,
|
||||||
|
content_length: usize,
|
||||||
|
|
||||||
|
pub fn init(callbacks: Callbacks) Parser {
|
||||||
|
return .{
|
||||||
|
.callbacks = callbacks,
|
||||||
|
.state = .init,
|
||||||
|
.current_header_is_content_length = false,
|
||||||
|
.content_length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < chars.len) {
|
||||||
|
switch (self.state) {
|
||||||
|
.body => |body| {
|
||||||
|
const to_consume = @min(chars.len - i, self.content_length - body.len);
|
||||||
|
const new_body = body.ptr[0 .. body.len + to_consume];
|
||||||
|
self.state = .{ .body = new_body };
|
||||||
|
i += to_consume;
|
||||||
|
|
||||||
|
const done = new_body.len >= self.content_length;
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
if (self.callbacks.body) |bodyCallback| {
|
||||||
|
bodyCallback(self.callbacks.self, new_body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.consumed = i,
|
||||||
|
.done = done,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
const res = try self.consumeChar(&chars[i]);
|
||||||
|
i += 1;
|
||||||
|
if (res == .done) return .{
|
||||||
|
.consumed = i,
|
||||||
|
.done = true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.consumed = chars.len,
|
||||||
|
.done = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
|
||||||
|
const c = c_ptr.*;
|
||||||
|
const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1];
|
||||||
|
switch (self.state) {
|
||||||
|
.init => switch (c) {
|
||||||
|
'D' => self.state = .method_d,
|
||||||
|
'G' => self.state = .method_g,
|
||||||
|
'H' => self.state = .method_h,
|
||||||
|
'P' => self.state = .method_p,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_d => switch (c) {
|
||||||
|
'E' => self.state = .method_de,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_g => switch (c) {
|
||||||
|
'E' => self.state = .method_ge,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_h => switch (c) {
|
||||||
|
'E' => self.state = .method_he,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_p => switch (c) {
|
||||||
|
'A' => self.state = .method_pa,
|
||||||
|
'O' => self.state = .method_po,
|
||||||
|
'U' => self.state = .method_pu,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_de => switch (c) {
|
||||||
|
'L' => self.state = .method_del,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_ge => switch (c) {
|
||||||
|
'T' => self.state = .methodComplete(.GET),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_he => switch (c) {
|
||||||
|
'A' => self.state = .method_hea,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pa => switch (c) {
|
||||||
|
'T' => self.state = .method_pat,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_po => switch (c) {
|
||||||
|
'S' => self.state = .method_pos,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pu => switch (c) {
|
||||||
|
'T' => self.state = .methodComplete(.PUT),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_del => switch (c) {
|
||||||
|
'E' => self.state = .method_dele,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_hea => switch (c) {
|
||||||
|
'D' => self.state = .methodComplete(.HEAD),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pat => switch (c) {
|
||||||
|
'C' => self.state = .method_patc,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pos => switch (c) {
|
||||||
|
'T' => self.state = .methodComplete(.POST),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_dele => switch (c) {
|
||||||
|
'T' => self.state = .method_delet,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_patc => switch (c) {
|
||||||
|
'H' => self.state = .methodComplete(.PATCH),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_delet => switch (c) {
|
||||||
|
'E' => self.state = .methodComplete(.DELETE),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_complete => |s| switch (c) {
|
||||||
|
' ' => self.state = .pathname(s.method, @as([*]const u8, @ptrCast(c_ptr))[1..1]),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.pathname_state => |s| switch (c) {
|
||||||
|
' ' => {
|
||||||
|
self.state = .pathname_complete;
|
||||||
|
if (self.callbacks.route) |routeCallback| {
|
||||||
|
routeCallback(self.callbacks.self, .{
|
||||||
|
.method = s.method,
|
||||||
|
.pathname = s.pathname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]),
|
||||||
|
},
|
||||||
|
.pathname_complete => switch (c) {
|
||||||
|
'H' => self.state = .version_h,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_h => switch (c) {
|
||||||
|
'T' => self.state = .version_ht,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_ht => switch (c) {
|
||||||
|
'T' => self.state = .version_htt,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_htt => switch (c) {
|
||||||
|
'P' => self.state = .version_http,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_http => switch (c) {
|
||||||
|
'/' => self.state = .@"version_http/@",
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.@"version_http/@" => switch (c) {
|
||||||
|
'1' => self.state = .@"version_http/1@",
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.@"version_http/1@" => switch (c) {
|
||||||
|
'.' => self.state = .@"version_http/1.@",
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.@"version_http/1.@" => switch (c) {
|
||||||
|
'1' => self.state = .version_complete,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_complete => switch (c) {
|
||||||
|
'\r' => self.state = .start_line_end,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.start_line_end => switch (c) {
|
||||||
|
'\n' => self.state = .header_name_start,
|
||||||
|
else => return error.MissingLineFeed,
|
||||||
|
},
|
||||||
|
.header_name_start => switch (c) {
|
||||||
|
'\r' => self.state = .headers_end,
|
||||||
|
else => self.state = .{ .header_name = c_slice },
|
||||||
|
},
|
||||||
|
.header_name => |name| switch (c) {
|
||||||
|
':' => {
|
||||||
|
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
|
||||||
|
self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length");
|
||||||
|
},
|
||||||
|
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
|
||||||
|
},
|
||||||
|
.header_value => |s| switch (c) {
|
||||||
|
'\r' => {
|
||||||
|
self.state = .header_line_end;
|
||||||
|
const value_trimmed = std.mem.trim(u8, s.value, " \t");
|
||||||
|
if (self.current_header_is_content_length) {
|
||||||
|
self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength;
|
||||||
|
self.current_header_is_content_length = false;
|
||||||
|
}
|
||||||
|
if (self.callbacks.header) |headerCallback| {
|
||||||
|
headerCallback(self.callbacks.self, s.name, value_trimmed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]),
|
||||||
|
},
|
||||||
|
.header_line_end => switch (c) {
|
||||||
|
'\n' => self.state = .header_name_start,
|
||||||
|
else => return error.MissingLineFeed,
|
||||||
|
},
|
||||||
|
.headers_end => switch (c) {
|
||||||
|
'\n' => {
|
||||||
|
if (self.content_length == 0) {
|
||||||
|
if (self.callbacks.body) |bodyCallback| {
|
||||||
|
bodyCallback(self.callbacks.self, &.{});
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
|
||||||
|
},
|
||||||
|
else => return error.MissingLineFeed,
|
||||||
|
},
|
||||||
|
.body => |body| {
|
||||||
|
const new_body = body.ptr[0 .. body.len + 1];
|
||||||
|
self.state = .{ .body = new_body };
|
||||||
|
if (new_body.len >= self.content_length) {
|
||||||
|
if (self.callbacks.body) |bodyCallback| {
|
||||||
|
bodyCallback(self.callbacks.self, new_body);
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return .not_done;
|
||||||
|
}
|
||||||
136
packages/myid/src/main.zig
Normal file
136
packages/myid/src/main.zig
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const default = @import("default.zig");
|
||||||
|
const http = @import("http.zig");
|
||||||
|
|
||||||
|
pub var allocator: std.mem.Allocator = undefined;
|
||||||
|
pub var args: [][:0]u8 = undefined;
|
||||||
|
pub var env: std.process.EnvMap = undefined;
|
||||||
|
pub var config: struct {
|
||||||
|
database_path: []const u8 = default.database_path,
|
||||||
|
socket_path: []const u8 = default.socket_path,
|
||||||
|
} = .{};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
|
||||||
|
allocator = gpa.allocator();
|
||||||
|
|
||||||
|
// --- READ CONFIG ---------------------------------------------------------
|
||||||
|
|
||||||
|
args = try std.process.argsAlloc(allocator);
|
||||||
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
|
env = try std.process.getEnvMap(allocator);
|
||||||
|
defer env.deinit();
|
||||||
|
|
||||||
|
// override config by environment variables
|
||||||
|
|
||||||
|
if (env.get("DATABASE_PATH")) |database_path| {
|
||||||
|
config.database_path = database_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.get("SOCKET_PATH")) |socket_path| {
|
||||||
|
config.socket_path = socket_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// override config by args
|
||||||
|
|
||||||
|
var show_help = false;
|
||||||
|
|
||||||
|
var arg_i: usize = 1;
|
||||||
|
while (arg_i < args.len) : (arg_i += 1) {
|
||||||
|
const arg = args[arg_i];
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, arg, "--database-path")) {
|
||||||
|
if (arg_i < args.len - 1) {
|
||||||
|
std.log.err("Missing argument for --database-path", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
arg_i += 1;
|
||||||
|
config.database_path = args[arg_i];
|
||||||
|
} else if (std.mem.eql(u8, arg, "--socket-path")) {
|
||||||
|
if (arg_i < args.len - 1) {
|
||||||
|
std.log.err("Missing argument for --socket-path", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
arg_i += 1;
|
||||||
|
config.socket_path = args[arg_i];
|
||||||
|
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||||
|
show_help = true;
|
||||||
|
} else {
|
||||||
|
std.log.err("Unrecognized option: {s}", .{arg});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show_help) {
|
||||||
|
const stderr = std.io.getStdErr().writer();
|
||||||
|
try stderr.print(
|
||||||
|
\\Usage: myid [flags]
|
||||||
|
\\
|
||||||
|
\\Flags:
|
||||||
|
\\ --database-path <path>
|
||||||
|
\\ The path to an SQLite 3 database file. You can specify ":memory:" to use
|
||||||
|
\\ in-memory ephemeral database.
|
||||||
|
\\ Default: "{[default_database_path]s}"
|
||||||
|
\\ --socket-path <path>
|
||||||
|
\\ The path to a unix socket on which the server will listen.
|
||||||
|
\\ Default: "{[default_socket_path]s}"
|
||||||
|
\\
|
||||||
|
\\Environment variables:
|
||||||
|
\\ DATABASE_PATH
|
||||||
|
\\ Equivalent to --database-path flag. When both are present, the flag
|
||||||
|
\\ takes precedence.
|
||||||
|
\\ SOCKET_PATH
|
||||||
|
\\ Equivalent to --socket-path flag. When both are present, the flag takes
|
||||||
|
\\ precedence.
|
||||||
|
\\
|
||||||
|
, .{
|
||||||
|
.default_database_path = default.database_path,
|
||||||
|
.default_socket_path = default.socket_path,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- INITIALIZE SERVER ---------------------------------------------------
|
||||||
|
|
||||||
|
std.posix.unlink(config.socket_path) catch |err| switch (err) {
|
||||||
|
error.FileNotFound => {},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
|
||||||
|
const address = try std.net.Address.initUnix(config.socket_path);
|
||||||
|
|
||||||
|
var server = try address.listen(.{});
|
||||||
|
defer {
|
||||||
|
server.deinit();
|
||||||
|
std.log.info("Server stopped", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
std.log.info("Listening on unix:{s}", .{config.socket_path});
|
||||||
|
|
||||||
|
// --- LOOP ----------------------------------------------------------------
|
||||||
|
|
||||||
|
while (server.accept()) |conn| {
|
||||||
|
const thread = std.Thread.spawn(.{ .allocator = allocator }, process, .{conn}) catch |err| {
|
||||||
|
std.log.err("Error while spawning thread: {}", .{err});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
thread.detach();
|
||||||
|
} else |err| {
|
||||||
|
std.log.err("Error while trying to accept connection: {}", .{err});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(conn: std.net.Server.Connection) void {
|
||||||
|
http.process(conn) catch |err| {
|
||||||
|
// TODO Bandaid spam prevention until we manage closing connections properly
|
||||||
|
if (err == error.ConnectionResetByPeer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std.log.err("Error while processing connection: {}", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
235
packages/myid/src/root.zig
Normal file
235
packages/myid/src/root.zig
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const sqlite = @import("sqlite");
|
||||||
|
const uuid = @import("uuid.zig");
|
||||||
|
|
||||||
|
fn Id(comptime _tag: @Type(.enum_literal)) type {
|
||||||
|
return struct {
|
||||||
|
pub const tag = _tag;
|
||||||
|
|
||||||
|
bytes: [16]u8,
|
||||||
|
|
||||||
|
pub fn new() @This() {
|
||||||
|
return .{ .bytes = uuid.uuid_v7() };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(a: @This(), b: @This()) bool {
|
||||||
|
return std.mem.eql(u8, &a.bytes, &b.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(encoded: *const [22]u8) !@This() {
|
||||||
|
var bytes: [16]u8 = undefined;
|
||||||
|
try std.base64.url_safe_no_pad.Decoder.decode(&bytes, encoded);
|
||||||
|
return .{ .bytes = bytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(self: @This()) [22]u8 {
|
||||||
|
var text: [22]u8 = undefined;
|
||||||
|
std.base64.url_safe_no_pad.Encoder.encode(&text, self.bytes);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const AppId = Id(.app_id);
|
||||||
|
pub const UserId = Id(.user_id);
|
||||||
|
|
||||||
|
pub const CreateAppResult = struct {
|
||||||
|
aid: AppId,
|
||||||
|
plain_secret: [32]u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Database = struct {
|
||||||
|
db: sqlite.Db,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database {
|
||||||
|
var db = try sqlite.Db.init(.{
|
||||||
|
.mode = .{ .File = path },
|
||||||
|
.open_flags = .{
|
||||||
|
.create = true,
|
||||||
|
.write = true,
|
||||||
|
},
|
||||||
|
.threading_mode = .MultiThread,
|
||||||
|
});
|
||||||
|
errdefer db.deinit();
|
||||||
|
|
||||||
|
_ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{});
|
||||||
|
_ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{});
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.db = db,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Database) void {
|
||||||
|
self.db.deinit();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MIGRATION -----------------------------------------------------------
|
||||||
|
|
||||||
|
fn getUserVersion(self: *Database) !i32 {
|
||||||
|
const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{});
|
||||||
|
return version.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setUserVersion(self: *Database, version: i32) !void {
|
||||||
|
var buf: [100]u8 = undefined;
|
||||||
|
const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable;
|
||||||
|
_ = try self.db.oneDynamic(void, query, .{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn migrate(self: *Database) !void {
|
||||||
|
var user_version = try self.getUserVersion();
|
||||||
|
|
||||||
|
if (user_version == 0) {
|
||||||
|
_ = try self.db.exec(
|
||||||
|
\\CREATE TABLE users (
|
||||||
|
\\ uid BLOB NOT NULL,
|
||||||
|
\\ name TEXT NOT NULL,
|
||||||
|
\\ email TEXT NOT NULL UNIQUE,
|
||||||
|
\\ password TEXT NOT NULL,
|
||||||
|
\\ PRIMARY KEY (uid)
|
||||||
|
\\)
|
||||||
|
, .{}, .{});
|
||||||
|
_ = try self.db.exec(
|
||||||
|
\\CREATE TABLE apps (
|
||||||
|
\\ aid BLOB NOT NULL,
|
||||||
|
\\ name TEXT NOT NULL,
|
||||||
|
\\ secret TEXT NOT NULL,
|
||||||
|
\\ PRIMARY KEY (aid)
|
||||||
|
\\)
|
||||||
|
, .{}, .{});
|
||||||
|
_ = try self.db.exec(
|
||||||
|
\\CREATE TABLE app_callbacks (
|
||||||
|
\\ aid BLOB NOT NULL,
|
||||||
|
\\ callback TEXT NOT NULL,
|
||||||
|
\\ PRIMARY KEY (aid, callback),
|
||||||
|
\\ FOREIGN KEY (aid) REFERENCES apps (aid)
|
||||||
|
\\ ON UPDATE CASCADE
|
||||||
|
\\ ON DELETE CASCADE
|
||||||
|
\\)
|
||||||
|
, .{}, .{});
|
||||||
|
user_version += 1;
|
||||||
|
try self.setUserVersion(user_version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- USERS ---------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !UserId {
|
||||||
|
const uid = UserId.new();
|
||||||
|
var password_buf: [1000]u8 = undefined;
|
||||||
|
const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{
|
||||||
|
.allocator = self.allocator,
|
||||||
|
.mode = .argon2id,
|
||||||
|
.params = .owasp_2id,
|
||||||
|
}, &password_buf);
|
||||||
|
|
||||||
|
try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{
|
||||||
|
.uid = uid.bytes,
|
||||||
|
.name = name,
|
||||||
|
.email = email,
|
||||||
|
.password = password,
|
||||||
|
});
|
||||||
|
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- APPS ----------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult {
|
||||||
|
_ = try self.db.exec("BEGIN", .{}, .{});
|
||||||
|
errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable;
|
||||||
|
|
||||||
|
const secret_bytes = blk: {
|
||||||
|
var bytes: [24]u8 = undefined;
|
||||||
|
std.crypto.random.bytes(&bytes);
|
||||||
|
break :blk bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
var plain_secret_buf: [32]u8 = undefined;
|
||||||
|
const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes);
|
||||||
|
std.debug.assert(plain_secret_buf.len == plain_secret.len);
|
||||||
|
|
||||||
|
const aid = AppId.new();
|
||||||
|
var secret_buf: [1000]u8 = undefined;
|
||||||
|
const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{
|
||||||
|
.allocator = self.allocator,
|
||||||
|
.mode = .argon2id,
|
||||||
|
.params = .owasp_2id,
|
||||||
|
}, &secret_buf);
|
||||||
|
|
||||||
|
try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{
|
||||||
|
.aid = aid.bytes,
|
||||||
|
.name = name,
|
||||||
|
.secret = secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)");
|
||||||
|
defer insert_callback.deinit();
|
||||||
|
for (callbacks) |callback| {
|
||||||
|
try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback });
|
||||||
|
insert_callback.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.db.exec("COMMIT", .{}, .{});
|
||||||
|
return .{
|
||||||
|
.aid = aid,
|
||||||
|
.plain_secret = plain_secret_buf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "user version" {
|
||||||
|
var db = try Database.init(":memory:", std.testing.allocator);
|
||||||
|
defer db.deinit();
|
||||||
|
|
||||||
|
try std.testing.expectEqual(0, try db.getUserVersion());
|
||||||
|
try db.setUserVersion(1);
|
||||||
|
try std.testing.expectEqual(1, try db.getUserVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "migrate" {
|
||||||
|
var db = try Database.init(":memory:", std.testing.allocator);
|
||||||
|
defer db.deinit();
|
||||||
|
|
||||||
|
try db.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "create user" {
|
||||||
|
var db = try Database.init(":memory:", std.testing.allocator);
|
||||||
|
defer db.deinit();
|
||||||
|
|
||||||
|
try db.migrate();
|
||||||
|
|
||||||
|
const uid = try db.createUser("admin", "admin@test.invalid", "admin");
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
const maybe_user = try db.db.oneAlloc(struct {
|
||||||
|
name: []const u8,
|
||||||
|
email: []const u8,
|
||||||
|
password: []const u8,
|
||||||
|
}, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{
|
||||||
|
.uid = uid.bytes,
|
||||||
|
});
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(maybe_user != null);
|
||||||
|
if (maybe_user) |user| {
|
||||||
|
try std.testing.expectEqualSlices(u8, "admin", user.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "create app" {
|
||||||
|
var db = try Database.init(":memory:", std.testing.allocator);
|
||||||
|
defer db.deinit();
|
||||||
|
|
||||||
|
try db.migrate();
|
||||||
|
|
||||||
|
_ = try db.createApp("app", &.{
|
||||||
|
"http://localhost:3000/callback",
|
||||||
|
"https://example.com/callback",
|
||||||
|
});
|
||||||
|
}
|
||||||
41
packages/myid/src/uuid.zig
Normal file
41
packages/myid/src/uuid.zig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
var lock: std.Thread.Mutex = .{};
|
||||||
|
var last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 };
|
||||||
|
var counter: std.atomic.Value(u32) = .{ .raw = 0 };
|
||||||
|
|
||||||
|
fn getCount(timestamp: u64) u32 {
|
||||||
|
lock.lock();
|
||||||
|
defer lock.unlock();
|
||||||
|
|
||||||
|
if (last_timestamp.swap(timestamp, .monotonic) != timestamp) {
|
||||||
|
counter.store(0, .monotonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
return counter.fetchAdd(1, .monotonic) % 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uuid_v7() [16]u8 {
|
||||||
|
const timestamp: u64 = @intCast(@max(0, std.time.milliTimestamp()));
|
||||||
|
const count = getCount(timestamp);
|
||||||
|
const random = blk: {
|
||||||
|
var bytes: [8]u8 = undefined;
|
||||||
|
std.crypto.random.bytes(&bytes);
|
||||||
|
break :blk bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
var res: [16]u8 = undefined;
|
||||||
|
|
||||||
|
res[0] = @truncate(timestamp >> 40);
|
||||||
|
res[1] = @truncate(timestamp >> 32);
|
||||||
|
res[2] = @truncate(timestamp >> 24);
|
||||||
|
res[3] = @truncate(timestamp >> 16);
|
||||||
|
res[4] = @truncate(timestamp >> 8);
|
||||||
|
res[5] = @truncate(timestamp);
|
||||||
|
res[6] = (@as(u8, 7) << 4) | @as(u8, @truncate((count >> 8) & 0x0F));
|
||||||
|
res[7] = @truncate(count);
|
||||||
|
res[8] = 0x80 | (random[0] & 0x3F);
|
||||||
|
@memcpy(res[9..16], random[1..8]);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user