(this.running)) {
354 | clientHandler.close();
355 | }
356 | }
357 |
358 | @Override
359 | public void closed(ClientHandler clientHandler) {
360 | this.running.remove(clientHandler);
361 | }
362 |
363 | @Override
364 | public void exec(ClientHandler clientHandler) {
365 | ++this.requestCount;
366 | Thread t = new Thread(clientHandler);
367 | t.setDaemon(true);
368 | t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")");
369 | this.running.add(clientHandler);
370 | t.start();
371 | }
372 | }
373 |
374 | /**
375 | * Default strategy for creating and cleaning up temporary files.
376 | *
377 | *
378 | * By default, files are created by File.createTempFile()
in
379 | * the directory specified.
380 | *
381 | */
382 | public static class DefaultTempFile implements TempFile {
383 |
384 | private final File file;
385 |
386 | private final OutputStream fstream;
387 |
388 | public DefaultTempFile(String tempdir) throws IOException {
389 | this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
390 | this.fstream = new FileOutputStream(this.file);
391 | }
392 |
393 | @Override
394 | public void delete() throws Exception {
395 | safeClose(this.fstream);
396 | if (!this.file.delete()) {
397 | throw new Exception("could not delete temporary file");
398 | }
399 | }
400 |
401 | @Override
402 | public String getName() {
403 | return this.file.getAbsolutePath();
404 | }
405 |
406 | @Override
407 | public OutputStream open() throws Exception {
408 | return this.fstream;
409 | }
410 | }
411 |
412 | /**
413 | * Default strategy for creating and cleaning up temporary files.
414 | *
415 | *
416 | * This class stores its files in the standard location (that is, wherever
417 | * java.io.tmpdir
points to). Files are added to an internal
418 | * list, and deleted when no longer needed (that is, when
419 | * clear()
is invoked at the end of processing a request).
420 | *
421 | */
422 | public static class DefaultTempFileManager implements TempFileManager {
423 |
424 | private final String tmpdir;
425 |
426 | private final List tempFiles;
427 |
428 | public DefaultTempFileManager() {
429 | this.tmpdir = System.getProperty("java.io.tmpdir");
430 | this.tempFiles = new ArrayList();
431 | }
432 |
433 | @Override
434 | public void clear() {
435 | for (TempFile file : this.tempFiles) {
436 | try {
437 | file.delete();
438 | } catch (Exception ignored) {
439 | NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored);
440 | }
441 | }
442 | this.tempFiles.clear();
443 | }
444 |
445 | @Override
446 | public TempFile createTempFile() throws Exception {
447 | DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
448 | this.tempFiles.add(tempFile);
449 | return tempFile;
450 | }
451 | }
452 |
453 | /**
454 | * Default strategy for creating and cleaning up temporary files.
455 | */
456 | private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
457 |
458 | @Override
459 | public TempFileManager create() {
460 | return new DefaultTempFileManager();
461 | }
462 | }
463 |
464 | private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)";
465 |
466 | private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE);
467 |
468 | private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)";
469 |
470 | private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE);
471 |
472 | private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]";
473 |
474 | private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX);
475 |
476 | protected class HTTPSession implements IHTTPSession {
477 |
478 | public static final int BUFSIZE = 8192;
479 |
480 | private final TempFileManager tempFileManager;
481 |
482 | private final OutputStream outputStream;
483 |
484 | private final PushbackInputStream inputStream;
485 |
486 | private int splitbyte;
487 |
488 | private int rlen;
489 |
490 | private String uri;
491 |
492 | private Method method;
493 |
494 | private Map parms;
495 |
496 | private Map headers;
497 |
498 | private CookieHandler cookies;
499 |
500 | private String queryParameterString;
501 |
502 | private String remoteIp;
503 |
504 | private String protocolVersion;
505 |
506 | public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
507 | this.tempFileManager = tempFileManager;
508 | this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
509 | this.outputStream = outputStream;
510 | }
511 |
512 | public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
513 | this.tempFileManager = tempFileManager;
514 | this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
515 | this.outputStream = outputStream;
516 | this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
517 | this.headers = new HashMap();
518 | }
519 |
520 | /**
521 | * Decodes the sent headers and loads the data into Key/value pairs
522 | */
523 | private void decodeHeader(BufferedReader in, Map pre, Map parms, Map headers) throws ResponseException {
524 | try {
525 | // Read the request line
526 | String inLine = in.readLine();
527 | if (inLine == null) {
528 | return;
529 | }
530 |
531 | StringTokenizer st = new StringTokenizer(inLine);
532 | if (!st.hasMoreTokens()) {
533 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
534 | }
535 |
536 | pre.put("method", st.nextToken());
537 |
538 | if (!st.hasMoreTokens()) {
539 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
540 | }
541 |
542 | String uri = st.nextToken();
543 |
544 | // Decode parameters from the URI
545 | int qmi = uri.indexOf('?');
546 | if (qmi >= 0) {
547 | decodeParms(uri.substring(qmi + 1), parms);
548 | uri = decodePercent(uri.substring(0, qmi));
549 | } else {
550 | uri = decodePercent(uri);
551 | }
552 |
553 | // If there's another token, its protocol version,
554 | // followed by HTTP headers.
555 | // NOTE: this now forces header names lower case since they are
556 | // case insensitive and vary by client.
557 | if (st.hasMoreTokens()) {
558 | protocolVersion = st.nextToken();
559 | } else {
560 | protocolVersion = "HTTP/1.1";
561 | NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1.");
562 | }
563 | String line = in.readLine();
564 | while (line != null && line.trim().length() > 0) {
565 | int p = line.indexOf(':');
566 | if (p >= 0) {
567 | headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
568 | }
569 | line = in.readLine();
570 | }
571 |
572 | pre.put("uri", uri);
573 | } catch (IOException ioe) {
574 | throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
575 | }
576 | }
577 |
578 | /**
579 | * Decodes the Multipart Body data and put it into Key/Value pairs.
580 | */
581 | private void decodeMultipartFormData(String boundary, ByteBuffer fbuf, Map parms, Map files) throws ResponseException {
582 | try {
583 | int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes());
584 | if (boundary_idxs.length < 2) {
585 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings.");
586 | }
587 |
588 | final int MAX_HEADER_SIZE = 1024;
589 | byte[] part_header_buff = new byte[MAX_HEADER_SIZE];
590 | for (int bi = 0; bi < boundary_idxs.length - 1; bi++) {
591 | fbuf.position(boundary_idxs[bi]);
592 | int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE;
593 | fbuf.get(part_header_buff, 0, len);
594 | ByteArrayInputStream bais = new ByteArrayInputStream(part_header_buff, 0, len);
595 | BufferedReader in = new BufferedReader(new InputStreamReader(bais, Charset.forName("US-ASCII")));
596 |
597 | // First line is boundary string
598 | String mpline = in.readLine();
599 | if (!mpline.contains(boundary)) {
600 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary.");
601 | }
602 |
603 | String part_name = null, file_name = null, content_type = null;
604 | // Parse the reset of the header lines
605 | mpline = in.readLine();
606 | while (mpline != null && mpline.trim().length() > 0) {
607 | Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline);
608 | if (matcher.matches()) {
609 | String attributeString = matcher.group(2);
610 | matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString);
611 | while (matcher.find()) {
612 | String key = matcher.group(1);
613 | if (key.equalsIgnoreCase("name")) {
614 | part_name = matcher.group(2);
615 | } else if (key.equalsIgnoreCase("filename")) {
616 | file_name = matcher.group(2);
617 | }
618 | }
619 | }
620 | matcher = CONTENT_TYPE_PATTERN.matcher(mpline);
621 | if (matcher.matches()) {
622 | content_type = matcher.group(2).trim();
623 | }
624 | mpline = in.readLine();
625 | }
626 |
627 | // Read the part data
628 | int part_header_len = len - (int) in.skip(MAX_HEADER_SIZE);
629 | if (part_header_len >= len - 4) {
630 | throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE.");
631 | }
632 | int part_data_start = boundary_idxs[bi] + part_header_len;
633 | int part_data_end = boundary_idxs[bi + 1] - 4;
634 |
635 | fbuf.position(part_data_start);
636 | if (content_type == null) {
637 | // Read the part into a string
638 | byte[] data_bytes = new byte[part_data_end - part_data_start];
639 | fbuf.get(data_bytes);
640 | parms.put(part_name, new String(data_bytes));
641 | } else {
642 | // Read it into a file
643 | String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start);
644 | if (!files.containsKey(part_name)) {
645 | files.put(part_name, path);
646 | } else {
647 | int count = 2;
648 | while (files.containsKey(part_name + count)) {
649 | count++;
650 | }
651 | files.put(part_name + count, path);
652 | }
653 | parms.put(part_name, file_name);
654 | }
655 | }
656 | } catch (ResponseException re) {
657 | throw re;
658 | } catch (Exception e) {
659 | throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString());
660 | }
661 | }
662 |
663 | /**
664 | * Decodes parameters in percent-encoded URI-format ( e.g.
665 | * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
666 | * Map. NOTE: this doesn't support multiple identical keys due to the
667 | * simplicity of Map.
668 | */
669 | private void decodeParms(String parms, Map p) {
670 | if (parms == null) {
671 | this.queryParameterString = "";
672 | return;
673 | }
674 |
675 | this.queryParameterString = parms;
676 | StringTokenizer st = new StringTokenizer(parms, "&");
677 | while (st.hasMoreTokens()) {
678 | String e = st.nextToken();
679 | int sep = e.indexOf('=');
680 | if (sep >= 0) {
681 | p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
682 | } else {
683 | p.put(decodePercent(e).trim(), "");
684 | }
685 | }
686 | }
687 |
688 | @Override
689 | public void execute() throws IOException {
690 | Response r = null;
691 | try {
692 | // Read the first 8192 bytes.
693 | // The full header should fit in here.
694 | // Apache's default header limit is 8KB.
695 | // Do NOT assume that a single read will get the entire header
696 | // at once!
697 | byte[] buf = new byte[HTTPSession.BUFSIZE];
698 | this.splitbyte = 0;
699 | this.rlen = 0;
700 |
701 | int read = -1;
702 | try {
703 | read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
704 | } catch (Exception e) {
705 | safeClose(this.inputStream);
706 | safeClose(this.outputStream);
707 | throw new SocketException("NanoHttpd Shutdown");
708 | }
709 | if (read == -1) {
710 | // socket was been closed
711 | safeClose(this.inputStream);
712 | safeClose(this.outputStream);
713 | throw new SocketException("NanoHttpd Shutdown");
714 | }
715 | while (read > 0) {
716 | this.rlen += read;
717 | this.splitbyte = findHeaderEnd(buf, this.rlen);
718 | if (this.splitbyte > 0) {
719 | break;
720 | }
721 | read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
722 | }
723 |
724 | if (this.splitbyte < this.rlen) {
725 | this.inputStream.unread(buf, this.splitbyte, this.rlen - this.splitbyte);
726 | }
727 |
728 | this.parms = new HashMap();
729 | if (null == this.headers) {
730 | this.headers = new HashMap();
731 | } else {
732 | this.headers.clear();
733 | }
734 |
735 | if (null != this.remoteIp) {
736 | this.headers.put("remote-addr", this.remoteIp);
737 | this.headers.put("http-client-ip", this.remoteIp);
738 | }
739 |
740 | // Create a BufferedReader for parsing the header.
741 | BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
742 |
743 | // Decode the header into parms and header java properties
744 | Map pre = new HashMap();
745 | decodeHeader(hin, pre, this.parms, this.headers);
746 |
747 | this.method = Method.lookup(pre.get("method"));
748 | if (this.method == null) {
749 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
750 | }
751 |
752 | this.uri = pre.get("uri");
753 |
754 | this.cookies = new CookieHandler(this.headers);
755 |
756 | String connection = this.headers.get("connection");
757 | boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*"));
758 |
759 | // Ok, now do the serve()
760 | r = serve(this);
761 | if (r == null) {
762 | throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
763 | } else {
764 | String acceptEncoding = this.headers.get("accept-encoding");
765 | this.cookies.unloadQueue(r);
766 | r.setRequestMethod(this.method);
767 | r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip"));
768 | r.setKeepAlive(keepAlive);
769 | r.send(this.outputStream);
770 | }
771 | if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) {
772 | throw new SocketException("NanoHttpd Shutdown");
773 | }
774 | } catch (SocketException e) {
775 | // throw it out to close socket object (finalAccept)
776 | throw e;
777 | } catch (SocketTimeoutException ste) {
778 | // treat socket timeouts the same way we treat socket exceptions
779 | // i.e. close the stream & finalAccept object by throwing the
780 | // exception up the call stack.
781 | throw ste;
782 | } catch (IOException ioe) {
783 | Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
784 | resp.send(this.outputStream);
785 | safeClose(this.outputStream);
786 | } catch (ResponseException re) {
787 | Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
788 | resp.send(this.outputStream);
789 | safeClose(this.outputStream);
790 | } finally {
791 | safeClose(r);
792 | this.tempFileManager.clear();
793 | }
794 | }
795 |
796 | /**
797 | * Find byte index separating header from body. It must be the last byte
798 | * of the first two sequential new lines.
799 | */
800 | private int findHeaderEnd(final byte[] buf, int rlen) {
801 | int splitbyte = 0;
802 | while (splitbyte + 3 < rlen) {
803 | if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
804 | return splitbyte + 4;
805 | }
806 | splitbyte++;
807 | }
808 | return 0;
809 | }
810 |
811 | /**
812 | * Find the byte positions where multipart boundaries start. This reads
813 | * a large block at a time and uses a temporary buffer to optimize
814 | * (memory mapped) file access.
815 | */
816 | private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
817 | int[] res = new int[0];
818 | if (b.remaining() < boundary.length) {
819 | return res;
820 | }
821 |
822 | int search_window_pos = 0;
823 | byte[] search_window = new byte[4 * 1024 + boundary.length];
824 |
825 | int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length;
826 | b.get(search_window, 0, first_fill);
827 | int new_bytes = first_fill - boundary.length;
828 |
829 | do {
830 | // Search the search_window
831 | for (int j = 0; j < new_bytes; j++) {
832 | for (int i = 0; i < boundary.length; i++) {
833 | if (search_window[j + i] != boundary[i])
834 | break;
835 | if (i == boundary.length - 1) {
836 | // Match found, add it to results
837 | int[] new_res = new int[res.length + 1];
838 | System.arraycopy(res, 0, new_res, 0, res.length);
839 | new_res[res.length] = search_window_pos + j;
840 | res = new_res;
841 | }
842 | }
843 | }
844 | search_window_pos += new_bytes;
845 |
846 | // Copy the end of the buffer to the start
847 | System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length);
848 |
849 | // Refill search_window
850 | new_bytes = search_window.length - boundary.length;
851 | new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes;
852 | b.get(search_window, boundary.length, new_bytes);
853 | } while (new_bytes > 0);
854 | return res;
855 | }
856 |
857 | @Override
858 | public CookieHandler getCookies() {
859 | return this.cookies;
860 | }
861 |
862 | @Override
863 | public final Map getHeaders() {
864 | return this.headers;
865 | }
866 |
867 | @Override
868 | public final InputStream getInputStream() {
869 | return this.inputStream;
870 | }
871 |
872 | @Override
873 | public final Method getMethod() {
874 | return this.method;
875 | }
876 |
877 | @Override
878 | public final Map getParms() {
879 | return this.parms;
880 | }
881 |
882 | @Override
883 | public String getQueryParameterString() {
884 | return this.queryParameterString;
885 | }
886 |
887 | private RandomAccessFile getTmpBucket() {
888 | try {
889 | TempFile tempFile = this.tempFileManager.createTempFile();
890 | return new RandomAccessFile(tempFile.getName(), "rw");
891 | } catch (Exception e) {
892 | throw new Error(e); // we won't recover, so throw an error
893 | }
894 | }
895 |
896 | @Override
897 | public final String getUri() {
898 | return this.uri;
899 | }
900 |
901 | @Override
902 | public void parseBody(Map files) throws IOException, ResponseException {
903 | final int REQUEST_BUFFER_LEN = 512;
904 | final int MEMORY_STORE_LIMIT = 1024;
905 | RandomAccessFile randomAccessFile = null;
906 | try {
907 | long size;
908 | if (this.headers.containsKey("content-length")) {
909 | size = Integer.parseInt(this.headers.get("content-length"));
910 | } else if (this.splitbyte < this.rlen) {
911 | size = this.rlen - this.splitbyte;
912 | } else {
913 | size = 0;
914 | }
915 |
916 | ByteArrayOutputStream baos = null;
917 | DataOutput request_data_output = null;
918 |
919 | // Store the request in memory or a file, depending on size
920 | if (size < MEMORY_STORE_LIMIT) {
921 | baos = new ByteArrayOutputStream();
922 | request_data_output = new DataOutputStream(baos);
923 | } else {
924 | randomAccessFile = getTmpBucket();
925 | request_data_output = randomAccessFile;
926 | }
927 |
928 | // Read all the body and write it to request_data_output
929 | byte[] buf = new byte[REQUEST_BUFFER_LEN];
930 | while (this.rlen >= 0 && size > 0) {
931 | this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN));
932 | size -= this.rlen;
933 | if (this.rlen > 0) {
934 | request_data_output.write(buf, 0, this.rlen);
935 | }
936 | }
937 |
938 | ByteBuffer fbuf = null;
939 | if (baos != null) {
940 | fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size());
941 | } else {
942 | fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
943 | randomAccessFile.seek(0);
944 | }
945 |
946 | // If the method is POST, there may be parameters
947 | // in data section, too, read it:
948 | if (Method.POST.equals(this.method)) {
949 | String contentType = "";
950 | String contentTypeHeader = this.headers.get("content-type");
951 |
952 | StringTokenizer st = null;
953 | if (contentTypeHeader != null) {
954 | st = new StringTokenizer(contentTypeHeader, ",; ");
955 | if (st.hasMoreTokens()) {
956 | contentType = st.nextToken();
957 | }
958 | }
959 |
960 | if ("multipart/form-data".equalsIgnoreCase(contentType)) {
961 | // Handle multipart/form-data
962 | if (!st.hasMoreTokens()) {
963 | throw new ResponseException(Response.Status.BAD_REQUEST,
964 | "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
965 | }
966 |
967 | String boundaryStartString = "boundary=";
968 | int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
969 | String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
970 | if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
971 | boundary = boundary.substring(1, boundary.length() - 1);
972 | }
973 |
974 | decodeMultipartFormData(boundary, fbuf, this.parms, files);
975 | } else {
976 | byte[] postBytes = new byte[fbuf.remaining()];
977 | fbuf.get(postBytes);
978 | String postLine = new String(postBytes).trim();
979 | // Handle application/x-www-form-urlencoded
980 | if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
981 | decodeParms(postLine, this.parms);
982 | } else if (postLine.length() != 0) {
983 | // Special case for raw POST data => create a
984 | // special files entry "postData" with raw content
985 | // data
986 | files.put("postData", postLine);
987 | }
988 | }
989 | } else if (Method.PUT.equals(this.method)) {
990 | files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
991 | }
992 | } finally {
993 | safeClose(randomAccessFile);
994 | }
995 | }
996 |
997 | /**
998 | * Retrieves the content of a sent file and saves it to a temporary
999 | * file. The full path to the saved file is returned.
1000 | */
1001 | private String saveTmpFile(ByteBuffer b, int offset, int len) {
1002 | String path = "";
1003 | if (len > 0) {
1004 | FileOutputStream fileOutputStream = null;
1005 | try {
1006 | TempFile tempFile = this.tempFileManager.createTempFile();
1007 | ByteBuffer src = b.duplicate();
1008 | fileOutputStream = new FileOutputStream(tempFile.getName());
1009 | FileChannel dest = fileOutputStream.getChannel();
1010 | src.position(offset).limit(offset + len);
1011 | dest.write(src.slice());
1012 | path = tempFile.getName();
1013 | } catch (Exception e) { // Catch exception if any
1014 | throw new Error(e); // we won't recover, so throw an error
1015 | } finally {
1016 | safeClose(fileOutputStream);
1017 | }
1018 | }
1019 | return path;
1020 | }
1021 | }
1022 |
1023 | /**
1024 | * Handles one session, i.e. parses the HTTP request and returns the
1025 | * response.
1026 | */
1027 | public interface IHTTPSession {
1028 |
1029 | void execute() throws IOException;
1030 |
1031 | CookieHandler getCookies();
1032 |
1033 | Map getHeaders();
1034 |
1035 | InputStream getInputStream();
1036 |
1037 | Method getMethod();
1038 |
1039 | Map getParms();
1040 |
1041 | String getQueryParameterString();
1042 |
1043 | /**
1044 | * @return the path part of the URL.
1045 | */
1046 | String getUri();
1047 |
1048 | /**
1049 | * Adds the files in the request body to the files map.
1050 | *
1051 | * @param files
1052 | * map to modify
1053 | */
1054 | void parseBody(Map files) throws IOException, ResponseException;
1055 | }
1056 |
1057 | /**
1058 | * HTTP Request methods, with the ability to decode a String
1059 | * back to its enum value.
1060 | */
1061 | public enum Method {
1062 | GET,
1063 | PUT,
1064 | POST,
1065 | DELETE,
1066 | HEAD,
1067 | OPTIONS,
1068 | TRACE,
1069 | CONNECT,
1070 | PATCH;
1071 |
1072 | static Method lookup(String method) {
1073 | for (Method m : Method.values()) {
1074 | if (m.toString().equalsIgnoreCase(method)) {
1075 | return m;
1076 | }
1077 | }
1078 | return null;
1079 | }
1080 | }
1081 |
1082 | /**
1083 | * HTTP response. Return one of these from serve().
1084 | */
1085 | public static class Response implements Closeable {
1086 |
1087 | public interface IStatus {
1088 |
1089 | String getDescription();
1090 |
1091 | int getRequestStatus();
1092 | }
1093 |
1094 | /**
1095 | * Some HTTP response status codes
1096 | */
1097 | public enum Status implements IStatus {
1098 | SWITCH_PROTOCOL(101, "Switching Protocols"),
1099 | OK(200, "OK"),
1100 | CREATED(201, "Created"),
1101 | ACCEPTED(202, "Accepted"),
1102 | NO_CONTENT(204, "No Content"),
1103 | PARTIAL_CONTENT(206, "Partial Content"),
1104 | REDIRECT(301, "Moved Permanently"),
1105 | NOT_MODIFIED(304, "Not Modified"),
1106 | BAD_REQUEST(400, "Bad Request"),
1107 | UNAUTHORIZED(401, "Unauthorized"),
1108 | FORBIDDEN(403, "Forbidden"),
1109 | NOT_FOUND(404, "Not Found"),
1110 | METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
1111 | REQUEST_TIMEOUT(408, "Request Timeout"),
1112 | RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
1113 | INTERNAL_ERROR(500, "Internal Server Error"),
1114 | UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported");
1115 |
1116 | private final int requestStatus;
1117 |
1118 | private final String description;
1119 |
1120 | Status(int requestStatus, String description) {
1121 | this.requestStatus = requestStatus;
1122 | this.description = description;
1123 | }
1124 |
1125 | @Override
1126 | public String getDescription() {
1127 | return "" + this.requestStatus + " " + this.description;
1128 | }
1129 |
1130 | @Override
1131 | public int getRequestStatus() {
1132 | return this.requestStatus;
1133 | }
1134 |
1135 | }
1136 |
1137 | /**
1138 | * Output stream that will automatically send every write to the wrapped
1139 | * OutputStream according to chunked transfer:
1140 | * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
1141 | */
1142 | private static class ChunkedOutputStream extends FilterOutputStream {
1143 |
1144 | public ChunkedOutputStream(OutputStream out) {
1145 | super(out);
1146 | }
1147 |
1148 | @Override
1149 | public void write(int b) throws IOException {
1150 | byte[] data = {
1151 | (byte) b
1152 | };
1153 | write(data, 0, 1);
1154 | }
1155 |
1156 | @Override
1157 | public void write(byte[] b) throws IOException {
1158 | write(b, 0, b.length);
1159 | }
1160 |
1161 | @Override
1162 | public void write(byte[] b, int off, int len) throws IOException {
1163 | if (len == 0)
1164 | return;
1165 | out.write(String.format("%x\r\n", len).getBytes());
1166 | out.write(b, off, len);
1167 | out.write("\r\n".getBytes());
1168 | }
1169 |
1170 | public void finish() throws IOException {
1171 | out.write("0\r\n\r\n".getBytes());
1172 | }
1173 |
1174 | }
1175 |
1176 | /**
1177 | * HTTP status code after processing, e.g. "200 OK", Status.OK
1178 | */
1179 | private IStatus status;
1180 |
1181 | /**
1182 | * MIME type of content, e.g. "text/html"
1183 | */
1184 | private String mimeType;
1185 |
1186 | /**
1187 | * Data of the response, may be null.
1188 | */
1189 | private InputStream data;
1190 |
1191 | private long contentLength;
1192 |
1193 | /**
1194 | * Headers for the HTTP response. Use addHeader() to add lines.
1195 | */
1196 | private final Map header = new HashMap();
1197 |
1198 | /**
1199 | * The request method that spawned this response.
1200 | */
1201 | private Method requestMethod;
1202 |
1203 | /**
1204 | * Use chunkedTransfer
1205 | */
1206 | private boolean chunkedTransfer;
1207 |
1208 | private boolean encodeAsGzip;
1209 |
1210 | private boolean keepAlive;
1211 |
1212 | /**
1213 | * Creates a fixed length response if totalBytes>=0, otherwise chunked.
1214 | */
1215 | protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) {
1216 | this.status = status;
1217 | this.mimeType = mimeType;
1218 | if (data == null) {
1219 | this.data = new ByteArrayInputStream(new byte[0]);
1220 | this.contentLength = 0L;
1221 | } else {
1222 | this.data = data;
1223 | this.contentLength = totalBytes;
1224 | }
1225 | this.chunkedTransfer = this.contentLength < 0;
1226 | keepAlive = true;
1227 | }
1228 |
1229 | @Override
1230 | public void close() throws IOException {
1231 | if (this.data != null) {
1232 | this.data.close();
1233 | }
1234 | }
1235 |
1236 | /**
1237 | * Adds given line to the header.
1238 | */
1239 | public void addHeader(String name, String value) {
1240 | this.header.put(name, value);
1241 | }
1242 |
1243 | public InputStream getData() {
1244 | return this.data;
1245 | }
1246 |
1247 | public String getHeader(String name) {
1248 | for (String headerName : header.keySet()) {
1249 | if (headerName.equalsIgnoreCase(name)) {
1250 | return header.get(headerName);
1251 | }
1252 | }
1253 | return null;
1254 | }
1255 |
1256 | public String getMimeType() {
1257 | return this.mimeType;
1258 | }
1259 |
1260 | public Method getRequestMethod() {
1261 | return this.requestMethod;
1262 | }
1263 |
1264 | public IStatus getStatus() {
1265 | return this.status;
1266 | }
1267 |
1268 | public void setGzipEncoding(boolean encodeAsGzip) {
1269 | this.encodeAsGzip = encodeAsGzip;
1270 | }
1271 |
1272 | public void setKeepAlive(boolean useKeepAlive) {
1273 | this.keepAlive = useKeepAlive;
1274 | }
1275 |
1276 | private boolean headerAlreadySent(Map header, String name) {
1277 | boolean alreadySent = false;
1278 | for (String headerName : header.keySet()) {
1279 | alreadySent |= headerName.equalsIgnoreCase(name);
1280 | }
1281 | return alreadySent;
1282 | }
1283 |
1284 | /**
1285 | * Sends given response to the socket.
1286 | */
1287 | protected void send(OutputStream outputStream) {
1288 | String mime = this.mimeType;
1289 | SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
1290 | gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
1291 |
1292 | try {
1293 | if (this.status == null) {
1294 | throw new Error("sendResponse(): Status can't be null.");
1295 | }
1296 | PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false);
1297 | pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n");
1298 |
1299 | if (mime != null) {
1300 | pw.print("Content-Type: " + mime + "\r\n");
1301 | }
1302 |
1303 | if (this.header == null || this.header.get("Date") == null) {
1304 | pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
1305 | }
1306 |
1307 | if (this.header != null) {
1308 | for (String key : this.header.keySet()) {
1309 | String value = this.header.get(key);
1310 | pw.print(key + ": " + value + "\r\n");
1311 | }
1312 | }
1313 |
1314 | if (!headerAlreadySent(header, "connection")) {
1315 | pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n");
1316 | }
1317 |
1318 | if (headerAlreadySent(this.header, "content-length")) {
1319 | encodeAsGzip = false;
1320 | }
1321 |
1322 | if (encodeAsGzip) {
1323 | pw.print("Content-Encoding: gzip\r\n");
1324 | setChunkedTransfer(true);
1325 | }
1326 |
1327 | long pending = this.data != null ? this.contentLength : 0;
1328 | if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
1329 | pw.print("Transfer-Encoding: chunked\r\n");
1330 | } else if (!encodeAsGzip) {
1331 | pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending);
1332 | }
1333 | pw.print("\r\n");
1334 | pw.flush();
1335 | sendBodyWithCorrectTransferAndEncoding(outputStream, pending);
1336 | outputStream.flush();
1337 | safeClose(this.data);
1338 | } catch (IOException ioe) {
1339 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
1340 | }
1341 | }
1342 |
1343 | private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {
1344 | if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
1345 | ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
1346 | sendBodyWithCorrectEncoding(chunkedOutputStream, -1);
1347 | chunkedOutputStream.finish();
1348 | } else {
1349 | sendBodyWithCorrectEncoding(outputStream, pending);
1350 | }
1351 | }
1352 |
1353 | private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException {
1354 | if (encodeAsGzip) {
1355 | GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
1356 | sendBody(gzipOutputStream, -1);
1357 | gzipOutputStream.finish();
1358 | } else {
1359 | sendBody(outputStream, pending);
1360 | }
1361 | }
1362 |
1363 | /**
1364 | * Sends the body to the specified OutputStream. The pending parameter
1365 | * limits the maximum amounts of bytes sent unless it is -1, in which
1366 | * case everything is sent.
1367 | *
1368 | * @param outputStream
1369 | * the OutputStream to send data to
1370 | * @param pending
1371 | * -1 to send everything, otherwise sets a max limit to the
1372 | * number of bytes sent
1373 | * @throws IOException
1374 | * if something goes wrong while sending the data.
1375 | */
1376 | private void sendBody(OutputStream outputStream, long pending) throws IOException {
1377 | long BUFFER_SIZE = 16 * 1024;
1378 | byte[] buff = new byte[(int) BUFFER_SIZE];
1379 | boolean sendEverything = pending == -1;
1380 | while (pending > 0 || sendEverything) {
1381 | long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE);
1382 | int read = this.data.read(buff, 0, (int) bytesToRead);
1383 | if (read <= 0) {
1384 | break;
1385 | }
1386 | outputStream.write(buff, 0, read);
1387 | if (!sendEverything) {
1388 | pending -= read;
1389 | }
1390 | }
1391 | }
1392 |
1393 | protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map header, long size) {
1394 | for (String headerName : header.keySet()) {
1395 | if (headerName.equalsIgnoreCase("content-length")) {
1396 | try {
1397 | return Long.parseLong(header.get(headerName));
1398 | } catch (NumberFormatException ex) {
1399 | return size;
1400 | }
1401 | }
1402 | }
1403 |
1404 | pw.print("Content-Length: " + size + "\r\n");
1405 | return size;
1406 | }
1407 |
1408 | public void setChunkedTransfer(boolean chunkedTransfer) {
1409 | this.chunkedTransfer = chunkedTransfer;
1410 | }
1411 |
1412 | public void setData(InputStream data) {
1413 | this.data = data;
1414 | }
1415 |
1416 | public void setMimeType(String mimeType) {
1417 | this.mimeType = mimeType;
1418 | }
1419 |
1420 | public void setRequestMethod(Method requestMethod) {
1421 | this.requestMethod = requestMethod;
1422 | }
1423 |
1424 | public void setStatus(IStatus status) {
1425 | this.status = status;
1426 | }
1427 | }
1428 |
1429 | public static final class ResponseException extends Exception {
1430 |
1431 | private static final long serialVersionUID = 6569838532917408380L;
1432 |
1433 | private final Response.Status status;
1434 |
1435 | public ResponseException(Response.Status status, String message) {
1436 | super(message);
1437 | this.status = status;
1438 | }
1439 |
1440 | public ResponseException(Response.Status status, String message, Exception e) {
1441 | super(message, e);
1442 | this.status = status;
1443 | }
1444 |
1445 | public Response.Status getStatus() {
1446 | return this.status;
1447 | }
1448 | }
1449 |
1450 | /**
1451 | * The runnable that will be used for the main listening thread.
1452 | */
1453 | public class ServerRunnable implements Runnable {
1454 |
1455 | private final int timeout;
1456 |
1457 | private IOException bindException;
1458 |
1459 | private boolean hasBinded = false;
1460 |
1461 | private ServerRunnable(int timeout) {
1462 | this.timeout = timeout;
1463 | }
1464 |
1465 | @Override
1466 | public void run() {
1467 | try {
1468 | myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
1469 | hasBinded = true;
1470 | } catch (IOException e) {
1471 | this.bindException = e;
1472 | return;
1473 | }
1474 | do {
1475 | try {
1476 | final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
1477 | if (this.timeout > 0) {
1478 | finalAccept.setSoTimeout(this.timeout);
1479 | }
1480 | final InputStream inputStream = finalAccept.getInputStream();
1481 | NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
1482 | } catch (IOException e) {
1483 | NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
1484 | }
1485 | } while (!NanoHTTPD.this.myServerSocket.isClosed());
1486 | }
1487 | }
1488 |
1489 | /**
1490 | * A temp file.
1491 | *
1492 | *
1493 | * Temp files are responsible for managing the actual temporary storage and
1494 | * cleaning themselves up when no longer needed.
1495 | *
1496 | */
1497 | public interface TempFile {
1498 |
1499 | void delete() throws Exception;
1500 |
1501 | String getName();
1502 |
1503 | OutputStream open() throws Exception;
1504 | }
1505 |
1506 | /**
1507 | * Temp file manager.
1508 | *
1509 | *
1510 | * Temp file managers are created 1-to-1 with incoming requests, to create
1511 | * and cleanup temporary files created as a result of handling the request.
1512 | *
1513 | */
1514 | public interface TempFileManager {
1515 |
1516 | void clear();
1517 |
1518 | TempFile createTempFile() throws Exception;
1519 | }
1520 |
1521 | /**
1522 | * Factory to create temp file managers.
1523 | */
1524 | public interface TempFileManagerFactory {
1525 |
1526 | TempFileManager create();
1527 | }
1528 |
1529 | /**
1530 | * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
1531 | * This is required as the Keep-Alive HTTP connections would otherwise block
1532 | * the socket reading thread forever (or as long the browser is open).
1533 | */
1534 | public static final int SOCKET_READ_TIMEOUT = 5000;
1535 |
1536 | /**
1537 | * Common MIME type for dynamic content: plain text
1538 | */
1539 | public static final String MIME_PLAINTEXT = "text/plain";
1540 |
1541 | /**
1542 | * Common MIME type for dynamic content: html
1543 | */
1544 | public static final String MIME_HTML = "text/html";
1545 |
1546 | /**
1547 | * Pseudo-Parameter to use to store the actual query string in the
1548 | * parameters map for later re-processing.
1549 | */
1550 | private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
1551 |
1552 | /**
1553 | * logger to log to.
1554 | */
1555 | private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
1556 |
1557 | /**
1558 | * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
1559 | * array of loaded KeyManagers. These objects must properly
1560 | * loaded/initialized by the caller.
1561 | */
1562 | public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException {
1563 | SSLServerSocketFactory res = null;
1564 | try {
1565 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
1566 | trustManagerFactory.init(loadedKeyStore);
1567 | SSLContext ctx = SSLContext.getInstance("TLS");
1568 | ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
1569 | res = ctx.getServerSocketFactory();
1570 | } catch (Exception e) {
1571 | throw new IOException(e.getMessage());
1572 | }
1573 | return res;
1574 | }
1575 |
1576 | /**
1577 | * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
1578 | * loaded KeyManagerFactory. These objects must properly loaded/initialized
1579 | * by the caller.
1580 | */
1581 | public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException {
1582 | SSLServerSocketFactory res = null;
1583 | try {
1584 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
1585 | trustManagerFactory.init(loadedKeyStore);
1586 | SSLContext ctx = SSLContext.getInstance("TLS");
1587 | ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
1588 | res = ctx.getServerSocketFactory();
1589 | } catch (Exception e) {
1590 | throw new IOException(e.getMessage());
1591 | }
1592 | return res;
1593 | }
1594 |
1595 | /**
1596 | * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
1597 | * certificate and passphrase
1598 | */
1599 | public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException {
1600 | SSLServerSocketFactory res = null;
1601 | try {
1602 | KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
1603 | InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath);
1604 | keystore.load(keystoreStream, passphrase);
1605 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
1606 | trustManagerFactory.init(keystore);
1607 | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
1608 | keyManagerFactory.init(keystore, passphrase);
1609 | SSLContext ctx = SSLContext.getInstance("TLS");
1610 | ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
1611 | res = ctx.getServerSocketFactory();
1612 | } catch (Exception e) {
1613 | throw new IOException(e.getMessage());
1614 | }
1615 | return res;
1616 | }
1617 |
1618 | private static final void safeClose(Object closeable) {
1619 | try {
1620 | if (closeable != null) {
1621 | if (closeable instanceof Closeable) {
1622 | ((Closeable) closeable).close();
1623 | } else if (closeable instanceof Socket) {
1624 | ((Socket) closeable).close();
1625 | } else if (closeable instanceof ServerSocket) {
1626 | ((ServerSocket) closeable).close();
1627 | } else {
1628 | throw new IllegalArgumentException("Unknown object to close");
1629 | }
1630 | }
1631 | } catch (IOException e) {
1632 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
1633 | }
1634 | }
1635 |
1636 | private final String hostname;
1637 |
1638 | private final int myPort;
1639 |
1640 | private ServerSocket myServerSocket;
1641 |
1642 | private SSLServerSocketFactory sslServerSocketFactory;
1643 |
1644 | private Thread myThread;
1645 |
1646 | /**
1647 | * Pluggable strategy for asynchronously executing requests.
1648 | */
1649 | protected AsyncRunner asyncRunner;
1650 |
1651 | /**
1652 | * Pluggable strategy for creating and cleaning up temporary files.
1653 | */
1654 | private TempFileManagerFactory tempFileManagerFactory;
1655 |
1656 | /**
1657 | * Constructs an HTTP server on given port.
1658 | */
1659 | public NanoHTTPD(int port) {
1660 | this(null, port);
1661 | }
1662 |
1663 | // -------------------------------------------------------------------------------
1664 | // //
1665 | //
1666 | // Threading Strategy.
1667 | //
1668 | // -------------------------------------------------------------------------------
1669 | // //
1670 |
1671 | /**
1672 | * Constructs an HTTP server on given hostname and port.
1673 | */
1674 | public NanoHTTPD(String hostname, int port) {
1675 | this.hostname = hostname;
1676 | this.myPort = port;
1677 | setTempFileManagerFactory(new DefaultTempFileManagerFactory());
1678 | setAsyncRunner(new DefaultAsyncRunner());
1679 | }
1680 |
1681 | /**
1682 | * Forcibly closes all connections that are open.
1683 | */
1684 | public synchronized void closeAllConnections() {
1685 | stop();
1686 | }
1687 |
1688 | /**
1689 | * create a instance of the client handler, subclasses can return a subclass
1690 | * of the ClientHandler.
1691 | *
1692 | * @param finalAccept
1693 | * the socket the cleint is connected to
1694 | * @param inputStream
1695 | * the input stream
1696 | * @return the client handler
1697 | */
1698 | protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) {
1699 | return new ClientHandler(inputStream, finalAccept);
1700 | }
1701 |
1702 | /**
1703 | * Instantiate the server runnable, can be overwritten by subclasses to
1704 | * provide a subclass of the ServerRunnable.
1705 | *
1706 | * @param timeout
1707 | * the socet timeout to use.
1708 | * @return the server runnable.
1709 | */
1710 | protected ServerRunnable createServerRunnable(final int timeout) {
1711 | return new ServerRunnable(timeout);
1712 | }
1713 |
1714 | /**
1715 | * Decode parameters from a URL, handing the case where a single parameter
1716 | * name might have been supplied several times, by return lists of values.
1717 | * In general these lists will contain a single element.
1718 | *
1719 | * @param parms
1720 | * original NanoHTTPD parameters values, as passed to the
1721 | * serve()
method.
1722 | * @return a map of String
(parameter name) to
1723 | * List<String>
(a list of the values supplied).
1724 | */
1725 | protected Map> decodeParameters(Map parms) {
1726 | return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER));
1727 | }
1728 |
1729 | // -------------------------------------------------------------------------------
1730 | // //
1731 |
1732 | /**
1733 | * Decode parameters from a URL, handing the case where a single parameter
1734 | * name might have been supplied several times, by return lists of values.
1735 | * In general these lists will contain a single element.
1736 | *
1737 | * @param queryString
1738 | * a query string pulled from the URL.
1739 | * @return a map of String
(parameter name) to
1740 | * List<String>
(a list of the values supplied).
1741 | */
1742 | protected Map> decodeParameters(String queryString) {
1743 | Map> parms = new HashMap>();
1744 | if (queryString != null) {
1745 | StringTokenizer st = new StringTokenizer(queryString, "&");
1746 | while (st.hasMoreTokens()) {
1747 | String e = st.nextToken();
1748 | int sep = e.indexOf('=');
1749 | String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
1750 | if (!parms.containsKey(propertyName)) {
1751 | parms.put(propertyName, new ArrayList());
1752 | }
1753 | String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null;
1754 | if (propertyValue != null) {
1755 | parms.get(propertyName).add(propertyValue);
1756 | }
1757 | }
1758 | }
1759 | return parms;
1760 | }
1761 |
1762 | /**
1763 | * Decode percent encoded String
values.
1764 | *
1765 | * @param str
1766 | * the percent encoded String
1767 | * @return expanded form of the input, for example "foo%20bar" becomes
1768 | * "foo bar"
1769 | */
1770 | protected String decodePercent(String str) {
1771 | String decoded = null;
1772 | try {
1773 | decoded = URLDecoder.decode(str, "UTF8");
1774 | } catch (UnsupportedEncodingException ignored) {
1775 | NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
1776 | }
1777 | return decoded;
1778 | }
1779 |
1780 | /**
1781 | * @return true if the gzip compression should be used if the client
1782 | * accespts it. Default this option is on for text content and off
1783 | * for everything else.
1784 | */
1785 | protected boolean useGzipWhenAccepted(Response r) {
1786 | return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/");
1787 | }
1788 |
1789 | public final int getListeningPort() {
1790 | return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort();
1791 | }
1792 |
1793 | public final boolean isAlive() {
1794 | return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive();
1795 | }
1796 |
1797 | /**
1798 | * Call before start() to serve over HTTPS instead of HTTP
1799 | */
1800 | public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) {
1801 | this.sslServerSocketFactory = sslServerSocketFactory;
1802 | }
1803 |
1804 | /**
1805 | * Create a response with unknown length (using HTTP 1.1 chunking).
1806 | */
1807 | public Response newChunkedResponse(IStatus status, String mimeType, InputStream data) {
1808 | return new Response(status, mimeType, data, -1);
1809 | }
1810 |
1811 | /**
1812 | * Create a response with known length.
1813 | */
1814 | public Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) {
1815 | return new Response(status, mimeType, data, totalBytes);
1816 | }
1817 |
1818 | /**
1819 | * Create a text response with known length.
1820 | */
1821 | public Response newFixedLengthResponse(IStatus status, String mimeType, String txt) {
1822 | if (txt == null) {
1823 | return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
1824 | } else {
1825 | byte[] bytes;
1826 | try {
1827 | bytes = txt.getBytes("UTF-8");
1828 | } catch (UnsupportedEncodingException e) {
1829 | NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
1830 | bytes = new byte[0];
1831 | }
1832 | return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length);
1833 | }
1834 | }
1835 |
1836 | /**
1837 | * Create a text response with known length.
1838 | */
1839 | public Response newFixedLengthResponse(String msg) {
1840 | return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg);
1841 | }
1842 |
1843 | /**
1844 | * Override this to customize the server.
1845 | *
1846 | *
1847 | * (By default, this returns a 404 "Not Found" plain text error response.)
1848 | *
1849 | * @param session
1850 | * The HTTP session
1851 | * @return HTTP response, see class Response for details
1852 | */
1853 | public Response serve(IHTTPSession session) {
1854 | Map files = new HashMap();
1855 | Method method = session.getMethod();
1856 | if (Method.PUT.equals(method) || Method.POST.equals(method)) {
1857 | try {
1858 | session.parseBody(files);
1859 | } catch (IOException ioe) {
1860 | return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
1861 | } catch (ResponseException re) {
1862 | return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
1863 | }
1864 | }
1865 |
1866 | Map parms = session.getParms();
1867 | parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
1868 | return serve(session.getUri(), method, session.getHeaders(), parms, files);
1869 | }
1870 |
1871 | /**
1872 | * Override this to customize the server.
1873 | *
1874 | *
1875 | * (By default, this returns a 404 "Not Found" plain text error response.)
1876 | *
1877 | * @param uri
1878 | * Percent-decoded URI without parameters, for example
1879 | * "/index.cgi"
1880 | * @param method
1881 | * "GET", "POST" etc.
1882 | * @param parms
1883 | * Parsed, percent decoded parameters from URI and, in case of
1884 | * POST, data.
1885 | * @param headers
1886 | * Header entries, percent decoded
1887 | * @return HTTP response, see class Response for details
1888 | */
1889 | @Deprecated
1890 | public Response serve(String uri, Method method, Map headers, Map parms, Map files) {
1891 | return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
1892 | }
1893 |
1894 | /**
1895 | * Pluggable strategy for asynchronously executing requests.
1896 | *
1897 | * @param asyncRunner
1898 | * new strategy for handling threads.
1899 | */
1900 | public void setAsyncRunner(AsyncRunner asyncRunner) {
1901 | this.asyncRunner = asyncRunner;
1902 | }
1903 |
1904 | /**
1905 | * Pluggable strategy for creating and cleaning up temporary files.
1906 | *
1907 | * @param tempFileManagerFactory
1908 | * new strategy for handling temp files.
1909 | */
1910 | public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
1911 | this.tempFileManagerFactory = tempFileManagerFactory;
1912 | }
1913 |
1914 | /**
1915 | * Start the server.
1916 | *
1917 | * @throws IOException
1918 | * if the socket is in use.
1919 | */
1920 | public void start() throws IOException {
1921 | start(NanoHTTPD.SOCKET_READ_TIMEOUT);
1922 | }
1923 |
1924 | /**
1925 | * Start the server.
1926 | *
1927 | * @param timeout
1928 | * timeout to use for socket connections.
1929 | * @throws IOException
1930 | * if the socket is in use.
1931 | */
1932 | public void start(final int timeout) throws IOException {
1933 | if (this.sslServerSocketFactory != null) {
1934 | SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket();
1935 | ss.setNeedClientAuth(false);
1936 | this.myServerSocket = ss;
1937 | } else {
1938 | this.myServerSocket = new ServerSocket();
1939 | }
1940 | this.myServerSocket.setReuseAddress(true);
1941 |
1942 | ServerRunnable serverRunnable = createServerRunnable(timeout);
1943 | this.myThread = new Thread(serverRunnable);
1944 | this.myThread.setDaemon(true);
1945 | this.myThread.setName("NanoHttpd Main Listener");
1946 | this.myThread.start();
1947 | while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
1948 | try {
1949 | Thread.sleep(10L);
1950 | } catch (Throwable e) {
1951 | // on android this may not be allowed, that's why we
1952 | // catch throwable the wait should be very short because we are
1953 | // just waiting for the bind of the socket
1954 | }
1955 | }
1956 | if (serverRunnable.bindException != null) {
1957 | throw serverRunnable.bindException;
1958 | }
1959 | }
1960 |
1961 | /**
1962 | * Stop the server.
1963 | */
1964 | public void stop() {
1965 | try {
1966 | safeClose(this.myServerSocket);
1967 | this.asyncRunner.closeAll();
1968 | if (this.myThread != null) {
1969 | this.myThread.join();
1970 | }
1971 | } catch (Exception e) {
1972 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
1973 | }
1974 | }
1975 |
1976 | public final boolean wasStarted() {
1977 | return this.myServerSocket != null && this.myThread != null;
1978 | }
1979 | }
1980 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AndroidHttpServer
3 |
4 | Hello world!
5 | Settings
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:1.2.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 10 15:27:10 PDT 2013
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------