fix loom and auth path
This commit is contained in:
@@ -477,3 +477,14 @@ Lobby Systems is **proprietary software — All Rights Reserved**. Public access
|
||||
- EasyAuth is intentionally marked incompatible while native Lobby Systems Auth is enabled.
|
||||
|
||||
For the intended hybrid mode, keep `online-mode=true` in `server.properties` and remove EasyAuth before testing.
|
||||
|
||||
## v1.7.1
|
||||
- Fixed hybrid `online-mode=true` login: confirmed non-premium players now receive a deterministic offline GameProfile and advance through the login state machine instead of falling into vanilla `Invalid session`.
|
||||
- Premium username lookup now prefers the modern `api.minecraftservices.com` public profile endpoint and falls back to the legacy Mojang endpoint.
|
||||
- Premium/unknown identities still use vanilla authentication; lookup failure never silently grants a local identity.
|
||||
- Local/cracked players remain password-gated after join. `requireLoginEveryJoin` defaults to `true`; session bypass is intentionally not enabled yet, so changing it currently does not weaken authentication.
|
||||
- `enforce-secure-profile=true` is intentionally left supported; cracked chat will be validated in server testing before any secure-chat compatibility override is added.
|
||||
|
||||
## v1.7.2
|
||||
- Fixed the authentication access widener so the private Minecraft login state type is widened during compilation, not only declared in mod metadata.
|
||||
- Added Loom `accessWidenerPath` configuration for compile-time access widening.
|
||||
|
||||
@@ -8,6 +8,10 @@ group = project.maven_group
|
||||
|
||||
base { archivesName = project.archives_base_name }
|
||||
|
||||
loom {
|
||||
accessWidenerPath = file("src/main/resources/lobby-systems.accesswidener")
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@ minecraft_version=26.2
|
||||
loader_version=0.19.3
|
||||
loom_version=1.17-SNAPSHOT
|
||||
fabric_api_version=0.156.0+26.2
|
||||
mod_version=1.7.0
|
||||
mod_version=1.7.2
|
||||
maven_group=id.dartokloning
|
||||
archives_base_name=lobby-systems
|
||||
|
||||
@@ -9,5 +9,7 @@ public final class AuthConfig {
|
||||
public int reminderIntervalSeconds = 8;
|
||||
public int minimumPasswordLength = 6;
|
||||
public int pbkdf2Iterations = 210000;
|
||||
public String mojangProfileUrl = "https://api.mojang.com/users/profiles/minecraft/{username}";
|
||||
public String mojangProfileUrl = "https://api.minecraftservices.com/minecraft/profile/lookup/name/{username}";
|
||||
public String legacyMojangProfileUrl = "https://api.mojang.com/users/profiles/minecraft/{username}";
|
||||
public boolean requireLoginEveryJoin = true;
|
||||
}
|
||||
|
||||
@@ -62,41 +62,56 @@ public final class AuthManager {
|
||||
|
||||
public AuthConfig config() { return config; }
|
||||
|
||||
/** Called from the login mixin. true = keep vanilla online authentication; false = local/offline path. */
|
||||
public boolean shouldUseVanillaAuthentication(String username) {
|
||||
if (!config.enabled || !config.premiumAutoLogin) return true;
|
||||
/** Called from the login mixin. */
|
||||
public LoginRoute resolveLoginRoute(String username) {
|
||||
if (!config.enabled || !config.premiumAutoLogin) return LoginRoute.PREMIUM;
|
||||
PremiumResult result = premiumStatus(username);
|
||||
// Fail closed: if Mojang cannot be checked, retain vanilla authentication rather than granting a local identity.
|
||||
return result != PremiumResult.NOT_PREMIUM;
|
||||
return switch (result) {
|
||||
case PREMIUM -> LoginRoute.PREMIUM;
|
||||
case NOT_PREMIUM -> LoginRoute.LOCAL;
|
||||
case UNKNOWN -> LoginRoute.UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
public PremiumResult premiumStatus(String username) {
|
||||
String key = username.toLowerCase(Locale.ROOT);
|
||||
PremiumCache cached = premiumCache.get(key);
|
||||
long now = System.currentTimeMillis();
|
||||
if (cached != null && now - cached.checkedAt < 300_000L) return cached.result;
|
||||
if (cached != null && now - cached.checkedAt < 86_400_000L) return cached.result;
|
||||
|
||||
PremiumResult result = lookupPremium(config.mojangProfileUrl, username);
|
||||
if (result == PremiumResult.UNKNOWN && config.legacyMojangProfileUrl != null && !config.legacyMojangProfileUrl.isBlank()) {
|
||||
result = lookupPremium(config.legacyMojangProfileUrl, username);
|
||||
}
|
||||
premiumCache.put(key, new PremiumCache(result, now));
|
||||
return result;
|
||||
}
|
||||
|
||||
private PremiumResult lookupPremium(String template, String username) {
|
||||
try {
|
||||
String url = config.mojangProfileUrl.replace("{username}", java.net.URLEncoder.encode(username, StandardCharsets.UTF_8));
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(5)).GET().build();
|
||||
String url = template.replace("{username}", java.net.URLEncoder.encode(username, StandardCharsets.UTF_8));
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(8)).header("Accept", "application/json").GET().build();
|
||||
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
PremiumResult result;
|
||||
if (response.statusCode() == 200 && response.body() != null && response.body().contains("\"id\"")) result = PremiumResult.PREMIUM;
|
||||
else if (response.statusCode() == 204 || response.statusCode() == 404) result = PremiumResult.NOT_PREMIUM;
|
||||
else result = PremiumResult.UNKNOWN;
|
||||
premiumCache.put(key, new PremiumCache(result, now));
|
||||
return result;
|
||||
if (response.statusCode() == 200 && response.body() != null && response.body().contains("\"id\"")) return PremiumResult.PREMIUM;
|
||||
if (response.statusCode() == 204 || response.statusCode() == 404) return PremiumResult.NOT_PREMIUM;
|
||||
LobbySystemsMod.LOGGER.warn("Premium username lookup returned HTTP {} for {} via {}", response.statusCode(), username, URI.create(url).getHost());
|
||||
return PremiumResult.UNKNOWN;
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.warn("Premium username lookup failed for {}: {}", username, e.toString());
|
||||
premiumCache.put(key, new PremiumCache(PremiumResult.UNKNOWN, now));
|
||||
LobbySystemsMod.LOGGER.warn("Premium username lookup failed for {} via {}: {}", username, safeHost(template), e.toString());
|
||||
return PremiumResult.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeHost(String template) {
|
||||
try { return URI.create(template.replace("{username}", "player")).getHost(); }
|
||||
catch (Exception ignored) { return "configured endpoint"; }
|
||||
}
|
||||
|
||||
public void onJoin(ServerPlayer player) {
|
||||
if (!config.enabled) return;
|
||||
String name = player.getName().getString();
|
||||
PremiumResult premium = premiumStatus(name);
|
||||
if (premium == PremiumResult.PREMIUM) {
|
||||
boolean local = LobbySystemsMod.consumeLocalLogin(name);
|
||||
if (!local) {
|
||||
sessions.put(player.getUUID(), new Session(true, true, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
||||
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
||||
return;
|
||||
@@ -215,6 +230,7 @@ public final class AuthManager {
|
||||
private void saveAccounts() { try { Files.createDirectories(ConfigPaths.AUTH_DIRECTORY); try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); } } catch (Exception e) { LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e); } }
|
||||
|
||||
public enum PremiumResult { PREMIUM, NOT_PREMIUM, UNKNOWN }
|
||||
public enum LoginRoute { PREMIUM, LOCAL, UNKNOWN }
|
||||
private record PremiumCache(PremiumResult result, long checkedAt) {}
|
||||
private static final class Account { String salt; String hash; int iterations; Account(String salt,String hash,int iterations){this.salt=salt;this.hash=hash;this.iterations=iterations;} }
|
||||
private static final class Session { boolean authenticated; boolean premium; int attempts; long joinedAt; long lastReminder; double x,y,z; float yaw,pitch; Session(boolean a,boolean p,int at,long j,double x,double y,double z,float yaw,float pitch){authenticated=a;premium=p;attempts=at;joinedAt=j;lastReminder=0;this.x=x;this.y=y;this.z=z;this.yaw=yaw;this.pitch=pitch;} }
|
||||
|
||||
@@ -49,13 +49,22 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
private static final StarterKitManager STARTER_KITS = new StarterKitManager();
|
||||
private static final AuthManager AUTH = new AuthManager();
|
||||
private static int saveTicker;
|
||||
private static final java.util.Set<String> LOCAL_LOGIN_NAMES = java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* Used by the NetherPortalBlock mixin to distinguish Lobby Systems visual
|
||||
* portals from ordinary vanilla Nether portals.
|
||||
*/
|
||||
public static boolean shouldUseVanillaAuthentication(String username) {
|
||||
return AUTH.shouldUseVanillaAuthentication(username);
|
||||
public static AuthManager.LoginRoute resolveLoginRoute(String username) {
|
||||
return AUTH.resolveLoginRoute(username);
|
||||
}
|
||||
|
||||
public static void markLocalLogin(String username) {
|
||||
LOCAL_LOGIN_NAMES.add(username.toLowerCase(java.util.Locale.ROOT));
|
||||
}
|
||||
|
||||
public static boolean consumeLocalLogin(String username) {
|
||||
return LOCAL_LOGIN_NAMES.remove(username.toLowerCase(java.util.Locale.ROOT));
|
||||
}
|
||||
|
||||
public static boolean isAuthenticated(ServerPlayer player) {
|
||||
|
||||
+34
-7
@@ -1,22 +1,49 @@
|
||||
package id.dartokloning.lobbysystems.mixin;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import id.dartokloning.lobbysystems.AuthManager;
|
||||
import id.dartokloning.lobbysystems.LobbySystemsMod;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.network.protocol.login.ServerboundHelloPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Hybrid authentication: preserve vanilla auth for premium names; permit offline profile only for non-premium names. */
|
||||
/**
|
||||
* Hybrid login gate.
|
||||
* Premium identities continue through vanilla online-mode authentication.
|
||||
* Confirmed non-premium identities are converted to a deterministic offline
|
||||
* profile and continue through the normal login state machine without Mojang
|
||||
* session authentication. Local password authentication happens after join.
|
||||
*/
|
||||
@Mixin(ServerLoginPacketListenerImpl.class)
|
||||
public abstract class ServerLoginPacketListenerImplMixin {
|
||||
@Redirect(
|
||||
@Shadow public GameProfile authenticatedProfile;
|
||||
@Shadow private ServerLoginPacketListenerImpl.State state;
|
||||
@Final @Shadow MinecraftServer server;
|
||||
|
||||
@Inject(
|
||||
method = "handleHello(Lnet/minecraft/network/protocol/login/ServerboundHelloPacket;)V",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;usesAuthentication()Z")
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;usesAuthentication()Z"),
|
||||
cancellable = true
|
||||
)
|
||||
private boolean lobbySystems$hybridAuthentication(MinecraftServer server, ServerboundHelloPacket packet) {
|
||||
if (!server.usesAuthentication()) return false;
|
||||
return LobbySystemsMod.shouldUseVanillaAuthentication(packet.name());
|
||||
private void lobbySystems$hybridAuthentication(ServerboundHelloPacket packet, CallbackInfo ci) {
|
||||
if (!server.usesAuthentication()) return;
|
||||
|
||||
AuthManager.LoginRoute route = LobbySystemsMod.resolveLoginRoute(packet.name());
|
||||
if (route != AuthManager.LoginRoute.LOCAL) {
|
||||
// PREMIUM and UNKNOWN deliberately stay on vanilla authentication.
|
||||
return;
|
||||
}
|
||||
|
||||
this.authenticatedProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(packet.name()), packet.name());
|
||||
this.state = ServerLoginPacketListenerImpl.State.VERIFYING;
|
||||
LobbySystemsMod.markLocalLogin(packet.name());
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
],
|
||||
"license": "All Rights Reserved",
|
||||
"environment": "server",
|
||||
"accessWidener": "lobby-systems.accesswidener",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"id.dartokloning.lobbysystems.LobbySystemsMod"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
accessWidener v1 official
|
||||
|
||||
accessible class net/minecraft/server/network/ServerLoginPacketListenerImpl$State
|
||||
accessible field net/minecraft/server/network/ServerLoginPacketListenerImpl authenticatedProfile Lcom/mojang/authlib/GameProfile;
|
||||
Reference in New Issue
Block a user