init
This commit is contained in:
commit
3560650ac1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/zig-*
|
1
assets.zig
Normal file
1
assets.zig
Normal file
|
@ -0,0 +1 @@
|
|||
pub const steve_skin = @embedFile("assets/steve_skin.png");
|
BIN
assets/steve_skin.png
Normal file
BIN
assets/steve_skin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
34
build.zig
Normal file
34
build.zig
Normal file
|
@ -0,0 +1,34 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const assets_mod = b.addModule("assets", .{
|
||||
.root_source_file = .{ .path = "assets.zig" },
|
||||
});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "anvilauth",
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
exe.root_module.addImport("assets", assets_mod);
|
||||
|
||||
exe.linkSystemLibrary("libpq");
|
||||
exe.linkSystemLibrary("openssl");
|
||||
|
||||
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);
|
||||
}
|
7
build.zig.zon
Normal file
7
build.zig.zon
Normal file
|
@ -0,0 +1,7 @@
|
|||
.{
|
||||
.name = "anvilauth",
|
||||
.version = "0.0.0",
|
||||
|
||||
.dependencies = .{},
|
||||
.paths = .{""},
|
||||
}
|
14
conf.json
Normal file
14
conf.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"bind": {
|
||||
"ip": "127.0.0.1",
|
||||
"port": 8080
|
||||
},
|
||||
"base_url": "http://localhost:8081/",
|
||||
"postgres_url": "postgres://anvilauth:alec@localhost:5432/anvilauth",
|
||||
"forgejo_url": "http://localhost:8082/",
|
||||
"skin_domains": [
|
||||
"localhost",
|
||||
".localhost"
|
||||
],
|
||||
"server_name": "AnvilAuth test server"
|
||||
}
|
9
src/Config.zig
Normal file
9
src/Config.zig
Normal file
|
@ -0,0 +1,9 @@
|
|||
bind: struct {
|
||||
ip: []const u8,
|
||||
port: u16,
|
||||
},
|
||||
base_url: []const u8,
|
||||
postgres_url: [:0]const u8,
|
||||
forgejo_url: []const u8,
|
||||
skin_domains: []const []const u8,
|
||||
server_name: []const u8,
|
140
src/Db.zig
Normal file
140
src/Db.zig
Normal file
|
@ -0,0 +1,140 @@
|
|||
const std = @import("std");
|
||||
|
||||
const ffi = @import("ffi.zig");
|
||||
const c = ffi.c;
|
||||
|
||||
const Id = @import("Id.zig");
|
||||
|
||||
con: *c.PGconn,
|
||||
|
||||
const Db = @This();
|
||||
|
||||
pub fn initDb(self: Db) !void {
|
||||
const query =
|
||||
\\CREATE TABLE IF NOT EXISTS users (
|
||||
\\ id UUID NOT NULL PRIMARY KEY,
|
||||
\\ name VARCHAR NOT NULL UNIQUE
|
||||
\\);
|
||||
\\
|
||||
\\CREATE TABLE IF NOT EXISTS tokens (
|
||||
\\ id UUID NOT NULL PRIMARY KEY,
|
||||
\\ userid UUID NOT NULL REFERENCES users (id),
|
||||
\\ expiry BIGINT NOT NULL,
|
||||
\\ client_token VARCHAR NOT NULL
|
||||
\\);
|
||||
\\
|
||||
\\CREATE TABLE IF NOT EXISTS joins (
|
||||
\\ userid UUID NOT NULL PRIMARY KEY REFERENCES users (id),
|
||||
\\ serverid VARCHAR NOT NULL
|
||||
\\);
|
||||
;
|
||||
|
||||
const status = self.exec(query);
|
||||
defer status.deinit();
|
||||
try status.expectCommand();
|
||||
}
|
||||
|
||||
pub fn exec(self: Db, query: [:0]const u8) Result {
|
||||
return .{ .res = c.PQexec(self.con, query.ptr) };
|
||||
}
|
||||
|
||||
pub fn execParams(self: Db, query: [:0]const u8, params: anytype) Result {
|
||||
var args_buf: [1024 * 2]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&args_buf);
|
||||
const alloc = fba.allocator();
|
||||
|
||||
const nparams = @typeInfo(@TypeOf(params)).Struct.fields.len;
|
||||
|
||||
const vals = alloc.alloc([*]const u8, nparams) catch return Result.oom;
|
||||
const lengths = alloc.alloc(c_int, nparams) catch return Result.oom;
|
||||
const formats = alloc.alloc(c_int, nparams) catch return Result.oom;
|
||||
|
||||
inline for (params, vals, lengths, formats) |param, *val, *len, *format| {
|
||||
switch (@TypeOf(param)) {
|
||||
[]const u8, [:0]const u8 => {
|
||||
val.* = param.ptr;
|
||||
len.* = @intCast(param.len);
|
||||
format.* = 1;
|
||||
},
|
||||
[*:0]const u8 => {
|
||||
val.* = param;
|
||||
len.* = -1;
|
||||
format.* = 0;
|
||||
},
|
||||
i64 => {
|
||||
const bytes = std.mem.asBytes(&std.mem.nativeToBig(i64, param));
|
||||
val.* = bytes.ptr;
|
||||
len.* = @intCast(bytes.len);
|
||||
format.* = 1;
|
||||
},
|
||||
Id => {
|
||||
val.* = ¶m.bytes;
|
||||
len.* = param.bytes.len;
|
||||
format.* = 1;
|
||||
},
|
||||
else => @compileError("unsupported parameter type: " ++ @typeName(@TypeOf(param))),
|
||||
}
|
||||
}
|
||||
|
||||
const ret = c.PQexecParams(
|
||||
self.con,
|
||||
query,
|
||||
nparams,
|
||||
null,
|
||||
vals.ptr,
|
||||
lengths.ptr,
|
||||
formats.ptr,
|
||||
1,
|
||||
);
|
||||
return .{ .res = ret };
|
||||
}
|
||||
|
||||
pub const Result = struct {
|
||||
pub const oom = Result{ .res = null };
|
||||
|
||||
res: ?*c.PGresult,
|
||||
|
||||
pub inline fn deinit(self: Result) void {
|
||||
if (self.res) |res| c.PQclear(res);
|
||||
}
|
||||
|
||||
pub fn expect(self: Result, expected: c_uint) !void {
|
||||
const actual = c.PQresultStatus(self.res); // safe to call with null
|
||||
if (actual != expected) {
|
||||
std.log.err("expected result `{s}`, got `{s}` (msg: `{s}`)", .{
|
||||
c.PQresStatus(expected),
|
||||
c.PQresStatus(actual),
|
||||
@as([*:0]const u8, c.PQresultErrorField(self.res, c.PG_DIAG_MESSAGE_PRIMARY) orelse "<not present>"),
|
||||
});
|
||||
return error.UnexpectedSqlStatus;
|
||||
}
|
||||
}
|
||||
|
||||
pub inline fn expectTuples(self: Result) !void {
|
||||
try self.expect(c.PGRES_TUPLES_OK);
|
||||
}
|
||||
|
||||
pub inline fn expectCommand(self: Result) !void {
|
||||
try self.expect(c.PGRES_COMMAND_OK);
|
||||
}
|
||||
|
||||
pub inline fn rows(self: Result) c_int {
|
||||
return c.PQntuples(self.res);
|
||||
}
|
||||
|
||||
pub inline fn cols(self: Result) c_int {
|
||||
return c.PQnfields(self.res);
|
||||
}
|
||||
|
||||
pub fn getOptional(self: Result, comptime T: type, row: c_int, col: c_int) ?T {
|
||||
return if (c.PQgetisnull(self.res, row, col) == 1) null else self.get(T, row, col);
|
||||
}
|
||||
|
||||
pub inline fn get(self: Result, comptime T: type, row: c_int, col: c_int) T {
|
||||
return switch (T) {
|
||||
[]const u8 => c.PQgetvalue(self.res, row, col)[0..@intCast(c.PQgetlength(self.res, row, col))],
|
||||
Id => .{ .bytes = c.PQgetvalue(self.res, row, col)[0..16].* },
|
||||
else => @compileError("unsuppored type: " ++ @typeName(T)),
|
||||
};
|
||||
}
|
||||
};
|
30
src/Id.zig
Normal file
30
src/Id.zig
Normal file
|
@ -0,0 +1,30 @@
|
|||
/// This file implements something similar to UUIDs without the bullshit.
|
||||
/// It implements a simple even U-er UID, which is simply 16 random bytes.
|
||||
const std = @import("std");
|
||||
|
||||
bytes: [16]u8,
|
||||
|
||||
const Id = @This();
|
||||
|
||||
/// Returns a hex encoded version of the ID.
|
||||
pub fn toString(self: Id) [32]u8 {
|
||||
var ret: [32]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&ret, "{}", .{std.fmt.fmtSliceHexLower(&self.bytes)}) catch unreachable;
|
||||
return ret;
|
||||
}
|
||||
|
||||
pub fn genRandom(rand: std.rand.Random) Id {
|
||||
var bytes: [16]u8 = undefined;
|
||||
rand.bytes(&bytes);
|
||||
return .{ .bytes = bytes };
|
||||
}
|
||||
|
||||
// Parses the given string as an ID, or returns null if it is invalid.
|
||||
pub fn parse(str: []const u8) ?Id {
|
||||
if (str.len != 32) return null;
|
||||
var bytes: [16]u8 = undefined;
|
||||
for (&bytes, 0..) |*out, ihalf| {
|
||||
out.* = std.fmt.parseInt(u8, str[ihalf * 2 ..][0..2], 16) catch return null;
|
||||
}
|
||||
return .{ .bytes = bytes };
|
||||
}
|
93
src/JsonUserProfile.zig
Normal file
93
src/JsonUserProfile.zig
Normal file
|
@ -0,0 +1,93 @@
|
|||
// TODO: implement JSON serializer instead of this jank
|
||||
const std = @import("std");
|
||||
const c = ffi.c;
|
||||
|
||||
const ffi = @import("ffi.zig");
|
||||
|
||||
const Id = @import("Id.zig");
|
||||
|
||||
pub const Property = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
signature: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
id: []const u8,
|
||||
name: []const u8,
|
||||
properties: [1]Property,
|
||||
|
||||
const JsonUserProfile = @This();
|
||||
|
||||
/// id and skin_url are copied, name is only copied for properties!
|
||||
pub fn init(
|
||||
alloc: std.mem.Allocator,
|
||||
id: Id,
|
||||
name: []const u8,
|
||||
skin_url: []const u8,
|
||||
rsa: ?*c.RSA,
|
||||
) !JsonUserProfile {
|
||||
const id_s = try alloc.dupe(u8, &id.toString());
|
||||
errdefer alloc.free(id_s);
|
||||
|
||||
const textures_value = .{
|
||||
.timestamp = std.time.timestamp(),
|
||||
.profileId = id_s,
|
||||
.profileName = name,
|
||||
.textures = .{
|
||||
.SKIN = .{
|
||||
.url = skin_url,
|
||||
},
|
||||
},
|
||||
};
|
||||
const textures_json = try std.json.stringifyAlloc(alloc, textures_value, .{});
|
||||
defer alloc.free(textures_json);
|
||||
|
||||
const textures_b64 = try alloc.alloc(u8, std.base64.standard.Encoder.calcSize(textures_json.len));
|
||||
errdefer alloc.free(textures_b64);
|
||||
std.debug.assert(std.base64.standard.Encoder.encode(textures_b64, textures_json).len == textures_b64.len);
|
||||
|
||||
var textures_prop = Property{
|
||||
.name = "textures",
|
||||
.value = textures_b64,
|
||||
};
|
||||
|
||||
if (rsa) |r| {
|
||||
var hasher = std.crypto.hash.Sha1.init(.{});
|
||||
hasher.update(textures_b64);
|
||||
|
||||
const retbuf = try alloc.alloc(u8, @intCast(c.RSA_size(r)));
|
||||
defer alloc.free(retbuf);
|
||||
|
||||
var retlen: c_uint = 0;
|
||||
if (c.RSA_sign(
|
||||
c.NID_sha1,
|
||||
&hasher.finalResult(),
|
||||
std.crypto.hash.Sha1.digest_length,
|
||||
retbuf.ptr,
|
||||
&retlen,
|
||||
r,
|
||||
) != 1) return error.OpenSSLBorked;
|
||||
|
||||
const signature = retbuf[0..retlen];
|
||||
|
||||
const sig_b64 = try alloc.alloc(u8, std.base64.standard.Encoder.calcSize(signature.len));
|
||||
errdefer alloc.free(sig_b64);
|
||||
std.debug.assert(std.base64.standard.Encoder.encode(sig_b64, signature).len == sig_b64.len);
|
||||
|
||||
textures_prop.signature = sig_b64;
|
||||
}
|
||||
|
||||
return .{
|
||||
.id = id_s,
|
||||
.name = name,
|
||||
.properties = .{textures_prop},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: JsonUserProfile, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.id);
|
||||
for (self.properties) |prop| {
|
||||
alloc.free(prop.value);
|
||||
if (prop.signature) |sig| alloc.free(sig);
|
||||
}
|
||||
}
|
63
src/State.zig
Normal file
63
src/State.zig
Normal file
|
@ -0,0 +1,63 @@
|
|||
const std = @import("std");
|
||||
const c = @import("ffi.zig").c;
|
||||
|
||||
const Db = @import("Db.zig");
|
||||
|
||||
pub const SkinCache = std.StringHashMapUnmanaged(struct { has_skin: bool, expiration: i64 });
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
base_url: []const u8,
|
||||
forgejo_url: []const u8,
|
||||
skin_domains: []const []const u8,
|
||||
server_name: []const u8,
|
||||
http: std.http.Client,
|
||||
db: Db,
|
||||
rand: std.rand.Random,
|
||||
rsa: *c.RSA,
|
||||
x509: *c.X509,
|
||||
default_skin_url: []const u8,
|
||||
skin_cache: SkinCache,
|
||||
skin_cache_mtx: std.Thread.Mutex = .{},
|
||||
|
||||
const State = @This();
|
||||
|
||||
/// Gets the skin URL for a given user, if the user has a skin URL set. May do network IO for checking.
|
||||
pub fn getSkinUrl(self: *State, username: []const u8) !?[]const u8 {
|
||||
self.skin_cache_mtx.lock();
|
||||
defer self.skin_cache_mtx.unlock();
|
||||
|
||||
const url = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}/{s}/.anvilauth/raw/branch/master/skin.png",
|
||||
.{ self.forgejo_url, username },
|
||||
);
|
||||
errdefer self.allocator.free(url);
|
||||
|
||||
if (self.skin_cache.get(username)) |entry| {
|
||||
if (std.time.milliTimestamp() < entry.expiration) {
|
||||
if (entry.has_skin)
|
||||
return url;
|
||||
|
||||
self.allocator.free(url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const res = try self.http.fetch(.{
|
||||
.method = .HEAD,
|
||||
.location = .{ .url = url },
|
||||
});
|
||||
|
||||
const username_d = try self.allocator.dupe(u8, username);
|
||||
errdefer self.allocator.free(username_d);
|
||||
try self.skin_cache.put(self.allocator, username_d, .{
|
||||
.has_skin = res.status == .ok,
|
||||
.expiration = std.time.milliTimestamp() + std.time.ms_per_day,
|
||||
});
|
||||
|
||||
if (res.status == .ok)
|
||||
return url;
|
||||
|
||||
self.allocator.free(url);
|
||||
return null;
|
||||
}
|
92
src/conutil.zig
Normal file
92
src/conutil.zig
Normal file
|
@ -0,0 +1,92 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn sendJsonError(
|
||||
req: *std.http.Server.Request,
|
||||
code: std.http.Status,
|
||||
comptime fmt: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
const JsonError = struct {
|
||||
@"error": []const u8,
|
||||
errorMessage: []const u8,
|
||||
};
|
||||
|
||||
var fmt_buf: [1024 * 4]u8 = undefined;
|
||||
const json_error = JsonError{
|
||||
.@"error" = code.phrase() orelse "Unknown Error",
|
||||
.errorMessage = try std.fmt.bufPrint(&fmt_buf, "[AnvilAuth] " ++ fmt, args),
|
||||
};
|
||||
|
||||
var ser_buf: [1024 * 4]u8 = undefined;
|
||||
var ser_fbs = std.io.fixedBufferStream(&ser_buf);
|
||||
try std.json.stringify(json_error, .{}, ser_fbs.writer());
|
||||
|
||||
try req.respond(ser_fbs.getWritten(), .{
|
||||
.status = code,
|
||||
.extra_headers = &.{.{
|
||||
.name = "Content-Type",
|
||||
.value = "application/json",
|
||||
}},
|
||||
});
|
||||
}
|
||||
|
||||
pub const QueryParameterError = error{ MissingParameter, InvalidParameters };
|
||||
|
||||
pub fn parseQueryParametersFromUri(uri: std.Uri, comptime T: type) QueryParameterError!T {
|
||||
return if (uri.query) |q| try parseQueryParameters(q, T) else error.MissingParameter;
|
||||
}
|
||||
|
||||
pub fn parseQueryParameters(params: []const u8, comptime T: type) QueryParameterError!T {
|
||||
const DefaultedT = comptime blk: {
|
||||
const info = @typeInfo(T);
|
||||
var opt_fields: [info.Struct.fields.len]std.builtin.Type.StructField = undefined;
|
||||
|
||||
for (&opt_fields, info.Struct.fields) |*ofield, field| {
|
||||
ofield.* = .{
|
||||
.name = field.name,
|
||||
.type = ?field.type,
|
||||
.default_value = @as(*const ?field.type, &null),
|
||||
.is_comptime = false,
|
||||
.alignment = 0,
|
||||
};
|
||||
}
|
||||
|
||||
break :blk @Type(.{ .Struct = .{
|
||||
.layout = .Auto,
|
||||
.fields = &opt_fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
};
|
||||
|
||||
const def_out = try parseQueryParametersOrDefaults(params, DefaultedT);
|
||||
|
||||
var out: T = undefined;
|
||||
inline for (comptime std.meta.fieldNames(T)) |fname| {
|
||||
@field(out, fname) = @field(def_out, fname) orelse return error.MissingParameter;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn parseQueryParametersOrDefaults(params: []const u8, comptime T: type) QueryParameterError!T {
|
||||
var out: T = .{};
|
||||
|
||||
var iter = std.mem.splitScalar(u8, params, '&');
|
||||
while (iter.next()) |param| {
|
||||
var psplit = std.mem.splitScalar(u8, param, '=');
|
||||
const key = psplit.next() orelse return error.InvalidParameters;
|
||||
const value = psplit.next() orelse return error.InvalidParameters;
|
||||
if (psplit.next()) |_| return error.InvalidParameters;
|
||||
|
||||
inline for (comptime std.meta.fieldNames(T)) |fname| {
|
||||
if (std.mem.eql(u8, key, fname)) {
|
||||
@field(out, fname) = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
32
src/ffi.zig
Normal file
32
src/ffi.zig
Normal file
|
@ -0,0 +1,32 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("libpq-fe.h");
|
||||
@cInclude("openssl/rsa.h");
|
||||
@cInclude("openssl/bio.h");
|
||||
@cInclude("openssl/pem.h");
|
||||
@cInclude("openssl/x509.h");
|
||||
});
|
||||
|
||||
const libpq_log = std.log.scoped(.libpq);
|
||||
|
||||
pub fn libpqNoticeReceiverCb(_: ?*anyopaque, res: ?*const c.PGresult) callconv(.C) void {
|
||||
const severity_str: [*:0]const u8 = c.PQresultErrorField(res, c.PG_DIAG_SEVERITY_NONLOCALIZED) orelse
|
||||
c.PQresultErrorField(res, c.PG_DIAG_SEVERITY) orelse
|
||||
"LOG";
|
||||
|
||||
const msg = c.PQresultErrorField(res, c.PG_DIAG_MESSAGE_PRIMARY);
|
||||
const sev = std.mem.span(severity_str);
|
||||
if (std.mem.eql(u8, sev, "ERROR") or
|
||||
std.mem.eql(u8, sev, "FATAL") or
|
||||
std.mem.eql(u8, sev, "PANIC"))
|
||||
{
|
||||
libpq_log.err("{s}", .{msg});
|
||||
} else if (std.mem.eql(u8, sev, "WARNING")) {
|
||||
libpq_log.warn("{s}", .{msg});
|
||||
} else if (std.mem.eql(u8, sev, "DEBUG")) {
|
||||
libpq_log.debug("{s}", .{msg});
|
||||
} else {
|
||||
libpq_log.info("{s}", .{msg});
|
||||
}
|
||||
}
|
207
src/main.zig
Normal file
207
src/main.zig
Normal file
|
@ -0,0 +1,207 @@
|
|||
const std = @import("std");
|
||||
const c = ffi.c;
|
||||
|
||||
const ffi = @import("ffi.zig");
|
||||
|
||||
const Config = @import("Config.zig");
|
||||
const Db = @import("Db.zig");
|
||||
const State = @import("State.zig");
|
||||
|
||||
pub const std_options = std.Options{
|
||||
.log_level = if (@import("builtin").mode == .Debug) .debug else .info,
|
||||
};
|
||||
|
||||
pub fn main() !u8 {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const alloc = gpa.allocator();
|
||||
|
||||
const confpath = conf: {
|
||||
var argiter = try std.process.argsWithAllocator(alloc);
|
||||
defer argiter.deinit();
|
||||
_ = argiter.next() orelse unreachable;
|
||||
|
||||
const confpath = argiter.next() orelse {
|
||||
std.log.err("Need a config argument!", .{});
|
||||
return error.InvalidArgs;
|
||||
};
|
||||
const confpath_d = try alloc.dupe(u8, confpath);
|
||||
errdefer alloc.free(confpath_d);
|
||||
|
||||
if (argiter.next()) |_| {
|
||||
std.log.err("Too many arguments!", .{});
|
||||
return error.InvalidArgs;
|
||||
}
|
||||
|
||||
break :conf confpath_d;
|
||||
};
|
||||
defer alloc.free(confpath);
|
||||
|
||||
var conffile = try std.fs.cwd().openFile(confpath, .{});
|
||||
defer conffile.close();
|
||||
|
||||
var json_reader = std.json.reader(alloc, conffile.reader());
|
||||
defer json_reader.deinit();
|
||||
|
||||
const config_parsed = try std.json.parseFromTokenSource(
|
||||
Config,
|
||||
alloc,
|
||||
&json_reader,
|
||||
.{ .ignore_unknown_fields = true },
|
||||
);
|
||||
defer config_parsed.deinit();
|
||||
|
||||
const postgres_con = c.PQconnectdb(config_parsed.value.postgres_url) orelse unreachable;
|
||||
defer c.PQfinish(postgres_con);
|
||||
|
||||
if (c.PQstatus(postgres_con) != c.CONNECTION_OK) {
|
||||
std.log.err("connecting to DB: {s}", .{c.PQerrorMessage(postgres_con)});
|
||||
return error.DatabaseConnect;
|
||||
}
|
||||
|
||||
_ = c.PQsetNoticeReceiver(postgres_con, ffi.libpqNoticeReceiverCb, null);
|
||||
|
||||
const db = Db{ .con = postgres_con };
|
||||
|
||||
std.log.info("initializing database", .{});
|
||||
try db.initDb();
|
||||
|
||||
var rand = rand: {
|
||||
var seed: [32]u8 = undefined;
|
||||
std.crypto.random.bytes(&seed);
|
||||
break :rand std.rand.DefaultCsprng.init(seed);
|
||||
};
|
||||
|
||||
const rsa = c.RSA_new() orelse return error.OutOfMemory;
|
||||
{
|
||||
errdefer c.RSA_free(rsa);
|
||||
|
||||
const bn = c.BN_new() orelse return error.OutOfMemory;
|
||||
defer c.BN_free(bn);
|
||||
|
||||
if (c.BN_set_word(bn, c.RSA_F4) != 1) return error.OpenSSLBorked;
|
||||
if (c.RSA_generate_key_ex(rsa, 1024 * 4, bn, null) != 1) return error.OpenSSLBorked;
|
||||
}
|
||||
|
||||
const pkey = c.EVP_PKEY_new() orelse return error.OutOfMemory;
|
||||
defer c.EVP_PKEY_free(pkey);
|
||||
|
||||
if (c.EVP_PKEY_assign_RSA(pkey, rsa) != 1) return error.OpenSSLBorked;
|
||||
|
||||
const x509 = c.X509_new() orelse return error.OutOfMemory;
|
||||
defer c.X509_free(x509);
|
||||
|
||||
{
|
||||
if (c.ASN1_INTEGER_set(c.X509_get_serialNumber(x509), 1) != 1) return error.OpenSSLBorked;
|
||||
_ = c.X509_gmtime_adj(c.X509_get_notBefore(x509), 0);
|
||||
_ = c.X509_gmtime_adj(c.X509_get_notAfter(x509), std.time.s_per_day * 365);
|
||||
if (c.X509_set_pubkey(x509, pkey) != 1) return error.OpenSSLBorked;
|
||||
|
||||
const subjname = c.X509_get_subject_name(x509);
|
||||
if (c.X509_set_issuer_name(x509, subjname) != 1) return error.OpenSSLBorked;
|
||||
if (c.X509_sign(x509, pkey, c.EVP_sha1()) == 0) return error.OpenSSLBorked;
|
||||
}
|
||||
|
||||
const base_url = std.mem.trimRight(u8, config_parsed.value.base_url, "/");
|
||||
const default_skin_url = try std.fmt.allocPrint(alloc, "{s}/default_skin", .{base_url});
|
||||
defer alloc.free(default_skin_url);
|
||||
|
||||
var state = State{
|
||||
.allocator = alloc,
|
||||
.base_url = base_url,
|
||||
.forgejo_url = std.mem.trimRight(u8, config_parsed.value.forgejo_url, "/"),
|
||||
.skin_domains = config_parsed.value.skin_domains,
|
||||
.server_name = config_parsed.value.server_name,
|
||||
.http = .{ .allocator = alloc },
|
||||
.db = .{ .con = postgres_con },
|
||||
.rand = rand.random(),
|
||||
.rsa = rsa,
|
||||
.x509 = x509,
|
||||
.default_skin_url = default_skin_url,
|
||||
.skin_cache = State.SkinCache{},
|
||||
};
|
||||
defer state.http.deinit();
|
||||
defer {
|
||||
var kiter = state.skin_cache.keyIterator();
|
||||
while (kiter.next()) |key| {
|
||||
alloc.free(key.*);
|
||||
}
|
||||
state.skin_cache.deinit(alloc);
|
||||
}
|
||||
|
||||
const addr = try std.net.Address.parseIp(config_parsed.value.bind.ip, config_parsed.value.bind.port);
|
||||
var server = try addr.listen(.{});
|
||||
std.log.info("listening on {}", .{addr});
|
||||
|
||||
while (true) {
|
||||
var con = try server.accept();
|
||||
errdefer con.stream.close();
|
||||
|
||||
const thread = try std.Thread.spawn(.{}, handleConnection, .{ con, &state });
|
||||
thread.detach();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn handleConnection(con: std.net.Server.Connection, state: *State) void {
|
||||
var read_buf: [1024 * 4]u8 = undefined;
|
||||
const http = std.http.Server.init(con, &read_buf);
|
||||
tryHandleConnection(http, state) catch |e| {
|
||||
std.log.warn("error in connection handler: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
fn tryHandleConnection(srv_: std.http.Server, state: *State) !void {
|
||||
var srv = srv_;
|
||||
defer srv.connection.stream.close();
|
||||
|
||||
while (true) {
|
||||
var req = srv.receiveHead() catch |e| switch (e) {
|
||||
error.HttpConnectionClosing => return,
|
||||
else => return e,
|
||||
};
|
||||
|
||||
const path = req.head.target;
|
||||
|
||||
std.log.info("{s} {s} from {}", .{
|
||||
@tagName(req.head.method),
|
||||
path,
|
||||
srv.connection.address,
|
||||
});
|
||||
|
||||
inline for (.{
|
||||
@import("routes/root.zig"),
|
||||
@import("routes/aliapi/index.zig"),
|
||||
@import("routes/aliapi/api/profiles/minecraft.zig"),
|
||||
@import("routes/aliapi/authserver/authenticate.zig"),
|
||||
@import("routes/aliapi/sessionserver/session/minecraft/has_joined.zig"),
|
||||
@import("routes/aliapi/sessionserver/session/minecraft/join.zig"),
|
||||
@import("routes/aliapi/sessionserver/session/minecraft/profile.zig"),
|
||||
@import("routes/default_skin.zig"),
|
||||
}) |route| {
|
||||
if (route.matches(path)) {
|
||||
route.call(&req, state) catch |e| {
|
||||
//if (res.state == .waited) {
|
||||
// try @import("conutil.zig").sendJsonError(
|
||||
// &res,
|
||||
// .internal_server_error,
|
||||
// "alec",
|
||||
// .{},
|
||||
// );
|
||||
//}
|
||||
return e;
|
||||
};
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
try req.respond("", .{ .status = .not_found });
|
||||
//res.status = .not_found;
|
||||
//res.transfer_encoding = .{ .content_length = 0 };
|
||||
//try res.send();
|
||||
//try res.finish();
|
||||
}
|
||||
|
||||
if (srv.state == .closing) break;
|
||||
}
|
||||
}
|
93
src/routes/aliapi/api/profiles/minecraft.zig
Normal file
93
src/routes/aliapi/api/profiles/minecraft.zig
Normal file
|
@ -0,0 +1,93 @@
|
|||
const std = @import("std");
|
||||
|
||||
const conutil = @import("../../../../conutil.zig");
|
||||
|
||||
const Id = @import("../../../../Id.zig");
|
||||
const State = @import("../../../../State.zig");
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.eql(u8, path, "/aliapi/api/profiles/minecraft");
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
if (req.head.method != .POST) {
|
||||
try conutil.sendJsonError(
|
||||
req,
|
||||
.method_not_allowed,
|
||||
"only POST requests are allowed to this endpoint!",
|
||||
.{},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var json_reader = std.json.reader(state.allocator, try req.reader());
|
||||
defer json_reader.deinit();
|
||||
|
||||
const usernames_req = try std.json.parseFromTokenSource(
|
||||
[][]u8,
|
||||
state.allocator,
|
||||
&json_reader,
|
||||
.{},
|
||||
);
|
||||
defer usernames_req.deinit();
|
||||
|
||||
if (usernames_req.value.len > 10) {
|
||||
try conutil.sendJsonError(
|
||||
req,
|
||||
.bad_request,
|
||||
"Only up to 10 usernames may be requested at a time, got {}",
|
||||
.{usernames_req.value.len},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (usernames_req.value) |username| {
|
||||
for (username) |*char|
|
||||
char.* = std.ascii.toLower(char.*);
|
||||
}
|
||||
|
||||
var param_str = std.ArrayList(u8).init(state.allocator);
|
||||
defer param_str.deinit();
|
||||
try param_str.append('{');
|
||||
for (usernames_req.value, 0..) |username, i| {
|
||||
if (i != 0) try param_str.append(',');
|
||||
try param_str.appendSlice(username);
|
||||
}
|
||||
try param_str.append('}');
|
||||
try param_str.append(0);
|
||||
|
||||
const db_res = state.db.execParams(
|
||||
\\SELECT id, name FROM users
|
||||
\\WHERE lower(name)=any($1::varchar[]);
|
||||
, .{@as([*:0]const u8, @ptrCast(param_str.items.ptr))});
|
||||
defer db_res.deinit();
|
||||
try db_res.expectTuples();
|
||||
|
||||
if (db_res.cols() != 2) return error.InvalidResultFromPostgresServer;
|
||||
|
||||
var response_json = std.ArrayList(u8).init(state.allocator);
|
||||
defer response_json.deinit();
|
||||
|
||||
var json_writer = std.json.writeStream(response_json.writer(), .{});
|
||||
|
||||
try json_writer.beginArray();
|
||||
for (0..@intCast(db_res.rows())) |rowidx| {
|
||||
const id = db_res.get(Id, @intCast(rowidx), 0);
|
||||
const name = db_res.get([]const u8, @intCast(rowidx), 1);
|
||||
|
||||
try json_writer.beginObject();
|
||||
try json_writer.objectField("id");
|
||||
try json_writer.write(&id.toString());
|
||||
try json_writer.objectField("name");
|
||||
try json_writer.write(name);
|
||||
try json_writer.endObject();
|
||||
}
|
||||
try json_writer.endArray();
|
||||
|
||||
try req.respond(response_json.items, .{
|
||||
.extra_headers = &.{.{
|
||||
.name = "Content-Type",
|
||||
.value = "application/json",
|
||||
}},
|
||||
});
|
||||
}
|
185
src/routes/aliapi/authserver/authenticate.zig
Normal file
185
src/routes/aliapi/authserver/authenticate.zig
Normal file
|
@ -0,0 +1,185 @@
|
|||
const std = @import("std");
|
||||
const c = ffi.c;
|
||||
|
||||
const ffi = @import("../../../ffi.zig");
|
||||
const conutil = @import("../../../conutil.zig");
|
||||
|
||||
const Id = @import("../../../Id.zig");
|
||||
const State = @import("../../../State.zig");
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.eql(u8, path, "/aliapi/authserver/authenticate");
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
if (req.head.method != .POST) {
|
||||
try conutil.sendJsonError(
|
||||
req,
|
||||
.method_not_allowed,
|
||||
"only POST requests are allowed to this endpoint!",
|
||||
.{},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const Request = struct {
|
||||
username: [:0]const u8,
|
||||
password: []const u8,
|
||||
clientToken: ?[:0]const u8 = null,
|
||||
requestUser: bool = false,
|
||||
};
|
||||
|
||||
var json_reader = std.json.reader(state.allocator, try req.reader());
|
||||
defer json_reader.deinit();
|
||||
const req_payload = std.json.parseFromTokenSource(Request, state.allocator, &json_reader, .{
|
||||
.ignore_unknown_fields = true,
|
||||
}) catch |e| {
|
||||
try conutil.sendJsonError(req, .bad_request, "unable to parse JSON payload: {}", .{e});
|
||||
return;
|
||||
};
|
||||
defer req_payload.deinit();
|
||||
|
||||
std.log.info("authentification attempt from user {s}", .{req_payload.value.username});
|
||||
|
||||
const valid = valid: {
|
||||
const forgejo_url = try std.fmt.allocPrint(state.allocator, "{s}/api/v1/user", .{state.forgejo_url});
|
||||
defer state.allocator.free(forgejo_url);
|
||||
|
||||
const unenc_auth = try std.fmt.allocPrint(
|
||||
state.allocator,
|
||||
"{s}:{s}",
|
||||
.{ req_payload.value.username, req_payload.value.password },
|
||||
);
|
||||
defer state.allocator.free(unenc_auth);
|
||||
|
||||
const auth_prefix = "Basic ";
|
||||
const auth_str = try state.allocator.alloc(
|
||||
u8,
|
||||
auth_prefix.len + std.base64.standard.Encoder.calcSize(unenc_auth.len),
|
||||
);
|
||||
defer state.allocator.free(auth_str);
|
||||
|
||||
@memcpy(auth_str[0..auth_prefix.len], auth_prefix);
|
||||
_ = std.base64.standard.Encoder.encode(auth_str[auth_prefix.len..], unenc_auth);
|
||||
|
||||
var fres = try state.http.fetch(.{
|
||||
.location = .{ .url = forgejo_url },
|
||||
.extra_headers = &.{.{
|
||||
.name = "Authorization",
|
||||
.value = auth_str,
|
||||
}},
|
||||
});
|
||||
|
||||
break :valid fres.status.class() == .success;
|
||||
};
|
||||
|
||||
if (valid) {
|
||||
std.log.info("issuing new token", .{});
|
||||
// Ensure user record exists
|
||||
const insert_result = state.db.execParams(
|
||||
"INSERT INTO users (id, name) VALUES (gen_random_uuid(), $1) ON CONFLICT DO NOTHING;",
|
||||
.{req_payload.value.username},
|
||||
);
|
||||
defer insert_result.deinit();
|
||||
try insert_result.expectCommand();
|
||||
|
||||
// Get user UUID
|
||||
const sel_result = state.db.execParams(
|
||||
"SELECT id FROM users WHERE name=$1::text;",
|
||||
.{req_payload.value.username},
|
||||
);
|
||||
defer sel_result.deinit();
|
||||
try sel_result.expectTuples();
|
||||
|
||||
if (sel_result.rows() != 1 or sel_result.cols() != 1)
|
||||
return error.InvalidResultFromPostgresServer;
|
||||
|
||||
const userid = sel_result.get(Id, 0, 0);
|
||||
|
||||
const Profile = struct {
|
||||
name: []const u8,
|
||||
id: []const u8,
|
||||
};
|
||||
|
||||
const ResponsePayload = struct {
|
||||
user: ?struct {
|
||||
username: []const u8,
|
||||
properties: []const struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
},
|
||||
id: []const u8,
|
||||
} = null,
|
||||
clientToken: []const u8,
|
||||
accessToken: []const u8,
|
||||
availableProfiles: []const Profile,
|
||||
selectedProfile: Profile,
|
||||
};
|
||||
|
||||
var gen_token_buf: [32:0]u8 = undefined;
|
||||
const client_token: [:0]const u8 = req_payload.value.clientToken orelse gentoken: {
|
||||
// TODO: according to https://wiki.vg/Legacy_Mojang_Authentication, the normal server
|
||||
// would invalidate all existing tokens here. This makes no sense, so we don't do it.
|
||||
@memcpy(&gen_token_buf, &Id.genRandom(state.rand).toString());
|
||||
break :gentoken &gen_token_buf;
|
||||
};
|
||||
|
||||
// remains valid for one week
|
||||
const expiry = std.time.timestamp() + std.time.ms_per_week;
|
||||
|
||||
const tokenid = Id.genRandom(state.rand);
|
||||
|
||||
const add_tok_stat = state.db.execParams(
|
||||
\\INSERT INTO tokens (id, userid, expiry, client_token)
|
||||
\\ VALUES ($1::uuid, $2::uuid, $3::bigint, $4::text);
|
||||
,
|
||||
.{ tokenid, userid, expiry, client_token },
|
||||
);
|
||||
defer add_tok_stat.deinit();
|
||||
try add_tok_stat.expectCommand();
|
||||
|
||||
const uid_hex = userid.toString();
|
||||
|
||||
const profile = Profile{
|
||||
.name = req_payload.value.username,
|
||||
.id = &uid_hex,
|
||||
};
|
||||
|
||||
const res_payload = ResponsePayload{
|
||||
.user = if (req_payload.value.requestUser) .{
|
||||
.username = req_payload.value.username,
|
||||
.id = &uid_hex,
|
||||
.properties = &.{
|
||||
// There is no acceptable real-world use-case where this would be incorrect.
|
||||
.{
|
||||
.name = "preferredLanguage",
|
||||
.value = "en",
|
||||
},
|
||||
},
|
||||
} else null,
|
||||
.clientToken = client_token,
|
||||
.accessToken = &tokenid.toString(),
|
||||
.availableProfiles = &.{profile},
|
||||
.selectedProfile = profile,
|
||||
};
|
||||
|
||||
const data = try std.json.stringifyAlloc(
|
||||
state.allocator,
|
||||
res_payload,
|
||||
.{ .emit_null_optional_fields = false },
|
||||
);
|
||||
defer state.allocator.free(data);
|
||||
|
||||
try req.respond(data, .{
|
||||
.extra_headers = &.{.{
|
||||
.name = "Content-Type",
|
||||
.value = "application/json",
|
||||
}},
|
||||
});
|
||||
} else {
|
||||
std.log.warn("credentials invalid", .{});
|
||||
|
||||
// .forbidden makes no sense here, but that was mojank's idea
|
||||
try conutil.sendJsonError(req, .forbidden, "invalid credentials", .{});
|
||||
}
|
||||
}
|
45
src/routes/aliapi/index.zig
Normal file
45
src/routes/aliapi/index.zig
Normal file
|
@ -0,0 +1,45 @@
|
|||
const std = @import("std");
|
||||
const c = ffi.c;
|
||||
|
||||
const ffi = @import("../../ffi.zig");
|
||||
|
||||
const State = @import("../../State.zig");
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.eql(u8, path, "/aliapi");
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
const bio = c.BIO_new(c.BIO_s_mem()) orelse return error.OutOfMemory;
|
||||
defer _ = c.BIO_free(bio);
|
||||
|
||||
if (c.PEM_write_bio_X509_PUBKEY(
|
||||
bio,
|
||||
c.X509_get_X509_PUBKEY(state.x509),
|
||||
) != 1) return error.OpenSSLBorked;
|
||||
|
||||
var dataptr: ?[*]const u8 = null;
|
||||
const datalen: usize = @intCast(c.BIO_get_mem_data(bio, &dataptr));
|
||||
const keydata = dataptr.?[0..datalen];
|
||||
|
||||
const response_payload = .{
|
||||
.meta = .{
|
||||
.serverName = state.server_name,
|
||||
.implementationName = "AnvilAuth",
|
||||
.implementationVersion = "0.0.0",
|
||||
.links = .{ .source = "https://git.tilera.org/Anvilcraft/AnvilAuth" },
|
||||
},
|
||||
.skinDomains = state.skin_domains,
|
||||
.signaturePublickey = keydata,
|
||||
};
|
||||
|
||||
const json = try std.json.stringifyAlloc(state.allocator, response_payload, .{});
|
||||
defer state.allocator.free(json);
|
||||
|
||||
try req.respond(json, .{
|
||||
.extra_headers = &.{.{
|
||||
.name = "Content-Type",
|
||||
.value = "application/json",
|
||||
}}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
const std = @import("std");
|
||||
const c = ffi.c;
|
||||
|
||||
const conutil = @import("../../../../../conutil.zig");
|
||||
const ffi = @import("../../../../../ffi.zig");
|
||||
|
||||
const Id = @import("../../../../../Id.zig");
|
||||
const JsonUserProfile = @import("../../../../../JsonUserProfile.zig");
|
||||
const State = @import("../../../../../State.zig");
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.startsWith(u8, path, "/aliapi/sessionserver/session/minecraft/hasJoined");
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
const req_url = try std.Uri.parseWithoutScheme(req.head.target);
|
||||
const params = conutil.parseQueryParametersFromUri(req_url, struct {
|
||||
serverId: []const u8,
|
||||
username: []const u8,
|
||||
}) catch |e| {
|
||||
try conutil.sendJsonError(req, .bad_request, "invalid query parameters: {}", .{e});
|
||||
return;
|
||||
};
|
||||
|
||||
const sel_dbret = state.db.execParams(
|
||||
\\SELECT users.id
|
||||
\\FROM users, joins
|
||||
\\WHERE
|
||||
\\ joins.userid = users.id AND
|
||||
\\ users.name = $1::text AND
|
||||
\\ joins.serverid = $2::text;
|
||||
, .{ params.username, params.serverId });
|
||||
defer sel_dbret.deinit();
|
||||
try sel_dbret.expectTuples();
|
||||
|
||||
if (sel_dbret.cols() != 1) return error.InvalidResultFromPostgresServer;
|
||||
|
||||
if (sel_dbret.rows() >= 1) {
|
||||
const id = sel_dbret.get(Id, 0, 0);
|
||||
|
||||
const skin_url = try state.getSkinUrl(params.username);
|
||||
defer if (skin_url) |url| state.allocator.free(url);
|
||||
|
||||
const profile = try JsonUserProfile.init(
|
||||
state.allocator,
|
||||
id,
|
||||
params.username,
|
||||
skin_url orelse state.default_skin_url,
|
||||
state.rsa,
|
||||
);
|
||||
defer profile.deinit(state.allocator);
|
||||
|
||||
const profile_json = try std.json.stringifyAlloc(
|
||||
state.allocator,
|
||||
profile,
|
||||
.{ .emit_null_optional_fields = false },
|
||||
);
|
||||
defer state.allocator.free(profile_json);
|
||||
|
||||
try req.respond(profile_json, .{ .extra_headers = &.{.{
|
||||
.name = "Content-Type",
|
||||
.value = "application/json",
|
||||
}} });
|
||||
|
||||
const del_dbret = state.db.execParams("DELETE FROM joins WHERE userid = $1::uuid;", .{id});
|
||||
defer del_dbret.deinit();
|
||||
try del_dbret.expectCommand();
|
||||
} else {
|
||||
// task failed successfully! (good api design, mojank!)
|
||||
try req.respond("", .{
|
||||
.status = .no_content,
|
||||
});
|
||||
}
|
||||
}
|
68
src/routes/aliapi/sessionserver/session/minecraft/join.zig
Normal file
68
src/routes/aliapi/sessionserver/session/minecraft/join.zig
Normal file
|
@ -0,0 +1,68 @@
|
|||
const std = @import("std");
|
||||
const c = ffi.c;
|
||||
|
||||
const conutil = @import("../../../../../conutil.zig");
|
||||
const ffi = @import("../../../../../ffi.zig");
|
||||
|
||||
const Id = @import("../../../../../Id.zig");
|
||||
const State = @import("../../../../../State.zig");
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.eql(u8, path, "/aliapi/sessionserver/session/minecraft/join");
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
const Request = struct {
|
||||
accessToken: []const u8,
|
||||
selectedProfile: []const u8,
|
||||
serverId: [:0]const u8,
|
||||
};
|
||||
|
||||
var json_reader = std.json.reader(state.allocator, try req.reader());
|
||||
defer json_reader.deinit();
|
||||
const req_payload = std.json.parseFromTokenSource(Request, state.allocator, &json_reader, .{
|
||||
.ignore_unknown_fields = true,
|
||||
}) catch |e| {
|
||||
try conutil.sendJsonError(req, .bad_request, "unable to parse JSON payload: {}", .{e});
|
||||
return;
|
||||
};
|
||||
defer req_payload.deinit();
|
||||
|
||||
const access_token = Id.parse(req_payload.value.accessToken) orelse {
|
||||
try conutil.sendJsonError(req, .bad_request, "accessToken is not a valid ID!", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const sel_profile = Id.parse(req_payload.value.selectedProfile) orelse {
|
||||
try conutil.sendJsonError(req, .bad_request, "selectedProfile is not a valid ID!", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const dbret = state.db.execParams("SELECT userid FROM tokens WHERE id=$1::uuid;", .{access_token});
|
||||
defer dbret.deinit();
|
||||
try dbret.expectTuples();
|
||||
|
||||
if (dbret.cols() != 1) return error.InvalidResultFromPostgresServer;
|
||||
|
||||
if (dbret.rows() >= 1) {
|
||||
const token_user = dbret.get(Id, 0, 0);
|
||||
if (std.mem.eql(u8, &sel_profile.bytes, &token_user.bytes)) {
|
||||
const ins_dbret = state.db.execParams(
|
||||
\\INSERT INTO joins (userid, serverid)
|
||||
\\VALUES ($1::uuid, $2::text)
|
||||
\\ON CONFLICT (userid) DO
|
||||
\\UPDATE SET serverid = EXCLUDED.serverid;
|
||||
, .{ token_user, req_payload.value.serverId });
|
||||
defer ins_dbret.deinit();
|
||||
try ins_dbret.expectCommand();
|
||||
|
||||
try req.respond("", .{ .status = .no_content });
|
||||
} else {
|
||||
// acces token belongs to other user (hehe)
|
||||
try conutil.sendJsonError(req, .forbidden, "invalid access token!", .{});
|
||||
}
|
||||
} else {
|
||||
// invalid access token
|
||||
try conutil.sendJsonError(req, .forbidden, "invalid access token!", .{});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
const std = @import("std");
|
||||
const c = ffi.c;
|
||||
|
||||
const ffi = @import("../../../../../ffi.zig");
|
||||
const conutil = @import("../../../../../conutil.zig");
|
||||
|
||||
const Id = @import("../../../../../Id.zig");
|
||||
const JsonUserProfile = @import("../../../../../JsonUserProfile.zig");
|
||||
const State = @import("../../../../../State.zig");
|
||||
|
||||
const path_prefix = "/aliapi/sessionserver/session/minecraft/profile/";
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.startsWith(u8, path, path_prefix);
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
const req_url = try std.Uri.parseWithoutScheme(req.head.target);
|
||||
|
||||
// This is sound as we only go here if the path starts with path_prefix.
|
||||
const profile_id = Id.parse(req_url.path[path_prefix.len..]) orelse {
|
||||
try conutil.sendJsonError(
|
||||
req,
|
||||
.bad_request,
|
||||
"not a valid UUID: {s} (NOTE: AnvilAuth technically doesn't use UUIDs, this endpoint expects 16 hex-encoded, undelimited bytes.)",
|
||||
.{req_url.path[path_prefix.len..]},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
const unsigned = unsigned: {
|
||||
if (req_url.query == null) break :unsigned true;
|
||||
|
||||
const params = conutil.parseQueryParametersOrDefaults(
|
||||
req_url.query.?,
|
||||
struct { unsigned: []const u8 = "true" },
|
||||
) catch |e| {
|
||||
try conutil.sendJsonError(req, .bad_request, "invalid query parameters: {}", .{e});
|
||||
return;
|
||||
};
|
||||
|
||||
if (std.mem.eql(u8, params.unsigned, "true")) {
|
||||
break :unsigned true;
|
||||
} else if (std.mem.eql(u8, params.unsigned, "false")) {
|
||||
break :unsigned false;
|
||||
} else {
|
||||
try conutil.sendJsonError(
|
||||
req,
|
||||
.bad_request,
|
||||
"`unsigned` parameter must be either `true` or `false`, got `{s}`",
|
||||
.{params.unsigned},
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const status = state.db.execParams(
|
||||
"SELECT name FROM users WHERE id=$1::uuid;",
|
||||
.{profile_id},
|
||||
);
|
||||
defer status.deinit();
|
||||
try status.expectTuples();
|
||||
|
||||
if (status.cols() != 1) return error.InvalidResultFromPostgresServer;
|
||||
|
||||
if (status.rows() >= 1) {
|
||||
const username = status.get([]const u8, 0, 0);
|
||||
|
||||
const skin_url = try state.getSkinUrl(username);
|
||||
defer if (skin_url) |url| state.allocator.free(url);
|
||||
|
||||
const uprofile = try JsonUserProfile.init(
|
||||
state.allocator,
|
||||
profile_id,
|
||||
username,
|
||||
skin_url orelse state.default_skin_url,
|
||||
if (unsigned) null else state.rsa,
|
||||
);
|
||||
defer uprofile.deinit(state.allocator);
|
||||
|
||||
const response_data = try std.json.stringifyAlloc(
|
||||
state.allocator,
|
||||
uprofile,
|
||||
.{ .emit_null_optional_fields = false },
|
||||
);
|
||||
defer state.allocator.free(response_data);
|
||||
|
||||
try req.respond(response_data, .{
|
||||
.extra_headers = &.{.{
|
||||
.name = "Content-Type",
|
||||
.value = "application/json",
|
||||
}},
|
||||
});
|
||||
} else {
|
||||
// -> User not found
|
||||
// This retarded API design brought to you by Mojang!
|
||||
try req.respond("", .{ .status = .no_content });
|
||||
}
|
||||
}
|
19
src/routes/default_skin.zig
Normal file
19
src/routes/default_skin.zig
Normal file
|
@ -0,0 +1,19 @@
|
|||
const std = @import("std");
|
||||
const assets = @import("assets");
|
||||
|
||||
const State = @import("../State.zig");
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.eql(u8, path, "/default_skin");
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
_ = state;
|
||||
|
||||
try req.respond(assets.steve_skin, .{
|
||||
.extra_headers = &.{.{
|
||||
.name = "Content-Type",
|
||||
.value = "image/png",
|
||||
}},
|
||||
});
|
||||
}
|
19
src/routes/root.zig
Normal file
19
src/routes/root.zig
Normal file
|
@ -0,0 +1,19 @@
|
|||
const std = @import("std");
|
||||
|
||||
const State = @import("../State.zig");
|
||||
|
||||
pub fn matches(path: []const u8) bool {
|
||||
return std.mem.eql(u8, path, "/");
|
||||
}
|
||||
|
||||
pub fn call(req: *std.http.Server.Request, state: *State) !void {
|
||||
const base_url = try std.fmt.allocPrint(state.allocator, "{s}/aliapi", .{state.base_url});
|
||||
defer state.allocator.free(base_url);
|
||||
|
||||
try req.respond("", .{
|
||||
.extra_headers = &.{.{
|
||||
.name = "x-authlib-injector-api-location",
|
||||
.value = base_url,
|
||||
}},
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue