diff --git a/streaming/streamer/Dockerfile b/streaming/streamer/Dockerfile index 7cdd753..961e498 100644 --- a/streaming/streamer/Dockerfile +++ b/streaming/streamer/Dockerfile @@ -1,3 +1,9 @@ +FROM eclipse-temurin:8-jdk AS jsobject +COPY jsobject /src +RUN mkdir /classes && \ + javac -d /classes $(find /src -name '*.java') && \ + jar cf /jsobject.jar -C /classes . + FROM ghcr.io/linuxserver/baseimage-selkies:debiantrixie RUN apt-get update && \ @@ -12,3 +18,4 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* COPY root/ / +COPY --from=jsobject /jsobject.jar /opt/flashpoint/jsobject.jar diff --git a/streaming/streamer/README.md b/streaming/streamer/README.md index 64ea793..1be94d8 100644 --- a/streaming/streamer/README.md +++ b/streaming/streamer/README.md @@ -33,3 +33,8 @@ different game, set `GAME_ID` and recreate the container. The launch log is at the game server, mounts the game's zip through the server's `mountzip` API, and runs the game's application with its HTTP proxy pointed at the game server. Selkies restarts the game when it exits (`RESTART_APP`). + +`appletviewer` lacks two things applets got from the browser's Java plugin, so +the image adds them: a `netscape.javascript.JSObject` stand-in (`jsobject/`) +whose window has a working `document.cookie`, and a policy allowing HTTP +requests through the game server's proxy. diff --git a/streaming/streamer/jsobject/netscape/javascript/Document.java b/streaming/streamer/jsobject/netscape/javascript/Document.java new file mode 100644 index 0000000..54e4973 --- /dev/null +++ b/streaming/streamer/jsobject/netscape/javascript/Document.java @@ -0,0 +1,37 @@ +package netscape.javascript; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** A document whose cookie member behaves like document.cookie in a browser. */ +class Document extends JSObject { + private final Map cookies = new LinkedHashMap(); + + @Override + public Object getMember(String name) throws JSException { + if (!name.equals("cookie")) { + return super.getMember(name); + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry cookie : cookies.entrySet()) { + if (sb.length() > 0) { + sb.append("; "); + } + sb.append(cookie.getKey()).append('=').append(cookie.getValue()); + } + return sb.toString(); + } + + @Override + public void setMember(String name, Object value) throws JSException { + if (!name.equals("cookie")) { + super.setMember(name, value); + return; + } + String pair = String.valueOf(value).split(";", 2)[0]; + int eq = pair.indexOf('='); + if (eq > 0) { + cookies.put(pair.substring(0, eq).trim(), pair.substring(eq + 1).trim()); + } + } +} diff --git a/streaming/streamer/jsobject/netscape/javascript/JSException.java b/streaming/streamer/jsobject/netscape/javascript/JSException.java new file mode 100644 index 0000000..f5448cc --- /dev/null +++ b/streaming/streamer/jsobject/netscape/javascript/JSException.java @@ -0,0 +1,10 @@ +package netscape.javascript; + +public class JSException extends RuntimeException { + public JSException() { + } + + public JSException(String message) { + super(message); + } +} diff --git a/streaming/streamer/jsobject/netscape/javascript/JSObject.java b/streaming/streamer/jsobject/netscape/javascript/JSObject.java new file mode 100644 index 0000000..adb5959 --- /dev/null +++ b/streaming/streamer/jsobject/netscape/javascript/JSObject.java @@ -0,0 +1,53 @@ +package netscape.javascript; + +import java.applet.Applet; +import java.util.HashMap; +import java.util.Map; + +/** + * Stand-in for the browser's LiveConnect bridge, which appletviewer lacks. The + * window holds a document with a working cookie; calls into JavaScript do nothing. + */ +public class JSObject { + private static JSObject window; + + private final Map members = new HashMap(); + + protected JSObject() { + } + + public static synchronized JSObject getWindow(Applet applet) throws JSException { + if (window == null) { + window = new JSObject(); + window.setMember("document", new Document()); + } + return window; + } + + public Object call(String methodName, Object[] args) throws JSException { + return null; + } + + public Object eval(String s) throws JSException { + return null; + } + + public Object getMember(String name) throws JSException { + return members.get(name); + } + + public void setMember(String name, Object value) throws JSException { + members.put(name, value); + } + + public void removeMember(String name) throws JSException { + members.remove(name); + } + + public Object getSlot(int index) throws JSException { + return null; + } + + public void setSlot(int index, Object value) throws JSException { + } +} diff --git a/streaming/streamer/root/opt/flashpoint/applet.policy b/streaming/streamer/root/opt/flashpoint/applet.policy new file mode 100644 index 0000000..fb38806 --- /dev/null +++ b/streaming/streamer/root/opt/flashpoint/applet.policy @@ -0,0 +1,3 @@ +grant { + permission java.net.URLPermission "http:*", "*:*"; +}; diff --git a/streaming/streamer/root/usr/local/bin/fp-launch b/streaming/streamer/root/usr/local/bin/fp-launch index e94b4d3..1812e57 100755 --- a/streaming/streamer/root/usr/local/bin/fp-launch +++ b/streaming/streamer/root/usr/local/bin/fp-launch @@ -34,6 +34,8 @@ fi echo "Launching $ID: $app $cmd" case ${app,,} in 'fpsoftware\startjava.bat' | 'fpsoftware\startjavainbrowser.bat') + # Applets expect the browser's JavaScript bridge and a browser-like network policy + export JAVA_TOOL_OPTIONS="-Xbootclasspath/a:/opt/flashpoint/jsobject.jar -Djava.security.policy=/opt/flashpoint/applet.policy" FP_PATH=$FP_PATH exec "$FP_PATH/FPSoftware/startJava.sh" "$cmd" ;; *) echo "Unsupported application: $app" >&2; exit 1 ;;