feat: implement support for capes

This commit is contained in:
LordMZTE 2024-03-06 16:08:04 +01:00
parent 94d96f1657
commit 69832efce5
Signed by: LordMZTE
GPG key ID: B64802DC33A64FF6
7 changed files with 175 additions and 45 deletions

View file

@ -6,10 +6,12 @@
"base_url": "http://localhost:8081/",
"postgres_url": "postgres://anvilauth:alec@localhost:5432/anvilauth",
"forgejo_url": "https://git.tilera.org/",
"anvillib_url": "https://api.tilera.xyz/anvillib/",
"skin_domains": [
"localhost",
".localhost",
"git.tilera.org"
"git.tilera.org",
"s3.tilera.xyz"
],
"server_name": "AnvilAuth test server"
}

View file

@ -5,5 +5,6 @@ bind: struct {
base_url: []const u8,
postgres_url: [:0]const u8,
forgejo_url: []const u8,
anvillib_url: ?[]const u8,
skin_domains: []const []const u8,
server_name: []const u8,

View file

@ -1,13 +1,20 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const UUID = @import("uuid").Uuid;
const Db = @import("Db.zig");
pub const SkinCache = std.StringHashMapUnmanaged(struct { has_skin: bool, expiration: i64 });
pub const UserCache = std.StringHashMapUnmanaged(struct {
skin_url: ?[]const u8,
cape_url: ?[]const u8,
expiration: i64,
});
allocator: std.mem.Allocator,
base_url: []const u8,
forgejo_url: []const u8,
anvillib_url: ?[]const u8,
skin_domains: []const []const u8,
server_name: []const u8,
http: std.http.Client,
@ -16,49 +23,155 @@ rand: std.rand.Random,
rsa: *c.RSA,
x509: *c.X509,
default_skin_url: []const u8,
skin_cache: SkinCache,
skin_cache_mtx: std.Thread.Mutex = .{},
user_cache: UserCache,
user_cache_mtx: std.Thread.Mutex = .{},
const State = @This();
pub const TextureUrls = struct {
skin_url: ?[]const u8,
cape_url: ?[]const u8,
};
/// 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();
pub fn getTextureUrls(self: *State, username: []const u8, uuid: UUID) !TextureUrls {
self.user_cache_mtx.lock();
defer self.user_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 (self.user_cache.get(username)) |entry| {
if (std.time.milliTimestamp() < entry.expiration) {
if (entry.has_skin)
return url;
self.allocator.free(url);
return null;
return .{
.skin_url = entry.skin_url,
.cape_url = entry.cape_url,
};
}
if (entry.skin_url) |skin| self.allocator.free(skin);
if (entry.cape_url) |cape| self.allocator.free(cape);
std.debug.assert(self.user_cache.remove(username));
}
std.log.info("checking presence of custom skin for user '{s}'", .{username});
const res = try self.http.fetch(.{
.method = .HEAD,
.location = .{ .url = url },
});
std.log.info("checking presence of custom skin and cape for user '{s}'", .{username});
const skin_url = skin: {
const skin_url = try std.fmt.allocPrint(
self.allocator,
"{s}/{s}/.anvilauth/raw/branch/master/skin.png",
.{ self.forgejo_url, username },
);
errdefer self.allocator.free(skin_url);
const skin_res = try self.http.fetch(.{
.method = .HEAD,
.location = .{ .url = skin_url },
});
if (skin_res.status == .ok)
break :skin skin_url;
self.allocator.free(skin_url);
break :skin null;
};
errdefer self.allocator.free(skin_url);
const cape_url = cape: {
if (self.anvillib_url == null) break :cape null;
const extra_headers = [_]std.http.Header{
.{ .name = "X-AnvilLib-Version", .value = "0.2.0" },
.{ .name = "X-Minecraft-Version", .value = "0.0.0-anvilauth" },
};
var header_buf: [1024]u8 = undefined;
const cape_id = players: {
const players_url = try std.fmt.allocPrint(
self.allocator,
"{s}/data/players/{s}",
.{ self.anvillib_url.?, &uuid.toStringWithDashes() },
);
defer self.allocator.free(players_url);
var players_req = try self.http.open(
.GET,
try std.Uri.parse(players_url),
.{
.server_header_buffer = &header_buf,
.extra_headers = &extra_headers,
},
);
defer players_req.deinit();
try players_req.send(.{});
try players_req.wait();
if (players_req.response.status != .ok) {
break :players null;
}
var players_json_reader = std.json.reader(self.allocator, players_req.reader());
defer players_json_reader.deinit();
const players_parsed = try std.json.parseFromTokenSource(
struct { cape: ?[]const u8 },
self.allocator,
&players_json_reader,
.{ .ignore_unknown_fields = true },
);
defer players_parsed.deinit();
break :players if (players_parsed.value.cape) |cape|
try self.allocator.dupe(u8, cape)
else
null;
};
defer if (cape_id) |u| self.allocator.free(u);
if (cape_id == null) break :cape null;
const capes_url = try std.fmt.allocPrint(
self.allocator,
"{s}/data/capes/{s}",
.{ self.anvillib_url.?, cape_id.? },
);
defer self.allocator.free(capes_url);
var capes_req = try self.http.open(
.GET,
try std.Uri.parse(capes_url),
.{
.server_header_buffer = &header_buf,
.extra_headers = &extra_headers,
},
);
defer capes_req.deinit();
try capes_req.send(.{});
try capes_req.wait();
var capes_json_reader = std.json.reader(self.allocator, capes_req.reader());
defer capes_json_reader.deinit();
const capes_parsed = try std.json.parseFromTokenSource(
struct { url: []const u8 },
self.allocator,
&capes_json_reader,
.{ .ignore_unknown_fields = true },
);
defer capes_parsed.deinit();
break :cape try self.allocator.dupe(u8, capes_parsed.value.url);
};
errdefer if (cape_url) |cape| self.allocator.free(cape);
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,
try self.user_cache.putNoClobber(self.allocator, username_d, .{
.cape_url = cape_url,
.skin_url = skin_url,
.expiration = std.time.milliTimestamp() + std.time.ms_per_day,
});
if (res.status == .ok)
return url;
self.allocator.free(url);
return null;
return .{
.skin_url = skin_url,
.cape_url = cape_url,
};
}

View file

@ -91,6 +91,7 @@ pub fn JsonUserWriter(Writer: type) type {
user_id: UUID,
user_name: []const u8,
skin_url: []const u8,
cape_url: ?[]const u8,
) !void {
var json_data = std.ArrayList(u8).init(self.alloc);
defer json_data.deinit();
@ -103,16 +104,24 @@ pub fn JsonUserWriter(Writer: type) type {
try write_stream.write(&user_id.toStringCompact());
try write_stream.objectField("profileName");
try write_stream.write(user_name);
try write_stream.objectField("textures");
{
try write_stream.objectField("textures");
try write_stream.beginObject();
try write_stream.objectField("SKIN");
{
try write_stream.objectField("SKIN");
try write_stream.beginObject();
try write_stream.objectField("url");
try write_stream.write(skin_url);
try write_stream.endObject();
}
if (cape_url) |cape| {
try write_stream.objectField("CAPE");
try write_stream.beginObject();
try write_stream.objectField("url");
try write_stream.write(cape);
try write_stream.endObject();
}
try write_stream.endObject();
}
try write_stream.endObject();

View file

@ -110,6 +110,10 @@ pub fn main() !u8 {
.allocator = alloc,
.base_url = base_url,
.forgejo_url = std.mem.trimRight(u8, config_parsed.value.forgejo_url, "/"),
.anvillib_url = if (config_parsed.value.anvillib_url) |alu|
std.mem.trimRight(u8, alu, "/")
else
null,
.skin_domains = config_parsed.value.skin_domains,
.server_name = config_parsed.value.server_name,
.http = .{ .allocator = alloc },
@ -118,15 +122,16 @@ pub fn main() !u8 {
.rsa = rsa,
.x509 = x509,
.default_skin_url = default_skin_url,
.skin_cache = State.SkinCache{},
.user_cache = State.UserCache{},
};
defer state.http.deinit();
defer {
var kiter = state.skin_cache.keyIterator();
while (kiter.next()) |key| {
alloc.free(key.*);
var iter = state.user_cache.iterator();
while (iter.next()) |kv| {
alloc.free(kv.key_ptr.*);
if (kv.value_ptr.cape_url) |cape| alloc.free(cape);
}
state.skin_cache.deinit(alloc);
state.user_cache.deinit(alloc);
}
const addr = try std.net.Address.parseIp(config_parsed.value.bind.ip, config_parsed.value.bind.port);

View file

@ -40,8 +40,7 @@ pub fn call(req: *std.http.Server.Request, state: *State) !void {
if (sel_dbret.rows() >= 1) {
const id = sel_dbret.get(UUID, 0, 0);
const skin_url = try state.getSkinUrl(params.username);
defer if (skin_url) |url| state.allocator.free(url);
const texture_urls = try state.getTextureUrls(params.username, id);
var profile_json = std.ArrayList(u8).init(state.allocator);
defer profile_json.deinit();
@ -50,7 +49,8 @@ pub fn call(req: *std.http.Server.Request, state: *State) !void {
try uprofile.texturesProperty(
id,
params.username,
skin_url orelse state.default_skin_url,
texture_urls.skin_url orelse state.default_skin_url,
texture_urls.cape_url,
);
try uprofile.finish();

View file

@ -67,8 +67,7 @@ pub fn call(req: *std.http.Server.Request, state: *State) !void {
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 texture_urls = try state.getTextureUrls(username, profile_id);
var response_data = std.ArrayList(u8).init(state.allocator);
defer response_data.deinit();
@ -81,7 +80,8 @@ pub fn call(req: *std.http.Server.Request, state: *State) !void {
try uprofile.texturesProperty(
profile_id,
username,
skin_url orelse state.default_skin_url,
texture_urls.skin_url orelse state.default_skin_url,
texture_urls.cape_url,
);
try uprofile.finish();