1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.FileInputStream
;
5 import java
.io
.IOException
;
6 import java
.io
.InputStream
;
7 import java
.security
.KeyStore
;
8 import java
.util
.ArrayList
;
9 import java
.util
.Arrays
;
10 import java
.util
.HashMap
;
11 import java
.util
.List
;
14 import javax
.net
.ssl
.KeyManagerFactory
;
15 import javax
.net
.ssl
.SSLServerSocketFactory
;
17 import be
.nikiroo
.fanfix
.Instance
;
18 import be
.nikiroo
.fanfix
.bundles
.Config
;
19 import be
.nikiroo
.fanfix
.data
.Chapter
;
20 import be
.nikiroo
.fanfix
.data
.MetaData
;
21 import be
.nikiroo
.fanfix
.data
.Paragraph
;
22 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
23 import be
.nikiroo
.fanfix
.data
.Story
;
24 import be
.nikiroo
.fanfix
.library
.WebLibraryServer
.WLoginResult
;
25 import be
.nikiroo
.fanfix
.library
.web
.WebLibraryServerIndex
;
26 import be
.nikiroo
.fanfix
.library
.web
.templates
.WebLibraryServerTemplates
;
27 import be
.nikiroo
.fanfix
.reader
.TextOutput
;
28 import be
.nikiroo
.utils
.IOUtils
;
29 import be
.nikiroo
.utils
.NanoHTTPD
;
30 import be
.nikiroo
.utils
.NanoHTTPD
.IHTTPSession
;
31 import be
.nikiroo
.utils
.NanoHTTPD
.Response
;
32 import be
.nikiroo
.utils
.NanoHTTPD
.Response
.Status
;
33 import be
.nikiroo
.utils
.TraceHandler
;
34 import be
.nikiroo
.utils
.Version
;
36 abstract class WebLibraryServerHtml
implements Runnable
{
37 private NanoHTTPD server
;
38 protected TraceHandler tracer
= new TraceHandler();
40 WebLibraryServerTemplates templates
= WebLibraryServerTemplates
43 abstract protected WLoginResult
login(String who
, String cookie
);
45 abstract protected WLoginResult
login(String who
, String key
,
48 abstract protected WLoginResult
login(boolean badLogin
, boolean badCookie
);
50 abstract protected Response
getList(String uri
, WLoginResult login
)
53 abstract protected Response
getStoryPart(String uri
, WLoginResult login
);
55 abstract protected Response
setStoryPart(String uri
, String value
,
56 WLoginResult login
) throws IOException
;
58 abstract protected Response
getCover(String uri
, WLoginResult login
)
61 abstract protected Response
setCover(String uri
, String luid
,
62 WLoginResult login
) throws IOException
;
64 abstract protected List
<MetaData
> metas(WLoginResult login
)
67 abstract protected Story
story(String luid
, WLoginResult login
)
70 protected abstract Response
imprt(String uri
, String url
,
71 WLoginResult login
) throws IOException
;
73 protected abstract Response
imprtProgress(String uri
, WLoginResult login
);
75 protected abstract Response
delete(String uri
, WLoginResult login
)
79 * Wait until all operations are done and stop the server.
81 * All the new R/W operations will be refused after a call to stop.
83 protected abstract Response
stop(WLoginResult login
);
85 public WebLibraryServerHtml(boolean secure
) throws IOException
{
86 Integer port
= Instance
.getInstance().getConfig()
87 .getInteger(Config
.SERVER_PORT
);
89 throw new IOException(
90 "Cannot start web server: port not specified");
93 SSLServerSocketFactory ssf
= null;
95 String keystorePath
= Instance
.getInstance().getConfig()
96 .getString(Config
.SERVER_SSL_KEYSTORE
, "");
97 String keystorePass
= Instance
.getInstance().getConfig()
98 .getString(Config
.SERVER_SSL_KEYSTORE_PASS
);
100 if (secure
&& keystorePath
.isEmpty()) {
101 throw new IOException(
102 "Cannot start a secure web server: no keystore.jks file povided");
105 if (!keystorePath
.isEmpty()) {
106 File keystoreFile
= new File(keystorePath
);
108 KeyStore keystore
= KeyStore
109 .getInstance(KeyStore
.getDefaultType());
110 InputStream keystoreStream
= new FileInputStream(
113 keystore
.load(keystoreStream
,
114 keystorePass
.toCharArray());
115 KeyManagerFactory keyManagerFactory
= KeyManagerFactory
116 .getInstance(KeyManagerFactory
117 .getDefaultAlgorithm());
118 keyManagerFactory
.init(keystore
,
119 keystorePass
.toCharArray());
120 ssf
= NanoHTTPD
.makeSSLSocketFactory(keystore
,
123 keystoreStream
.close();
125 } catch (Exception e
) {
126 throw new IOException(e
.getMessage());
131 server
= new NanoHTTPD(port
) {
133 public Response
serve(final IHTTPSession session
) {
134 super.serve(session
);
136 String query
= session
.getQueryParameterString(); // a=a%20b&dd=2
137 Method method
= session
.getMethod(); // GET, POST..
138 String uri
= session
.getUri(); // /home.html
140 // need them in real time (not just those sent by the UA)
141 Map
<String
, String
> cookies
= new HashMap
<String
, String
>();
142 for (String cookie
: session
.getCookies()) {
143 cookies
.put(cookie
, session
.getCookies().read(cookie
));
146 WLoginResult login
= null;
147 Map
<String
, String
> params
= session
.getParms();
148 String who
= session
.getRemoteHostName()
149 + session
.getRemoteIpAddress();
150 if (params
.get("login") != null) {
151 login
= login(who
, params
.get("password"),
152 params
.get("login"));
154 String cookie
= cookies
.get("cookie");
155 login
= login(who
, cookie
);
158 if (login
.isSuccess()) {
160 session
.getCookies().set(new Cookie("cookie",
161 login
.getCookie(), "30; path=/"));
164 String optionName
= params
.get("optionName");
165 if (optionName
!= null && !optionName
.isEmpty()) {
166 String optionNo
= params
.get("optionNo");
167 String optionValue
= params
.get("optionValue");
168 if (optionNo
!= null || optionValue
== null
169 || optionValue
.isEmpty()) {
170 session
.getCookies().delete(optionName
);
171 cookies
.remove(optionName
);
173 session
.getCookies().set(new Cookie(optionName
,
174 optionValue
, "; path=/"));
175 cookies
.put(optionName
, optionValue
);
182 if (!login
.isSuccess()
183 && WebLibraryUrls
.isSupportedUrl(uri
, true)) {
184 rep
= loginPage(login
, uri
);
188 if (WebLibraryUrls
.isSupportedUrl(uri
, false)) {
189 if (WebLibraryUrls
.INDEX_URL
.equals(uri
)) {
190 rep
= root(session
, cookies
, login
);
191 } else if (WebLibraryUrls
.VERSION_URL
.equals(uri
)) {
192 rep
= newFixedLengthResponse(Status
.OK
,
194 Version
.getCurrentVersion().toString());
195 } else if (WebLibraryUrls
.isCoverUrl(uri
)) {
196 String luid
= params
.get("luid");
198 rep
= setCover(uri
, luid
, login
);
200 rep
= getCover(uri
, login
);
202 } else if (WebLibraryUrls
.isListUrl(uri
)) {
203 rep
= getList(uri
, login
);
204 } else if (WebLibraryUrls
.isStoryUrl(uri
)) {
205 String value
= params
.get("value");
207 rep
= setStoryPart(uri
, value
, login
);
209 rep
= getStoryPart(uri
, login
);
211 } else if (WebLibraryUrls
.isViewUrl(uri
)) {
212 rep
= getViewer(cookies
, uri
, login
);
213 } else if (WebLibraryUrls
.LOGOUT_URL
.equals(uri
)) {
214 session
.getCookies().delete("cookie");
215 cookies
.remove("cookie");
216 rep
= loginPage(login(false, false), uri
);
217 } else if (WebLibraryUrls
.isImprtUrl(uri
)) {
218 String url
= params
.get("url");
220 rep
= imprt(uri
, url
, login
);
222 rep
= imprtProgress(uri
, login
);
224 } else if (WebLibraryUrls
.isDeleteUrl(uri
)) {
225 rep
= delete(uri
, login
);
226 } else if (WebLibraryUrls
.EXIT_URL
.equals(uri
)) {
227 rep
= WebLibraryServerHtml
.this.stop(login
);
229 getTraceHandler().error(
230 "Supported URL was not processed: "
232 rep
= newFixedLengthResponse(
233 Status
.INTERNAL_ERROR
,
234 NanoHTTPD
.MIME_PLAINTEXT
,
235 "An error happened");
238 if (uri
.startsWith("/"))
239 uri
= uri
.substring(1);
240 InputStream in
= IOUtils
.openResource(
241 WebLibraryServerIndex
.class, uri
);
243 String mimeType
= MIME_PLAINTEXT
;
244 if (uri
.endsWith(".css")) {
245 mimeType
= "text/css";
246 } else if (uri
.endsWith(".html")) {
247 mimeType
= "text/html";
248 } else if (uri
.endsWith(".js")) {
249 mimeType
= "text/javascript";
250 } else if (uri
.endsWith(".png")) {
251 mimeType
= "image/png";
252 } else if (uri
.endsWith(".ico")) {
253 mimeType
= "image/x-icon";
254 } else if (uri
.endsWith(".java")) {
255 mimeType
= "text/plain";
257 rep
= newChunkedResponse(Status
.OK
, mimeType
,
262 getTraceHandler().trace("404: " + uri
);
263 rep
= newFixedLengthResponse(Status
.NOT_FOUND
,
264 NanoHTTPD
.MIME_PLAINTEXT
, "Not Found");
268 } catch (Exception e
) {
269 Instance
.getInstance().getTraceHandler().error(
270 new IOException("Cannot process web request", e
));
271 rep
= newFixedLengthResponse(Status
.INTERNAL_ERROR
,
272 NanoHTTPD
.MIME_PLAINTEXT
, "An error occured");
280 getTraceHandler().trace("Install SSL on the web server...");
281 server
.makeSecure(ssf
, null);
282 getTraceHandler().trace("Done.");
289 server
.start(NanoHTTPD
.SOCKET_READ_TIMEOUT
, false);
290 } catch (IOException e
) {
291 tracer
.error(new IOException("Cannot start the web server", e
));
295 protected void doStop() {
300 * The traces handler for this {@link WebLibraryServerHtml}.
302 * @return the traces handler
304 public TraceHandler
getTraceHandler() {
309 * The traces handler for this {@link WebLibraryServerHtml}.
312 * the new traces handler
314 public void setTraceHandler(TraceHandler tracer
) {
315 if (tracer
== null) {
316 tracer
= new TraceHandler(false, false, false);
319 this.tracer
= tracer
;
322 private Response
loginPage(WLoginResult login
, String uri
)
324 List
<Template
> content
= new ArrayList
<Template
>();
326 if (login
.isBadLogin()) {
327 content
.add(templates
.message("Bad login or password", true));
328 } else if (login
.isBadCookie()) {
329 content
.add(templates
.message("Your session timed out", true));
332 content
.add(templates
.login(uri
));
334 return NanoHTTPD
.newChunkedResponse(Status
.FORBIDDEN
,
336 templates
.index(true, false, content
).read());
339 private Response
root(IHTTPSession session
, Map
<String
, String
> cookies
,
340 WLoginResult login
) throws IOException
{
341 BasicLibrary lib
= Instance
.getInstance().getLibrary();
342 MetaResultList result
= new MetaResultList(metas(login
));
344 Map
<String
, String
> params
= session
.getParms();
346 String filter
= cookies
.get("filter");
347 if (params
.get("optionNo") != null)
349 if (filter
== null) {
353 String browser
= params
.get("browser") == null ?
""
354 : params
.get("browser");
355 String browser2
= params
.get("browser2") == null ?
""
356 : params
.get("browser2");
357 String browser3
= params
.get("browser3") == null ?
""
358 : params
.get("browser3");
360 String filterSource
= null;
361 String filterAuthor
= null;
362 String filterTag
= null;
364 // TODO: javascript in realtime, using visible=false + hide [submit]
366 List
<Template
> selects
= new ArrayList
<Template
>();
367 boolean sourcesSel
= false;
368 boolean authorsSel
= false;
369 boolean tagsSel
= false;
371 if (!browser
.isEmpty()) {
372 List
<Template
> options
= new ArrayList
<Template
>();
374 if (browser
.equals("sources")) {
376 filterSource
= browser2
.isEmpty() ? filterSource
: browser2
;
378 // TODO: if 1 group -> no group
379 Map
<String
, List
<String
>> sources
= result
.getSourcesGrouped();
380 for (String source
: sources
.keySet()) {
382 templates
.browserOption(source
, source
, browser2
));
384 } else if (browser
.equals("authors")) {
386 filterAuthor
= browser2
.isEmpty() ? filterAuthor
: browser2
;
388 // TODO: if 1 group -> no group
389 Map
<String
, List
<String
>> authors
= result
.getAuthorsGrouped();
390 for (String author
: authors
.keySet()) {
392 templates
.browserOption(author
, author
, browser2
));
394 } else if (browser
.equals("tags")) {
396 filterTag
= browser2
.isEmpty() ? filterTag
: browser2
;
398 for (String tag
: result
.getTags()) {
399 options
.add(templates
.browserOption(tag
, tag
, browser2
));
403 selects
.add(templates
.browserSelect("browser2", browser2
, options
));
406 if (!browser2
.isEmpty()) {
407 List
<Template
> options
= new ArrayList
<Template
>();
409 if (browser
.equals("sources")) {
410 filterSource
= browser3
.isEmpty() ? filterSource
: browser3
;
411 Map
<String
, List
<String
>> sourcesGrouped
= result
412 .getSourcesGrouped();
413 List
<String
> sources
= sourcesGrouped
.get(browser2
);
414 if (sources
!= null && !sources
.isEmpty()) {
415 // TODO: single empty value
416 for (String source
: sources
) {
417 options
.add(templates
.browserOption(source
, source
,
421 } else if (browser
.equals("authors")) {
422 filterAuthor
= browser3
.isEmpty() ? filterAuthor
: browser3
;
423 Map
<String
, List
<String
>> authorsGrouped
= result
424 .getAuthorsGrouped();
425 List
<String
> authors
= authorsGrouped
.get(browser2
);
426 if (authors
!= null && !authors
.isEmpty()) {
427 // TODO: single empty value
428 for (String author
: authors
) {
429 options
.add(templates
.browserOption(author
, author
,
435 selects
.add(templates
.browserSelect("browser3", browser3
, options
));
438 List
<Template
> booklines
= new ArrayList
<Template
>();
439 for (MetaData meta
: result
.getMetas()) {
440 if (!filter
.isEmpty() && !meta
.getTitle().toLowerCase()
441 .contains(filter
.toLowerCase())) {
446 if (filterSource
!= null
447 && !filterSource
.equals(meta
.getSource())) {
452 if (filterAuthor
!= null
453 && !filterAuthor
.equals(meta
.getAuthor())) {
457 if (filterTag
!= null && !meta
.getTags().contains(filterTag
)) {
462 if (meta
.getAuthor() != null && !meta
.getAuthor().isEmpty()) {
463 author
= "(" + meta
.getAuthor() + ")";
466 booklines
.add(templates
.bookline( //
468 WebLibraryUrls
.getViewUrl(meta
.getLuid(), null, null), //
471 lib
.isCached(meta
.getLuid()) //
475 // Add the browser in front of the booklines
476 booklines
.add(0, templates
.browser(browser
, filter
, selects
));
478 return newInputStreamResponse(NanoHTTPD
.MIME_HTML
,
479 templates
.index(true, false, booklines
).read());
482 private Response
getViewer(Map
<String
, String
> cookies
, String uri
,
483 WLoginResult login
) {
484 String
[] cover
= uri
.split("/");
487 if (cover
.length
< off
+ 2) {
488 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
489 NanoHTTPD
.MIME_PLAINTEXT
, null);
492 String type
= cover
[off
+ 0];
493 String luid
= cover
[off
+ 1];
494 String chapterStr
= cover
.length
< off
+ 3 ?
null : cover
[off
+ 2];
495 String paragraphStr
= cover
.length
< off
+ 4 ?
null : cover
[off
+ 3];
497 // 1-based (0 = desc)
499 if (chapterStr
!= null) {
501 chapter
= Integer
.parseInt(chapterStr
);
503 throw new NumberFormatException();
505 } catch (NumberFormatException e
) {
506 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
507 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter is not valid");
513 if (paragraphStr
!= null) {
515 paragraph
= Integer
.parseInt(paragraphStr
);
516 if (paragraph
<= 0) {
517 throw new NumberFormatException();
519 } catch (NumberFormatException e
) {
520 return NanoHTTPD
.newFixedLengthResponse(Status
.BAD_REQUEST
,
521 NanoHTTPD
.MIME_PLAINTEXT
, "Paragraph is not valid");
526 Story story
= story(luid
, login
);
528 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
529 NanoHTTPD
.MIME_PLAINTEXT
, "Story not found");
532 // For images documents, always go to the images if not chap 0 desc
533 if (story
.getMeta().isImageDocument()) {
534 if (chapter
> 0 && paragraph
<= 0)
540 chap
= story
.getMeta().getResume();
543 chap
= story
.getChapters().get(chapter
- 1);
544 } catch (IndexOutOfBoundsException e
) {
545 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
546 NanoHTTPD
.MIME_PLAINTEXT
, "Chapter not found");
550 String first
, previous
, next
, last
;
552 boolean disabledLeft
= false;
553 boolean disabledRight
= false;
554 boolean disabledZoomReal
= false;
555 boolean disabledZoomWidth
= false;
556 boolean disabledZoomWidthLimited
= false;
557 boolean disabledZoomHeight
= false;
559 Template viewerItem
= null;
560 if (paragraph
<= 0) {
561 first
= WebLibraryUrls
.getViewUrl(luid
, 0, null);
562 previous
= WebLibraryUrls
.getViewUrl(luid
,
563 (Math
.max(chapter
- 1, 0)), null);
564 next
= WebLibraryUrls
.getViewUrl(luid
,
565 (Math
.min(chapter
+ 1, story
.getChapters().size())),
567 last
= WebLibraryUrls
.getViewUrl(luid
,
568 story
.getChapters().size(), null);
570 Template desc
= null;
572 List
<Template
> desclines
= new ArrayList
<Template
>();
573 Map
<String
, String
> details
= BasicLibrary
574 .getMetaDesc(story
.getMeta());
575 for (String key
: details
.keySet()) {
576 desclines
.add(templates
.viewerDescline(key
,
580 desc
= templates
.viewerDesc( //
581 story
.getMeta().getTitle(), //
583 WebLibraryUrls
.getStoryUrlCover(luid
), //
589 if (chap
.getParagraphs().size() <= 0) {
590 content
= "No content provided.";
592 content
= new TextOutput(false).convert(chap
, chapter
> 0);
595 viewerItem
= templates
.viewerText(desc
, content
);
599 if (chapter
>= story
.getChapters().size())
600 disabledRight
= true;
602 first
= WebLibraryUrls
.getViewUrl(luid
, chapter
, 1);
603 previous
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
604 (Math
.max(paragraph
- 1, 1)));
605 next
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
606 (Math
.min(paragraph
+ 1, chap
.getParagraphs().size())));
607 last
= WebLibraryUrls
.getViewUrl(luid
, chapter
,
608 chap
.getParagraphs().size());
612 if (paragraph
>= chap
.getParagraphs().size())
613 disabledRight
= true;
615 // First -> previous *chapter*
617 disabledLeft
= false;
618 first
= WebLibraryUrls
.getViewUrl(luid
,
619 (Math
.max(chapter
- 1, 0)), null);
620 if (paragraph
<= 1) {
624 Paragraph para
= null;
626 para
= chap
.getParagraphs().get(paragraph
- 1);
627 } catch (IndexOutOfBoundsException e
) {
628 return NanoHTTPD
.newFixedLengthResponse(Status
.NOT_FOUND
,
629 NanoHTTPD
.MIME_PLAINTEXT
,
630 "Paragraph " + paragraph
+ " not found");
633 if (para
.getType() == ParagraphType
.IMAGE
) {
635 String zoomStyle
= "max-width: 800px;";
636 disabledZoomWidthLimited
= true;
638 String zoomOption
= cookies
.get("zoom");
639 if (zoomOption
!= null && !zoomOption
.isEmpty()) {
640 if (zoomOption
.equals("real")) {
642 disabledZoomWidthLimited
= false;
643 disabledZoomReal
= true;
644 } else if (zoomOption
.equals("widthlimited")) {
646 } else if (zoomOption
.equals("width")) {
647 zoomStyle
= "max-width: 100%;";
648 disabledZoomWidthLimited
= false;
649 disabledZoomWidth
= true;
650 } else if (zoomOption
.equals("height")) {
651 // see height of navbar + optionbar
652 zoomStyle
= "max-height: calc(100% - 128px);";
653 disabledZoomWidthLimited
= false;
654 disabledZoomHeight
= true;
658 viewerItem
= templates
.viewerImage(
659 WebLibraryUrls
.getStoryUrl(luid
, chapter
,
661 disabledRight ?
null : next
, //
665 viewerItem
= templates
.viewerText(null,
666 new TextOutput(false).convert(para
));
670 // List of chap/para links for navbar
672 List
<Template
> links
= new ArrayList
<Template
>();
673 links
.add(templates
.viewerLink( //
675 WebLibraryUrls
.getViewUrl(luid
, 0, null), //
676 paragraph
== 0 && chapter
== 0 //
679 for (int i
= 1; i
<= chap
.getParagraphs().size(); i
++) {
680 links
.add(templates
.viewerLink( //
682 WebLibraryUrls
.getViewUrl(luid
, chapter
, i
), //
688 for (Chapter c
: story
.getChapters()) {
689 String chapName
= "Chapter " + c
.getNumber();
690 if (c
.getName() != null && !c
.getName().isEmpty()) {
691 chapName
+= ": " + c
.getName();
694 links
.add(templates
.viewerLink( //
696 WebLibraryUrls
.getViewUrl(luid
, i
, null), //
706 Template navbar
= templates
.viewerNavbar( //
707 paragraph
> 0 ? paragraph
: chapter
, //
719 // Buttons on the optionbar
721 List
<Template
> buttons
= new ArrayList
<Template
>();
722 buttons
.add(templates
.viewerOptionbarButton( //
723 "Back", "/", "back", false));
725 buttons
.add(templates
.viewerOptionbarButton( //
726 "1:1", uri
+ "?optionName=zoom&optionValue=real",
727 "zoomreal", disabledZoomReal
));
728 buttons
.add(templates
.viewerOptionbarButton( //
730 uri
+ "?optionName=zoom&optionValue=widthlimited",
731 "zoomwidthlimited", disabledZoomWidthLimited
));
732 buttons
.add(templates
.viewerOptionbarButton( //
733 "Width", uri
+ "?optionName=zoom&optionValue=width",
734 "zoomwidth", disabledZoomWidth
));
735 buttons
.add(templates
.viewerOptionbarButton( //
736 "Height", uri
+ "?optionName=zoom&optionValue=height",
737 "zoomheight", disabledZoomHeight
));
742 Template optionbar
= templates
.viewerOptionbar( //
743 (paragraph
> 0) ?
5 : 1, //
749 return newInputStreamResponse(NanoHTTPD
.MIME_HTML
, //
750 templates
.index(false, (paragraph
> 0), Arrays
.asList( //
755 } catch (IOException e
) {
756 Instance
.getInstance().getTraceHandler()
757 .error(new IOException("Cannot get image: " + uri
, e
));
758 return NanoHTTPD
.newFixedLengthResponse(Status
.INTERNAL_ERROR
,
759 NanoHTTPD
.MIME_PLAINTEXT
, "Error when processing request");
763 protected Response
newInputStreamResponse(String mimeType
, InputStream in
) {
765 return NanoHTTPD
.newFixedLengthResponse(Status
.NO_CONTENT
, "",
768 return NanoHTTPD
.newChunkedResponse(Status
.OK
, mimeType
, in
);