From 1f7eea84143426d9e93f28533d3ee02d648c90ba Mon Sep 17 00:00:00 2001 From: DDSRem <73049927+DDSRem@users.noreply.github.com> Date: Sun, 5 Jan 2025 17:55:02 +0800 Subject: [PATCH] chore: xiaoya proxy migration warehouse --- .github/workflows/xiaoya_proxy.yml | 87 - xiaoya_proxy/.gitignore | 8 - xiaoya_proxy/Dockerfile | 20 - xiaoya_proxy/README.md | 14 - xiaoya_proxy/entrypoint.sh | 3 - xiaoya_proxy/pom.xml | 71 - .../java/com/ddsrem/xiaoya_proxy/Logger.java | 16 - .../com/ddsrem/xiaoya_proxy/MySSLCompat.java | 110 - .../com/ddsrem/xiaoya_proxy/NanoHTTPD.java | 2073 ----------------- .../xiaoya_proxy/XiaoyaProxyHandler.java | 509 ---- .../ddsrem/xiaoya_proxy/XiaoyaProxyRun.java | 28 - .../xiaoya_proxy/XiaoyaProxyServer.java | 51 - 12 files changed, 2990 deletions(-) delete mode 100644 .github/workflows/xiaoya_proxy.yml delete mode 100644 xiaoya_proxy/.gitignore delete mode 100644 xiaoya_proxy/Dockerfile delete mode 100644 xiaoya_proxy/README.md delete mode 100644 xiaoya_proxy/entrypoint.sh delete mode 100644 xiaoya_proxy/pom.xml delete mode 100644 xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/Logger.java delete mode 100644 xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/MySSLCompat.java delete mode 100644 xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/NanoHTTPD.java delete mode 100644 xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyHandler.java delete mode 100644 xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyRun.java delete mode 100644 xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyServer.java diff --git a/.github/workflows/xiaoya_proxy.yml b/.github/workflows/xiaoya_proxy.yml deleted file mode 100644 index 7083fd4c9e..0000000000 --- a/.github/workflows/xiaoya_proxy.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: xiaoya proxy - -on: - workflow_dispatch: - push: - branches: - - master - paths: - - "xiaoya_proxy/**" - - ".github/workflows/xiaoya_proxy.yml" - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@master - - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '8' - cache: 'maven' - cache-dependency-path: 'xiaoya_proxy/pom.xml' - - - name: Build xiaoya_proxy.jar - id: build_jar - run: | - cd xiaoya_proxy - version=$(cat pom.xml | grep -A1 xiaoya_proxy | grep version | perl -pe "s|.*((\d+\.?){3,}).*|\1|") - echo "XIAOYA_PROXY_VERSION=${version}" >> $GITHUB_ENV - mvn clean package - - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ secrets.DOCKER_USERNAME }}/xiaoya-proxy - tags: | - type=raw,value=latest - type=raw,value=${{ env.XIAOYA_PROXY_VERSION }} - - - - name: Set Up QEMU - uses: docker/setup-qemu-action@v3 - - - - name: Set Up Buildx - uses: docker/setup-buildx-action@v3 - - - - name: Login DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - - name: Build - uses: docker/build-push-action@v6 - with: - context: ./xiaoya_proxy - file: xiaoya_proxy/Dockerfile - platforms: | - linux/amd64 - linux/arm64/v8 - linux/arm/v7 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha, scope=${{ github.workflow }}_xiaoya_proxy - cache-to: type=gha, scope=${{ github.workflow }}_xiaoya_proxy - - - - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v4 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - repository: ${{ secrets.DOCKER_USERNAME }}/xiaoya-proxy - short-description: 小雅Alist的相关周边 - readme-filepath: ./xiaoya_proxy/README.md diff --git a/xiaoya_proxy/.gitignore b/xiaoya_proxy/.gitignore deleted file mode 100644 index de97d28b6e..0000000000 --- a/xiaoya_proxy/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# IntelliJ Idea & VSCode -/.idea -/*.iml -/.vscode - -# Maven -/target -dependency-reduced-pom.xml \ No newline at end of file diff --git a/xiaoya_proxy/Dockerfile b/xiaoya_proxy/Dockerfile deleted file mode 100644 index ebbd4ab9f5..0000000000 --- a/xiaoya_proxy/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM alpine:3.21 - -ENV LANG=zh_CN.UTF-8 \ - TZ=Asia/Shanghai \ - DEBUG=true \ - PS1="\[\e[32m\][\[\e[m\]\[\e[36m\]\u \[\e[m\]\[\e[37m\]@ \[\e[m\]\[\e[34m\]\h\[\e[m\]\[\e[32m\]]\[\e[m\] \[\e[37;35m\]in\[\e[m\] \[\e[33m\]\w\[\e[m\] \[\e[32m\][\[\e[m\]\[\e[37m\]\d\[\e[m\] \[\e[m\]\[\e[37m\]\t\[\e[m\]\[\e[32m\]]\[\e[m\] \n\[\e[1;31m\]$ \[\e[0m\]" - -RUN apk add --no-cache \ - openjdk8-jre \ - dumb-init \ - tzdata \ - bash && \ - rm -rf /var/cache/apk/* /tmp/* - -COPY --chmod=755 entrypoint.sh /entrypoint.sh -COPY --chmod=755 target/xiaoya_proxy-*.jar /xiaoya_proxy.jar - -EXPOSE 9988 - -ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/xiaoya_proxy/README.md b/xiaoya_proxy/README.md deleted file mode 100644 index 5a7460d668..0000000000 --- a/xiaoya_proxy/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Xiaoya Proxy - -小雅容器代理工具,确保 UA 统一。 - -## Run - -```shell -docker run -d \ - --name=xiaoya-proxy \ - --restart=always \ - --net=host \ - -e TZ=Asia/Shanghai \ - ddsderek/xiaoya-proxy:latest -``` diff --git a/xiaoya_proxy/entrypoint.sh b/xiaoya_proxy/entrypoint.sh deleted file mode 100644 index 9f4593c601..0000000000 --- a/xiaoya_proxy/entrypoint.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -exec dumb-init java -jar /xiaoya_proxy.jar diff --git a/xiaoya_proxy/pom.xml b/xiaoya_proxy/pom.xml deleted file mode 100644 index aa52b69b00..0000000000 --- a/xiaoya_proxy/pom.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - 4.0.0 - com.ddsrem - xiaoya_proxy - 1.0.0 - jar - Xiaoya Proxy - Xiaoya Alist Proxy - - - - com.squareup.okhttp3 - okhttp - 3.12.13 - - - org.json - json - 20210307 - - - - UTF-8 - UTF-8 - 1.8 - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - package - - shade - - - - - *:* - - META-INF/MANIFEST.MF - - - - - - com.ddsrem.xiaoya_proxy.XiaoyaProxyRun - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - 1.8 - 1.8 - - - - - diff --git a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/Logger.java b/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/Logger.java deleted file mode 100644 index 812dde37fa..0000000000 --- a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/Logger.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ddsrem.xiaoya_proxy; - -public class Logger { - static boolean dbg = "true".equals(System.getenv("DEBUG")); - - public static void log(String message, boolean force) { - if(!dbg && !force){ - return; - } - System.out.println(message); - } - - public static void log(String message) { - Logger.log(message, false); - } -} diff --git a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/MySSLCompat.java b/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/MySSLCompat.java deleted file mode 100644 index 6479f36935..0000000000 --- a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/MySSLCompat.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.ddsrem.xiaoya_proxy; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -public class MySSLCompat extends SSLSocketFactory { - - private SSLSocketFactory factory; - private String[] cipherSuites; - private String[] protocols; - - public MySSLCompat() { - try { - List list = new LinkedList<>(); - SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket(); - for (String protocol : socket.getSupportedProtocols()) if (!protocol.toUpperCase().contains("SSL")) list.add(protocol); - protocols = list.toArray(new String[0]); - List allowedCiphers = Arrays.asList("TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECHDE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"); - List availableCiphers = Arrays.asList(socket.getSupportedCipherSuites()); - HashSet preferredCiphers = new HashSet<>(allowedCiphers); - preferredCiphers.retainAll(availableCiphers); - preferredCiphers.addAll(new HashSet<>(Arrays.asList(socket.getEnabledCipherSuites()))); - cipherSuites = preferredCiphers.toArray(new String[0]); - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, new X509TrustManager[]{TM}, null); - HttpsURLConnection.setDefaultSSLSocketFactory(factory = context.getSocketFactory()); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Override - public String[] getDefaultCipherSuites() { - return cipherSuites; - } - - @Override - public String[] getSupportedCipherSuites() { - return cipherSuites; - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { - Socket ssl = factory.createSocket(s, host, port, autoClose); - if (ssl instanceof SSLSocket) upgradeTLS((SSLSocket) ssl); - return ssl; - } - - @Override - public Socket createSocket(String host, int port) throws IOException { - Socket ssl = factory.createSocket(host, port); - if (ssl instanceof SSLSocket) upgradeTLS((SSLSocket) ssl); - return ssl; - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { - Socket ssl = factory.createSocket(host, port, localHost, localPort); - if (ssl instanceof SSLSocket) upgradeTLS((SSLSocket) ssl); - return ssl; - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - Socket ssl = factory.createSocket(host, port); - if (ssl instanceof SSLSocket) upgradeTLS((SSLSocket) ssl); - return ssl; - } - - @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - Socket ssl = factory.createSocket(address, port, localAddress, localPort); - if (ssl instanceof SSLSocket) upgradeTLS((SSLSocket) ssl); - return ssl; - } - - private void upgradeTLS(SSLSocket ssl) { - if (protocols != null) ssl.setEnabledProtocols(protocols); - if (cipherSuites != null) ssl.setEnabledCipherSuites(cipherSuites); - } - - //@SuppressLint({"TrustAllX509TrustManager", "CustomX509TrustManager"}) - public static final X509TrustManager TM = new X509TrustManager() { - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) { - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) { - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[]{}; - } - }; -} diff --git a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/NanoHTTPD.java b/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/NanoHTTPD.java deleted file mode 100644 index 3166c5d94a..0000000000 --- a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/NanoHTTPD.java +++ /dev/null @@ -1,2073 +0,0 @@ -package com.ddsrem.xiaoya_proxy; - -/* - * #%L - * NanoHttpd-Core - * %% - * Copyright (C) 2012 - 2015 nanohttpd - * %% - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. Neither the name of the nanohttpd nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE - * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED - * OF THE POSSIBILITY OF SUCH DAMAGE. - * #L% - */ - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.DataOutput; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.RandomAccessFile; -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.Charset; -import java.security.KeyStore; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.StringTokenizer; -import java.util.TimeZone; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.GZIPOutputStream; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.SSLServerSocketFactory; -import javax.net.ssl.TrustManagerFactory; - -import com.ddsrem.xiaoya_proxy.NanoHTTPD.Response.IStatus; -import com.ddsrem.xiaoya_proxy.NanoHTTPD.Response.Status; - -/** - * A simple, tiny, nicely embeddable HTTP server in Java - *

- *

- * NanoHTTPD - *

- * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, - * 2010 by Konstantinos Togias - *

- *

- *

- * Features + limitations: - *

    - *

    - *

  • Only one Java file
  • - *
  • Java 5 compatible
  • - *
  • Released as open source, Modified BSD licence
  • - *
  • No fixed config files, logging, authorization etc. (Implement yourself if - * you need them.)
  • - *
  • Supports parameter parsing of GET and POST methods (+ rudimentary PUT - * support in 1.25)
  • - *
  • Supports both dynamic content and file serving
  • - *
  • Supports file upload (since version 1.2, 2010)
  • - *
  • Supports partial content (streaming)
  • - *
  • Supports ETags
  • - *
  • Never caches anything
  • - *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • - *
  • Default code serves files and shows all HTTP parameters and headers
  • - *
  • File server supports directory listing, index.html and index.htm
  • - *
  • File server supports partial content (streaming)
  • - *
  • File server supports ETags
  • - *
  • File server does the 301 redirection trick for directories without '/'
  • - *
  • File server supports simple skipping for files (continue download)
  • - *
  • File server serves also very long files without memory overhead
  • - *
  • Contains a built-in list of most common MIME types
  • - *
  • All header names are converted to lower case so they don't vary between - * browsers/clients
  • - *

    - *

- *

- *

- * How to use: - *

    - *

    - *

  • Subclass and implement serve() and embed to your own program
  • - *

    - *

- *

- * See the separate "LICENSE.md" file for the distribution license (Modified BSD - * licence) - */ -public abstract class NanoHTTPD { - - /** - * Pluggable strategy for asynchronously executing requests. - */ - public interface AsyncRunner { - - void closeAll(); - - void closed(ClientHandler clientHandler); - - void exec(ClientHandler code); - } - - /** - * The runnable that will be used for every new client connection. - */ - public class ClientHandler implements Runnable { - - private final InputStream inputStream; - - private final Socket acceptSocket; - - private ClientHandler(InputStream inputStream, Socket acceptSocket) { - this.inputStream = inputStream; - this.acceptSocket = acceptSocket; - } - - public void close() { - safeClose(this.inputStream); - safeClose(this.acceptSocket); - } - - @Override - public void run() { - OutputStream outputStream = null; - try { - outputStream = this.acceptSocket.getOutputStream(); - TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); - HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress()); - while (!this.acceptSocket.isClosed()) { - session.execute(); - } - } catch (Exception e) { - // When the socket is closed by the client, - // we throw our own SocketException - // to break the "keep alive" loop above. If - // the exception was anything other - // than the expected SocketException OR a - // SocketTimeoutException, print the - // stacktrace - if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) { - NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); - } - } finally { - safeClose(outputStream); - safeClose(this.inputStream); - safeClose(this.acceptSocket); - NanoHTTPD.this.asyncRunner.closed(this); - } - } - } - - public static class Cookie { - - public static String getHTTPTime(int days) { - Calendar calendar = Calendar.getInstance(); - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - calendar.add(Calendar.DAY_OF_MONTH, days); - return dateFormat.format(calendar.getTime()); - } - - private final String n, v, e; - - public Cookie(String name, String value) { - this(name, value, 30); - } - - public Cookie(String name, String value, int numDays) { - this.n = name; - this.v = value; - this.e = getHTTPTime(numDays); - } - - public Cookie(String name, String value, String expires) { - this.n = name; - this.v = value; - this.e = expires; - } - - public String getHTTPHeader() { - String fmt = "%s=%s; expires=%s"; - return String.format(fmt, this.n, this.v, this.e); - } - } - - /** - * Provides rudimentary support for cookies. Doesn't support 'path', - * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported - * features. - * - * @author LordFokas - */ - public class CookieHandler implements Iterable { - - private final HashMap cookies = new HashMap(); - - private final ArrayList queue = new ArrayList(); - - public CookieHandler(Map httpHeaders) { - String raw = httpHeaders.get("cookie"); - if (raw != null) { - String[] tokens = raw.split(";"); - for (String token : tokens) { - String[] data = token.trim().split("="); - if (data.length == 2) { - this.cookies.put(data[0], data[1]); - } - } - } - } - - /** - * Set a cookie with an expiration date from a month ago, effectively - * deleting it on the client side. - * - * @param name - * The cookie name. - */ - public void delete(String name) { - set(name, "-delete-", -30); - } - - @Override - public Iterator iterator() { - return this.cookies.keySet().iterator(); - } - - /** - * Read a cookie from the HTTP Headers. - * - * @param name - * The cookie's name. - * @return The cookie's value if it exists, null otherwise. - */ - public String read(String name) { - return this.cookies.get(name); - } - - public void set(Cookie cookie) { - this.queue.add(cookie); - } - - /** - * Sets a cookie. - * - * @param name - * The cookie's name. - * @param value - * The cookie's value. - * @param expires - * How many days until the cookie expires. - */ - public void set(String name, String value, int expires) { - this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); - } - - /** - * Internally used by the webserver to add all queued cookies into the - * Response's HTTP Headers. - * - * @param response - * The Response object to which headers the queued cookies - * will be added. - */ - public void unloadQueue(Response response) { - for (Cookie cookie : this.queue) { - response.addHeader("Set-Cookie", cookie.getHTTPHeader()); - } - } - } - - /** - * Default threading strategy for NanoHTTPD. - *

- *

- * By default, the server spawns a new Thread for every incoming request. - * These are set to daemon status, and named according to the request - * number. The name is useful when profiling the application. - *

- */ - public static class DefaultAsyncRunner implements AsyncRunner { - - private long requestCount; - - private final List running = Collections.synchronizedList(new ArrayList()); - - /** - * @return a list with currently running clients. - */ - public List getRunning() { - return running; - } - - @Override - public void closeAll() { - // copy of the list for concurrency - for (ClientHandler clientHandler : new ArrayList(this.running)) { - clientHandler.close(); - } - } - - @Override - public void closed(ClientHandler clientHandler) { - this.running.remove(clientHandler); - } - - @Override - public void exec(ClientHandler clientHandler) { - ++this.requestCount; - Thread t = new Thread(clientHandler); - t.setDaemon(true); - t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")"); - this.running.add(clientHandler); - t.start(); - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - *

- *

- * By default, files are created by File.createTempFile() in - * the directory specified. - *

- */ - public static class DefaultTempFile implements TempFile { - - private final File file; - - private final OutputStream fstream; - - public DefaultTempFile(String tempdir) throws IOException { - this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); - this.fstream = new FileOutputStream(this.file); - } - - @Override - public void delete() throws Exception { - safeClose(this.fstream); - if (!this.file.delete()) { - throw new Exception("could not delete temporary file"); - } - } - - @Override - public String getName() { - return this.file.getAbsolutePath(); - } - - @Override - public OutputStream open() throws Exception { - return this.fstream; - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - *

- *

- * This class stores its files in the standard location (that is, wherever - * java.io.tmpdir points to). Files are added to an internal - * list, and deleted when no longer needed (that is, when - * clear() is invoked at the end of processing a request). - *

- */ - public static class DefaultTempFileManager implements TempFileManager { - - private final String tmpdir; - - private final List tempFiles; - - public DefaultTempFileManager() { - this.tmpdir = System.getProperty("java.io.tmpdir"); - this.tempFiles = new ArrayList(); - } - - @Override - public void clear() { - for (TempFile file : this.tempFiles) { - try { - file.delete(); - } catch (Exception ignored) { - NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); - } - } - this.tempFiles.clear(); - } - - @Override - public TempFile createTempFile(String filename_hint) throws Exception { - DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); - this.tempFiles.add(tempFile); - return tempFile; - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - */ - private class DefaultTempFileManagerFactory implements TempFileManagerFactory { - - @Override - public TempFileManager create() { - return new DefaultTempFileManager(); - } - } - - private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; - - private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); - - private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; - - private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); - - private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; - - private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); - - protected class HTTPSession implements IHTTPSession { - - private static final int REQUEST_BUFFER_LEN = 512; - - private static final int MEMORY_STORE_LIMIT = 1024; - - public static final int BUFSIZE = 8192; - - private final TempFileManager tempFileManager; - - private final OutputStream outputStream; - - private final BufferedInputStream inputStream; - - private int splitbyte; - - private int rlen; - - private String uri; - - private Method method; - - private Map parms; - - private Map headers; - - private CookieHandler cookies; - - private String queryParameterString; - - private String remoteIp; - - private String protocolVersion; - - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { - this.tempFileManager = tempFileManager; - this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); - this.outputStream = outputStream; - } - - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { - this.tempFileManager = tempFileManager; - this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); - this.outputStream = outputStream; - this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); - this.headers = new HashMap(); - } - - /** - * Decodes the sent headers and loads the data into Key/value pairs - */ - private void decodeHeader(BufferedReader in, Map pre, Map parms, Map headers) throws ResponseException { - try { - // Read the request line - String inLine = in.readLine(); - if (inLine == null) { - return; - } - - StringTokenizer st = new StringTokenizer(inLine); - if (!st.hasMoreTokens()) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); - } - - pre.put("method", st.nextToken()); - - if (!st.hasMoreTokens()) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); - } - - String uri = st.nextToken(); - - // Decode parameters from the URI - int qmi = uri.indexOf('?'); - if (qmi >= 0) { - decodeParms(uri.substring(qmi + 1), parms); - uri = decodePercent(uri.substring(0, qmi)); - } else { - uri = decodePercent(uri); - } - - // If there's another token, its protocol version, - // followed by HTTP headers. - // NOTE: this now forces header names lower case since they are - // case insensitive and vary by client. - if (st.hasMoreTokens()) { - protocolVersion = st.nextToken(); - } else { - protocolVersion = "HTTP/1.1"; - NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1."); - } - String line = in.readLine(); - while (line != null && line.trim().length() > 0) { - int p = line.indexOf(':'); - if (p >= 0) { - headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); - } - line = in.readLine(); - } - - pre.put("uri", uri); - } catch (IOException ioe) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); - } - } - - /** - * Decodes the Multipart Body data and put it into Key/Value pairs. - */ - private void decodeMultipartFormData(String boundary, ByteBuffer fbuf, Map parms, Map files) throws ResponseException { - try { - int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes()); - if (boundary_idxs.length < 2) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); - } - - final int MAX_HEADER_SIZE = 1024; - byte[] part_header_buff = new byte[MAX_HEADER_SIZE]; - for (int bi = 0; bi < boundary_idxs.length - 1; bi++) { - fbuf.position(boundary_idxs[bi]); - int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; - fbuf.get(part_header_buff, 0, len); - ByteArrayInputStream bais = new ByteArrayInputStream(part_header_buff, 0, len); - BufferedReader in = new BufferedReader(new InputStreamReader(bais, Charset.forName("US-ASCII"))); - - // First line is boundary string - String mpline = in.readLine(); - if (!mpline.contains(boundary)) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); - } - - String part_name = null, file_name = null, content_type = null; - // Parse the reset of the header lines - mpline = in.readLine(); - while (mpline != null && mpline.trim().length() > 0) { - Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); - if (matcher.matches()) { - String attributeString = matcher.group(2); - matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); - while (matcher.find()) { - String key = matcher.group(1); - if (key.equalsIgnoreCase("name")) { - part_name = matcher.group(2); - } else if (key.equalsIgnoreCase("filename")) { - file_name = matcher.group(2); - } - } - } - matcher = CONTENT_TYPE_PATTERN.matcher(mpline); - if (matcher.matches()) { - content_type = matcher.group(2).trim(); - } - mpline = in.readLine(); - } - - // Read the part data - int part_header_len = len - (int) in.skip(MAX_HEADER_SIZE); - if (part_header_len >= len - 4) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); - } - int part_data_start = boundary_idxs[bi] + part_header_len; - int part_data_end = boundary_idxs[bi + 1] - 4; - - fbuf.position(part_data_start); - if (content_type == null) { - // Read the part into a string - byte[] data_bytes = new byte[part_data_end - part_data_start]; - fbuf.get(data_bytes); - parms.put(part_name, new String(data_bytes)); - } else { - // Read it into a file - String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start, file_name); - if (!files.containsKey(part_name)) { - files.put(part_name, path); - } else { - int count = 2; - while (files.containsKey(part_name + count)) { - count++; - } - files.put(part_name + count, path); - } - parms.put(part_name, file_name); - } - } - } catch (ResponseException re) { - throw re; - } catch (Exception e) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); - } - } - - /** - * Decodes parameters in percent-encoded URI-format ( e.g. - * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given - * Map. NOTE: this doesn't support multiple identical keys due to the - * simplicity of Map. - */ - private void decodeParms(String parms, Map p) { - if (parms == null) { - this.queryParameterString = ""; - return; - } - - this.queryParameterString = parms; - StringTokenizer st = new StringTokenizer(parms, "&"); - while (st.hasMoreTokens()) { - String e = st.nextToken(); - int sep = e.indexOf('='); - if (sep >= 0) { - p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1))); - } else { - p.put(decodePercent(e).trim(), ""); - } - } - } - - @Override - public void execute() throws IOException { - Response r = null; - try { - // Read the first 8192 bytes. - // The full header should fit in here. - // Apache's default header limit is 8KB. - // Do NOT assume that a single read will get the entire header - // at once! - byte[] buf = new byte[HTTPSession.BUFSIZE]; - this.splitbyte = 0; - this.rlen = 0; - - int read = -1; - this.inputStream.mark(HTTPSession.BUFSIZE); - try { - read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); - } catch (Exception e) { - safeClose(this.inputStream); - safeClose(this.outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - if (read == -1) { - // socket was been closed - safeClose(this.inputStream); - safeClose(this.outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - while (read > 0) { - this.rlen += read; - this.splitbyte = findHeaderEnd(buf, this.rlen); - if (this.splitbyte > 0) { - break; - } - read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen); - } - - if (this.splitbyte < this.rlen) { - this.inputStream.reset(); - this.inputStream.skip(this.splitbyte); - } - - this.parms = new HashMap(); - if (null == this.headers) { - this.headers = new HashMap(); - } else { - this.headers.clear(); - } - - if (null != this.remoteIp) { - this.headers.put("remote-addr", this.remoteIp); - this.headers.put("http-client-ip", this.remoteIp); - } - - // Create a BufferedReader for parsing the header. - BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen))); - - // Decode the header into parms and header java properties - Map pre = new HashMap(); - decodeHeader(hin, pre, this.parms, this.headers); - - this.method = Method.lookup(pre.get("method")); - if (this.method == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); - } - - this.uri = pre.get("uri"); - - this.cookies = new CookieHandler(this.headers); - - String connection = this.headers.get("connection"); - boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*")); - - // Ok, now do the serve() - - // TODO: long body_size = getBodySize(); - // TODO: long pos_before_serve = this.inputStream.totalRead() - // (requires implementaion for totalRead()) - r = serve(this); - // TODO: this.inputStream.skip(body_size - - // (this.inputStream.totalRead() - pos_before_serve)) - - if (r == null) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); - } else { - String acceptEncoding = this.headers.get("accept-encoding"); - this.cookies.unloadQueue(r); - r.setRequestMethod(this.method); - r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); - r.setKeepAlive(keepAlive); - r.send(this.outputStream); - } - if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) { - throw new SocketException("NanoHttpd Shutdown"); - } - } catch (SocketException e) { - // throw it out to close socket object (finalAccept) - throw e; - } catch (SocketTimeoutException ste) { - // treat socket timeouts the same way we treat socket exceptions - // i.e. close the stream & finalAccept object by throwing the - // exception up the call stack. - throw ste; - } catch (IOException ioe) { - Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); - } catch (ResponseException re) { - Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); - } finally { - safeClose(r); - this.tempFileManager.clear(); - } - } - - /** - * Find byte index separating header from body. It must be the last byte - * of the first two sequential new lines. - */ - private int findHeaderEnd(final byte[] buf, int rlen) { - int splitbyte = 0; - while (splitbyte + 3 < rlen) { - if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { - return splitbyte + 4; - } - splitbyte++; - } - return 0; - } - - /** - * Find the byte positions where multipart boundaries start. This reads - * a large block at a time and uses a temporary buffer to optimize - * (memory mapped) file access. - */ - private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { - int[] res = new int[0]; - if (b.remaining() < boundary.length) { - return res; - } - - int search_window_pos = 0; - byte[] search_window = new byte[4 * 1024 + boundary.length]; - - int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; - b.get(search_window, 0, first_fill); - int new_bytes = first_fill - boundary.length; - - do { - // Search the search_window - for (int j = 0; j < new_bytes; j++) { - for (int i = 0; i < boundary.length; i++) { - if (search_window[j + i] != boundary[i]) - break; - if (i == boundary.length - 1) { - // Match found, add it to results - int[] new_res = new int[res.length + 1]; - System.arraycopy(res, 0, new_res, 0, res.length); - new_res[res.length] = search_window_pos + j; - res = new_res; - } - } - } - search_window_pos += new_bytes; - - // Copy the end of the buffer to the start - System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); - - // Refill search_window - new_bytes = search_window.length - boundary.length; - new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; - b.get(search_window, boundary.length, new_bytes); - } while (new_bytes > 0); - return res; - } - - @Override - public CookieHandler getCookies() { - return this.cookies; - } - - @Override - public final Map getHeaders() { - return this.headers; - } - - @Override - public final InputStream getInputStream() { - return this.inputStream; - } - - @Override - public final Method getMethod() { - return this.method; - } - - @Override - public final Map getParms() { - return this.parms; - } - - @Override - public String getQueryParameterString() { - return this.queryParameterString; - } - - private RandomAccessFile getTmpBucket() { - try { - TempFile tempFile = this.tempFileManager.createTempFile(null); - return new RandomAccessFile(tempFile.getName(), "rw"); - } catch (Exception e) { - throw new Error(e); // we won't recover, so throw an error - } - } - - @Override - public final String getUri() { - return this.uri; - } - - /** - * Deduce body length in bytes. Either from "content-length" header or - * read bytes. - */ - public long getBodySize() { - if (this.headers.containsKey("content-length")) { - return Integer.parseInt(this.headers.get("content-length")); - } else if (this.splitbyte < this.rlen) { - return this.rlen - this.splitbyte; - } - return 0; - } - - @Override - public void parseBody(Map files) throws IOException, ResponseException { - RandomAccessFile randomAccessFile = null; - try { - long size = getBodySize(); - ByteArrayOutputStream baos = null; - DataOutput request_data_output = null; - - // Store the request in memory or a file, depending on size - if (size < MEMORY_STORE_LIMIT) { - baos = new ByteArrayOutputStream(); - request_data_output = new DataOutputStream(baos); - } else { - randomAccessFile = getTmpBucket(); - request_data_output = randomAccessFile; - } - - // Read all the body and write it to request_data_output - byte[] buf = new byte[REQUEST_BUFFER_LEN]; - while (this.rlen >= 0 && size > 0) { - this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); - size -= this.rlen; - if (this.rlen > 0) { - request_data_output.write(buf, 0, this.rlen); - } - } - - ByteBuffer fbuf = null; - if (baos != null) { - fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); - } else { - fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); - randomAccessFile.seek(0); - } - - // If the method is POST, there may be parameters - // in data section, too, read it: - if (Method.POST.equals(this.method)) { - String contentType = ""; - String contentTypeHeader = this.headers.get("content-type"); - - StringTokenizer st = null; - if (contentTypeHeader != null) { - st = new StringTokenizer(contentTypeHeader, ",; "); - if (st.hasMoreTokens()) { - contentType = st.nextToken(); - } - } - - if ("multipart/form-data".equalsIgnoreCase(contentType)) { - // Handle multipart/form-data - if (!st.hasMoreTokens()) { - throw new ResponseException(Response.Status.BAD_REQUEST, - "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); - } - - String boundaryStartString = "boundary="; - int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); - String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); - if (boundary.startsWith("\"") && boundary.endsWith("\"")) { - boundary = boundary.substring(1, boundary.length() - 1); - } - - decodeMultipartFormData(boundary, fbuf, this.parms, files); - } else { - byte[] postBytes = new byte[fbuf.remaining()]; - fbuf.get(postBytes); - String postLine = new String(postBytes).trim(); - // Handle application/x-www-form-urlencoded - if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { - decodeParms(postLine, this.parms); - } else if (postLine.length() != 0) { - // Special case for raw POST data => create a - // special files entry "postData" with raw content - // data - files.put("postData", postLine); - } - } - } else if (Method.PUT.equals(this.method)) { - files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); - } - } finally { - safeClose(randomAccessFile); - } - } - - /** - * Retrieves the content of a sent file and saves it to a temporary - * file. The full path to the saved file is returned. - */ - private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { - String path = ""; - if (len > 0) { - FileOutputStream fileOutputStream = null; - try { - TempFile tempFile = this.tempFileManager.createTempFile(filename_hint); - ByteBuffer src = b.duplicate(); - fileOutputStream = new FileOutputStream(tempFile.getName()); - FileChannel dest = fileOutputStream.getChannel(); - src.position(offset).limit(offset + len); - dest.write(src.slice()); - path = tempFile.getName(); - } catch (Exception e) { // Catch exception if any - throw new Error(e); // we won't recover, so throw an error - } finally { - safeClose(fileOutputStream); - } - } - return path; - } - } - - /** - * Handles one session, i.e. parses the HTTP request and returns the - * response. - */ - public interface IHTTPSession { - - void execute() throws IOException; - - CookieHandler getCookies(); - - Map getHeaders(); - - InputStream getInputStream(); - - Method getMethod(); - - Map getParms(); - - String getQueryParameterString(); - - /** - * @return the path part of the URL. - */ - String getUri(); - - /** - * Adds the files in the request body to the files map. - * - * @param files - * map to modify - */ - void parseBody(Map files) throws IOException, ResponseException; - } - - /** - * HTTP Request methods, with the ability to decode a String - * back to its enum value. - */ - public enum Method { - GET, - PUT, - POST, - DELETE, - HEAD, - OPTIONS, - TRACE, - CONNECT, - PATCH; - - static Method lookup(String method) { - for (Method m : Method.values()) { - if (m.toString().equalsIgnoreCase(method)) { - return m; - } - } - return null; - } - } - - /** - * HTTP response. Return one of these from serve(). - */ - public static class Response implements Closeable { - - public interface IStatus { - - String getDescription(); - - int getRequestStatus(); - } - - /** - * Some HTTP response status codes - */ - public enum Status implements IStatus { - SWITCH_PROTOCOL(101, "Switching Protocols"), - OK(200, "OK"), - CREATED(201, "Created"), - ACCEPTED(202, "Accepted"), - NO_CONTENT(204, "No Content"), - PARTIAL_CONTENT(206, "Partial Content"), - REDIRECT(301, "Moved Permanently"), - NOT_MODIFIED(304, "Not Modified"), - BAD_REQUEST(400, "Bad Request"), - UNAUTHORIZED(401, "Unauthorized"), - FORBIDDEN(403, "Forbidden"), - NOT_FOUND(404, "Not Found"), - METHOD_NOT_ALLOWED(405, "Method Not Allowed"), - NOT_ACCEPTABLE(406, "Not Acceptable"), - REQUEST_TIMEOUT(408, "Request Timeout"), - CONFLICT(409, "Conflict"), - RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), - INTERNAL_ERROR(500, "Internal Server Error"), - NOT_IMPLEMENTED(501, "Not Implemented"), - UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); - - private final int requestStatus; - - private final String description; - - Status(int requestStatus, String description) { - this.requestStatus = requestStatus; - this.description = description; - } - - public static Status lookup(int requestStatus) { - for (Status status : Status.values()) { - if (status.getRequestStatus() == requestStatus) { - return status; - } - } - return null; - } - - @Override - public String getDescription() { - return "" + this.requestStatus + " " + this.description; - } - - @Override - public int getRequestStatus() { - return this.requestStatus; - } - - } - - /** - * Output stream that will automatically send every write to the wrapped - * OutputStream according to chunked transfer: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 - */ - private static class ChunkedOutputStream extends FilterOutputStream { - - public ChunkedOutputStream(OutputStream out) { - super(out); - } - - @Override - public void write(int b) throws IOException { - byte[] data = { - (byte) b - }; - write(data, 0, 1); - } - - @Override - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - if (len == 0) - return; - out.write(String.format("%x\r\n", len).getBytes()); - out.write(b, off, len); - out.write("\r\n".getBytes()); - } - - public void finish() throws IOException { - out.write("0\r\n\r\n".getBytes()); - } - - } - - /** - * HTTP status code after processing, e.g. "200 OK", Status.OK - */ - private IStatus status; - - /** - * MIME type of content, e.g. "text/html" - */ - private String mimeType; - - /** - * Data of the response, may be null. - */ - private InputStream data; - - private long contentLength; - - /** - * Headers for the HTTP response. Use addHeader() to add lines. - */ - private final Map header = new HashMap(); - - /** - * The request method that spawned this response. - */ - private Method requestMethod; - - /** - * Use chunkedTransfer - */ - private boolean chunkedTransfer; - - private boolean encodeAsGzip; - - private boolean keepAlive; - - /** - * Creates a fixed length response if totalBytes>=0, otherwise chunked. - */ - protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { - this.status = status; - this.mimeType = mimeType; - if (data == null) { - this.data = new ByteArrayInputStream(new byte[0]); - this.contentLength = 0L; - } else { - this.data = data; - this.contentLength = totalBytes; - } - this.chunkedTransfer = this.contentLength < 0; - keepAlive = true; - } - - @Override - public void close() throws IOException { - if (this.data != null) { - this.data.close(); - } - } - - /** - * Adds given line to the header. - */ - public void addHeader(String name, String value) { - this.header.put(name, value); - } - - public InputStream getData() { - return this.data; - } - - public String getHeader(String name) { - for (String headerName : header.keySet()) { - if (headerName.equalsIgnoreCase(name)) { - return header.get(headerName); - } - } - return null; - } - - public String getMimeType() { - return this.mimeType; - } - - public Method getRequestMethod() { - return this.requestMethod; - } - - public IStatus getStatus() { - return this.status; - } - - public void setGzipEncoding(boolean encodeAsGzip) { - this.encodeAsGzip = encodeAsGzip; - } - - public void setKeepAlive(boolean useKeepAlive) { - this.keepAlive = useKeepAlive; - } - - private static boolean headerAlreadySent(Map header, String name) { - boolean alreadySent = false; - for (String headerName : header.keySet()) { - alreadySent |= headerName.equalsIgnoreCase(name); - } - return alreadySent; - } - - /** - * Sends given response to the socket. - */ - protected void send(OutputStream outputStream) { - String mime = this.mimeType; - SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); - gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); - - try { - if (this.status == null) { - throw new Error("sendResponse(): Status can't be null."); - } - PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false); - pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n"); - - if (mime != null) { - pw.print("Content-Type: " + mime + "\r\n"); - } - - if (this.header == null || this.header.get("Date") == null) { - pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); - } - - if (this.header != null) { - for (String key : this.header.keySet()) { - String value = this.header.get(key); - pw.print(key + ": " + value + "\r\n"); - } - } - - if (!headerAlreadySent(header, "connection")) { - pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n"); - } - - if (headerAlreadySent(this.header, "content-length")) { - encodeAsGzip = false; - } - - if (encodeAsGzip) { - pw.print("Content-Encoding: gzip\r\n"); - setChunkedTransfer(true); - } - - long pending = this.data != null ? this.contentLength : 0; - if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { - pw.print("Transfer-Encoding: chunked\r\n"); - } else if (!encodeAsGzip) { - pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending); - } - pw.print("\r\n"); - pw.flush(); - sendBodyWithCorrectTransferAndEncoding(outputStream, pending); - outputStream.flush(); - safeClose(this.data); - } catch (IOException ioe) { - NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); - } - } - - private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { - if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { - ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); - sendBodyWithCorrectEncoding(chunkedOutputStream, -1); - chunkedOutputStream.finish(); - } else { - sendBodyWithCorrectEncoding(outputStream, pending); - } - } - - private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { - if (encodeAsGzip) { - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); - sendBody(gzipOutputStream, -1); - gzipOutputStream.finish(); - } else { - sendBody(outputStream, pending); - } - } - - /** - * Sends the body to the specified OutputStream. The pending parameter - * limits the maximum amounts of bytes sent unless it is -1, in which - * case everything is sent. - * - * @param outputStream - * the OutputStream to send data to - * @param pending - * -1 to send everything, otherwise sets a max limit to the - * number of bytes sent - * @throws IOException - * if something goes wrong while sending the data. - */ - private void sendBody(OutputStream outputStream, long pending) throws IOException { - long BUFFER_SIZE = 16 * 1024; - byte[] buff = new byte[(int) BUFFER_SIZE]; - boolean sendEverything = pending == -1; - while (pending > 0 || sendEverything) { - long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); - int read = this.data.read(buff, 0, (int) bytesToRead); - if (read <= 0) { - break; - } - outputStream.write(buff, 0, read); - if (!sendEverything) { - pending -= read; - } - } - } - - protected static long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map header, long size) { - for (String headerName : header.keySet()) { - if (headerName.equalsIgnoreCase("content-length")) { - try { - return Long.parseLong(header.get(headerName)); - } catch (NumberFormatException ex) { - return size; - } - } - } - - pw.print("Content-Length: " + size + "\r\n"); - return size; - } - - public void setChunkedTransfer(boolean chunkedTransfer) { - this.chunkedTransfer = chunkedTransfer; - } - - public void setData(InputStream data) { - this.data = data; - } - - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } - - public void setRequestMethod(Method requestMethod) { - this.requestMethod = requestMethod; - } - - public void setStatus(IStatus status) { - this.status = status; - } - } - - public static final class ResponseException extends Exception { - - private static final long serialVersionUID = 6569838532917408380L; - - private final Response.Status status; - - public ResponseException(Response.Status status, String message) { - super(message); - this.status = status; - } - - public ResponseException(Response.Status status, String message, Exception e) { - super(message, e); - this.status = status; - } - - public Response.Status getStatus() { - return this.status; - } - } - - /** - * The runnable that will be used for the main listening thread. - */ - public class ServerRunnable implements Runnable { - - private final int timeout; - - private IOException bindException; - - private boolean hasBinded = false; - - private ServerRunnable(int timeout) { - this.timeout = timeout; - } - - @Override - public void run() { - try { - myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); - hasBinded = true; - } catch (IOException e) { - this.bindException = e; - return; - } - do { - try { - final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); - if (this.timeout > 0) { - finalAccept.setSoTimeout(this.timeout); - } - final InputStream inputStream = finalAccept.getInputStream(); - NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); - } catch (IOException e) { - NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); - } - } while (!NanoHTTPD.this.myServerSocket.isClosed()); - } - } - - /** - * A temp file. - *

- *

- * Temp files are responsible for managing the actual temporary storage and - * cleaning themselves up when no longer needed. - *

- */ - public interface TempFile { - - void delete() throws Exception; - - String getName(); - - OutputStream open() throws Exception; - } - - /** - * Temp file manager. - *

- *

- * Temp file managers are created 1-to-1 with incoming requests, to create - * and cleanup temporary files created as a result of handling the request. - *

- */ - public interface TempFileManager { - - void clear(); - - TempFile createTempFile(String filename_hint) throws Exception; - } - - /** - * Factory to create temp file managers. - */ - public interface TempFileManagerFactory { - - TempFileManager create(); - } - - /** - * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) - * This is required as the Keep-Alive HTTP connections would otherwise block - * the socket reading thread forever (or as long the browser is open). - */ - public static final int SOCKET_READ_TIMEOUT = 5000; - - /** - * Common MIME type for dynamic content: plain text - */ - public static final String MIME_PLAINTEXT = "text/plain"; - - /** - * Common MIME type for dynamic content: html - */ - public static final String MIME_HTML = "text/html"; - - /** - * Pseudo-Parameter to use to store the actual query string in the - * parameters map for later re-processing. - */ - private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; - - /** - * logger to log to. - */ - private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); - - /** - * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE - */ - protected static Map MIME_TYPES; - - public static Map mimeTypes() { - if (MIME_TYPES == null) { - MIME_TYPES = new HashMap(); - loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); - loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); - if (MIME_TYPES.isEmpty()) { - LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); - } - } - return MIME_TYPES; - } - - private static void loadMimeTypes(Map result, String resourceName) { - try { - Enumeration resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); - while (resources.hasMoreElements()) { - URL url = (URL) resources.nextElement(); - Properties properties = new Properties(); - InputStream stream = null; - try { - stream = url.openStream(); - properties.load(url.openStream()); - } catch (IOException e) { - LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); - } finally { - safeClose(stream); - } - result.putAll((Map) properties); - } - } catch (IOException e) { - LOG.log(Level.INFO, "no mime types available at " + resourceName); - } - }; - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an - * array of loaded KeyManagers. These objects must properly - * loaded/initialized by the caller. - */ - public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { - SSLServerSocketFactory res = null; - try { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(loadedKeyStore); - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); - res = ctx.getServerSocketFactory(); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - return res; - } - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a - * loaded KeyManagerFactory. These objects must properly loaded/initialized - * by the caller. - */ - public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { - try { - return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - } - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your - * certificate and passphrase - */ - public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { - try { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); - keystore.load(keystoreStream, passphrase); - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keystore, passphrase); - return makeSSLSocketFactory(keystore, keyManagerFactory); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - } - - /** - * Get MIME type from file name extension, if possible - * - * @param uri - * the string representing a file - * @return the connected mime/type - */ - public static String getMimeTypeForFile(String uri) { - int dot = uri.lastIndexOf('.'); - String mime = null; - if (dot >= 0) { - mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); - } - return mime == null ? "application/octet-stream" : mime; - } - - private static final void safeClose(Object closeable) { - try { - if (closeable != null) { - if (closeable instanceof Closeable) { - ((Closeable) closeable).close(); - } else if (closeable instanceof Socket) { - ((Socket) closeable).close(); - } else if (closeable instanceof ServerSocket) { - ((ServerSocket) closeable).close(); - } else { - throw new IllegalArgumentException("Unknown object to close"); - } - } - } catch (IOException e) { - NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); - } - } - - private final String hostname; - - private final int myPort; - - private volatile ServerSocket myServerSocket; - - private SSLServerSocketFactory sslServerSocketFactory; - - private String[] sslProtocols; - - private Thread myThread; - - /** - * Pluggable strategy for asynchronously executing requests. - */ - protected AsyncRunner asyncRunner; - - /** - * Pluggable strategy for creating and cleaning up temporary files. - */ - private TempFileManagerFactory tempFileManagerFactory; - - /** - * Constructs an HTTP server on given port. - */ - public NanoHTTPD(int port) { - this(null, port); - } - - // ------------------------------------------------------------------------------- - // // - // - // Threading Strategy. - // - // ------------------------------------------------------------------------------- - // // - - /** - * Constructs an HTTP server on given hostname and port. - */ - public NanoHTTPD(String hostname, int port) { - this.hostname = hostname; - this.myPort = port; - setTempFileManagerFactory(new DefaultTempFileManagerFactory()); - setAsyncRunner(new DefaultAsyncRunner()); - } - - /** - * Forcibly closes all connections that are open. - */ - public synchronized void closeAllConnections() { - stop(); - } - - /** - * create a instance of the client handler, subclasses can return a subclass - * of the ClientHandler. - * - * @param finalAccept - * the socket the cleint is connected to - * @param inputStream - * the input stream - * @return the client handler - */ - protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { - return new ClientHandler(inputStream, finalAccept); - } - - /** - * Instantiate the server runnable, can be overwritten by subclasses to - * provide a subclass of the ServerRunnable. - * - * @param timeout - * the socet timeout to use. - * @return the server runnable. - */ - protected ServerRunnable createServerRunnable(final int timeout) { - return new ServerRunnable(timeout); - } - - /** - * Decode parameters from a URL, handing the case where a single parameter - * name might have been supplied several times, by return lists of values. - * In general these lists will contain a single element. - * - * @param parms - * original NanoHTTPD parameters values, as passed to the - * serve() method. - * @return a map of String (parameter name) to - * List<String> (a list of the values supplied). - */ - protected static Map> decodeParameters(Map parms) { - return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); - } - - // ------------------------------------------------------------------------------- - // // - - /** - * Decode parameters from a URL, handing the case where a single parameter - * name might have been supplied several times, by return lists of values. - * In general these lists will contain a single element. - * - * @param queryString - * a query string pulled from the URL. - * @return a map of String (parameter name) to - * List<String> (a list of the values supplied). - */ - protected static Map> decodeParameters(String queryString) { - Map> parms = new HashMap>(); - if (queryString != null) { - StringTokenizer st = new StringTokenizer(queryString, "&"); - while (st.hasMoreTokens()) { - String e = st.nextToken(); - int sep = e.indexOf('='); - String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); - if (!parms.containsKey(propertyName)) { - parms.put(propertyName, new ArrayList()); - } - String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; - if (propertyValue != null) { - parms.get(propertyName).add(propertyValue); - } - } - } - return parms; - } - - /** - * Decode percent encoded String values. - * - * @param str - * the percent encoded String - * @return expanded form of the input, for example "foo%20bar" becomes - * "foo bar" - */ - protected static String decodePercent(String str) { - String decoded = null; - try { - decoded = URLDecoder.decode(str, "UTF8"); - } catch (UnsupportedEncodingException ignored) { - NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); - } - return decoded; - } - - /** - * @return true if the gzip compression should be used if the client - * accespts it. Default this option is on for text content and off - * for everything. Override this for custom semantics. - */ - protected boolean useGzipWhenAccepted(Response r) { - return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/"); - } - - public final int getListeningPort() { - return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); - } - - public final boolean isAlive() { - return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); - } - - /** - * Call before start() to serve over HTTPS instead of HTTP - */ - public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { - this.sslServerSocketFactory = sslServerSocketFactory; - this.sslProtocols = sslProtocols; - } - - /** - * Create a response with unknown length (using HTTP 1.1 chunking). - */ - public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { - return new Response(status, mimeType, data, -1); - } - - /** - * Create a response with known length. - */ - public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { - return new Response(status, mimeType, data, totalBytes); - } - - /** - * Create a text response with known length. - */ - public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { - if (txt == null) { - return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); - } else { - byte[] bytes; - try { - bytes = txt.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); - bytes = new byte[0]; - } - return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); - } - } - - /** - * Create a text response with known length. - */ - public static Response newFixedLengthResponse(String msg) { - return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); - } - - /** - * Override this to customize the server. - *

- *

- * (By default, this returns a 404 "Not Found" plain text error response.) - * - * @param session - * The HTTP session - * @return HTTP response, see class Response for details - */ - public Response serve(IHTTPSession session) { - Map files = new HashMap(); - Method method = session.getMethod(); - if (Method.PUT.equals(method) || Method.POST.equals(method)) { - try { - session.parseBody(files); - } catch (IOException ioe) { - return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - } catch (ResponseException re) { - return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); - } - } - - Map parms = session.getParms(); - parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); - return serve(session.getUri(), method, session.getHeaders(), parms, files); - } - - /** - * Override this to customize the server. - *

- *

- * (By default, this returns a 404 "Not Found" plain text error response.) - * - * @param uri - * Percent-decoded URI without parameters, for example - * "/index.cgi" - * @param method - * "GET", "POST" etc. - * @param parms - * Parsed, percent decoded parameters from URI and, in case of - * POST, data. - * @param headers - * Header entries, percent decoded - * @return HTTP response, see class Response for details - */ - @Deprecated - public Response serve(String uri, Method method, Map headers, Map parms, Map files) { - return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); - } - - /** - * Pluggable strategy for asynchronously executing requests. - * - * @param asyncRunner - * new strategy for handling threads. - */ - public void setAsyncRunner(AsyncRunner asyncRunner) { - this.asyncRunner = asyncRunner; - } - - /** - * Pluggable strategy for creating and cleaning up temporary files. - * - * @param tempFileManagerFactory - * new strategy for handling temp files. - */ - public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { - this.tempFileManagerFactory = tempFileManagerFactory; - } - - /** - * Start the server. - * - * @throws IOException - * if the socket is in use. - */ - public void start() throws IOException { - start(NanoHTTPD.SOCKET_READ_TIMEOUT); - } - - /** - * Start the server. - * - * @param timeout - * timeout to use for socket connections. - * @param daemon - * start the thread daemon or not. - * @throws IOException - * if the socket is in use. - */ - public void start(final int timeout, boolean daemon) throws IOException { - if (this.sslServerSocketFactory != null) { - SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); - if (this.sslProtocols != null) { - ss.setEnabledProtocols(this.sslProtocols); - } else { - ss.setEnabledProtocols(ss.getSupportedProtocols()); - } - ss.setUseClientMode(false); - ss.setWantClientAuth(false); - ss.setNeedClientAuth(false); - ss.setSoTimeout(timeout); - this.myServerSocket = ss; - } else { - this.myServerSocket = new ServerSocket(); - } - this.myServerSocket.setReuseAddress(true); - - ServerRunnable serverRunnable = createServerRunnable(timeout); - this.myThread = new Thread(serverRunnable); - this.myThread.setDaemon(daemon); - this.myThread.setName("NanoHttpd Main Listener"); - this.myThread.start(); - while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { - try { - Thread.sleep(10L); - } catch (Throwable e) { - // on android this may not be allowed, that's why we - // catch throwable the wait should be very short because we are - // just waiting for the bind of the socket - } - } - if (serverRunnable.bindException != null) { - throw serverRunnable.bindException; - } - } - - /** - * Starts the server (in setDaemon(true) mode). - */ - public void start(final int timeout) throws IOException { - start(timeout, true); - } - - /** - * Stop the server. - */ - public void stop() { - try { - safeClose(this.myServerSocket); - this.asyncRunner.closeAll(); - if (this.myThread != null) { - this.myThread.join(); - } - } catch (Exception e) { - NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); - } - } - - public final boolean wasStarted() { - return this.myServerSocket != null && this.myThread != null; - } -} diff --git a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyHandler.java b/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyHandler.java deleted file mode 100644 index 06db637113..0000000000 --- a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyHandler.java +++ /dev/null @@ -1,509 +0,0 @@ -package com.ddsrem.xiaoya_proxy; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Map; -import java.util.Arrays; -import java.util.List; -import java.util.TreeMap; - -import okhttp3.Response; -import static com.ddsrem.xiaoya_proxy.NanoHTTPD.newFixedLengthResponse; -import okhttp3.Request; -import okhttp3.Headers; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.Queue; -import java.util.LinkedList; -import java.util.regex.Pattern; -import java.util.regex.Matcher; -import java.io.InputStream; -import java.net.URL; -import okhttp3.OkHttpClient; -import okhttp3.Dispatcher; -import okhttp3.FormBody; -import okhttp3.RequestBody; -import org.json.JSONObject; -import java.util.HashMap; -import okhttp3.Call; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.Callable; - -public class XiaoyaProxyHandler { - - private static class QurakLinkCacheInfo { - long cacheTime; - String cacheLink; - String cookie; - } - - private static class QurakLinkCacheManager { - static HashMap map = new HashMap<>(); - public static QurakLinkCacheInfo getLinkCache(String url) { - QurakLinkCacheInfo cacheInfo = map.get(url); - if (cacheInfo != null) { - long currentTime = System.currentTimeMillis(); - long cacheTime = cacheInfo.cacheTime; - if (currentTime - cacheTime <= 10 * 60 * 1000) { - return cacheInfo; - } else { - map.remove(url); - return null; - } - } else { - return null; - } - } - - public static void putLinkCache(String url, QurakLinkCacheInfo value) { - long currentTime = System.currentTimeMillis(); - value.cacheTime = currentTime; - map.put(url, value); - map.entrySet().removeIf(entry -> currentTime - entry.getValue().cacheTime > 10 * 60 * 1000); - } - } - - private static class HttpDownloader extends InputStream { - public String contentType = ""; - public long contentLength = -1; - long contentEnd; - public Headers header; - public int statusCode = 200; - String directUrl = null; - volatile static int curConnId = 0; - volatile boolean closed = false; - int connId; - InputStream is = null; - Queue> callableQueue = new LinkedList<>(); - Queue> futureQueue = new LinkedList<>(); - static HashMap downloaderMap = new HashMap<>(); - ExecutorService executorService = Executors.newFixedThreadPool(128); - boolean supportRange = true; - int blockSize = 10 * 1024 * 1024; //默认10MB - int threadNum = 2; //默认2线程 - String cookie = null; - String referer = null; - int blockCounter = 0; - OkHttpClient downloadClient = null; - OkHttpClient defaultClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).hostnameVerifier((hostname, session) -> true).sslSocketFactory(new MySSLCompat(), MySSLCompat.TM).build(); - - private HttpDownloader(Map params) { - - Thread currentThread = Thread.currentThread(); - currentThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - Logger.log("未捕获的异常1:" + e.getMessage(), true); - } - }); - - try{ - Dispatcher dispatcher = new Dispatcher(); - dispatcher.setMaxRequests(3000000); - dispatcher.setMaxRequestsPerHost(1000000); - downloadClient = defaultClient.newBuilder().dispatcher(dispatcher) - .connectTimeout(3, TimeUnit.SECONDS) - .readTimeout(3, TimeUnit.SECONDS) - .writeTimeout(3, TimeUnit.SECONDS) - .build(); - connId = curConnId++; - String url = params.get("url"); - //播放初始阶段,播放器会多次请求不同的range,快速关闭同一个链接的已有的下载器 - downloaderMap.entrySet().removeIf(entry -> entry.getValue().closed); - HttpDownloader cacheDownloader = downloaderMap.get(url); - if (cacheDownloader != null) { - cacheDownloader.close(); - } - downloaderMap.put(url, this); - - if(params.get("thread") != null){ - threadNum = Integer.parseInt(params.get("thread")); - } - if(params.get("size") != null){ - blockSize = Integer.parseInt(params.get("size")); - } - if(params.get("cookie") != null){ - //如果发送是EncodeURIComponet过的,get会自动转码,不需要手工转,坑啊 - cookie = params.get("cookie"); - } - - Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - List keys = Arrays.asList("referer", "icy-metadata", "range", "connection", "accept-encoding", "user-agent", "cookie", "authorization"); - for (String key : params.keySet()) if (keys.contains(key)) headers.put(key, params.get(key)); - if(url.contains("夸克")) { - headers.put("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch"); - } - String range = ""; - if (params.get("range") != null) { - range = params.get("range"); - } - Logger.log(connId + "[HttpDownloader]:播放器携带的下载链接:" + url + "播放器指定的range:" + range); - this.getHeader(url, headers); - this.createDownloadTask(directUrl, headers); - } catch (Exception e) { - Logger.log(connId + "[HttpDownloader]:发生错误:" + e.getMessage()); - } - } - - private void createDownloadTask(String url, Map headers) { - Logger.log(connId + "[createDownloadTask]:下载链接:" + url); - Request.Builder requestBuilder = new Request.Builder().url(url); - for (Map.Entry entry : headers.entrySet()) { - requestBuilder.addHeader(entry.getKey(), entry.getValue()); - } - Request request = requestBuilder.build(); - //不支持断点续传,单线程下载 - if(!this.supportRange || threadNum == 0) { - Logger.log(connId + "[createDownloadTask]:单线程模式下载,配置线程数:" + threadNum); - Callable callable = () -> { - return downloadTask(url, headers, "", 0); - }; - callableQueue.add(callable); - return; - } - - //多线程下载 - long start = 0; - long end = this.contentEnd ; - String range = request.headers().get("Range"); - range = range == null ? "0-" : range; - range = range + "-" + this.contentEnd; - range = range.replace("--", "-"); - String pattern = "bytes=(\\d+)-(\\d+)"; - Pattern r = Pattern.compile(pattern); - Matcher m = r.matcher(range); - if (m.find()) { - String startString = m.group(1); - String endString = m.group(2); - start = Long.parseLong(startString); - end = Long.parseLong(endString); - } - Logger.log(connId + "[createDownloadTask]:多线程模式下载,配置线程数:" + threadNum + "播放器指定的范围:" + range); - - int sliceNum = 0; - while (start <= end) { - long curEnd = start + blockSize - 1; - curEnd = curEnd > end ? end : curEnd; - String ra = "bytes=" + start + "-" + curEnd; - final int _sliceNum = sliceNum; - Callable callable = () -> { - return downloadTask(url, headers, ra, _sliceNum); - }; - callableQueue.add(callable); - start = curEnd + 1; - sliceNum++; - } - } - - private InputStream downloadTask(String url, Map headers, String range, int sliceNum) { - Thread currentThread = Thread.currentThread(); - currentThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - Logger.log("未捕获的异常2:" + e.getMessage(), true); - } - }); - return _downloadTask(url,headers,range,sliceNum); - } - - private InputStream _downloadTask(String url, Map headers, String range, int sliceNum) { - if(closed){ - return null; - } - Logger.log(connId + "[_downloadTask]:下载分片:" + range); - Request.Builder requestBuilder = new Request.Builder().url(url); - for (Map.Entry entry : headers.entrySet()) { - requestBuilder.addHeader(entry.getKey(), entry.getValue()); - } - if (!range.isEmpty()) { - requestBuilder.removeHeader("Range").addHeader("Range", range); - } - if (cookie != null) { - requestBuilder.removeHeader("Cookie").addHeader("Cookie", cookie); - } - if (referer != null) { - requestBuilder.removeHeader("Referer").addHeader("Referer", referer); - } - Request request = requestBuilder.build(); - int retryCount = 0; - int maxRetry = 5; - byte[] downloadbBuffer = new byte[1024*1024]; - Response response = null; - Call call = null; - boolean directResp = false; - while (retryCount < maxRetry && !closed) { - try { - directResp = false; - call = downloadClient.newCall(request); - response = call.execute(); - if (!response.isSuccessful()) { - continue; - } - // 单线程模式 - if (range.isEmpty()) { - directResp = true; - return response.body().byteStream(); - } - - //第一片加速读取 - if(sliceNum==0){ - directResp = true; - return response.body().byteStream(); - } - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int bytesRead; - while (!closed && (bytesRead = response.body().byteStream().read(downloadbBuffer)) != -1) { - baos.write(downloadbBuffer, 0, bytesRead); - } - Logger.log(connId + "[_downloadTask]:分片完成:" + range); - return new ByteArrayInputStream(baos.toByteArray()); - } catch (Exception e) {} finally { - if(response != null && !directResp){ - call.cancel(); - response.close(); - } - retryCount++; - } - } - Logger.log(connId + "[_downloadTask]:连接异常终止,下载分片:" + range); - return null; - } - - private void getHeader(String url, Map headers) { - getQuarkLink(url, headers); - int count = 0; - while (statusCode == 302 && count < 3){ - _getHeader(directUrl, headers); - count++; - } - Headers originalHeaders = this.header; - Headers.Builder headersBuilder = new Headers.Builder(); - for (int i = 0; i < originalHeaders.size(); i++) { - String name = originalHeaders.name(i); - String value = originalHeaders.value(i); - if(!name.equals("Content-Length") && !name.equals("Content-Type") && !name.equals("Transfer-Encoding")){ - headersBuilder.add(name, value); - } - } - this.header = headersBuilder.build(); - } - - private void getQuarkLink(String url, Map headers) { - try { - //先假装自己重定向到自己 - statusCode = 302; - directUrl = url; - if (!(url.contains("/d/") && url.contains("夸克"))) { - return; - } - Logger.log(connId + "[getQuarkLink]播放器连接请求:" + url); - - QurakLinkCacheInfo info = QurakLinkCacheManager.getLinkCache(url); - if(info != null){ - cookie = info.cookie; - directUrl = info.cacheLink; - referer = "https://pan.quark.cn"; - Logger.log(connId + "[getQuarkLink]获取到夸克下载直链缓存:" + directUrl); - return; - } - - URL urlObj = new URL(url); - String host = urlObj.getProtocol() + "://" + urlObj.getHost(); - int port = urlObj.getPort(); - if (port != -1) { - host = host + ":" + port; - } - String path = ""; - int index = url.indexOf("/d/"); - if (index != -1) { - path = "/" + url.substring(index + 3); - } - String alistApi = host + "/api/fs/other"; - Map params = new HashMap<>(); - params.put("path", path); - params.put("method", "video_download"); - FormBody.Builder formBody = new FormBody.Builder(); - if (params != null) for (String key : params.keySet()) formBody.add(key, params.get(key)); - RequestBody requestBody = formBody.build(); - Request.Builder requestBuilder = new Request.Builder().post(requestBody).url(alistApi); - for (Map.Entry entry : headers.entrySet()) { - requestBuilder.addHeader(entry.getKey(), entry.getValue()); - } - Request request = requestBuilder.build(); - Response response = defaultClient.newCall(request).execute(); - JSONObject object = new JSONObject(response.body().string()); - JSONObject dataObject = object.getJSONObject("data"); - cookie = dataObject.getString("cookie"); - String location = dataObject.getString("download_link"); - location = unescapeUnicode(location); - if(location != null && cookie != null && !location.isEmpty() && !cookie.isEmpty()){ - QurakLinkCacheInfo var = new QurakLinkCacheInfo(); - var.cacheLink = location; - var.cookie = cookie; - QurakLinkCacheManager.putLinkCache(url, var); - } - referer = "https://pan.quark.cn"; - Logger.log(connId + "[getQuarkLink]获取到夸克下载直链:" + location); - directUrl = location == null ? url : location; - } catch (Exception e) { - Logger.log(connId + "[getQuarkLink]获取夸克发生错误:" + e.getMessage()); - } - } - - private String unescapeUnicode(String unicodeString) { - Pattern pattern = Pattern.compile("\\\\u([0-9a-fA-F]{4})"); - Matcher matcher = pattern.matcher(unicodeString); - - StringBuffer sb = new StringBuffer(); - while (matcher.find()) { - char ch = (char) Integer.parseInt(matcher.group(1), 16); - matcher.appendReplacement(sb, String.valueOf(ch)); - } - matcher.appendTail(sb); - - return sb.toString(); - } - - private void _getHeader(String url, Map headers) { - statusCode = 200; - this.supportRange = true; - Response response = null; - Call call = null; - String hContentLength = ""; - try { - Request.Builder requestBuilder = new Request.Builder().url(url); - for (Map.Entry entry : headers.entrySet()) { - requestBuilder.addHeader(entry.getKey(), entry.getValue()); - } - - if (cookie != null) { - requestBuilder.removeHeader("Cookie").addHeader("Cookie", cookie); - } - if (referer != null) { - requestBuilder.removeHeader("Referer").addHeader("Referer", referer); - } - Request request = requestBuilder.build(); - call = defaultClient.newBuilder().followRedirects(false).followSslRedirects(false).build().newCall(request); - response = call.execute(); - this.header = response.headers(); - statusCode = response.code(); - this.contentType = this.header.get("Content-Type"); - hContentLength = this.header.get("Content-Length"); - String location = this.header.get("Location"); - if(location != null && statusCode == 302){ - directUrl = location; - URL urlObj = new URL(url); - String host = urlObj.getProtocol() + "://" + urlObj.getHost(); - int port = urlObj.getPort(); - if (port != -1) { - host = host + ":" + port; - } - if(!directUrl.startsWith("http")){ - directUrl = host + directUrl; - } - } else { - directUrl = url; - } - this.contentLength = hContentLength != null ? Long.parseLong(hContentLength) : -1; - this.contentEnd = this.contentLength - 1; - String hContentEnd = this.header.get("Content-Range"); - if (hContentEnd != null) { - hContentEnd = hContentEnd.split("/")[1]; - this.contentEnd = Long.parseLong(hContentEnd) - 1; - } - if (this.header.get("Accept-Ranges") == null || !this.header.get("Accept-Ranges").toLowerCase().equals("bytes")) { - this.supportRange = false; - } - } catch (Exception e) { - Logger.log(connId + "[_getHeader]:发生错误:" + e.getMessage()); - this.supportRange = false; - return; - } finally { - if(response!=null){ - call.cancel(); - response.close(); - } - } - } - - private void runTask(int num) { - while(num-- > 0 && callableQueue.size() > 0) { - Future future = this.executorService.submit(callableQueue.remove()); - this.futureQueue.add(future); - } - } - - @Override - public synchronized int read(byte[] buffer, int off, int len) throws IOException { - try { - if (closed) { - return -1; - } - - if (this.is == null ) { - runTask(threadNum < 1 ? 1 : threadNum); - this.is = this.futureQueue.remove().get(); - runTask(1); - Logger.log(connId + "[read]:读取数据块:" + blockCounter); - blockCounter++; - } - int ol = this.is.read(buffer, off, len); - if ( ol == -1 ) { - this.is = this.futureQueue.remove().get(); - runTask(1); - Logger.log(connId + "[read]:读取数据块:" + blockCounter); - blockCounter++; - return this.is.read(buffer, off, len); - } - return ol; - } catch (Exception e) { - Logger.log(connId + "[read]:发生错误:" + e.getMessage()); - return -1; - } - } - - @Override - public int read() throws IOException { - throw new IOException("方法未实现,不能调用!"); - } - - @Override - public void close() throws IOException { - if (closed) { - return; - } - Logger.log("播放器主动关闭数据流"); - closed = true; - if(this.executorService != null) { - this.executorService.shutdownNow(); - } - futureQueue.clear(); - callableQueue.clear(); - } - } - - public static Object[] proxy(Map params) throws Exception { - switch (params.get("do")) { - case "dbg": - Logger.dbg = true; - return new Object[]{200, "text/plain; charset=utf-8", new ByteArrayInputStream("ok".getBytes("UTF-8"))}; - case "genck": - return new Object[]{200, "text/plain; charset=utf-8", new ByteArrayInputStream("ok".getBytes("UTF-8"))}; - case "gen": - return genProxy(params); - default: - return null; - } - } - - private synchronized static Object[] genProxy(Map params) throws Exception { - HttpDownloader httpDownloader = new HttpDownloader(params); - NanoHTTPD.Response.IStatus status = NanoHTTPD.Response.Status.lookup(httpDownloader.statusCode); - NanoHTTPD.Response resp = newFixedLengthResponse(status, httpDownloader.contentType, httpDownloader, httpDownloader.contentLength); - for (String key : httpDownloader.header.names()) resp.addHeader(key, httpDownloader.header.get(key)); - return new Object[]{resp}; - } -} diff --git a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyRun.java b/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyRun.java deleted file mode 100644 index 01c1565a83..0000000000 --- a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyRun.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.ddsrem.xiaoya_proxy; - -public class XiaoyaProxyRun { - public static void main(String[] args) { - try { - XiaoyaProxyServer proxyServer = XiaoyaProxyServer.get(); - proxyServer.start(); - System.out.println("XiaoyaProxyServer 启动成功!"); - } catch (Exception e) { - System.err.println("XiaoyaProxyServer 启动失败,错误原因如下:"); - e.printStackTrace(); - System.exit(1); - } - - Object lock = new Object(); - synchronized(lock) { - while(true) { - try { - lock.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - System.err.println("主线程已中断,立即退出!"); - System.exit(1); - } - } - } - } -} diff --git a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyServer.java b/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyServer.java deleted file mode 100644 index 6a4baad0bc..0000000000 --- a/xiaoya_proxy/src/main/java/com/ddsrem/xiaoya_proxy/XiaoyaProxyServer.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ddsrem.xiaoya_proxy; - -import java.io.InputStream; -import java.util.Map; -import java.io.IOException; - -public class XiaoyaProxyServer extends NanoHTTPD { - - private static class Loader { - static volatile XiaoyaProxyServer INSTANCE = new XiaoyaProxyServer(9988); - } - - public XiaoyaProxyServer(int port) { - super(port); - } - - public static XiaoyaProxyServer get() { - return Loader.INSTANCE; - } - - @Override - public Response serve(IHTTPSession session) { - try { - Map params = session.getParms(); - Map headers = session.getHeaders(); - for (Map.Entry entry : headers.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - if (!params.containsKey(key)) { - params.put(key, value); - } - } - Object[] rs = XiaoyaProxyHandler.proxy(params); - return rs[0] instanceof Response ? (Response) rs[0] : newChunkedResponse(Response.Status.lookup((Integer) rs[0]), (String) rs[1], (InputStream) rs[2]); - } catch (Exception e) { - return newFixedLengthResponse(Response.Status.lookup(500), MIME_PLAINTEXT, e.getMessage()); - } - } - - @Override - public void start() throws IOException { - if(!super.isAlive()) { - super.start(); - } - } - - @Override - public void stop() { - super.stop(); - } -}