Persist applet cookies to /config/saves

The JSObject stand-in kept cookies in memory, so saves were lost when the
game restarted. Write them to a file the session manager can sync, with the
policy granting only the agent jar access to that directory.
This commit is contained in:
2026-10-11 00:33:10 -07:00
parent 46ff37e7fb
commit 59d9ec5b4a
3 changed files with 66 additions and 14 deletions
+1 -1
View File
@@ -37,6 +37,6 @@ 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 whose window has
a working `document.cookie`, and a policy allowing HTTP requests through the
a `document.cookie` that persists to `/config/saves/cookies.txt`, and a policy allowing HTTP requests through the
game server's proxy. The same jar (`applet-support/`) is loaded as a Java agent
that removes appletviewer's menu bar and status line.
@@ -1,25 +1,37 @@
package netscape.javascript;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** A document whose cookie member behaves like document.cookie in a browser. */
/**
* A document whose cookie member behaves like document.cookie in a browser.
* Cookies persist to ~/saves/cookies.txt, one name=value pair per line.
*/
class Document extends JSObject {
private final File file = AccessController.doPrivileged(
(PrivilegedAction<File>) () -> new File(System.getProperty("user.home"), "saves/cookies.txt"));
private final Map<String, String> cookies = new LinkedHashMap<String, String>();
Document() {
for (String line : AccessController.doPrivileged((PrivilegedAction<List<String>>) this::read)) {
put(line);
}
}
@Override
public Object getMember(String name) throws JSException {
if (!name.equals("cookie")) {
return super.getMember(name);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> cookie : cookies.entrySet()) {
if (sb.length() > 0) {
sb.append("; ");
}
sb.append(cookie.getKey()).append('=').append(cookie.getValue());
}
return sb.toString();
return String.join("; ", lines());
}
@Override
@@ -28,10 +40,44 @@ class Document extends JSObject {
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());
if (put(String.valueOf(value).split(";", 2)[0])) {
AccessController.doPrivileged((PrivilegedAction<Void>) this::write);
}
}
private boolean put(String pair) {
int eq = pair.indexOf('=');
if (eq <= 0) {
return false;
}
cookies.put(pair.substring(0, eq).trim(), pair.substring(eq + 1).trim());
return true;
}
private List<String> lines() {
List<String> lines = new ArrayList<String>();
for (Map.Entry<String, String> cookie : cookies.entrySet()) {
lines.add(cookie.getKey() + "=" + cookie.getValue());
}
return lines;
}
private List<String> read() {
try {
return file.isFile() ? Files.readAllLines(file.toPath(), StandardCharsets.UTF_8) : new ArrayList<String>();
} catch (IOException e) {
System.err.println("Failed to read " + file + ": " + e);
return new ArrayList<String>();
}
}
private Void write() {
try {
file.getParentFile().mkdirs();
Files.write(file.toPath(), lines(), StandardCharsets.UTF_8);
} catch (IOException e) {
System.err.println("Failed to write " + file + ": " + e);
}
return null;
}
}
@@ -1,3 +1,9 @@
grant {
permission java.net.URLPermission "http:*", "*:*";
};
grant codeBase "file:/opt/flashpoint/applet-support.jar" {
permission java.util.PropertyPermission "user.home", "read";
permission java.io.FilePermission "${user.home}${/}saves", "read,write";
permission java.io.FilePermission "${user.home}${/}saves${/}-", "read,write";
};