diff --git a/Dockerfile b/Dockerfile index c8eeda6..89fd2c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,7 @@ FROM git.zontreck.com/packages/flutter:latest as BUILDER WORKDIR /app -COPY ./pubspec.yaml ./ -COPY ./cli ./ -COPY ./lib ./ -COPY ./android ./ -COPY ./linux ./ -COPY ./ios ./ -COPY ./macos ./ -COPY ./test ./ -COPY ./web ./ -COPY ./windows ./ -COPY ./analysis_options.yaml ./ +COPY ./* /app/ RUN dart pub get diff --git a/cli/server.dart b/cli/server.dart index 6c67cb5..577ba1e 100644 --- a/cli/server.dart +++ b/cli/server.dart @@ -1,8 +1,44 @@ +import 'dart:io'; + +import 'package:bugvault/Constants.dart'; +import 'package:bugvault/SessionData.dart'; +import 'package:bugvault/server/main.dart'; +import 'package:libac_dart/nbt/NbtIo.dart'; +import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/packets/packets.dart'; + Future main(List args) async { print("Starting BugVault Server..."); // Check for settings.dat, which should contain all basic information. // Then check if any data exists. If not, create data hierarchy + File settings = File("settings.dat"); + if (await settings.exists()) { + // Load! + SessionData.g_nbtConfiguration = + await NbtIo.read("settings.dat") as CompoundTag; + } else { + SessionData.g_nbtConfiguration = CompoundTag(); + print("No existing configuration found"); + } + await BugVaultServer.InstantiateSettings(SessionData.g_nbtConfiguration); + print("Loaded settings..."); + + print("Registering packet handlers..."); + await BugVaultServer.RegisterPacketHandlers(); + + print("Opening data port..."); + + print("\n\n"); + print("=".padLeft(20, "=")); + print("BugVault Dedicated Server\nVersion: ${Constants.VERSION}"); + print("=".padLeft(20, "=")); + + while (!SessionData.g_bShutdownPending) { + try { + await PacketServer.start(BugVaultServer.g_iPortNumber); + } catch (E) {} + } print("Thank you for choosing BugVault! - Shutting down..."); return 0; diff --git a/lib/Constants.dart b/lib/Constants.dart index 4ee6b9a..01f15f3 100644 --- a/lib/Constants.dart +++ b/lib/Constants.dart @@ -1,4 +1,4 @@ class Constants { - static const VERSION = "1.0.031525+0003"; + static const VERSION = "1.0.031525+0137"; static const APP_NAME = "BugVault"; } diff --git a/lib/SessionData.dart b/lib/SessionData.dart index 741ae44..87d0708 100644 --- a/lib/SessionData.dart +++ b/lib/SessionData.dart @@ -3,4 +3,7 @@ import 'package:libac_dart/nbt/impl/CompoundTag.dart'; class SessionData { static var g_bDarkMode = true; static CompoundTag g_nbtConfiguration = CompoundTag(); + + /// This flag is only used by the server to indicate if a packet has demanded the server shut down. + static var g_bShutdownPending = false; } diff --git a/lib/server/main.dart b/lib/server/main.dart new file mode 100644 index 0000000..d02637c --- /dev/null +++ b/lib/server/main.dart @@ -0,0 +1,61 @@ +import 'dart:io'; + +import 'package:bugvault/server/packets.dart'; +import 'package:bugvault/users/User.dart'; +import 'package:libac_dart/nbt/NbtIo.dart'; +import 'package:libac_dart/nbt/NbtUtils.dart'; +import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/nbt/impl/ListTag.dart'; +import 'package:libac_dart/packets/packets.dart'; + +class BugVaultServer { + static int g_iPortNumber = 8372; + static bool g_bAllowAnonymousLogin = true; + static List UserDB = List.empty(); + + static Future InstantiateSettings(CompoundTag ct) async { + g_iPortNumber = ct.get("port")?.asInt() ?? 8372; + if (ct.containsKey("anonymous")) + g_bAllowAnonymousLogin = NbtUtils.readBoolean(ct, "anonymous"); + + await _loadUserDB(); + } + + static Future _loadUserDB() async { + File users = File("users.dat"); + if (await users.exists()) { + CompoundTag ct = await NbtIo.read("users.dat") as CompoundTag; + ListTag userList = ct.get("users")! as ListTag; + for (var tag in userList.value) { + CompoundTag userEntry = tag.asCompoundTag(); + UserDB.add(DBUser.load(serialized: userEntry)); + } + } + } + + static Future RegisterPacketHandlers() async { + PacketRegistry registry = PacketRegistry(); + BVSPacketImpl.Register(registry); + } + + static DBUser? TryGetUser(String name) { + for (var entry in UserDB) { + if (entry.sName == name) { + return entry; + } + } + + return null; + } +} + +class BVSPacketImpl { + static Future Register(PacketRegistry registry) async { + registry.register(C2SPacketLogin(), () { + return C2SPacketLogin(); + }); + registry.register(S2CLoginReply(), () { + return S2CLoginReply(); + }); + } +} diff --git a/lib/server/packets.dart b/lib/server/packets.dart new file mode 100644 index 0000000..39bb12a --- /dev/null +++ b/lib/server/packets.dart @@ -0,0 +1,165 @@ +import 'dart:convert'; + +import 'package:bugvault/server/main.dart'; +import 'package:bugvault/users/User.dart'; +import 'package:libac_dart/nbt/Tag.dart'; +import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/nbt/impl/IntTag.dart'; +import 'package:libac_dart/nbt/impl/StringTag.dart'; +import 'package:libac_dart/packets/packets.dart'; + +class C2SPacketLogin implements IPacket { + String username = ""; + + @override + void decodeJson(String params) { + fromJson(json.decode(params)); + } + + @override + void decodeTag(Tag tag) { + CompoundTag ct = tag.asCompoundTag(); + username = ct.get("user")?.asString() ?? ""; + } + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return json.encode(toJson()); + } + + @override + Tag encodeTag() { + CompoundTag ct = CompoundTag(); + ct.put("user", StringTag.valueOf(username)); + + return ct; + } + + @override + void fromJson(Map js) { + username = js['user'] as String; + } + + @override + String getChannelID() { + return "BVPKTLOGIN"; + } + + @override + Future handleClientPacket() async { + return; + } + + @override + Future handleServerPacket() async { + S2CResponse response = S2CResponse(); + S2CLoginReply loginReply = S2CLoginReply(); + loginReply.username = username; + + // Check if the user exists + DBUser? user = BugVaultServer.TryGetUser(username); + if (user == null && username != "") { + loginReply.g_ixLoginState |= LoginStates.NOT_FOUND; + loginReply.g_ixLoginState |= LoginStates.PROVIDE_EMAIL; + } + + if (user != null) { + // Tell the user to send their TOTP Code + loginReply.g_ixLoginState |= LoginStates.FOUND; + loginReply.g_ixLoginState |= LoginStates.REQUIRE_MFA; + } + + if (username == "") { + if (BugVaultServer.g_bAllowAnonymousLogin) { + loginReply.g_ixLoginState = LoginStates.LOGGED_IN; + } else { + loginReply.g_ixLoginState = LoginStates.ANONYMOUS_NOT_ALLOWED; + } + } + + response.contents = loginReply.encodeTag().asCompoundTag(); + return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); + } + + @override + Map toJson() { + return {"user": username}; + } +} + +class LoginStates { + static const NOT_FOUND = 1; + static const FOUND = 2; + static const REQUIRE_MFA = 4; + static const PROVIDE_EMAIL = 8; + static const REQUIRE_EMAIL_CODE = 16; + static const LOGGED_IN = 32; + static const ANONYMOUS_NOT_ALLOWED = 64; +} + +class S2CLoginReply implements IPacket { + String username = ""; + int g_ixLoginState = 0; + + @override + void decodeJson(String params) { + return fromJson(json.decode(params)); + } + + @override + void decodeTag(Tag tag) { + CompoundTag ct = tag.asCompoundTag(); + username = ct.get("user")?.asString() ?? ""; + g_ixLoginState = ct.get("state")?.asInt() ?? 0; + } + + @override + NetworkDirection direction() { + return NetworkDirection.ServerToClient; + } + + @override + String encodeJson() { + return json.encode(toJson()); + } + + @override + Tag encodeTag() { + CompoundTag ct = CompoundTag(); + ct.put("user", StringTag.valueOf(username)); + ct.put("state", IntTag.valueOf(g_ixLoginState)); + + return ct; + } + + @override + void fromJson(Map js) { + username = js['user'] as String; + g_ixLoginState = js['state'] as int; + } + + @override + String getChannelID() { + return "BVSPKTLOGINREPLY"; + } + + @override + Future handleClientPacket() async { + // do handling stuff + } + + @override + Future handleServerPacket() async { + throw UnimplementedError(); // Client side only, so this never gets invoked. + } + + @override + Map toJson() { + return {"user": username, "state": g_ixLoginState}; + } +} diff --git a/lib/users/User.dart b/lib/users/User.dart index 2564638..e06b0b8 100644 --- a/lib/users/User.dart +++ b/lib/users/User.dart @@ -1,3 +1,10 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:base32/base32.dart'; +import 'package:libac_dart/nbt/NbtUtils.dart'; +import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/utils/uuid/NbtUUID.dart'; import 'package:libac_dart/utils/uuid/UUID.dart'; /// The user class is a user object. @@ -9,3 +16,39 @@ class User { User({required this.sName, required this.ID}); } + +String generateTOTPSecret({int length = 32}) { + final random = Random.secure(); + final List bytes = List.generate(length, (_) => random.nextInt(256)); + return base32.encode(Uint8List.fromList(bytes)); +} + +class DBUser { + String sName; + UUID ID; + late String TOTPSecret; + + DBUser({required this.sName, required this.ID, String? totp}) { + if (totp == null) + TOTPSecret = generateTOTPSecret(); + else + TOTPSecret = totp!; + } + + factory DBUser.load({required CompoundTag serialized}) { + UUID IDv4 = UUID.generate(4); + NbtUUID saved = NbtUtils.readUUID(serialized, "id"); + IDv4 = saved.toUUID(); + DBUser user = DBUser( + sName: serialized.get("name")?.asString() ?? "", + ID: IDv4, + totp: serialized.get("mfa_secret")?.asString() ?? "", + ); + + return user; + } + + void regenerateTOTP() { + TOTPSecret = generateTOTPSecret(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 6631199..5e392f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.031525+0003 +version: 1.0.031525+0137 environment: sdk: ^3.7.0 @@ -40,7 +40,10 @@ dependencies: libac_dart: hosted: https://git.zontreck.com/api/packages/Packages/pub/ version: 1.4.20325+1215 + base32: ^2.1.3 totp: ^0.1.0 + hotp: ^0.1.0 + barcode: ^2.2.9 dev_dependencies: flutter_test: