mirror of
https://git.xeondev.com/LR/S.git
synced 2026-02-04 15:05:17 +01:00
Release 0.1.0
This commit is contained in:
59
confsv/src/encryption.zig
Normal file
59
confsv/src/encryption.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
const std = @import("std");
|
||||
const aes = std.crypto.core.aes;
|
||||
|
||||
const Random = std.Random;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub fn CBC(comptime BlockCipher: anytype) type {
|
||||
const EncryptCtx = aes.AesEncryptCtx(BlockCipher);
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
const block_length = EncryptCtx.block_length;
|
||||
|
||||
ctx: EncryptCtx,
|
||||
|
||||
pub fn init(key: [BlockCipher.key_bits / 8]u8) Self {
|
||||
return .{ .ctx = BlockCipher.initEnc(key) };
|
||||
}
|
||||
|
||||
pub fn paddedLength(length: usize) usize {
|
||||
return (std.math.divCeil(usize, length + 1, block_length) catch unreachable) * EncryptCtx.block_length;
|
||||
}
|
||||
|
||||
pub fn encrypt(self: Self, dst: []u8, src: []const u8, iv: *const [block_length]u8) void {
|
||||
const padded_length = paddedLength(src.len);
|
||||
std.debug.assert(dst.len == padded_length); // destination buffer must hold the padded plaintext
|
||||
var cv = iv.*;
|
||||
var i: usize = 0;
|
||||
while (i + block_length <= src.len) : (i += block_length) {
|
||||
const in = src[i..][0..block_length];
|
||||
for (cv[0..], in) |*x, y| x.* ^= y;
|
||||
self.ctx.encrypt(&cv, &cv);
|
||||
@memcpy(dst[i..][0..block_length], &cv);
|
||||
}
|
||||
|
||||
// Last block
|
||||
var in: [block_length]u8 = @splat(0);
|
||||
const padding_length: u8 = @intCast(padded_length - src.len);
|
||||
@memset(&in, padding_length);
|
||||
@memcpy(in[0 .. src.len - i], src[i..]);
|
||||
for (cv[0..], in) |*x, y| x.* ^= y;
|
||||
self.ctx.encrypt(&cv, &cv);
|
||||
@memcpy(dst[i..], cv[0 .. dst.len - i]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Caller owns the returned buffer.
|
||||
pub fn encryptAlloc(gpa: Allocator, random: Random, key: [16]u8, data: []const u8) Allocator.Error![]u8 {
|
||||
const Cipher = CBC(aes.Aes128);
|
||||
const result = try gpa.alloc(u8, Cipher.block_length + Cipher.paddedLength(data.len));
|
||||
|
||||
random.bytes(result[0..Cipher.block_length]); // IV
|
||||
|
||||
const cipher: Cipher = .init(key);
|
||||
cipher.encrypt(result[Cipher.block_length..], data, result[0..Cipher.block_length]);
|
||||
|
||||
return result;
|
||||
}
|
||||
95
confsv/src/http.zig
Normal file
95
confsv/src/http.zig
Normal file
@@ -0,0 +1,95 @@
|
||||
const std = @import("std");
|
||||
const routes = @import("routes.zig");
|
||||
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Server = std.http.Server;
|
||||
|
||||
const net = Io.net;
|
||||
const request_timeout: Io.Duration = .fromSeconds(5);
|
||||
|
||||
pub const ConcurrencyAvailability = enum {
|
||||
undetermined,
|
||||
unavailable,
|
||||
available,
|
||||
};
|
||||
|
||||
pub const IoOptions = struct {
|
||||
// Indicates whether Io.concurrent() should be considered.
|
||||
concurrency: ConcurrencyAvailability,
|
||||
// Specifies the preferred system clock.
|
||||
preferred_clock: Io.Clock,
|
||||
};
|
||||
|
||||
pub fn processClient(
|
||||
io: Io,
|
||||
stream: net.Stream,
|
||||
gpa: Allocator,
|
||||
options: IoOptions,
|
||||
) Io.Cancelable!void {
|
||||
const log = std.log.scoped(.http);
|
||||
defer stream.close(io);
|
||||
|
||||
log.debug("new connection from '{f}'", .{stream.socket.address});
|
||||
defer log.debug("client from '{f}' disconnected", .{stream.socket.address});
|
||||
|
||||
var recv_buffer: [8192]u8 = undefined;
|
||||
var send_buffer: [8192]u8 = undefined;
|
||||
|
||||
var reader = stream.reader(io, &recv_buffer);
|
||||
var writer = stream.writer(io, &send_buffer);
|
||||
|
||||
var server: Server = .init(&reader.interface, &writer.interface);
|
||||
var request = receiveRequest(io, options, &server) catch |err| switch (err) {
|
||||
error.Canceled, error.ConcurrencyUnavailable => return,
|
||||
else => |e| {
|
||||
log.err("failed to receive request from '{f}': {t}", .{ stream.socket.address, e });
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
log.info(
|
||||
"received request from '{f}': {s} ({t})",
|
||||
.{ stream.socket.address, request.head.target, request.head.method },
|
||||
);
|
||||
|
||||
routes.process(io, gpa, &request) catch |err| switch (err) {
|
||||
error.Canceled => return,
|
||||
error.RouteNotFound => {
|
||||
log.warn(
|
||||
"route '{s}' not found, requested by: '{f}'",
|
||||
.{ request.head.target, stream.socket.address },
|
||||
);
|
||||
|
||||
request.respond("Not Found", .{ .status = .not_found }) catch return;
|
||||
},
|
||||
error.MethodNotAllowed => request.respond("Method Not Allowed", .{ .status = .method_not_allowed }) catch
|
||||
return,
|
||||
else => |e| log.err(
|
||||
"failed to process request from '{f}': {t}",
|
||||
.{ stream.socket.address, e },
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn receiveRequest(io: Io, options: IoOptions, server: *Server) !Server.Request {
|
||||
return switch (options.concurrency) {
|
||||
.undetermined => unreachable,
|
||||
.unavailable => try server.receiveHead(),
|
||||
.available => {
|
||||
var receive = try io.concurrent(Server.receiveHead, .{server});
|
||||
errdefer _ = receive.cancel(io) catch {};
|
||||
|
||||
var sleep = try io.concurrent(Io.sleep, .{ io, request_timeout, options.preferred_clock });
|
||||
defer sleep.cancel(io) catch {};
|
||||
|
||||
return switch (try io.select(.{
|
||||
.receive = &receive,
|
||||
.sleep = &sleep,
|
||||
})) {
|
||||
.sleep => try receive.cancel(io),
|
||||
.receive => |request| request,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
122
confsv/src/main.zig
Normal file
122
confsv/src/main.zig
Normal file
@@ -0,0 +1,122 @@
|
||||
const std = @import("std");
|
||||
const common = @import("common");
|
||||
const http = @import("http.zig");
|
||||
|
||||
const Io = std.Io;
|
||||
const Init = std.process.Init;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ConcurrencyAvailability = http.ConcurrencyAvailability;
|
||||
|
||||
const net = Io.net;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const log = std.log.scoped(.confsv);
|
||||
|
||||
const Options = struct {
|
||||
listen_address: []const u8 = "127.0.0.1:10001",
|
||||
};
|
||||
|
||||
fn start(init: Init.Minimal, io: Io, gpa: Allocator) u8 {
|
||||
const args = common.args.parseOrPrintUsageAlloc(Options, gpa, init.args) orelse return 1;
|
||||
defer args.deinit();
|
||||
|
||||
std.debug.print(
|
||||
\\ __ ____ _____
|
||||
\\ / / / __ \ / ___/
|
||||
\\ / / / /_/ /_____\__ \
|
||||
\\ / /___/ _, _/_____/__/ /
|
||||
\\/_____/_/ |_| /____/
|
||||
\\
|
||||
, .{});
|
||||
|
||||
const listen_address = net.IpAddress.parseLiteral(args.options.listen_address) catch {
|
||||
log.err("Invalid listen address specified.", .{});
|
||||
return 1;
|
||||
};
|
||||
|
||||
var server = listen_address.listen(io, .{ .reuse_address = true }) catch |err| switch (err) {
|
||||
error.AddressInUse => {
|
||||
log.err(
|
||||
"Address '{f}' is in use. Another instance of this server might be already running.",
|
||||
.{listen_address},
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
else => |e| {
|
||||
log.err("Failed to listen at '{f}': {t}", .{ listen_address, e });
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
|
||||
defer server.deinit(io);
|
||||
|
||||
var http_processors: Io.Group = .init;
|
||||
defer http_processors.cancel(io);
|
||||
|
||||
var preferred_clock: Io.Clock = .awake; // Prefer monotonic clock by default. Fallback to realtime.
|
||||
var concurrency_availability: ConcurrencyAvailability = .undetermined;
|
||||
|
||||
log.info("listening at {f}", .{listen_address});
|
||||
defer log.info("shutting down...", .{});
|
||||
|
||||
accept_loop: while (true) {
|
||||
const stream = server.accept(io) catch |err| switch (err) {
|
||||
error.Canceled => break, // Shutdown requested
|
||||
error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, error.SystemResources => {
|
||||
// System is overloaded. Stop accepting new connections for now.
|
||||
while (true) {
|
||||
if (io.sleep(.fromSeconds(1), preferred_clock)) break else |sleep_err| switch (sleep_err) {
|
||||
error.Canceled => break :accept_loop, // Shutdown requested
|
||||
error.UnsupportedClock => preferred_clock = if (preferred_clock == .awake)
|
||||
.real
|
||||
else
|
||||
continue :accept_loop, // No clock available.
|
||||
error.Unexpected => continue :accept_loop, // Sleep is unimportant then.
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
},
|
||||
else => |e| { // Something else happened. We probably want to report this and continue.
|
||||
log.err("TCP accept failed: {t}", .{e});
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
var io_options: http.IoOptions = .{
|
||||
.preferred_clock = preferred_clock,
|
||||
.concurrency = .available,
|
||||
};
|
||||
|
||||
if (http_processors.concurrent(io, http.processClient, .{ io, stream, gpa, io_options })) {
|
||||
concurrency_availability = .available;
|
||||
} else |err| switch (err) {
|
||||
error.ConcurrencyUnavailable => switch (concurrency_availability) {
|
||||
.available => stream.close(io), // Can't process more connections atm.
|
||||
.unavailable, .undetermined => {
|
||||
// The environment doesn't support concurrency.
|
||||
if (concurrency_availability != .unavailable)
|
||||
log.warn("Environment doesn't support concurrency. One request at a time will be processed.", .{});
|
||||
|
||||
concurrency_availability = .unavailable;
|
||||
io_options.concurrency = .unavailable;
|
||||
http_processors.async(io, http.processClient, .{ io, stream, gpa, io_options });
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn main(init: Init.Minimal) u8 {
|
||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer assert(.ok == debug_allocator.deinit());
|
||||
const gpa = debug_allocator.allocator();
|
||||
|
||||
var poll: common.io.Poll(.{}) = .init(gpa);
|
||||
defer poll.deinit();
|
||||
const io = poll.io();
|
||||
|
||||
return start(init, io, gpa);
|
||||
}
|
||||
66
confsv/src/routes.zig
Normal file
66
confsv/src/routes.zig
Normal file
@@ -0,0 +1,66 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Request = std.http.Server.Request;
|
||||
|
||||
const version = @import("routes/version.zig");
|
||||
const config = @import("routes/config.zig");
|
||||
const server_list = @import("routes/server_list.zig");
|
||||
|
||||
const routes = .{
|
||||
.{ "/api/game/get_latest", version.getOnlineAppVersion },
|
||||
.{ "/api/remote_config/v2/1003/prod-obt/default/Windows/game_config", config.getRemoteGameConfig },
|
||||
.{ "/get_server_list", server_list.get },
|
||||
.{ "/api/game/get_latest_resources", version.getLatestResources },
|
||||
};
|
||||
|
||||
const Route = blk: {
|
||||
var field_names: [routes.len][:0]const u8 = undefined;
|
||||
var field_values: [routes.len]usize = undefined;
|
||||
|
||||
for (routes, 0..) |route, i| {
|
||||
const path, _ = route;
|
||||
field_names[i] = path;
|
||||
field_values[i] = i;
|
||||
}
|
||||
|
||||
break :blk @Enum(usize, .exhaustive, &field_names, &field_values);
|
||||
};
|
||||
|
||||
pub const ProcessError = error{ RouteNotFound, MethodNotAllowed } || Error;
|
||||
pub const Error = Io.Cancelable || Request.ExpectContinueError || Allocator.Error;
|
||||
|
||||
pub fn process(
|
||||
io: Io,
|
||||
gpa: Allocator,
|
||||
request: *Request,
|
||||
) ProcessError!void {
|
||||
const log = std.log.scoped(.routing);
|
||||
|
||||
switch (request.head.method) {
|
||||
.GET, .POST => {},
|
||||
else => |method| {
|
||||
log.debug("method not allowed: {t}", .{method});
|
||||
return error.MethodNotAllowed;
|
||||
},
|
||||
}
|
||||
|
||||
const path = if (std.mem.findScalar(u8, request.head.target, '?')) |query_i|
|
||||
request.head.target[0..query_i]
|
||||
else
|
||||
request.head.target;
|
||||
|
||||
const route = std.meta.stringToEnum(Route, path) orelse
|
||||
return error.RouteNotFound;
|
||||
|
||||
switch (route) {
|
||||
inline else => |tag| inline for (routes) |pair| {
|
||||
const name, const processFn = pair;
|
||||
if (comptime std.mem.eql(u8, name, @tagName(tag))) {
|
||||
try processFn(io, gpa, request);
|
||||
break;
|
||||
}
|
||||
} else comptime unreachable,
|
||||
}
|
||||
}
|
||||
35
confsv/src/routes/config.zig
Normal file
35
confsv/src/routes/config.zig
Normal file
@@ -0,0 +1,35 @@
|
||||
const std = @import("std");
|
||||
const routes = @import("../routes.zig");
|
||||
const encryption = @import("../encryption.zig");
|
||||
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Request = std.http.Server.Request;
|
||||
const Base64Encoder = std.base64.standard.Encoder;
|
||||
|
||||
const config_key = [16]u8{ 0x71, 0x99, 0xBC, 0xE9, 0x47, 0xC3, 0xA7, 0xF9, 0x20, 0x27, 0x76, 0xA0, 0x2B, 0x1F, 0x87, 0x64 };
|
||||
|
||||
pub fn getRemoteGameConfig(io: Io, gpa: Allocator, request: *Request) routes.Error!void {
|
||||
var response_buffer: [1024]u8 = undefined;
|
||||
var body = try request.respondStreaming(&response_buffer, .{});
|
||||
|
||||
const response: RemoteGameCfg = .{
|
||||
.enableHotUpdate = false,
|
||||
.mockLogin = true,
|
||||
};
|
||||
|
||||
const content = try std.fmt.allocPrint(gpa, "{f}", .{std.json.fmt(response, .{})});
|
||||
defer gpa.free(content);
|
||||
|
||||
const io_source: std.Random.IoSource = .{ .io = io };
|
||||
const ciphertext = try encryption.encryptAlloc(gpa, io_source.interface(), config_key, content);
|
||||
defer gpa.free(ciphertext);
|
||||
|
||||
try body.writer.print("{b64}", .{ciphertext});
|
||||
try body.end();
|
||||
}
|
||||
|
||||
const RemoteGameCfg = struct {
|
||||
enableHotUpdate: bool,
|
||||
mockLogin: bool,
|
||||
};
|
||||
31
confsv/src/routes/server_list.zig
Normal file
31
confsv/src/routes/server_list.zig
Normal file
@@ -0,0 +1,31 @@
|
||||
const std = @import("std");
|
||||
const routes = @import("../routes.zig");
|
||||
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Request = std.http.Server.Request;
|
||||
|
||||
pub fn get(io: Io, gpa: Allocator, request: *Request) routes.Error!void {
|
||||
_ = io;
|
||||
_ = gpa;
|
||||
|
||||
var response_buffer: [1024]u8 = undefined;
|
||||
var body = try request.respondStreaming(&response_buffer, .{});
|
||||
|
||||
const response: ServerList = .{
|
||||
.servers = &.{.{ .name = "LR", .addr = "127.0.0.1", .port = 30000 }},
|
||||
};
|
||||
|
||||
try body.writer.print("{f}", .{std.json.fmt(response, .{})});
|
||||
try body.end();
|
||||
}
|
||||
|
||||
const ServerDesc = struct {
|
||||
name: []const u8,
|
||||
addr: []const u8,
|
||||
port: i32,
|
||||
};
|
||||
|
||||
const ServerList = struct {
|
||||
servers: []const ServerDesc,
|
||||
};
|
||||
99
confsv/src/routes/version.zig
Normal file
99
confsv/src/routes/version.zig
Normal file
@@ -0,0 +1,99 @@
|
||||
const std = @import("std");
|
||||
const routes = @import("../routes.zig");
|
||||
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Request = std.http.Server.Request;
|
||||
|
||||
pub fn getOnlineAppVersion(io: Io, gpa: Allocator, request: *Request) routes.Error!void {
|
||||
_ = io;
|
||||
_ = gpa;
|
||||
|
||||
var response_buffer: [1024]u8 = undefined;
|
||||
var body = try request.respondStreaming(&response_buffer, .{});
|
||||
|
||||
const response: OnlineAppVersionResponse = .{
|
||||
.action = 0,
|
||||
.version = "1.0.14",
|
||||
.request_version = "1.0.14",
|
||||
.pkg = .{
|
||||
.packs = &.{},
|
||||
.total_size = 0,
|
||||
.file_path = "https://beyond.hg-cdn.com/YDUTE5gscDZ229CW/1.0/update/6/6/Windows/1.0.14_Qk2mXHuAH1JWKF37/files",
|
||||
.url = "",
|
||||
.md5 = "",
|
||||
.package_size = "0",
|
||||
.file_id = "0",
|
||||
.sub_channel = "6",
|
||||
.game_files_md5 = "c36ad08e5d4a7cfd580228971d7a4563",
|
||||
},
|
||||
.patch = null,
|
||||
.state = 0,
|
||||
.launcher_action = 0,
|
||||
};
|
||||
|
||||
try body.writer.print("{f}", .{std.json.fmt(response, .{})});
|
||||
try body.end();
|
||||
}
|
||||
|
||||
pub fn getLatestResources(io: Io, gpa: Allocator, request: *Request) routes.Error!void {
|
||||
_ = io;
|
||||
_ = gpa;
|
||||
|
||||
var response_buffer: [1024]u8 = undefined;
|
||||
var body = try request.respondStreaming(&response_buffer, .{});
|
||||
|
||||
const response: ResVersionData = .{
|
||||
.resources = &.{
|
||||
.{
|
||||
.name = "main",
|
||||
.version = "5439650-20",
|
||||
.path = "https://beyond.hg-cdn.com/YDUTE5gscDZ229CW/1.0/resource/Windows/main/5439650-20_PEuAF7OENsVNjc1L/files",
|
||||
},
|
||||
.{
|
||||
.name = "initial",
|
||||
.version = "5439650-20",
|
||||
.path = "https://beyond.hg-cdn.com/YDUTE5gscDZ229CW/1.0/resource/Windows/initial/5439650-20_2HA0Xw0M0B0XWdBV/files",
|
||||
},
|
||||
},
|
||||
.configs = "{\"kick_flag\":false}",
|
||||
.res_version = "initial_5439650-20_main_5439650-20",
|
||||
.patch_index_path = "",
|
||||
.domain = "https://beyond.hg-cdn.com",
|
||||
};
|
||||
|
||||
try body.writer.print("{f}", .{std.json.fmt(response, .{})});
|
||||
try body.end();
|
||||
}
|
||||
|
||||
const OnlineAppVersionResponse = struct {
|
||||
action: i32,
|
||||
version: []const u8,
|
||||
request_version: []const u8,
|
||||
pkg: struct {
|
||||
packs: []const struct {},
|
||||
total_size: u64,
|
||||
file_path: []const u8,
|
||||
url: []const u8,
|
||||
md5: []const u8,
|
||||
package_size: []const u8,
|
||||
file_id: []const u8,
|
||||
sub_channel: []const u8,
|
||||
game_files_md5: []const u8,
|
||||
},
|
||||
patch: ?struct {},
|
||||
state: u32,
|
||||
launcher_action: u32,
|
||||
};
|
||||
|
||||
const ResVersionData = struct {
|
||||
resources: []const struct {
|
||||
name: []const u8,
|
||||
version: []const u8,
|
||||
path: []const u8,
|
||||
},
|
||||
configs: []const u8,
|
||||
res_version: []const u8,
|
||||
patch_index_path: []const u8,
|
||||
domain: []const u8,
|
||||
};
|
||||
Reference in New Issue
Block a user