Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc2915604 |
@@ -73,7 +73,7 @@ The SHA-1 response must contain a 40-character hexadecimal SHA-1. Both of these
|
|||||||
### Bedrock Edition through GeyserMC
|
### Bedrock Edition through GeyserMC
|
||||||
|
|
||||||
- Preferred remote mode: set `bedrockResourcePackUrl` to a direct `.mcpack` or `.zip` download URL. RemotePackSync creates a Geyser `UrlPackCodec` and registers it through the public `SessionLoadResourcePacksEvent` API, so Geyser supplies the URL to connecting Bedrock clients.
|
- Preferred remote mode: set `bedrockResourcePackUrl` to a direct `.mcpack` or `.zip` download URL. RemotePackSync creates a Geyser `UrlPackCodec` and registers it through the public `SessionLoadResourcePacksEvent` API, so Geyser supplies the URL to connecting Bedrock clients.
|
||||||
- The remote server must return `Content-Type: application/zip` and an exact `Content-Length`. Redirects are allowed only when they ultimately resolve to a valid direct download. The URL should be stable and should expose a changed ETag or URL when the pack changes.
|
- The remote server must return `Content-Type: application/zip` and an exact `Content-Length`. RemotePackSync verifies those headers (following redirects) before registering the URL; a provider that returns `application/octet-stream` is not compatible with Bedrock URL packs. Redirects are allowed only when they ultimately resolve to a valid direct download. The URL should be stable and should expose a changed ETag or URL when the pack changes.
|
||||||
- Geyser validates/downloads remote URL packs during its lifecycle. Use `/remotepacksync reload` after changing the URL or after replacing a pack at the same URL; players already connected must reconnect.
|
- Geyser validates/downloads remote URL packs during its lifecycle. Use `/remotepacksync reload` after changing the URL or after replacing a pack at the same URL; players already connected must reconnect.
|
||||||
- Local alternative: set `bedrockResourcePack` to a `.mcpack` or `.zip` file. The source is checked asynchronously at `refreshInterval`; changes receive a deterministic manifest UUID and are managed under `plugins/RemotePackSync/bedrock-managed/`.
|
- Local alternative: set `bedrockResourcePack` to a `.mcpack` or `.zip` file. The source is checked asynchronously at `refreshInterval`; changes receive a deterministic manifest UUID and are managed under `plugins/RemotePackSync/bedrock-managed/`.
|
||||||
- The original configured local pack is never modified.
|
- The original configured local pack is never modified.
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import org.geysermc.geyser.api.pack.option.ResourcePackOption;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -33,6 +37,9 @@ import java.util.zip.ZipOutputStream;
|
|||||||
/** Maintains a managed Bedrock pack and registers its current version for new Geyser sessions. */
|
/** Maintains a managed Bedrock pack and registers its current version for new Geyser sessions. */
|
||||||
public final class BedrockPackManager {
|
public final class BedrockPackManager {
|
||||||
private static final int MAX_MANIFEST_BYTES = 1024 * 1024;
|
private static final int MAX_MANIFEST_BYTES = 1024 * 1024;
|
||||||
|
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.build();
|
||||||
private static final Pattern HEADER_PROPERTY = Pattern.compile("\\\"header\\\"\\s*:\\s*\\{");
|
private static final Pattern HEADER_PROPERTY = Pattern.compile("\\\"header\\\"\\s*:\\s*\\{");
|
||||||
private static final Pattern UUID_PROPERTY = Pattern.compile("\\\"uuid\\\"\\s*:\\s*\\\"[^\\\"]*\\\"");
|
private static final Pattern UUID_PROPERTY = Pattern.compile("\\\"uuid\\\"\\s*:\\s*\\\"[^\\\"]*\\\"");
|
||||||
|
|
||||||
@@ -41,6 +48,7 @@ public final class BedrockPackManager {
|
|||||||
|
|
||||||
private volatile BedrockPackSettings settings;
|
private volatile BedrockPackSettings settings;
|
||||||
private BukkitTask refreshTask;
|
private BukkitTask refreshTask;
|
||||||
|
private GeyserEventRegistrar geyserEventRegistrar;
|
||||||
private boolean subscribed;
|
private boolean subscribed;
|
||||||
|
|
||||||
public BedrockPackManager(JavaPlugin plugin) {
|
public BedrockPackManager(JavaPlugin plugin) {
|
||||||
@@ -76,6 +84,16 @@ public final class BedrockPackManager {
|
|||||||
refreshTask.cancel();
|
refreshTask.cancel();
|
||||||
refreshTask = null;
|
refreshTask = null;
|
||||||
}
|
}
|
||||||
|
if (geyserEventRegistrar != null) {
|
||||||
|
try {
|
||||||
|
GeyserApi.api().eventBus().unregisterAll(geyserEventRegistrar);
|
||||||
|
} catch (RuntimeException | LinkageError exception) {
|
||||||
|
plugin.getLogger().log(Level.FINE, "Could not unregister the Geyser Bedrock resource-pack handler.", exception);
|
||||||
|
} finally {
|
||||||
|
geyserEventRegistrar = null;
|
||||||
|
subscribed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void subscribeToGeyser() {
|
private void subscribeToGeyser() {
|
||||||
@@ -89,11 +107,13 @@ public final class BedrockPackManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
GeyserEventRegistrar registrar = new GeyserEventRegistrar();
|
geyserEventRegistrar = new GeyserEventRegistrar();
|
||||||
GeyserApi.api().eventBus().subscribe(registrar, SessionLoadResourcePacksEvent.class, this::registerForSession);
|
GeyserApi.api().eventBus().subscribe(
|
||||||
|
geyserEventRegistrar, SessionLoadResourcePacksEvent.class, this::registerForSession);
|
||||||
subscribed = true;
|
subscribed = true;
|
||||||
plugin.getLogger().info("Registered Geyser Bedrock resource-pack session handler.");
|
plugin.getLogger().info("Registered Geyser Bedrock resource-pack session handler.");
|
||||||
} catch (RuntimeException | LinkageError exception) {
|
} catch (RuntimeException | LinkageError exception) {
|
||||||
|
geyserEventRegistrar = null;
|
||||||
plugin.getLogger().log(Level.WARNING, "Could not initialize Geyser Bedrock resource-pack support: "
|
plugin.getLogger().log(Level.WARNING, "Could not initialize Geyser Bedrock resource-pack support: "
|
||||||
+ exception.getMessage(), exception);
|
+ exception.getMessage(), exception);
|
||||||
}
|
}
|
||||||
@@ -120,6 +140,7 @@ public final class BedrockPackManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (activeSettings.isRemote()) {
|
if (activeSettings.isRemote()) {
|
||||||
|
validateRemotePackUrl(activeSettings.remoteUrl());
|
||||||
ResourcePack resourcePack = ResourcePack.create(PackCodec.url(activeSettings.remoteUrl()));
|
ResourcePack resourcePack = ResourcePack.create(PackCodec.url(activeSettings.remoteUrl()));
|
||||||
if (activeSettings != settings) {
|
if (activeSettings != settings) {
|
||||||
return;
|
return;
|
||||||
@@ -156,6 +177,39 @@ public final class BedrockPackManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geyser gives URL packs to Bedrock clients as URLs. Unlike Geyser's server-side
|
||||||
|
* download, the Bedrock client requires a direct response with application/zip
|
||||||
|
* and a real Content-Length, so catch bad hosting before registering the pack.
|
||||||
|
*/
|
||||||
|
private static void validateRemotePackUrl(String url) throws IOException, InterruptedException {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.method("HEAD", HttpRequest.BodyPublishers.noBody())
|
||||||
|
.header("Accept", "application/zip")
|
||||||
|
.build();
|
||||||
|
HttpResponse<Void> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.discarding());
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new IOException("Remote Bedrock pack returned HTTP " + response.statusCode() + ".");
|
||||||
|
}
|
||||||
|
validateRemotePackHeaders(
|
||||||
|
response.headers().firstValue("Content-Type").orElse(null),
|
||||||
|
response.headers().firstValue("Content-Length").orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void validateRemotePackHeaders(String contentType, String contentLength) throws IOException {
|
||||||
|
if (contentType == null || !contentType.toLowerCase(java.util.Locale.ROOT).startsWith("application/zip")) {
|
||||||
|
throw new IOException("Remote Bedrock pack must return Content-Type: application/zip, but returned "
|
||||||
|
+ (contentType == null ? "no Content-Type header" : contentType) + ".");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (contentLength == null || Long.parseLong(contentLength) <= 0) {
|
||||||
|
throw new NumberFormatException();
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
throw new IOException("Remote Bedrock pack must return a positive, exact Content-Length header.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Path writeManagedPack(Path source, byte[] sourceSha256) throws IOException {
|
private Path writeManagedPack(Path source, byte[] sourceSha256) throws IOException {
|
||||||
String sourceName = source.getFileName().toString().toLowerCase(java.util.Locale.ROOT);
|
String sourceName = source.getFileName().toString().toLowerCase(java.util.Locale.ROOT);
|
||||||
String extension = sourceName.endsWith(".mcpack") ? ".mcpack" : ".zip";
|
String extension = sourceName.endsWith(".mcpack") ? ".mcpack" : ".zip";
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ public final class RemotePackSyncPlugin extends JavaPlugin implements Listener,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getServer().getPluginManager().getPlugin("Geyser-Spigot") == null) {
|
if (getServer().getPluginManager().getPlugin("Geyser-Spigot") == null) {
|
||||||
|
if (bedrockPackManager != null) {
|
||||||
|
bedrockPackManager.configure(null);
|
||||||
|
}
|
||||||
getLogger().warning("bedrockResourcePack is configured, but Geyser-Spigot is not installed; Bedrock delivery is disabled.");
|
getLogger().warning("bedrockResourcePack is configured, but Geyser-Spigot is not installed; Bedrock delivery is disabled.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
@@ -34,6 +35,22 @@ final class BedrockPackManagerTest {
|
|||||||
assertEquals(1, occurrences(updated, "module-uuid-must-not-change"));
|
assertEquals(1, occurrences(updated, "module-uuid-must-not-change"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void acceptsBedrockClientCompatibleRemoteHeaders() {
|
||||||
|
assertDoesNotThrow(() -> BedrockPackManager.validateRemotePackHeaders("application/zip", "10406764"));
|
||||||
|
assertDoesNotThrow(() -> BedrockPackManager.validateRemotePackHeaders("application/zip; charset=binary", "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsRemoteHeadersThatBedrockClientsCannotUse() {
|
||||||
|
IOException contentType = assertThrows(IOException.class,
|
||||||
|
() -> BedrockPackManager.validateRemotePackHeaders("application/octet-stream", "10406764"));
|
||||||
|
assertEquals("Remote Bedrock pack must return Content-Type: application/zip, but returned application/octet-stream.",
|
||||||
|
contentType.getMessage());
|
||||||
|
assertThrows(IOException.class, () -> BedrockPackManager.validateRemotePackHeaders("application/zip", null));
|
||||||
|
assertThrows(IOException.class, () -> BedrockPackManager.validateRemotePackHeaders("application/zip", "0"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsManifestWithoutHeaderUuid() {
|
void rejectsManifestWithoutHeaderUuid() {
|
||||||
assertThrows(IOException.class, () -> BedrockPackManager.replaceHeaderUuid(
|
assertThrows(IOException.class, () -> BedrockPackManager.replaceHeaderUuid(
|
||||||
|
|||||||
Reference in New Issue
Block a user