Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/main/java/me/itzg/helpers/users/ManageUsersCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.errors.GenericException;
Expand All @@ -25,7 +26,10 @@
import me.itzg.helpers.json.ObjectMappers;
import me.itzg.helpers.users.model.JavaOp;
import me.itzg.helpers.users.model.JavaUser;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.maven.artifact.versioning.ComparableVersion;

import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.ExitCode;
Expand All @@ -46,6 +50,9 @@ public class ManageUsersCommand implements Callable<Integer> {
@Option(names = {"--help", "-h"}, usageHelp = true)
boolean help;

@Option(names = {"--offline"}, required = false, description = "Use for offline server, UUIDs are generated")
boolean offline;

@Option(names = "--output-directory", defaultValue = ".")
Path outputDirectory;

Expand Down Expand Up @@ -240,6 +247,10 @@ private JavaUser resolveJavaUserId(SharedFetch sharedFetch, List<? extends JavaU
}
}

if (offline) {
return getOfflineUUID(input);
}

final UserApi userApi;
switch (userApiProvider) {
case mojang:
Expand Down Expand Up @@ -329,4 +340,32 @@ private void processInputAsFile(SharedFetch sharedFetch, String filePathUrl) thr
private boolean usesTextUserList() {
return version != null && new ComparableVersion(version).compareTo(MIN_VERSION_USES_JSON) < 0;
}

private static JavaUser getOfflineUUID(String username) {
byte[] bytes = DigestUtils.md5("OfflinePlayer:"+username);

// Force version = 3 (bits 12-15 of time_hi_and_version)
bytes[6] &= 0x0F;
bytes[6] |= 0x30;

// Force variant = 2 (bits 6-7 of clock_seq_hi_and_reserved)
bytes[8] &= 0x3F;
bytes[8] |= 0x80;

long msb = 0;
long lsb = 0;

for (int i = 0; i < 8; i++) {
msb = (msb << 8) | (bytes[i] & 0xFF);
}

for (int i = 8; i < 16; i++) {
lsb = (lsb << 8) | (bytes[i] & 0xFF);
}

return JavaUser.builder()
.name(username)
.uuid(new UUID(msb, lsb).toString())
.build();
}
}
41 changes: 41 additions & 0 deletions src/test/java/me/itzg/helpers/users/ManageUsersCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ class ManageUsersCommandTest {

private static final String USER1_ID = "3f5f20286a85445fa7b46100e70c2b3a";
private static final String USER1_UUID = "3f5f2028-6a85-445f-a7b4-6100e70c2b3a";
private static final String USER1_OFFLINE_UUID = "fb4cdad9-642b-358f-8f6f-717981c9f42b";
private static final String USER2_ID = "5e5a1b2294b14f5892466062597e4c91";
private static final String USER2_UUID = "5e5a1b22-94b1-4f58-9246-6062597e4c91";
private static final String USER2_OFFLINE_UUID = "6e7d9aa0-0da2-390c-ab6a-377df9d77518";

@TempDir
Path tempDir;
Expand Down Expand Up @@ -406,6 +408,45 @@ void givenUuidsAndAllExist(WireMockRuntimeInfo wmInfo) throws IOException {
}
}

@Nested
public class whitelistOffline {
@Test
void givenNames(WireMockRuntimeInfo wmInfo) {
setupUserStubs();

final int exitCode = new CommandLine(
new ManageUsersCommand()
)
.execute(
"--mojang-api-base-url", wmInfo.getHttpBaseUrl(),
"--user-api-provider", "mojang",
"--offline",
"--type", "JAVA_WHITELIST",
"--output-directory", tempDir.toString(),
"user1", "user2"
);

assertThat(exitCode).isEqualTo(0);

final Path expectedFile = tempDir.resolve("whitelist.json");

assertThat(expectedFile).exists();

assertJson(expectedFile)
.isArrayContainingExactlyInAnyOrder(
conditions()
.satisfies(conditions()
.at("/name").hasValue("user1")
.at("/uuid").hasValue(USER1_OFFLINE_UUID)
)
.satisfies(conditions()
.at("/name").hasValue("user2")
.at("/uuid").hasValue(USER2_OFFLINE_UUID)
)
);
}
}

@Nested
public class whitelistOrOpsText {

Expand Down