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 boolean bl; 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, boolean bl) { this.success = success; this.rw = rw; this.wl = wl; this.bl = bl; this.wookie = CookieUtils.generateCookie(who + key, 0); String opts = ""; if (rw) opts += "|rw"; if (!wl) opts += "|wl"; if (!bl) opts += "|bl"; 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.bl = !opts.contains("|bl"); } } } } this.badToken = !success; } // No token -> no bad token } public boolean isSuccess() { return success; } public boolean isRw() { return rw; } public boolean isWl() { return wl; } public boolean isBl() { return bl; } 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(); 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"); } int cacheMb = Instance.getInstance().getConfig() .getInteger(Config.SERVER_MAX_CACHE_MB, 100); maxStoryCacheSize = cacheMb * 1024 * 1024; setTraceHandler(Instance.getInstance().getTraceHandler()); whitelist = Instance.getInstance().getConfig() .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)); } 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")); } else { String token = cookies.get("token"); login = login(who, token); } 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 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() && (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, login); } else if (uri.startsWith(LIST_URL)) { rep = getList(uri, login); } else if (uri.startsWith(STORY_URL_BASE)) { rep = getStoryPart(uri, login); } else if (uri.startsWith(VIEWER_URL_BASE)) { rep = getViewer(cookies, uri, login); } 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; } }; 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 = Instance.getInstance().getConfig().getList( Config.SERVER_ALLOWED_SUBKEYS, new ArrayList()); String realKey = Instance.getInstance().getConfig() .getString(Config.SERVER_KEY, ""); return new LoginResult(token, who, realKey, subkeys); } // allow rw/wl private LoginResult login(String who, String key, String subkey) { String realKey = Instance.getInstance().getConfig() .getString(Config.SERVER_KEY, ""); // I don't like NULLs... key = key == null ? "" : key; subkey = subkey == null ? "" : subkey; if (!realKey.equals(key)) { return new LoginResult(null, null, null, false, false, false, false); } // defaults are true (as previous versions without the feature) boolean rw = true; boolean wl = true; boolean bl = true; 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 } if ((subkey + "|").contains("|bl|")) { bl = false; // |bl| = bypass blacklist } } else { return new LoginResult(null, null, null, false, false, false, false); } } return new LoginResult(who, key, subkey, true, rw, wl, bl); } 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("
Your session timed out
"); } if (uri.equals("/logout")) { uri = "/"; } builder.append( "\n"); appendPostHtml(builder); return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN, NanoHTTPD.MIME_HTML, builder.toString()); } protected Response getList(String uri, LoginResult login) throws IOException { if (uri.equals("/list/luids")) { List jsons = new ArrayList(); for (MetaData meta : metas(login)) { jsons.add(JsonIO.toJson(meta)); } return newInputStreamResponse("application/json", new ByteArrayInputStream( new JSONArray(jsons).toString().getBytes())); } return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, null); } private Response root(IHTTPSession session, Map cookies, LoginResult 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, LoginResult login) { String[] cover = uri.split("/"); int off = 2; if (cover.length < off + 2) { return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, null); } String luid = cover[off + 0]; String chapterStr = cover[off + 1]; String imageStr = cover.length < off + 3 ? null : cover[off + 2]; // 1-based (0 = desc) int chapter = 0; if (chapterStr != null && !"cover".equals(chapterStr) && !"metadata".equals(chapterStr) && !"json".equals(chapterStr)) { 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 = 1; if (imageStr != null) { try { paragraph = Integer.parseInt(imageStr); if (paragraph < 0) { throw new NumberFormatException(); } } catch (NumberFormatException e) { return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid"); } } String mimeType = NanoHTTPD.MIME_PLAINTEXT; InputStream in = null; try { if ("cover".equals(chapterStr)) { Image img = getCover(luid, login); if (img != null) { in = img.newInputStream(); } // TODO: get correct image type mimeType = "image/png"; } else if ("metadata".equals(chapterStr)) { MetaData meta = meta(luid, login); JSONObject json = JsonIO.toJson(meta); mimeType = "application/json"; in = new ByteArrayInputStream(json.toString().getBytes()); } else if ("json".equals(chapterStr)) { Story story = story(luid, login); JSONObject json = JsonIO.toJson(story); mimeType = "application/json"; in = new ByteArrayInputStream(json.toString().getBytes()); } else { Story story = story(luid, login); if (story != null) { if (chapter == 0) { StringBuilder builder = new StringBuilder(); for (Paragraph p : story.getMeta().getResume()) { if (builder.length() == 0) { builder.append("\n"); } builder.append(p.getContent()); } in = new ByteArrayInputStream( builder.toString().getBytes("utf-8")); } else { Paragraph para = story.getChapters().get(chapter - 1) .getParagraphs().get(paragraph - 1); Image img = para.getContentImage(); if (para.getType() == ParagraphType.IMAGE) { // TODO: get correct image type mimeType = "image/png"; in = img.newInputStream(); } else { in = new ByteArrayInputStream( para.getContent().getBytes("utf-8")); } } } } } catch (IndexOutOfBoundsException e) { return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Chapter or paragraph does not exist"); } 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"); } return newInputStreamResponse(mimeType, in); } private Response getViewer(Map cookies, String uri, LoginResult 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 = getViewUrl(luid, 0, null); previous = getViewUrl(luid, (Math.max(chapter - 1, 0)), null); next = getViewUrl(luid, (Math.min(chapter + 1, story.getChapters().size())), null); last = 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 = getViewUrl(luid, chapter, 1); previous = getViewUrl(luid, chapter, (Math.max(paragraph - 1, 1))); next = getViewUrl(luid, chapter, (Math.min(paragraph + 1, chap.getParagraphs().size()))); last = 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 = 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, // 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 String getViewUrl(String luid, Integer chap, Integer para) { return VIEWER_URL // .replace("{luid}", luid) // .replace("/{chap}", chap == null ? "" : "/" + chap) // .replace("/{para}", (chap == null || para == null) ? "" : "/" + para); } private String getStoryUrl(String luid, int chap, Integer para) { return STORY_URL // .replace("{luid}", luid) // .replace("{chap}", Integer.toString(chap)) // .replace("{para}", para == null ? "" : Integer.toString(para)); } private String getStoryUrlCover(String luid) { return STORY_URL_COVER // .replace("{luid}", luid); } private boolean isAllowed(MetaData meta, LoginResult 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(LoginResult login) throws IOException { BasicLibrary lib = Instance.getInstance().getLibrary(); List metas = new ArrayList(); for (MetaData meta : lib.getList().getMetas()) { if (isAllowed(meta, login)) { metas.add(meta); } } return metas; } private MetaData meta(String luid, LoginResult 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, LoginResult 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, LoginResult login) throws IOException { synchronized (storyCache) { if (storyCache.containsKey(luid)) { Story story = storyCache.get(luid); if (!isAllowed(story.getMeta(), login)) return null; return story; } } Story story = null; MetaData meta = meta(luid, login); if (meta != null) { BasicLibrary lib = Instance.getInstance().getLibrary(); story = lib.getStory(luid, null); long size = sizeOf(story); synchronized (storyCache) { // Could have been added by another request if (!storyCache.containsKey(luid)) { while (!storyCacheOrder.isEmpty() && storyCacheSize + size > maxStoryCacheSize) { String oldestLuid = storyCacheOrder.removeFirst(); Story oldestStory = storyCache.remove(oldestLuid); maxStoryCacheSize -= sizeOf(oldestStory); } storyCacheOrder.add(luid); storyCache.put(luid, story); } } } 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(); } } } return size; } 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"); } public static void main(String[] args) throws IOException { Instance.init(); WebLibraryServer web = new WebLibraryServer(false); web.run(); } }