From: Niki Roo
Date: Mon, 11 May 2020 21:57:44 +0000 (+0200)
Subject: Merge branch 'master' into subtree
X-Git-Url: http://git.nikiroo.be/?a=commitdiff_plain;ds=inline;h=5f3671e17febc5b7f6abbfc62c66c4045d47ec8d;hp=-c;p=fanfix.git
Merge branch 'master' into subtree
---
5f3671e17febc5b7f6abbfc62c66c4045d47ec8d
diff --combined Instance.java
index c3c086f,a2cb90a..a2cb90a
--- a/Instance.java
+++ b/Instance.java
@@@ -15,6 -15,7 +15,7 @@@ import be.nikiroo.fanfix.library.BasicL
import be.nikiroo.fanfix.library.CacheLibrary;
import be.nikiroo.fanfix.library.LocalLibrary;
import be.nikiroo.fanfix.library.RemoteLibrary;
+ import be.nikiroo.fanfix.library.WebLibrary;
import be.nikiroo.utils.Cache;
import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.Image;
@@@ -67,19 -68,21 +68,21 @@@ public class Instance
}
/**
- * Initialise the instance -- if already initialised, nothing will happen unless
- * you pass TRUE to force.
+ * Initialise the instance -- if already initialised, nothing will happen
+ * unless you pass TRUE to force.
*
- * Before calling this method, you may call {@link Bundles#setDirectory(String)}
- * if wanted.
+ * Before calling this method, you may call
+ * {@link Bundles#setDirectory(String)} if wanted.
*
- * Note: forcing the initialisation can be dangerous, so make sure to only make
- * it under controlled circumstances -- for instance, at the start of the
- * program, you could call {@link Instance#init()}, change some settings because
- * you want to force those settings (it will also forbid users to change them!)
- * and then call {@link Instance#init(boolean)} with force set to TRUE.
- *
- * @param force force the initialisation even if already initialised
+ * Note: forcing the initialisation can be dangerous, so make sure to only
+ * make it under controlled circumstances -- for instance, at the start of
+ * the program, you could call {@link Instance#init()}, change some settings
+ * because you want to force those settings (it will also forbid users to
+ * change them!) and then call {@link Instance#init(boolean)} with
+ * force set to TRUE.
+ *
+ * @param force
+ * force the initialisation even if already initialised
*/
static public void init(boolean force) {
synchronized (instancelock) {
@@@ -95,7 -98,8 +98,8 @@@
*
* Usually for DEBUG/Test purposes.
*
- * @param instance the actual Instance to use
+ * @param instance
+ * the actual Instance to use
*/
static public void init(Instance instance) {
Instance.instance = instance;
@@@ -113,8 -117,8 +117,8 @@@
/**
* Actually initialise the instance.
*
- * Before calling this method, you may call {@link Bundles#setDirectory(String)}
- * if wanted.
+ * Before calling this method, you may call
+ * {@link Bundles#setDirectory(String)} if wanted.
*/
protected Instance() {
// Before we can configure it:
@@@ -156,21 -160,24 +160,24 @@@
int hoursLarge = config.getInteger(Config.CACHE_MAX_TIME_STABLE, 0);
cache = new DataLoader(tmp, ua, hours, hoursLarge);
} catch (IOException e) {
- tracer.error(new IOException("Cannot create cache (will continue without cache)", e));
+ tracer.error(new IOException(
+ "Cannot create cache (will continue without cache)", e));
cache = new DataLoader(ua);
}
cache.setTraceHandler(tracer);
// readerTmp / coverDir
- readerTmp = getFile(UiConfig.CACHE_DIR_LOCAL_READER, configDir, "tmp-reader");
+ readerTmp = getFile(UiConfig.CACHE_DIR_LOCAL_READER, configDir,
+ "tmp-reader");
coverDir = getFile(Config.DEFAULT_COVERS_DIR, configDir, "covers");
coverDir.mkdirs();
try {
tempFiles = new TempFiles("fanfix");
} catch (IOException e) {
- tracer.error(new IOException("Cannot create temporary directory", e));
+ tracer.error(
+ new IOException("Cannot create temporary directory", e));
}
}
@@@ -188,7 -195,8 +195,8 @@@
/**
* The traces handler for this {@link Cache}.
*
- * @param tracer the new traces handler or NULL
+ * @param tracer
+ * the new traces handler or NULL
*/
public void setTraceHandler(TraceHandler tracer) {
if (tracer == null) {
@@@ -220,7 -228,8 +228,8 @@@
/**
* Reset the configuration.
*
- * @param resetTrans also reset the translation files
+ * @param resetTrans
+ * also reset the translation files
*/
public void resetConfig(boolean resetTrans) {
String dir = Bundles.getDirectory();
@@@ -330,7 -339,8 +339,8 @@@
* Return the directory where to store temporary files for the remote
* {@link LocalLibrary}.
*
- * @param host the remote for this host
+ * @param host
+ * the remote for this host
*
* @return the directory
*/
@@@ -342,8 -352,10 +352,10 @@@
* Return the directory where to store temporary files for the remote
* {@link LocalLibrary}.
*
- * @param remoteDir the base remote directory
- * @param host the remote for this host
+ * @param remoteDir
+ * the base remote directory
+ * @param host
+ * the remote for this host
*
* @return the directory
*/
@@@ -364,10 -376,13 +376,13 @@@
*/
public boolean isVersionCheckNeeded() {
try {
- long wait = config.getInteger(Config.NETWORK_UPDATE_INTERVAL, 0) * 24 * 60 * 60 * 1000;
+ long wait = config.getInteger(Config.NETWORK_UPDATE_INTERVAL, 0)
+ * 24 * 60 * 60 * 1000;
if (wait >= 0) {
- String lastUpString = IOUtils.readSmallFile(new File(configDir, "LAST_UPDATE"));
- long delay = new Date().getTime() - Long.parseLong(lastUpString);
+ String lastUpString = IOUtils
+ .readSmallFile(new File(configDir, "LAST_UPDATE"));
+ long delay = new Date().getTime()
+ - Long.parseLong(lastUpString);
if (delay > wait) {
return true;
}
@@@ -387,7 -402,8 +402,8 @@@
*/
public void setVersionChecked() {
try {
- IOUtils.writeSmallFile(new File(configDir), "LAST_UPDATE", Long.toString(new Date().getTime()));
+ IOUtils.writeSmallFile(new File(configDir), "LAST_UPDATE",
+ Long.toString(new Date().getTime()));
} catch (IOException e) {
tracer.error(e);
}
@@@ -405,8 -421,8 +421,8 @@@
}
/**
- * The configuration directory (will check, in order of preference, the system
- * properties, the environment and then defaults to
+ * The configuration directory (will check, in order of preference, the
+ * system properties, the environment and then defaults to
* {@link Instance#getHome()}/.fanfix).
*
* @return the config directory
@@@ -430,9 -446,11 +446,11 @@@
* {@link Instance#uiconfig}, {@link Instance#trans} and
* {@link Instance#transGui}).
*
- * @param configDir the directory where to find the configuration files
- * @param refresh TRUE to reset the configuration files from the default
- * included ones
+ * @param configDir
+ * the directory where to find the configuration files
+ * @param refresh
+ * TRUE to reset the configuration files from the default
+ * included ones
*/
private void createConfigs(String configDir, boolean refresh) {
if (!refresh) {
@@@ -476,37 -494,56 +494,56 @@@
/**
* Create the default library as specified by the config.
*
- * @param remoteDir the base remote directory if needed
+ * @param remoteDir
+ * the base remote directory if needed
*
* @return the default {@link BasicLibrary}
*/
private BasicLibrary createDefaultLibrary(File remoteDir) {
BasicLibrary lib = null;
- boolean useRemote = config.getBoolean(Config.REMOTE_LIBRARY_ENABLED, false);
+ boolean useRemote = config.getBoolean(Config.REMOTE_LIBRARY_ENABLED,
+ false);
if (useRemote) {
String host = null;
int port = -1;
try {
- host = config.getString(Config.REMOTE_LIBRARY_HOST);
+ host = config.getString(Config.REMOTE_LIBRARY_HOST,
+ "fanfix://localhost");
port = config.getInteger(Config.REMOTE_LIBRARY_PORT, -1);
String key = config.getString(Config.REMOTE_LIBRARY_KEY);
+ if (!host.startsWith("http://") && !host.startsWith("https://")
+ && !host.startsWith("fanfix://")) {
+ host = "fanfix://" + host;
+ }
+
tracer.trace("Selecting remote library " + host + ":" + port);
- lib = new RemoteLibrary(key, host, port);
- lib = new CacheLibrary(getRemoteDir(remoteDir, host), lib, uiconfig);
+
+ if (host.startsWith("fanfix://")) {
+ lib = new RemoteLibrary(key, host, port);
+ } else {
+ lib = new WebLibrary(key, host, port);
+ }
+
+ lib = new CacheLibrary(getRemoteDir(remoteDir, host), lib,
+ uiconfig);
} catch (Exception e) {
- tracer.error(new IOException("Cannot create remote library for: " + host + ":" + port, e));
+ tracer.error(
+ new IOException("Cannot create remote library for: "
+ + host + ":" + port, e));
}
} else {
String libDir = System.getenv("BOOKS_DIR");
if (libDir == null || libDir.isEmpty()) {
- libDir = getFile(Config.LIBRARY_DIR, configDir, "$HOME/Books").getPath();
+ libDir = getFile(Config.LIBRARY_DIR, configDir, "$HOME/Books")
+ .getPath();
}
try {
lib = new LocalLibrary(new File(libDir), config);
} catch (Exception e) {
- tracer.error(new IOException("Cannot create library for directory: " + libDir, e));
+ tracer.error(new IOException(
+ "Cannot create library for directory: " + libDir, e));
}
}
@@@ -516,9 -553,12 +553,12 @@@
/**
* Return a path, but support the special $HOME variable.
*
- * @param id the key for the path, which may contain "$HOME"
- * @param configDir the directory to use as base if not absolute
- * @param def the default value if none (will be configDir-rooted if needed)
+ * @param id
+ * the key for the path, which may contain "$HOME"
+ * @param configDir
+ * the directory to use as base if not absolute
+ * @param def
+ * the default value if none (will be configDir-rooted if needed)
* @return the path, with expanded "$HOME" if needed
*/
protected File getFile(Config id, String configDir, String def) {
@@@ -529,9 -569,12 +569,12 @@@
/**
* Return a path, but support the special $HOME variable.
*
- * @param id the key for the path, which may contain "$HOME"
- * @param configDir the directory to use as base if not absolute
- * @param def the default value if none (will be configDir-rooted if needed)
+ * @param id
+ * the key for the path, which may contain "$HOME"
+ * @param configDir
+ * the directory to use as base if not absolute
+ * @param def
+ * the default value if none (will be configDir-rooted if needed)
* @return the path, with expanded "$HOME" if needed
*/
protected File getFile(UiConfig id, String configDir, String def) {
@@@ -542,8 -585,10 +585,10 @@@
/**
* Return a path, but support the special $HOME variable.
*
- * @param path the path, which may contain "$HOME"
- * @param configDir the directory to use as base if not absolute
+ * @param path
+ * the path, which may contain "$HOME"
+ * @param configDir
+ * the directory to use as base if not absolute
* @return the path, with expanded "$HOME" if needed
*/
protected File getFile(String path, String configDir) {
@@@ -567,8 -612,8 +612,8 @@@
* properties.
*
* The environment variable is tested first. Then, the custom property
- * "fanfix.home" is tried, followed by the usual "user.home" then "java.io.tmp"
- * if nothing else is found.
+ * "fanfix.home" is tried, followed by the usual "user.home" then
+ * "java.io.tmp" if nothing else is found.
*
* @return the home
*/
@@@ -615,7 -660,8 +660,8 @@@
String lang = config.getString(Config.LANG);
if (lang == null || lang.isEmpty()) {
- if (System.getenv("LANG") != null && !System.getenv("LANG").isEmpty()) {
+ if (System.getenv("LANG") != null
+ && !System.getenv("LANG").isEmpty()) {
lang = System.getenv("LANG");
}
}
@@@ -630,7 -676,8 +676,8 @@@
/**
* Check that the given environment variable is "enabled".
*
- * @param key the variable to check
+ * @param key
+ * the variable to check
*
* @return TRUE if it is
*/
@@@ -638,7 -685,8 +685,8 @@@
String value = System.getenv(key);
if (value != null) {
value = value.trim().toLowerCase();
- if ("yes".equals(value) || "true".equals(value) || "on".equals(value) || "1".equals(value)
+ if ("yes".equals(value) || "true".equals(value)
+ || "on".equals(value) || "1".equals(value)
|| "y".equals(value)) {
return true;
}
diff --combined Main.java
index c0dd9e0,7be305a..7be305a
--- a/Main.java
+++ b/Main.java
@@@ -19,6 -19,7 +19,7 @@@ import be.nikiroo.fanfix.library.CacheL
import be.nikiroo.fanfix.library.LocalLibrary;
import be.nikiroo.fanfix.library.RemoteLibrary;
import be.nikiroo.fanfix.library.RemoteLibraryServer;
+ import be.nikiroo.fanfix.library.WebLibraryServer;
import be.nikiroo.fanfix.output.BasicOutput;
import be.nikiroo.fanfix.output.BasicOutput.OutputType;
import be.nikiroo.fanfix.reader.BasicReader;
@@@ -29,7 -30,6 +30,6 @@@ import be.nikiroo.fanfix.supported.Supp
import be.nikiroo.utils.Progress;
import be.nikiroo.utils.Version;
import be.nikiroo.utils.VersionCheck;
- import be.nikiroo.utils.serial.server.ServerObject;
/**
* Main program entry point.
@@@ -79,7 -79,7 +79,7 @@@ public class Main
*
--version: get the version of the program
*
--server: start the server mode (see config file for parameters)
*
--stop-server: stop the running server on this port if any
- *
--remote [key] [host] [port]: use a the given remote library
+ *
--remote [key] [host] [port]: use the given remote library
*
*
* @param args
@@@ -626,15 -626,8 +626,8 @@@
}
break;
case SERVER:
- key = Instance.getInstance().getConfig().getString(Config.SERVER_KEY);
- port = Instance.getInstance().getConfig().getInteger(Config.SERVER_PORT);
- if (port == null) {
- System.err.println("No port configured in the config file");
- exitCode = 15;
- break;
- }
try {
- startServer(key, port);
+ startServer();
} catch (IOException e) {
Instance.getInstance().getTraceHandler().error(e);
}
@@@ -1037,20 -1030,29 +1030,29 @@@
/**
* Start a Fanfix server.
*
- * @param key
- * the key taht will be needed to contact the Fanfix server
- * @param port
- * the port on which to run
- *
* @throws IOException
* in case of I/O errors
* @throws SSLException
* when the key was not accepted
*/
- private void startServer(String key, int port) throws IOException {
- ServerObject server = new RemoteLibraryServer(key, port);
- server.setTraceHandler(Instance.getInstance().getTraceHandler());
- server.run();
+ private void startServer() throws IOException {
+ String mode = Instance.getInstance().getConfig()
+ .getString(Config.SERVER_MODE, "fanfix");
+ if (mode.equals("fanfix")) {
+ RemoteLibraryServer server = new RemoteLibraryServer();
+ server.setTraceHandler(Instance.getInstance().getTraceHandler());
+ server.run();
+ } else if (mode.equals("http")) {
+ WebLibraryServer server = new WebLibraryServer(false);
+ server.setTraceHandler(Instance.getInstance().getTraceHandler());
+ server.run();
+ } else if (mode.equals("https")) {
+ WebLibraryServer server = new WebLibraryServer(true);
+ server.setTraceHandler(Instance.getInstance().getTraceHandler());
+ server.run();
+ } else {
+ throw new IOException("Unknown server mode: " + mode);
+ }
}
/**
diff --combined bundles/Config.java
index 3af83c1,c96ed22..c96ed22
--- a/bundles/Config.java
+++ b/bundles/Config.java
@@@ -58,7 -58,7 +58,7 @@@ public enum Config
@Meta(description = "Use the remote Fanfix server configured here instead of the local library (if FALSE, the local library will be used instead)",//
format = Format.BOOLEAN, def = "false")
REMOTE_LIBRARY_ENABLED, //
- @Meta(description = "The remote Fanfix server to connect to",//
+ @Meta(description = "The remote Fanfix server to connect to (fanfix://, http://, https:// -- if not specified, fanfix:// is assumed)",//
format = Format.STRING)
REMOTE_LIBRARY_HOST, //
@Meta(description = "The port to use for the remote Fanfix server",//
@@@ -84,10 -84,19 +84,19 @@@
@Meta(description = "Remote Server configuration\nNote that the key is structured: \"KEY|SUBKEY|wl|rw\"\n- \"KEY\" is the actual encryption key (it can actually be empty, which will still encrypt the messages but of course it will be easier to guess the key)\n- \"SUBKEY\" is the (optional) subkey to use to get additional privileges\n- \"wl\" is a special privilege that allows that subkey to ignore white lists\n- \"rw\" is a special privilege that allows that subkey to modify the library, even if it is not in RW (by default) mode\n\nSome examples:\n- \"super-secret\": a normal key, no special privileges\n- \"you-will-not-guess|azOpd8|wl\": a white-list ignoring key\n- \"new-password|subpass|rw\": a key that allows modifications on the library",//
group = true)
SERVER, //
+ @Meta(description = "Remote Server mode: you can use the fanfix protocol (which is encrypted), http (which is not) or https (which requires a keystore.jks file)",//
+ format = Format.FIXED_LIST, list = { "fanfix", "http", "https" }, def = "fanfix")
+ SERVER_MODE,
@Meta(description = "The port on which we can start the server (must be a valid port, from 1 to 65535)", //
format = Format.INT, def = "58365")
SERVER_PORT, //
- @Meta(description = "The encryption key for the server (NOT including a subkey), it cannot contain the pipe character \"|\" but can be empty (it is *still* encrypted, but with an empty, easy to guess key)",//
+ @Meta(description = "A keystore.jks file, required to use HTTPS (the server will refuse to start in HTTPS mode without this file)", //
+ format = Format.STRING, def = "")
+ SERVER_SSL_KEYSTORE,
+ @Meta(description = "The pass phrase required to open the keystore.jks file (required for HTTPS mode)", //
+ format = Format.PASSWORD, def = "")
+ SERVER_SSL_KEYSTORE_PASS,
+ @Meta(description = "The encryption key for the server (NOT including a subkey), it cannot contain the pipe character \"|\" but can be empty -- is used to encrypt the traffic in fanfix mode (even if empty, traffic will be encrypted in fanfix mode), and used as a password for HTTP (clear text protocol) and HTTPS modes",//
format = Format.PASSWORD, def = "")
SERVER_KEY, //
@Meta(description = "Allow write access to the clients (download story, move story...) without RW subkeys", //
@@@ -96,9 -105,12 +105,12 @@@
@Meta(description = "If not empty, only the EXACT listed sources will be available for clients without BL subkeys",//
array = true, format = Format.STRING, def = "")
SERVER_WHITELIST, //
- @Meta(description = "The subkeys that the server will allow, including the modes\nA subkey ", //
+ @Meta(description = "The subkeys that the server will allow, including the modes\nA subkey is used as a login for HTTP (clear text protocol) and HTTPS modes", //
array = true, format = Format.STRING, def = "")
SERVER_ALLOWED_SUBKEYS, //
+ @Meta(description = "The maximum size of the cache, in MegaBytes, for HTTP and HTTPS servers", //
+ format = Format.INT, def = "100")
+ SERVER_MAX_CACHE_MB,
@Meta(description = "DEBUG options",//
group = true)
diff --combined data/JsonIO.java
index 0000000,db81db6..db81db6
mode 000000,100644..100644
--- a/data/JsonIO.java
+++ b/data/JsonIO.java
@@@ -1,0 -1,164 +1,164 @@@
+ package be.nikiroo.fanfix.data;
+
+ import java.util.ArrayList;
+ import java.util.List;
+
+ import org.json.JSONArray;
+ import org.json.JSONException;
+ import org.json.JSONObject;
+
+ public class JsonIO {
+ static public JSONObject toJson(MetaData meta) {
+ if (meta == null) {
+ return null;
+ }
+
+ JSONObject json = new JSONObject();
+ put(json, "", MetaData.class.getName());
+ put(json, "luid", meta.getLuid());
+ put(json, "title", meta.getTitle());
+ put(json, "author", meta.getAuthor());
+ put(json, "source", meta.getSource());
+ put(json, "url", meta.getUrl());
+ put(json, "words", meta.getWords());
+ put(json, "creation_date", meta.getCreationDate());
+ put(json, "date", meta.getDate());
+ put(json, "lang", meta.getLang());
+ put(json, "publisher", meta.getPublisher());
+ put(json, "subject", meta.getSubject());
+ put(json, "type", meta.getType());
+ put(json, "uuid", meta.getUuid());
+ put(json, "resume", toJson(meta.getResume()));
+ put(json, "tags", new JSONArray(meta.getTags()));
+
+ return json;
+ }
+
+ /**
+ *
+ * @param json
+ *
+ * @return
+ *
+ * @throws JSONException
+ * when it cannot be converted
+ */
+ static public MetaData toMetaData(JSONObject json) {
+ if (json == null) {
+ return null;
+ }
+
+ MetaData meta = new MetaData();
+ meta.setLuid(getString(json, "luid"));
+ meta.setTitle(getString(json, "title"));
+ meta.setAuthor(getString(json, "author"));
+ meta.setSource(getString(json, "source"));
+ meta.setUrl(getString(json, "url"));
+ meta.setWords(getLong(json, "words", 0));
+ meta.setCreationDate(getString(json, "creation_date"));
+ meta.setDate(getString(json, "date"));
+ meta.setLang(getString(json, "lang"));
+ meta.setPublisher(getString(json, "publisher"));
+ meta.setSubject(getString(json, "subject"));
+ meta.setType(getString(json, "type"));
+ meta.setUuid(getString(json, "uuid"));
+
+ meta.setResume(toChapter(getJson(json, "resume")));
+ meta.setTags(toListString(getJsonArr(json, "tags")));
+
+ return meta;
+ }
+
+ static public JSONObject toJson(Chapter chap) {
+ if (chap == null) {
+ return null;
+ }
+
+ JSONObject json = new JSONObject();
+
+ // TODO
+
+ return json;
+ }
+
+ /**
+ *
+ * @param json
+ *
+ * @return
+ *
+ * @throws JSONException
+ * when it cannot be converted
+ */
+ static public Chapter toChapter(JSONObject json) {
+ if (json == null) {
+ return null;
+ }
+
+ Chapter chap = new Chapter(0, "");
+
+ // TODO
+
+ return chap;
+ }
+
+ static public List toListString(JSONArray array) {
+ if (array != null) {
+ List values = new ArrayList();
+ for (Object value : array.toList()) {
+ values.add(value == null ? null : value.toString());
+ }
+ return values;
+ }
+
+ return null;
+ }
+
+ static private void put(JSONObject json, String key, Object o) {
+ json.put(key, o == null ? JSONObject.NULL : o);
+ }
+
+ static String getString(JSONObject json, String key) {
+ if (json.has(key)) {
+ Object o = json.get(key);
+ if (o instanceof String) {
+ return (String) o;
+ }
+ }
+
+ return null;
+ }
+
+ static long getLong(JSONObject json, String key, long def) {
+ if (json.has(key)) {
+ Object o = json.get(key);
+ if (o instanceof Long) {
+ return (Long) o;
+ }
+ }
+
+ return def;
+ }
+
+ static JSONObject getJson(JSONObject json, String key) {
+ if (json.has(key)) {
+ Object o = json.get(key);
+ if (o instanceof JSONObject) {
+ return (JSONObject) o;
+ }
+ }
+
+ return null;
+ }
+
+ static JSONArray getJsonArr(JSONObject json, String key) {
+ if (json.has(key)) {
+ Object o = json.get(key);
+ if (o instanceof JSONArray) {
+ return (JSONArray) o;
+ }
+ }
+
+ return null;
+ }
+ }
diff --combined library/MetaResultList.java
index 0903740,8b8a167..8b8a167
--- a/library/MetaResultList.java
+++ b/library/MetaResultList.java
@@@ -224,7 -224,7 +224,7 @@@ public class MetaResultList
if (sources == null && authors == null && tags == null) {
return metas;
}
-
+
// allow "sources/" hierarchy
if (sources != null) {
List folders = new ArrayList();
diff --combined library/RemoteLibrary.java
index a4f00ce,9fa8c66..9fa8c66
--- a/library/RemoteLibrary.java
+++ b/library/RemoteLibrary.java
@@@ -21,6 -21,8 +21,8 @@@ import be.nikiroo.utils.serial.server.C
* This {@link BasicLibrary} will access a remote server to list the available
* stories, and download the ones you try to load to the local directory
* specified in the configuration.
+ *
+ * This remote library uses a custom fanfix:// protocol.
*
* @author niki
*/
@@@ -35,8 -37,8 +37,8 @@@ public class RemoteLibrary extends Basi
}
@Override
- public Object send(Object data) throws IOException,
- NoSuchFieldException, NoSuchMethodException,
+ public Object send(Object data)
+ throws IOException, NoSuchFieldException, NoSuchMethodException,
ClassNotFoundException {
Object rep = super.send(data);
if (rep instanceof RemoteLibraryException) {
@@@ -113,20 -115,26 +115,26 @@@
this.subkey = "";
}
+ if (host.startsWith("fanfix://")) {
+ host = host.substring("fanfix://".length());
+ }
+
this.host = host;
this.port = port;
}
@Override
public String getLibraryName() {
- return (rw ? "[READ-ONLY] " : "") + host + ":" + port;
+ return (rw ? "[READ-ONLY] " : "") + "fanfix://" + host + ":" + port;
}
@Override
public Status getStatus() {
- Instance.getInstance().getTraceHandler().trace("Getting remote lib status...");
+ Instance.getInstance().getTraceHandler()
+ .trace("Getting remote lib status...");
Status status = getStatusDo();
- Instance.getInstance().getTraceHandler().trace("Remote lib status: " + status);
+ Instance.getInstance().getTraceHandler()
+ .trace("Remote lib status: " + status);
return status;
}
@@@ -180,8 -188,8 +188,8 @@@
@Override
public void action(ConnectActionClientObject action)
throws Exception {
- Object rep = action.send(new Object[] { subkey, "GET_COVER",
- luid });
+ Object rep = action
+ .send(new Object[] { subkey, "GET_COVER", luid });
result[0] = (Image) rep;
}
});
@@@ -232,8 -240,8 +240,8 @@@
pg = new Progress();
}
- Object rep = action.send(new Object[] { subkey, "GET_STORY",
- luid });
+ Object rep = action
+ .send(new Object[] { subkey, "GET_STORY", luid });
MetaData meta = null;
if (rep instanceof MetaData) {
@@@ -354,7 -362,7 +362,7 @@@
// Could work (more slowly) without it
public MetaData imprt(final URL url, Progress pg) throws IOException {
// Import the file locally if it is actually a file
-
+
if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
return super.imprt(url, pg);
}
@@@ -374,8 -382,8 +382,8 @@@
throws Exception {
Progress pg = pgF;
- Object rep = action.send(new Object[] { subkey, "IMPORT",
- url.toString() });
+ Object rep = action.send(
+ new Object[] { subkey, "IMPORT", url.toString() });
while (true) {
if (!RemoteLibraryServer.updateProgress(pg, rep)) {
@@@ -524,8 -532,8 +532,8 @@@
pg = new Progress();
}
- Object rep = action.send(new Object[] { subkey, "GET_METADATA",
- luid });
+ Object rep = action
+ .send(new Object[] { subkey, "GET_METADATA", luid });
while (true) {
if (!RemoteLibraryServer.updateProgress(pg, rep)) {
diff --combined library/RemoteLibraryServer.java
index f92c37e,c150a01..c150a01
--- a/library/RemoteLibraryServer.java
+++ b/library/RemoteLibraryServer.java
@@@ -70,16 -70,16 +70,16 @@@ public class RemoteLibraryServer extend
* Note: the key we use here is the encryption key (it must not contain a
* subkey).
*
- * @param key
- * the key that will restrict access to this server
- * @param port
- * the port to listen on
- *
* @throws IOException
* in case of I/O error
*/
- public RemoteLibraryServer(String key, int port) throws IOException {
- super("Fanfix remote library", port, key);
+ public RemoteLibraryServer() throws IOException {
+ super("Fanfix remote library",
+ Instance.getInstance().getConfig()
+ .getInteger(Config.SERVER_PORT),
+ Instance.getInstance().getConfig()
+ .getString(Config.SERVER_KEY));
+
setTraceHandler(Instance.getInstance().getTraceHandler());
}
diff --combined library/WebLibrary.java
index 0000000,369eb23..369eb23
mode 000000,100644..100644
--- a/library/WebLibrary.java
+++ b/library/WebLibrary.java
@@@ -1,0 -1,236 +1,236 @@@
+ package be.nikiroo.fanfix.library;
+
+ import java.io.File;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.net.URL;
+ import java.util.ArrayList;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+
+ import org.json.JSONArray;
+ import org.json.JSONObject;
+
+ import be.nikiroo.fanfix.Instance;
+ import be.nikiroo.fanfix.data.JsonIO;
+ import be.nikiroo.fanfix.data.MetaData;
+ import be.nikiroo.fanfix.data.Story;
+ import be.nikiroo.utils.IOUtils;
+ import be.nikiroo.utils.Image;
+ import be.nikiroo.utils.Progress;
+
+ /**
+ * This {@link BasicLibrary} will access a remote server to list the available
+ * stories, and download the ones you try to load to the local directory
+ * specified in the configuration.
+ *
+ * This remote library uses http:// or https://.
+ *
+ * @author niki
+ */
+ public class WebLibrary extends BasicLibrary {
+ private String host;
+ private int port;
+ private final String key;
+ private final String subkey;
+
+ // informative only (server will make the actual checks)
+ private boolean rw;
+
+ /**
+ * Create a {@link RemoteLibrary} linked to the given server.
+ *
+ * Note that the key is structured:
+ * xxx(|yyy|wl)(|rw)
+ *
+ * Note that anything before the first pipe (|) character is
+ * considered to be the encryption key, anything after that character is
+ * called the subkey (including the other pipe characters and flags!).
+ *
+ * This is important because the subkey (including the pipe characters and
+ * flags) must be present as-is in the server configuration file to be
+ * allowed.
+ *
+ *
xxx: the encryption key used to communicate with the
+ * server
+ *
yyy: the secondary key
+ *
rw: flag to allow read and write access if it is not the
+ * default on this server
+ *
wl: flag to allow access to all the stories (bypassing the
+ * whitelist if it exists)
+ *
+ *
+ * Some examples:
+ *
+ *
my_key: normal connection, will take the default server
+ * options
+ *
my_key|agzyzz|wl: will ask to bypass the white list (if it
+ * exists)
+ *
my_key|agzyzz|rw: will ask read-write access (if the default
+ * is read-only)
+ *
my_key|agzyzz|wl|rw: will ask both read-write access and white
+ * list bypass
+ *
+ *
+ * @param key
+ * the key that will allow us to exchange information with the
+ * server
+ * @param host
+ * the host to contact or NULL for localhost
+ * @param port
+ * the port to contact it on
+ */
+ public WebLibrary(String key, String host, int port) {
+ int index = -1;
+ if (key != null) {
+ index = key.indexOf('|');
+ }
+
+ if (index >= 0) {
+ this.key = key.substring(0, index);
+ this.subkey = key.substring(index + 1);
+ } else {
+ this.key = key;
+ this.subkey = "";
+ }
+
+ this.rw = subkey.contains("|rw");
+
+ this.host = host;
+ this.port = port;
+
+ // TODO: not supported yet
+ this.rw = false;
+ }
+
+ @Override
+ public Status getStatus() {
+ try {
+ download("/");
+ } catch (IOException e) {
+ try {
+ download("/style.css");
+ return Status.UNAUTHORIZED;
+ } catch (IOException ioe) {
+ return Status.INVALID;
+ }
+ }
+
+ return rw ? Status.READ_WRITE : Status.READ_ONLY;
+ }
+
+ @Override
+ public String getLibraryName() {
+ return (rw ? "[READ-ONLY] " : "") + host + ":" + port;
+ }
+
+ @Override
+ public Image getCover(String luid) throws IOException {
+ InputStream in = download("/story/" + luid + "/cover");
+ if (in != null) {
+ return new Image(in);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setSourceCover(String source, String luid) throws IOException {
+ // TODO Auto-generated method stub
+ throw new IOException("Not implemented yet");
+ }
+
+ @Override
+ public void setAuthorCover(String author, String luid) throws IOException {
+ // TODO Auto-generated method stub
+ throw new IOException("Not implemented yet");
+ }
+
+ @Override
+ protected List getMetas(Progress pg) throws IOException {
+ List metas = new ArrayList();
+ InputStream in = download("/list/luids");
+ JSONArray jsonArr = new JSONArray(IOUtils.readSmallStream(in));
+ for (int i = 0; i < jsonArr.length(); i++) {
+ JSONObject json = jsonArr.getJSONObject(i);
+ metas.add(JsonIO.toMetaData(json));
+ }
+
+ return metas;
+ }
+
+ @Override
+ // Could work (more slowly) without it
+ public MetaData imprt(final URL url, Progress pg) throws IOException {
+ if (true)
+ throw new IOException("Not implemented yet");
+
+ // Import the file locally if it is actually a file
+
+ if (url == null || url.getProtocol().equalsIgnoreCase("file")) {
+ return super.imprt(url, pg);
+ }
+
+ // Import it remotely if it is an URL
+
+ // TODO
+ return super.imprt(url, pg);
+ }
+
+ @Override
+ // Could work (more slowly) without it
+ protected synchronized void changeSTA(final String luid,
+ final String newSource, final String newTitle,
+ final String newAuthor, Progress pg) throws IOException {
+ // TODO
+ super.changeSTA(luid, newSource, newTitle, newAuthor, pg);
+ }
+
+ @Override
+ protected void updateInfo(MetaData meta) {
+ // Will be taken care of directly server side
+ }
+
+ @Override
+ protected void invalidateInfo(String luid) {
+ // Will be taken care of directly server side
+ }
+
+ // The following methods are only used by Save and Delete in BasicLibrary:
+
+ @Override
+ protected int getNextId() {
+ throw new java.lang.InternalError("Should not have been called");
+ }
+
+ @Override
+ protected void doDelete(String luid) throws IOException {
+ throw new java.lang.InternalError("Should not have been called");
+ }
+
+ @Override
+ protected Story doSave(Story story, Progress pg) throws IOException {
+ throw new java.lang.InternalError("Should not have been called");
+ }
+
+ //
+
+ @Override
+ public File getFile(final String luid, Progress pg) {
+ throw new java.lang.InternalError(
+ "Operation not supportorted on remote Libraries");
+ }
+
+ // starts with "/"
+ private InputStream download(String path) throws IOException {
+ URL url = new URL(host + ":" + port + path);
+
+ Map post = new HashMap();
+ post.put("login", subkey);
+ post.put("password", key);
+
+ return Instance.getInstance().getCache().openNoCache(url, null, post,
+ null, null);
+ }
+ }
diff --combined library/WebLibraryServer.java
index 0000000,e0096fc..e0096fc
mode 000000,100644..100644
--- a/library/WebLibraryServer.java
+++ b/library/WebLibraryServer.java
@@@ -1,0 -1,1092 +1,1092 @@@
+ package be.nikiroo.fanfix.library;
+
+ import java.io.ByteArrayInputStream;
+ import java.io.File;
+ import java.io.FileInputStream;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.security.KeyStore;
+ import java.util.ArrayList;
+ import java.util.HashMap;
+ import java.util.LinkedList;
+ import java.util.List;
+ import java.util.Map;
+
+ import javax.net.ssl.KeyManagerFactory;
+ import javax.net.ssl.SSLServerSocketFactory;
+
+ import org.json.JSONArray;
+ import org.json.JSONObject;
+
+ import be.nikiroo.fanfix.Instance;
+ import be.nikiroo.fanfix.bundles.Config;
+ import be.nikiroo.fanfix.bundles.UiConfig;
+ import be.nikiroo.fanfix.data.Chapter;
+ import be.nikiroo.fanfix.data.JsonIO;
+ import be.nikiroo.fanfix.data.MetaData;
+ import be.nikiroo.fanfix.data.Paragraph;
+ import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
+ import be.nikiroo.fanfix.data.Story;
+ import be.nikiroo.fanfix.library.web.WebLibraryServerIndex;
+ import be.nikiroo.fanfix.reader.TextOutput;
+ import be.nikiroo.utils.CookieUtils;
+ import be.nikiroo.utils.IOUtils;
+ import be.nikiroo.utils.Image;
+ import be.nikiroo.utils.NanoHTTPD;
+ import be.nikiroo.utils.NanoHTTPD.IHTTPSession;
+ import be.nikiroo.utils.NanoHTTPD.Response;
+ import be.nikiroo.utils.NanoHTTPD.Response.Status;
+ import be.nikiroo.utils.TraceHandler;
+ import be.nikiroo.utils.Version;
+
+ public class WebLibraryServer implements Runnable {
+ static private String VIEWER_URL_BASE = "/view/story/";
+ static private String VIEWER_URL = VIEWER_URL_BASE + "{luid}/{chap}/{para}";
+ static private String STORY_URL_BASE = "/story/";
+ static private String STORY_URL = STORY_URL_BASE + "{luid}/{chap}/{para}";
+ static private String STORY_URL_COVER = STORY_URL_BASE + "{luid}/cover";
+ static private String LIST_URL = "/list/";
+
+ private class LoginResult {
+ private boolean success;
+ private boolean rw;
+ private boolean wl;
+ private String wookie;
+ private String token;
+ private boolean badLogin;
+ private boolean badToken;
+
+ public LoginResult(String who, String key, String subkey,
+ boolean success, boolean rw, boolean wl) {
+ this.success = success;
+ this.rw = rw;
+ this.wl = wl;
+ this.wookie = CookieUtils.generateCookie(who + key, 0);
+
+ String opts = "";
+ if (rw)
+ opts += "|rw";
+ if (!wl)
+ opts += "|wl";
+
+ this.token = wookie + "~"
+ + CookieUtils.generateCookie(wookie + subkey + opts, 0)
+ + "~" + opts;
+ this.badLogin = !success;
+ }
+
+ public LoginResult(String token, String who, String key,
+ List subkeys) {
+
+ if (token != null) {
+ String hashes[] = token.split("~");
+ if (hashes.length >= 2) {
+ String wookie = hashes[0];
+ String rehashed = hashes[1];
+ String opts = hashes.length > 2 ? hashes[2] : "";
+
+ if (CookieUtils.validateCookie(who + key, wookie)) {
+ if (subkeys == null) {
+ subkeys = new ArrayList();
+ }
+ subkeys = new ArrayList(subkeys);
+ subkeys.add("");
+
+ for (String subkey : subkeys) {
+ if (CookieUtils.validateCookie(
+ wookie + subkey + opts, rehashed)) {
+ this.wookie = wookie;
+ this.token = token;
+ this.success = true;
+
+ this.rw = opts.contains("|rw");
+ this.wl = !opts.contains("|wl");
+ }
+ }
+ }
+ }
+
+ this.badToken = !success;
+ }
+
+ // No token -> no bad token
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public boolean isRw() {
+ return rw;
+ }
+
+ public boolean isWl() {
+ return wl;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public boolean isBadLogin() {
+ return badLogin;
+ }
+
+ public boolean isBadToken() {
+ return badToken;
+ }
+ }
+
+ private NanoHTTPD server;
+ private Map storyCache = new HashMap();
+ private LinkedList storyCacheOrder = new LinkedList();
+ private long storyCacheSize = 0;
+ private long maxStoryCacheSize;
+ private TraceHandler tracer = new TraceHandler();
+
+ public WebLibraryServer(boolean secure) throws IOException {
+ Integer port = Instance.getInstance().getConfig()
+ .getInteger(Config.SERVER_PORT);
+ if (port == null) {
+ throw new IOException(
+ "Cannot start web server: port not specified");
+ }
+
+ int cacheMb = Instance.getInstance().getConfig()
+ .getInteger(Config.SERVER_MAX_CACHE_MB, 100);
+ maxStoryCacheSize = cacheMb * 1024 * 1024;
+
+ setTraceHandler(Instance.getInstance().getTraceHandler());
+
+ SSLServerSocketFactory ssf = null;
+ if (secure) {
+ String keystorePath = Instance.getInstance().getConfig()
+ .getString(Config.SERVER_SSL_KEYSTORE, "");
+ String keystorePass = Instance.getInstance().getConfig()
+ .getString(Config.SERVER_SSL_KEYSTORE_PASS);
+
+ if (secure && keystorePath.isEmpty()) {
+ throw new IOException(
+ "Cannot start a secure web server: no keystore.jks file povided");
+ }
+
+ if (!keystorePath.isEmpty()) {
+ File keystoreFile = new File(keystorePath);
+ try {
+ KeyStore keystore = KeyStore
+ .getInstance(KeyStore.getDefaultType());
+ InputStream keystoreStream = new FileInputStream(
+ keystoreFile);
+ try {
+ keystore.load(keystoreStream,
+ keystorePass.toCharArray());
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory
+ .getInstance(KeyManagerFactory
+ .getDefaultAlgorithm());
+ keyManagerFactory.init(keystore,
+ keystorePass.toCharArray());
+ ssf = NanoHTTPD.makeSSLSocketFactory(keystore,
+ keyManagerFactory);
+ } finally {
+ keystoreStream.close();
+ }
+ } catch (Exception e) {
+ throw new IOException(e.getMessage());
+ }
+ }
+ }
+
+ server = new NanoHTTPD(port) {
+ @Override
+ public Response serve(final IHTTPSession session) {
+ super.serve(session);
+
+ String query = session.getQueryParameterString(); // a=a%20b&dd=2
+ Method method = session.getMethod(); // GET, POST..
+ String uri = session.getUri(); // /home.html
+
+ // need them in real time (not just those sent by the UA)
+ Map cookies = new HashMap();
+ for (String cookie : session.getCookies()) {
+ cookies.put(cookie, session.getCookies().read(cookie));
+ }
+
+ List whitelist = Instance.getInstance().getConfig()
+ .getList(Config.SERVER_WHITELIST);
+ if (whitelist == null) {
+ whitelist = new ArrayList();
+ }
+
+ LoginResult login = null;
+ Map params = session.getParms();
+ String who = session.getRemoteHostName()
+ + session.getRemoteIpAddress();
+ if (params.get("login") != null) {
+ login = login(who, params.get("password"),
+ params.get("login"), whitelist);
+ } else {
+ String token = cookies.get("token");
+ login = login(who, token, Instance.getInstance().getConfig()
+ .getList(Config.SERVER_ALLOWED_SUBKEYS));
+ }
+
+ if (login.isSuccess()) {
+ // refresh token
+ session.getCookies().set(new Cookie("token",
+ login.getToken(), "30; path=/"));
+
+ // set options
+ String optionName = params.get("optionName");
+ if (optionName != null && !optionName.isEmpty()) {
+ String optionValue = params.get("optionValue");
+ if (optionValue == null || optionValue.isEmpty()) {
+ session.getCookies().delete(optionName);
+ cookies.remove(optionName);
+ } else {
+ session.getCookies().set(new Cookie(optionName,
+ optionValue, "; path=/"));
+ cookies.put(optionName, optionValue);
+ }
+ }
+ }
+
+ Response rep = null;
+ if (!login.isSuccess() && (uri.equals("/") //
+ || uri.startsWith(STORY_URL_BASE) //
+ || uri.startsWith(VIEWER_URL_BASE) //
+ || uri.startsWith(LIST_URL))) {
+ rep = loginPage(login, uri);
+ }
+
+ if (rep == null) {
+ try {
+ if (uri.equals("/")) {
+ rep = root(session, cookies, whitelist);
+ } else if (uri.startsWith(LIST_URL)) {
+ rep = getList(uri, whitelist);
+ } else if (uri.startsWith(STORY_URL_BASE)) {
+ rep = getStoryPart(uri, whitelist);
+ } else if (uri.startsWith(VIEWER_URL_BASE)) {
+ rep = getViewer(cookies, uri, whitelist);
+ } else if (uri.equals("/logout")) {
+ session.getCookies().delete("token");
+ cookies.remove("token");
+ rep = loginPage(login, uri);
+ } else {
+ if (uri.startsWith("/"))
+ uri = uri.substring(1);
+ InputStream in = IOUtils.openResource(
+ WebLibraryServerIndex.class, uri);
+ if (in != null) {
+ String mimeType = MIME_PLAINTEXT;
+ if (uri.endsWith(".css")) {
+ mimeType = "text/css";
+ } else if (uri.endsWith(".html")) {
+ mimeType = "text/html";
+ } else if (uri.endsWith(".js")) {
+ mimeType = "text/javascript";
+ }
+ rep = newChunkedResponse(Status.OK, mimeType,
+ in);
+ } else {
+ getTraceHandler().trace("404: " + uri);
+ }
+ }
+
+ if (rep == null) {
+ rep = newFixedLengthResponse(Status.NOT_FOUND,
+ NanoHTTPD.MIME_PLAINTEXT, "Not Found");
+ }
+ } catch (Exception e) {
+ Instance.getInstance().getTraceHandler().error(
+ new IOException("Cannot process web request",
+ e));
+ rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
+ NanoHTTPD.MIME_PLAINTEXT, "An error occured");
+ }
+ }
+
+ return rep;
+
+ // Get status: for story, use "luid" + active map of current
+ // luids
+ // map must use a addRef/removeRef and delete at 0
+
+ // http://localhost:2000/?token=ok
+
+ //
+ // MetaData meta = new MetaData();
+ // meta.setTitle("Title");
+ // meta.setLuid("000");
+ //
+ // JSONObject json = new JSONObject();
+ // json.put("", MetaData.class.getName());
+ // json.put("title", meta.getTitle());
+ // json.put("luid", meta.getLuid());
+ //
+ // return newFixedLengthResponse(json.toString());
+ }
+ };
+
+ if (ssf != null) {
+ getTraceHandler().trace("Install SSL on the web server...");
+ server.makeSecure(ssf, null);
+ getTraceHandler().trace("Done.");
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
+ } catch (IOException e) {
+ tracer.error(new IOException("Cannot start the web server", e));
+ }
+ }
+
+ /**
+ * Start the server (listen on the network for new connections).
+ *
+ * Can only be called once.
+ *
+ * This call is asynchronous, and will just start a new {@link Thread} on
+ * itself (see {@link WebLibraryServer#run()}).
+ */
+ public void start() {
+ new Thread(this).start();
+ }
+
+ /**
+ * The traces handler for this {@link WebLibraryServer}.
+ *
+ * @return the traces handler
+ */
+ public TraceHandler getTraceHandler() {
+ return tracer;
+ }
+
+ /**
+ * The traces handler for this {@link WebLibraryServer}.
+ *
+ * @param tracer
+ * the new traces handler
+ */
+ public void setTraceHandler(TraceHandler tracer) {
+ if (tracer == null) {
+ tracer = new TraceHandler(false, false, false);
+ }
+
+ this.tracer = tracer;
+ }
+
+ private LoginResult login(String who, String token, List subkeys) {
+ String realKey = Instance.getInstance().getConfig()
+ .getString(Config.SERVER_KEY);
+ realKey = realKey == null ? "" : realKey;
+ return new LoginResult(token, who, realKey, subkeys);
+ }
+
+ // allow rw/wl
+ private LoginResult login(String who, String key, String subkey,
+ List whitelist) {
+ String realKey = Instance.getInstance().getConfig()
+ .getString(Config.SERVER_KEY);
+
+ // I don't like NULLs...
+ realKey = realKey == null ? "" : realKey;
+ key = key == null ? "" : key;
+ subkey = subkey == null ? "" : subkey;
+
+ if (!realKey.equals(key)) {
+ return new LoginResult(null, null, null, false, false, false);
+ }
+
+ // defaults are positive (as previous versions without the feature)
+ boolean rw = true;
+ boolean wl = true;
+
+ if (whitelist.isEmpty()) {
+ wl = false;
+ }
+
+ rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
+ rw);
+ if (!subkey.isEmpty()) {
+ List allowed = Instance.getInstance().getConfig()
+ .getList(Config.SERVER_ALLOWED_SUBKEYS);
+ if (allowed != null && allowed.contains(subkey)) {
+ if ((subkey + "|").contains("|rw|")) {
+ rw = true;
+ }
+ if ((subkey + "|").contains("|wl|")) {
+ wl = false; // |wl| = bypass whitelist
+ }
+ } else {
+ return new LoginResult(null, null, null, false, false, false);
+ }
+ }
+
+ return new LoginResult(who, key, subkey, true, rw, wl);
+ }
+
+ private Response loginPage(LoginResult login, String uri) {
+ StringBuilder builder = new StringBuilder();
+
+ appendPreHtml(builder, true);
+
+ if (login.isBadLogin()) {
+ builder.append("
Bad login or password
");
+ } else if (login.isBadToken()) {
+ builder.append("