From 33d40b9f752a30ba2c9ed09a554a63cf6083de32 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Wed, 13 May 2020 23:24:15 +0200 Subject: [PATCH] weblib: separate html code from lib code --- .../nikiroo/fanfix/library/BasicLibrary.java | 4 +- .../fanfix/library/WebLibraryServer.java | 881 +----------------- .../fanfix/library/WebLibraryServerHtml.java | 839 +++++++++++++++++ .../fanfix/library/WebLibraryUrls.java | 2 +- 4 files changed, 891 insertions(+), 835 deletions(-) create mode 100644 src/be/nikiroo/fanfix/library/WebLibraryServerHtml.java diff --git a/src/be/nikiroo/fanfix/library/BasicLibrary.java b/src/be/nikiroo/fanfix/library/BasicLibrary.java index 11ca03f4..af7920bf 100644 --- a/src/be/nikiroo/fanfix/library/BasicLibrary.java +++ b/src/be/nikiroo/fanfix/library/BasicLibrary.java @@ -43,7 +43,9 @@ abstract public class BasicLibrary { READ_ONLY, /** You are not allowed to access this library. */ UNAUTHORIZED, - /** The library is currently out of commission. */ + /** The library is invalid, and will never work as is. */ + INVALID, + /** The library is currently out of commission, but may work later. */ UNAVAILABLE; /** diff --git a/src/be/nikiroo/fanfix/library/WebLibraryServer.java b/src/be/nikiroo/fanfix/library/WebLibraryServer.java index b6d11f9b..97f2f0e8 100644 --- a/src/be/nikiroo/fanfix/library/WebLibraryServer.java +++ b/src/be/nikiroo/fanfix/library/WebLibraryServer.java @@ -1,46 +1,33 @@ 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.IOUtils; import be.nikiroo.utils.Image; import be.nikiroo.utils.LoginResult; 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 { - private class WLoginResult extends LoginResult { +public class WebLibraryServer extends WebLibraryServerHtml { + class WLoginResult extends LoginResult { private boolean rw; private boolean wl; private boolean bl; @@ -77,23 +64,16 @@ public class WebLibraryServer implements Runnable { } } - 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(); private List whitelist; private List blacklist; 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"); - } + super(secure); int cacheMb = Instance.getInstance().getConfig() .getInteger(Config.SERVER_MAX_CACHE_MB, 100); @@ -105,179 +85,6 @@ public class WebLibraryServer implements Runnable { .getList(Config.SERVER_WHITELIST, new ArrayList()); blacklist = Instance.getInstance().getConfig() .getList(Config.SERVER_BLACKLIST, new ArrayList()); - - 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)); - } - - WLoginResult 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")); - } else { - String cookie = cookies.get("cookie"); - login = login(who, cookie); - } - - if (login.isSuccess()) { - // refresh cookie - session.getCookies().set(new Cookie("cookie", - login.getCookie(), "30; path=/")); - - // set options - String optionName = params.get("optionName"); - if (optionName != null && !optionName.isEmpty()) { - String optionNo = params.get("optionNo"); - String optionValue = params.get("optionValue"); - if (optionNo != null || 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() && WebLibraryUrls.isSupportedUrl(uri)) { - rep = loginPage(login, uri); - } - - if (rep == null) { - try { - if (WebLibraryUrls.isSupportedUrl(uri)) { - if (WebLibraryUrls.INDEX_URL.equals(uri)) { - rep = root(session, cookies, login); - } else if (WebLibraryUrls.VERSION_URL.equals(uri)) { - rep = newFixedLengthResponse(Status.OK, - MIME_PLAINTEXT, - Version.getCurrentVersion().toString()); - } else if (WebLibraryUrls.isListUrl(uri)) { - rep = getList(uri, login); - } else if (WebLibraryUrls.isStoryUrl(uri)) { - rep = getStoryPart(uri, login); - } else if (WebLibraryUrls.isViewUrl(uri)) { - rep = getViewer(cookies, uri, login); - } else if (WebLibraryUrls.LOGOUT_URL.equals(uri)) { - session.getCookies().delete("cookie"); - cookies.remove("cookie"); - rep = loginPage(new WLoginResult(false, false), - uri); - } else { - getTraceHandler().error( - "Supported URL was not processed: " - + uri); - rep = newFixedLengthResponse( - Status.INTERNAL_ERROR, - NanoHTTPD.MIME_PLAINTEXT, - "An error happened"); - } - } 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); - } - - if (rep == null) { - getTraceHandler().trace("404: " + uri); - 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; - } - }; - - 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)); - } } /** @@ -292,30 +99,13 @@ public class WebLibraryServer implements Runnable { 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; + @Override + protected WLoginResult login(boolean badLogin, boolean badCookie) { + return new WLoginResult(false, false); } - private WLoginResult login(String who, String cookie) { + @Override + protected WLoginResult login(String who, String cookie) { List subkeys = Instance.getInstance().getConfig() .getList(Config.SERVER_ALLOWED_SUBKEYS); String realKey = Instance.getInstance().getConfig() @@ -325,7 +115,8 @@ public class WebLibraryServer implements Runnable { } // allow rw/wl - private WLoginResult login(String who, String key, String subkey) { + @Override + protected WLoginResult login(String who, String key, String subkey) { String realKey = Instance.getInstance().getConfig() .getString(Config.SERVER_KEY, ""); @@ -367,36 +158,7 @@ public class WebLibraryServer implements Runnable { return new WLoginResult(who, key, subkey, rw, wl, bl); } - private Response loginPage(WLoginResult login, String uri) { - StringBuilder builder = new StringBuilder(); - - appendPreHtml(builder, true); - - if (login.isBadLogin()) { - builder.append("
Bad login or password
"); - } else if (login.isBadCookie()) { - builder.append("
Your session timed out
"); - } - - if (WebLibraryUrls.LOGOUT_URL.equals(uri)) { - uri = WebLibraryUrls.INDEX_URL; - } - - builder.append( - "\n"); - - appendPostHtml(builder); - - return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN, - NanoHTTPD.MIME_HTML, builder.toString()); - } - + @Override protected Response getList(String uri, WLoginResult login) throws IOException { if (WebLibraryUrls.LIST_URL_METADATA.equals(uri)) { @@ -414,185 +176,12 @@ public class WebLibraryServer implements Runnable { NanoHTTPD.MIME_PLAINTEXT, null); } - private Response root(IHTTPSession session, Map cookies, - WLoginResult login) throws IOException { - BasicLibrary lib = Instance.getInstance().getLibrary(); - MetaResultList result = new MetaResultList(metas(login)); - StringBuilder builder = new StringBuilder(); - - appendPreHtml(builder, true); - - Map params = session.getParms(); - - String filter = cookies.get("filter"); - if (params.get("optionNo") != null) - filter = null; - if (filter == null) { - filter = ""; - } - - String browser = params.get("browser") == null ? "" - : params.get("browser"); - String browser2 = params.get("browser2") == null ? "" - : params.get("browser2"); - String browser3 = params.get("browser3") == null ? "" - : params.get("browser3"); - - String filterSource = null; - String filterAuthor = null; - String filterTag = null; - - // TODO: javascript in realtime, using visible=false + hide [submit] - - builder.append("
\n"); - builder.append("\n"); - - // TODO: javascript in realtime, using visible=false + hide [submit] - builder.append("
\n"); - builder.append("\tFilter: \n"); - builder.append( - "\t\n"); - builder.append("\t\n"); - builder.append("\t"); - builder.append( - "\t\n"); - builder.append("
\n"); - builder.append("
\n"); - - builder.append("\t
"); - for (MetaData meta : result.getMetas()) { - if (!filter.isEmpty() && !meta.getTitle().toLowerCase() - .contains(filter.toLowerCase())) { - continue; - } - - // TODO Sub sources - if (filterSource != null - && !filterSource.equals(meta.getSource())) { - continue; - } - - // TODO: sub authors - if (filterAuthor != null - && !filterAuthor.equals(meta.getAuthor())) { - continue; - } - - if (filterTag != null && !meta.getTags().contains(filterTag)) { - continue; - } - - builder.append("\n"); - } - builder.append("
"); - - appendPostHtml(builder); - return NanoHTTPD.newFixedLengthResponse(builder.toString()); - } - // /story/luid/chapter/para <-- text/image // /story/luid/cover <-- image // /story/luid/metadata <-- json // /story/luid/json <-- json, whole chapter (no images) - private Response getStoryPart(String uri, WLoginResult login) { + @Override + protected Response getStoryPart(String uri, WLoginResult login) { String[] cover = uri.split("/"); int off = 2; @@ -639,7 +228,7 @@ public class WebLibraryServer implements Runnable { InputStream in = null; try { if ("cover".equals(chapterStr)) { - Image img = getCover(luid, login); + Image img = cover(luid, login); if (img != null) { in = img.newInputStream(); } @@ -698,320 +287,8 @@ public class WebLibraryServer implements Runnable { return newInputStreamResponse(mimeType, in); } - private Response getViewer(Map cookies, String uri, - WLoginResult login) { - String[] cover = uri.split("/"); - int off = 2; - - if (cover.length < off + 2) { - return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, - NanoHTTPD.MIME_PLAINTEXT, null); - } - - String type = cover[off + 0]; - String luid = cover[off + 1]; - String chapterStr = cover.length < off + 3 ? null : cover[off + 2]; - String paragraphStr = cover.length < off + 4 ? null : cover[off + 3]; - - // 1-based (0 = desc) - int chapter = 0; - if (chapterStr != null) { - try { - chapter = Integer.parseInt(chapterStr); - if (chapter < 0) { - throw new NumberFormatException(); - } - } catch (NumberFormatException e) { - return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, - NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid"); - } - } - - // 1-based - int paragraph = 0; - if (paragraphStr != null) { - try { - paragraph = Integer.parseInt(paragraphStr); - if (paragraph <= 0) { - throw new NumberFormatException(); - } - } catch (NumberFormatException e) { - return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, - NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid"); - } - } - - try { - Story story = story(luid, login); - if (story == null) { - return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, - NanoHTTPD.MIME_PLAINTEXT, "Story not found"); - } - - StringBuilder builder = new StringBuilder(); - appendPreHtml(builder, false); - - // For images documents, always go to the images if not chap 0 desc - if (story.getMeta().isImageDocument()) { - if (chapter > 0 && paragraph <= 0) - paragraph = 1; - } - - Chapter chap = null; - if (chapter <= 0) { - chap = story.getMeta().getResume(); - } else { - try { - chap = story.getChapters().get(chapter - 1); - } catch (IndexOutOfBoundsException e) { - return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, - NanoHTTPD.MIME_PLAINTEXT, "Chapter not found"); - } - } - - String first, previous, next, last; - - StringBuilder content = new StringBuilder(); - - String disabledLeft = ""; - String disabledRight = ""; - String disabledZoomReal = ""; - String disabledZoomWidth = ""; - String disabledZoomHeight = ""; - - if (paragraph <= 0) { - first = WebLibraryUrls.getViewUrl(luid, 0, null); - previous = WebLibraryUrls.getViewUrl(luid, - (Math.max(chapter - 1, 0)), null); - next = WebLibraryUrls.getViewUrl(luid, - (Math.min(chapter + 1, story.getChapters().size())), - null); - last = WebLibraryUrls.getViewUrl(luid, - story.getChapters().size(), null); - - StringBuilder desc = new StringBuilder(); - - if (chapter <= 0) { - desc.append("

"); - desc.append(story.getMeta().getTitle()); - desc.append("

\n"); - desc.append("
\n"); - desc.append("\t\n"); - desc.append("\t\t\n"); - desc.append("\t\n"); - desc.append("\t\n"); - Map details = BasicLibrary - .getMetaDesc(story.getMeta()); - for (String key : details.keySet()) { - appendTableRow(desc, 2, key, details.get(key)); - } - desc.append("\t
\n"); - desc.append("
\n"); - desc.append("

Description

\n"); - } - - content.append("
\n"); - content.append(desc); - String description = new TextOutput(false).convert(chap, - chapter > 0); - content.append(chap.getParagraphs().size() <= 0 - ? "No content provided." - : description); - content.append("
\n"); - - if (chapter <= 0) - disabledLeft = " disabled='disbaled'"; - if (chapter >= story.getChapters().size()) - disabledRight = " disabled='disbaled'"; - } else { - first = WebLibraryUrls.getViewUrl(luid, chapter, 1); - previous = WebLibraryUrls.getViewUrl(luid, chapter, - (Math.max(paragraph - 1, 1))); - next = WebLibraryUrls.getViewUrl(luid, chapter, - (Math.min(paragraph + 1, chap.getParagraphs().size()))); - last = WebLibraryUrls.getViewUrl(luid, chapter, - chap.getParagraphs().size()); - - if (paragraph <= 1) - disabledLeft = " disabled='disbaled'"; - if (paragraph >= chap.getParagraphs().size()) - disabledRight = " disabled='disbaled'"; - - // First -> previous *chapter* - if (chapter > 0) - disabledLeft = ""; - first = WebLibraryUrls.getViewUrl(luid, - (Math.max(chapter - 1, 0)), null); - if (paragraph <= 1) { - previous = first; - } - - Paragraph para = null; - try { - para = chap.getParagraphs().get(paragraph - 1); - } catch (IndexOutOfBoundsException e) { - return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, - NanoHTTPD.MIME_PLAINTEXT, - "Paragraph " + paragraph + " not found"); - } - - if (para.getType() == ParagraphType.IMAGE) { - String zoomStyle = "max-width: 100%;"; - disabledZoomWidth = " disabled='disabled'"; - String zoomOption = cookies.get("zoom"); - if (zoomOption != null && !zoomOption.isEmpty()) { - if (zoomOption.equals("real")) { - zoomStyle = ""; - disabledZoomWidth = ""; - disabledZoomReal = " disabled='disabled'"; - } else if (zoomOption.equals("width")) { - zoomStyle = "max-width: 100%;"; - } else if (zoomOption.equals("height")) { - // see height of navbar + optionbar - zoomStyle = "max-height: calc(100% - 128px);"; - disabledZoomWidth = ""; - disabledZoomHeight = " disabled='disabled'"; - } - } - - String javascript = "document.getElementById(\"previous\").click(); return false;"; - content.append(String.format("" // - + "" - + "" - + "", // - javascript, // - next, // - zoomStyle, // - WebLibraryUrls.getStoryUrl(luid, chapter, - paragraph))); - } else { - content.append(String.format("" // - + "
%s
", // - para.getContent())); - } - } - - builder.append(String.format("" // - + "\n", // - disabledRight, next, // - disabledRight, last // - )); - - builder.append(content); - - builder.append("
\n"); - builder.append(" BACK\n"); - - if (paragraph > 0) { - builder.append(String.format("" // - + "\tREAL\n"// - + "\tWIDTH\n"// - + "\tHEIGHT\n"// - + "
\n", // - disabledZoomReal, - uri + "?optionName=zoom&optionValue=real", // - disabledZoomWidth, - uri + "?optionName=zoom&optionValue=width", // - disabledZoomHeight, - uri + "?optionName=zoom&optionValue=height" // - )); - } - - appendPostHtml(builder); - return NanoHTTPD.newFixedLengthResponse(Status.OK, - NanoHTTPD.MIME_HTML, builder.toString()); - } catch (IOException e) { - Instance.getInstance().getTraceHandler() - .error(new IOException("Cannot get image: " + uri, e)); - return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, - NanoHTTPD.MIME_PLAINTEXT, "Error when processing request"); - } - } - - private Response newInputStreamResponse(String mimeType, InputStream in) { - if (in == null) { - return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "", - null); - } - return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in); - } - - private String getContentOf(String file) { - InputStream in = IOUtils.openResource(WebLibraryServerIndex.class, - file); - if (in != null) { - try { - return IOUtils.readSmallStream(in); - } catch (IOException e) { - Instance.getInstance().getTraceHandler().error( - new IOException("Cannot get file: index.pre.html", e)); - } - } - - return ""; - } - - private boolean isAllowed(MetaData meta, WLoginResult login) { - if (login.isWl() && !whitelist.isEmpty() - && !whitelist.contains(meta.getSource())) { - return false; - } - if (login.isBl() && blacklist.contains(meta.getSource())) { - return false; - } - - return true; - } - - private List metas(WLoginResult login) throws IOException { + @Override + protected List metas(WLoginResult login) throws IOException { BasicLibrary lib = Instance.getInstance().getLibrary(); List metas = new ArrayList(); for (MetaData meta : lib.getList().getMetas()) { @@ -1023,27 +300,9 @@ public class WebLibraryServer implements Runnable { return metas; } - private MetaData meta(String luid, WLoginResult login) throws IOException { - BasicLibrary lib = Instance.getInstance().getLibrary(); - MetaData meta = lib.getInfo(luid); - if (!isAllowed(meta, login)) - return null; - - return meta; - } - - private Image getCover(String luid, WLoginResult login) throws IOException { - MetaData meta = meta(luid, login); - if (meta != null) { - BasicLibrary lib = Instance.getInstance().getLibrary(); - return lib.getCover(meta.getLuid()); - } - - return null; - } - // NULL if not whitelist OK or if not found - private Story story(String luid, WLoginResult login) throws IOException { + @Override + protected Story story(String luid, WLoginResult login) throws IOException { synchronized (storyCache) { if (storyCache.containsKey(luid)) { Story story = storyCache.get(luid); @@ -1080,94 +339,50 @@ public class WebLibraryServer implements Runnable { return story; } - private long sizeOf(Story story) { - long size = 0; - for (Chapter chap : story) { - for (Paragraph para : chap) { - if (para.getType() == ParagraphType.IMAGE) { - size += para.getContentImage().getSize(); - } else { - size += para.getContent().length(); - } - } - } + private MetaData meta(String luid, WLoginResult login) throws IOException { + BasicLibrary lib = Instance.getInstance().getLibrary(); + MetaData meta = lib.getInfo(luid); + if (!isAllowed(meta, login)) + return null; - return size; + return meta; } - private void appendPreHtml(StringBuilder builder, boolean banner) { - String favicon = "favicon.ico"; - String icon = Instance.getInstance().getUiConfig() - .getString(UiConfig.PROGRAM_ICON); - if (icon != null) { - favicon = "icon_" + icon.replace("-", "_") + ".png"; - } - - builder.append( - getContentOf("index.pre.html").replace("favicon.ico", favicon)); - - if (banner) { - builder.append("\n"); + private Image cover(String luid, WLoginResult login) throws IOException { + MetaData meta = meta(luid, login); + if (meta != null) { + BasicLibrary lib = Instance.getInstance().getLibrary(); + return lib.getCover(meta.getLuid()); } - } - private void appendPostHtml(StringBuilder builder) { - builder.append(getContentOf("index.post.html")); + return null; } - private void appendOption(StringBuilder builder, int depth, String name, - String value, String selected) { - for (int i = 0; i < depth; i++) { - builder.append("\t"); - } - builder.append("\n"); - } - - private void appendTableRow(StringBuilder builder, int depth, - String... tds) { - for (int i = 0; i < depth; i++) { - builder.append("\t"); + if (login.isBl() && blacklist.contains(meta.getSource())) { + return false; } - int col = 1; - builder.append(""); - for (String td : tds) { - builder.append(""); - builder.append(td); - builder.append(""); - } - builder.append("\n"); + return true; } - private void appendItemA(StringBuilder builder, int depth, String link, - String name, boolean selected) { - for (int i = 0; i < depth; i++) { - builder.append("\t"); + private long sizeOf(Story story) { + long size = 0; + for (Chapter chap : story) { + for (Paragraph para : chap) { + if (para.getType() == ParagraphType.IMAGE) { + size += para.getContentImage().getSize(); + } else { + size += para.getContent().length(); + } + } } - builder.append(""); - builder.append(name); - builder.append("\n"); + return size; } public static void main(String[] args) throws IOException { diff --git a/src/be/nikiroo/fanfix/library/WebLibraryServerHtml.java b/src/be/nikiroo/fanfix/library/WebLibraryServerHtml.java new file mode 100644 index 00000000..c28188cd --- /dev/null +++ b/src/be/nikiroo/fanfix/library/WebLibraryServerHtml.java @@ -0,0 +1,839 @@ +package be.nikiroo.fanfix.library; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLServerSocketFactory; + +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.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.WebLibraryServer.WLoginResult; +import be.nikiroo.fanfix.library.web.WebLibraryServerIndex; +import be.nikiroo.fanfix.reader.TextOutput; +import be.nikiroo.utils.IOUtils; +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; + +abstract class WebLibraryServerHtml implements Runnable { + private NanoHTTPD server; + protected TraceHandler tracer = new TraceHandler(); + + abstract protected WLoginResult login(String who, String cookie); + + abstract protected WLoginResult login(String who, String key, + String subkey); + + abstract protected WLoginResult login(boolean badLogin, boolean badCookie); + + abstract protected Response getList(String uri, WLoginResult login) + throws IOException; + + abstract protected Response getStoryPart(String uri, WLoginResult login); + + abstract protected List metas(WLoginResult login) + throws IOException; + + abstract protected Story story(String luid, WLoginResult login) + throws IOException; + + public WebLibraryServerHtml(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"); + } + + 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)); + } + + WLoginResult 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")); + } else { + String cookie = cookies.get("cookie"); + login = login(who, cookie); + } + + if (login.isSuccess()) { + // refresh cookie + session.getCookies().set(new Cookie("cookie", + login.getCookie(), "30; path=/")); + + // set options + String optionName = params.get("optionName"); + if (optionName != null && !optionName.isEmpty()) { + String optionNo = params.get("optionNo"); + String optionValue = params.get("optionValue"); + if (optionNo != null || 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() && WebLibraryUrls.isSupportedUrl(uri)) { + rep = loginPage(login, uri); + } + + if (rep == null) { + try { + if (WebLibraryUrls.isSupportedUrl(uri)) { + if (WebLibraryUrls.INDEX_URL.equals(uri)) { + rep = root(session, cookies, login); + } else if (WebLibraryUrls.VERSION_URL.equals(uri)) { + rep = newFixedLengthResponse(Status.OK, + MIME_PLAINTEXT, + Version.getCurrentVersion().toString()); + } else if (WebLibraryUrls.isListUrl(uri)) { + rep = getList(uri, login); + } else if (WebLibraryUrls.isStoryUrl(uri)) { + rep = getStoryPart(uri, login); + } else if (WebLibraryUrls.isViewUrl(uri)) { + rep = getViewer(cookies, uri, login); + } else if (WebLibraryUrls.LOGOUT_URL.equals(uri)) { + session.getCookies().delete("cookie"); + cookies.remove("cookie"); + rep = loginPage(login(false, false), uri); + } else { + getTraceHandler().error( + "Supported URL was not processed: " + + uri); + rep = newFixedLengthResponse( + Status.INTERNAL_ERROR, + NanoHTTPD.MIME_PLAINTEXT, + "An error happened"); + } + } 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); + } + + if (rep == null) { + getTraceHandler().trace("404: " + uri); + 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; + } + }; + + 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)); + } + } + + /** + * The traces handler for this {@link WebLibraryServerHtml}. + * + * @return the traces handler + */ + public TraceHandler getTraceHandler() { + return tracer; + } + + /** + * The traces handler for this {@link WebLibraryServerHtml}. + * + * @param tracer + * the new traces handler + */ + public void setTraceHandler(TraceHandler tracer) { + if (tracer == null) { + tracer = new TraceHandler(false, false, false); + } + + this.tracer = tracer; + } + + private Response loginPage(WLoginResult login, String uri) { + StringBuilder builder = new StringBuilder(); + + appendPreHtml(builder, true); + + if (login.isBadLogin()) { + builder.append("
Bad login or password
"); + } else if (login.isBadCookie()) { + builder.append("
Your session timed out
"); + } + + if (WebLibraryUrls.LOGOUT_URL.equals(uri)) { + uri = WebLibraryUrls.INDEX_URL; + } + + builder.append( + "\n"); + + appendPostHtml(builder); + + return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN, + NanoHTTPD.MIME_HTML, builder.toString()); + } + + private Response root(IHTTPSession session, Map cookies, + WLoginResult login) throws IOException { + BasicLibrary lib = Instance.getInstance().getLibrary(); + MetaResultList result = new MetaResultList(metas(login)); + StringBuilder builder = new StringBuilder(); + + appendPreHtml(builder, true); + + Map params = session.getParms(); + + String filter = cookies.get("filter"); + if (params.get("optionNo") != null) + filter = null; + if (filter == null) { + filter = ""; + } + + String browser = params.get("browser") == null ? "" + : params.get("browser"); + String browser2 = params.get("browser2") == null ? "" + : params.get("browser2"); + String browser3 = params.get("browser3") == null ? "" + : params.get("browser3"); + + String filterSource = null; + String filterAuthor = null; + String filterTag = null; + + // TODO: javascript in realtime, using visible=false + hide [submit] + + builder.append("
\n"); + builder.append("\n"); + + // TODO: javascript in realtime, using visible=false + hide [submit] + builder.append("
\n"); + builder.append("\tFilter: \n"); + builder.append( + "\t\n"); + builder.append("\t\n"); + builder.append("\t"); + builder.append( + "\t\n"); + builder.append("
\n"); + builder.append("
\n"); + + builder.append("\t
"); + for (MetaData meta : result.getMetas()) { + if (!filter.isEmpty() && !meta.getTitle().toLowerCase() + .contains(filter.toLowerCase())) { + continue; + } + + // TODO Sub sources + if (filterSource != null + && !filterSource.equals(meta.getSource())) { + continue; + } + + // TODO: sub authors + if (filterAuthor != null + && !filterAuthor.equals(meta.getAuthor())) { + continue; + } + + if (filterTag != null && !meta.getTags().contains(filterTag)) { + continue; + } + + builder.append("\n"); + } + builder.append("
"); + + appendPostHtml(builder); + return NanoHTTPD.newFixedLengthResponse(builder.toString()); + } + + private Response getViewer(Map cookies, String uri, + WLoginResult login) { + String[] cover = uri.split("/"); + int off = 2; + + if (cover.length < off + 2) { + return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, + NanoHTTPD.MIME_PLAINTEXT, null); + } + + String type = cover[off + 0]; + String luid = cover[off + 1]; + String chapterStr = cover.length < off + 3 ? null : cover[off + 2]; + String paragraphStr = cover.length < off + 4 ? null : cover[off + 3]; + + // 1-based (0 = desc) + int chapter = 0; + if (chapterStr != null) { + try { + chapter = Integer.parseInt(chapterStr); + if (chapter < 0) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, + NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid"); + } + } + + // 1-based + int paragraph = 0; + if (paragraphStr != null) { + try { + paragraph = Integer.parseInt(paragraphStr); + if (paragraph <= 0) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, + NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid"); + } + } + + try { + Story story = story(luid, login); + if (story == null) { + return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, + NanoHTTPD.MIME_PLAINTEXT, "Story not found"); + } + + StringBuilder builder = new StringBuilder(); + appendPreHtml(builder, false); + + // For images documents, always go to the images if not chap 0 desc + if (story.getMeta().isImageDocument()) { + if (chapter > 0 && paragraph <= 0) + paragraph = 1; + } + + Chapter chap = null; + if (chapter <= 0) { + chap = story.getMeta().getResume(); + } else { + try { + chap = story.getChapters().get(chapter - 1); + } catch (IndexOutOfBoundsException e) { + return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, + NanoHTTPD.MIME_PLAINTEXT, "Chapter not found"); + } + } + + String first, previous, next, last; + + StringBuilder content = new StringBuilder(); + + String disabledLeft = ""; + String disabledRight = ""; + String disabledZoomReal = ""; + String disabledZoomWidth = ""; + String disabledZoomHeight = ""; + + if (paragraph <= 0) { + first = WebLibraryUrls.getViewUrl(luid, 0, null); + previous = WebLibraryUrls.getViewUrl(luid, + (Math.max(chapter - 1, 0)), null); + next = WebLibraryUrls.getViewUrl(luid, + (Math.min(chapter + 1, story.getChapters().size())), + null); + last = WebLibraryUrls.getViewUrl(luid, + story.getChapters().size(), null); + + StringBuilder desc = new StringBuilder(); + + if (chapter <= 0) { + desc.append("

"); + desc.append(story.getMeta().getTitle()); + desc.append("

\n"); + desc.append("
\n"); + desc.append("\t\n"); + desc.append("\t\t\n"); + desc.append("\t\n"); + desc.append("\t\n"); + Map details = BasicLibrary + .getMetaDesc(story.getMeta()); + for (String key : details.keySet()) { + appendTableRow(desc, 2, key, details.get(key)); + } + desc.append("\t
\n"); + desc.append("
\n"); + desc.append("

Description

\n"); + } + + content.append("
\n"); + content.append(desc); + String description = new TextOutput(false).convert(chap, + chapter > 0); + content.append(chap.getParagraphs().size() <= 0 + ? "No content provided." + : description); + content.append("
\n"); + + if (chapter <= 0) + disabledLeft = " disabled='disbaled'"; + if (chapter >= story.getChapters().size()) + disabledRight = " disabled='disbaled'"; + } else { + first = WebLibraryUrls.getViewUrl(luid, chapter, 1); + previous = WebLibraryUrls.getViewUrl(luid, chapter, + (Math.max(paragraph - 1, 1))); + next = WebLibraryUrls.getViewUrl(luid, chapter, + (Math.min(paragraph + 1, chap.getParagraphs().size()))); + last = WebLibraryUrls.getViewUrl(luid, chapter, + chap.getParagraphs().size()); + + if (paragraph <= 1) + disabledLeft = " disabled='disbaled'"; + if (paragraph >= chap.getParagraphs().size()) + disabledRight = " disabled='disbaled'"; + + // First -> previous *chapter* + if (chapter > 0) + disabledLeft = ""; + first = WebLibraryUrls.getViewUrl(luid, + (Math.max(chapter - 1, 0)), null); + if (paragraph <= 1) { + previous = first; + } + + Paragraph para = null; + try { + para = chap.getParagraphs().get(paragraph - 1); + } catch (IndexOutOfBoundsException e) { + return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, + NanoHTTPD.MIME_PLAINTEXT, + "Paragraph " + paragraph + " not found"); + } + + if (para.getType() == ParagraphType.IMAGE) { + String zoomStyle = "max-width: 100%;"; + disabledZoomWidth = " disabled='disabled'"; + String zoomOption = cookies.get("zoom"); + if (zoomOption != null && !zoomOption.isEmpty()) { + if (zoomOption.equals("real")) { + zoomStyle = ""; + disabledZoomWidth = ""; + disabledZoomReal = " disabled='disabled'"; + } else if (zoomOption.equals("width")) { + zoomStyle = "max-width: 100%;"; + } else if (zoomOption.equals("height")) { + // see height of navbar + optionbar + zoomStyle = "max-height: calc(100% - 128px);"; + disabledZoomWidth = ""; + disabledZoomHeight = " disabled='disabled'"; + } + } + + String javascript = "document.getElementById(\"previous\").click(); return false;"; + content.append(String.format("" // + + "" + + "" + + "", // + javascript, // + next, // + zoomStyle, // + WebLibraryUrls.getStoryUrl(luid, chapter, + paragraph))); + } else { + content.append(String.format("" // + + "
%s
", // + para.getContent())); + } + } + + builder.append(String.format("" // + + "\n", // + disabledRight, next, // + disabledRight, last // + )); + + builder.append(content); + + builder.append("
\n"); + builder.append(" BACK\n"); + + if (paragraph > 0) { + builder.append(String.format("" // + + "\tREAL\n"// + + "\tWIDTH\n"// + + "\tHEIGHT\n"// + + "
\n", // + disabledZoomReal, + uri + "?optionName=zoom&optionValue=real", // + disabledZoomWidth, + uri + "?optionName=zoom&optionValue=width", // + disabledZoomHeight, + uri + "?optionName=zoom&optionValue=height" // + )); + } + + appendPostHtml(builder); + return NanoHTTPD.newFixedLengthResponse(Status.OK, + NanoHTTPD.MIME_HTML, builder.toString()); + } catch (IOException e) { + Instance.getInstance().getTraceHandler() + .error(new IOException("Cannot get image: " + uri, e)); + return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, + NanoHTTPD.MIME_PLAINTEXT, "Error when processing request"); + } + } + + protected Response newInputStreamResponse(String mimeType, InputStream in) { + if (in == null) { + return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "", + null); + } + return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in); + } + + private String getContentOf(String file) { + InputStream in = IOUtils.openResource(WebLibraryServerIndex.class, + file); + if (in != null) { + try { + return IOUtils.readSmallStream(in); + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error( + new IOException("Cannot get file: index.pre.html", e)); + } + } + + return ""; + } + + private void appendPreHtml(StringBuilder builder, boolean banner) { + String favicon = "favicon.ico"; + String icon = Instance.getInstance().getUiConfig() + .getString(UiConfig.PROGRAM_ICON); + if (icon != null) { + favicon = "icon_" + icon.replace("-", "_") + ".png"; + } + + builder.append( + getContentOf("index.pre.html").replace("favicon.ico", favicon)); + + if (banner) { + builder.append("\n"); + } + } + + private void appendPostHtml(StringBuilder builder) { + builder.append(getContentOf("index.post.html")); + } + + private void appendOption(StringBuilder builder, int depth, String name, + String value, String selected) { + for (int i = 0; i < depth; i++) { + builder.append("\t"); + } + builder.append("\n"); + } + + private void appendTableRow(StringBuilder builder, int depth, + String... tds) { + for (int i = 0; i < depth; i++) { + builder.append("\t"); + } + + int col = 1; + builder.append(""); + for (String td : tds) { + builder.append(""); + builder.append(td); + builder.append(""); + } + builder.append("\n"); + } + + private void appendItemA(StringBuilder builder, int depth, String link, + String name, boolean selected) { + for (int i = 0; i < depth; i++) { + builder.append("\t"); + } + + builder.append(""); + builder.append(name); + builder.append("\n"); + } +} diff --git a/src/be/nikiroo/fanfix/library/WebLibraryUrls.java b/src/be/nikiroo/fanfix/library/WebLibraryUrls.java index 816e269c..be4cf256 100644 --- a/src/be/nikiroo/fanfix/library/WebLibraryUrls.java +++ b/src/be/nikiroo/fanfix/library/WebLibraryUrls.java @@ -1,6 +1,6 @@ package be.nikiroo.fanfix.library; -public class WebLibraryUrls { +class WebLibraryUrls { static public final String INDEX_URL = "/"; static public final String VERSION_URL = "/version"; -- 2.27.0