From 072639b5b22e816df9f78b5cd8a7d4e5379b6aff Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Wed, 17 Sep 2014 20:51:45 +0200 Subject: Changed project structure Switched from custom layout to standard gradle project structure --- .../antennapod/util/service/download/HTTPBin.java | 346 ----- .../util/service/download/NanoHTTPD.java | 1420 -------------------- 2 files changed, 1766 deletions(-) delete mode 100644 src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java delete mode 100644 src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java (limited to 'src/instrumentationTest/de/test/antennapod/util/service') diff --git a/src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java b/src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java deleted file mode 100644 index fc5025b14..000000000 --- a/src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java +++ /dev/null @@ -1,346 +0,0 @@ -package instrumentationTest.de.test.antennapod.util.service.download; - -import android.util.Base64; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; - -import java.io.*; -import java.net.URLConnection; -import java.util.*; -import java.util.zip.GZIPOutputStream; - -/** - * Http server for testing purposes - *

- * Supported features: - *

- * /status/code: Returns HTTP response with the given status code - * /redirect/n: Redirects n times - * /delay/n: Delay response for n seconds - * /basic-auth/username/password: Basic auth with username and password - * /gzip/n: Send gzipped data of size n bytes - * /files/id: Accesses the file with the specified ID (this has to be added first via serveFile). - */ -public class HTTPBin extends NanoHTTPD { - private static final String TAG = "HTTPBin"; - public static final int PORT = 8124; - public static final String BASE_URL = "http://127.0.0.1:" + HTTPBin.PORT; - - - private static final String MIME_HTML = "text/html"; - private static final String MIME_PLAIN = "text/plain"; - - private List servedFiles; - - public HTTPBin() { - super(PORT); - this.servedFiles = new ArrayList(); - } - - /** - * Adds the given file to the server. - * - * @return The ID of the file or -1 if the file could not be added to the server. - */ - public synchronized int serveFile(File file) { - if (file == null) throw new IllegalArgumentException("file = null"); - if (!file.exists()) { - return -1; - } - for (int i = 0; i < servedFiles.size(); i++) { - if (servedFiles.get(i).getAbsolutePath().equals(file.getAbsolutePath())) { - return i; - } - } - servedFiles.add(file); - return servedFiles.size() - 1; - } - - /** - * Removes the file with the given ID from the server. - * - * @return True if a file was removed, false otherwise - */ - public synchronized boolean removeFile(int id) { - if (id < 0) throw new IllegalArgumentException("ID < 0"); - if (id >= servedFiles.size()) { - return false; - } else { - return servedFiles.remove(id) != null; - } - } - - public synchronized File accessFile(int id) { - if (id < 0 || id >= servedFiles.size()) { - return null; - } else { - return servedFiles.get(id); - } - } - - @Override - public Response serve(IHTTPSession session) { - - if (BuildConfig.DEBUG) Log.d(TAG, "Requested url: " + session.getUri()); - - String[] segments = session.getUri().split("/"); - if (segments.length < 3) { - Log.w(TAG, String.format("Invalid number of URI segments: %d %s", segments.length, Arrays.toString(segments))); - get404Error(); - } - - final String func = segments[1]; - final String param = segments[2]; - final Map headers = session.getHeaders(); - - if (func.equalsIgnoreCase("status")) { - try { - int code = Integer.parseInt(param); - return getStatus(code); - } catch (NumberFormatException e) { - e.printStackTrace(); - return getInternalError(); - } - - } else if (func.equalsIgnoreCase("redirect")) { - try { - int times = Integer.parseInt(param); - if (times < 0) { - throw new NumberFormatException("times <= 0: " + times); - } - - return getRedirectResponse(times - 1); - } catch (NumberFormatException e) { - e.printStackTrace(); - return getInternalError(); - } - } else if (func.equalsIgnoreCase("delay")) { - try { - int sec = Integer.parseInt(param); - if (sec <= 0) { - throw new NumberFormatException("sec <= 0: " + sec); - } - - Thread.sleep(sec * 1000L); - return getOKResponse(); - } catch (NumberFormatException e) { - e.printStackTrace(); - return getInternalError(); - } catch (InterruptedException e) { - e.printStackTrace(); - return getInternalError(); - } - } else if (func.equalsIgnoreCase("basic-auth")) { - if (!headers.containsKey("authorization")) { - Log.w(TAG, "No credentials provided"); - return getUnauthorizedResponse(); - } - try { - String credentials = new String(Base64.decode(headers.get("authorization").split(" ")[1], 0), "UTF-8"); - String[] credentialParts = credentials.split(":"); - if (credentialParts.length != 2) { - Log.w(TAG, "Unable to split credentials: " + Arrays.toString(credentialParts)); - return getInternalError(); - } - if (credentialParts[0].equals(segments[2]) - && credentialParts[1].equals(segments[3])) { - Log.i(TAG, "Credentials accepted"); - return getOKResponse(); - } else { - Log.w(TAG, String.format("Invalid credentials. Expected %s, %s, but was %s, %s", - segments[2], segments[3], credentialParts[0], credentialParts[1])); - return getUnauthorizedResponse(); - } - - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - return getInternalError(); - } - } else if (func.equalsIgnoreCase("gzip")) { - try { - int size = Integer.parseInt(param); - if (size <= 0) { - Log.w(TAG, "Invalid size for gzipped data: " + size); - throw new NumberFormatException(); - } - - return getGzippedResponse(size); - } catch (NumberFormatException e) { - e.printStackTrace(); - return getInternalError(); - } catch (IOException e) { - e.printStackTrace(); - return getInternalError(); - } - } else if (func.equalsIgnoreCase("files")) { - try { - int id = Integer.parseInt(param); - if (id < 0) { - Log.w(TAG, "Invalid ID: " + id); - throw new NumberFormatException(); - } - return getFileAccessResponse(id, headers); - - } catch (NumberFormatException e) { - e.printStackTrace(); - return getInternalError(); - } - } - - return get404Error(); - } - - private synchronized Response getFileAccessResponse(int id, Map header) { - File file = accessFile(id); - if (file == null || !file.exists()) { - Log.w(TAG, "File not found: " + id); - return get404Error(); - } - InputStream inputStream = null; - String contentRange = null; - Response.Status status; - boolean successful = false; - try { - inputStream = new FileInputStream(file); - if (header.containsKey("range")) { - // read range header field - final String value = header.get("range"); - final String[] segments = value.split("="); - if (segments.length != 2) { - Log.w(TAG, "Invalid segment length: " + Arrays.toString(segments)); - return getInternalError(); - } - final String type = StringUtils.substringBefore(value, "="); - if (!type.equalsIgnoreCase("bytes")) { - Log.w(TAG, "Range is not specified in bytes: " + value); - return getInternalError(); - } - try { - long start = Long.parseLong(StringUtils.substringBefore(segments[1], "-")); - if (start >= file.length()) { - return getRangeNotSatisfiable(); - } - - // skip 'start' bytes - IOUtils.skipFully(inputStream, start); - contentRange = "bytes " + start + (file.length() - 1) + "/" + file.length(); - - } catch (NumberFormatException e) { - e.printStackTrace(); - return getInternalError(); - } catch (IOException e) { - e.printStackTrace(); - return getInternalError(); - } - - status = Response.Status.PARTIAL_CONTENT; - - } else { - // request did not contain range header field - status = Response.Status.OK; - } - successful = true; - } catch (FileNotFoundException e) { - e.printStackTrace(); - - return getInternalError(); - } finally { - if (!successful && inputStream != null) { - IOUtils.closeQuietly(inputStream); - } - } - - Response response = new Response(status, URLConnection.guessContentTypeFromName(file.getAbsolutePath()), inputStream); - - response.addHeader("Accept-Ranges", "bytes"); - if (contentRange != null) { - response.addHeader("Content-Range", contentRange); - } - response.addHeader("Content-Length", String.valueOf(file.length())); - return response; - } - - private Response getGzippedResponse(int size) throws IOException { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - final byte[] buffer = new byte[size]; - Random random = new Random(System.currentTimeMillis()); - random.nextBytes(buffer); - - ByteArrayOutputStream compressed = new ByteArrayOutputStream(); - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(compressed); - gzipOutputStream.write(buffer); - - InputStream inputStream = new ByteArrayInputStream(compressed.toByteArray()); - Response response = new Response(Response.Status.OK, MIME_PLAIN, inputStream); - response.addHeader("Content-encoding", "gzip"); - response.addHeader("Content-length", String.valueOf(compressed.size())); - return response; - } - - private Response getStatus(final int code) { - Response.IStatus status = (code == 200) ? Response.Status.OK : - (code == 201) ? Response.Status.CREATED : - (code == 206) ? Response.Status.PARTIAL_CONTENT : - (code == 301) ? Response.Status.REDIRECT : - (code == 304) ? Response.Status.NOT_MODIFIED : - (code == 400) ? Response.Status.BAD_REQUEST : - (code == 401) ? Response.Status.UNAUTHORIZED : - (code == 403) ? Response.Status.FORBIDDEN : - (code == 404) ? Response.Status.NOT_FOUND : - (code == 405) ? Response.Status.METHOD_NOT_ALLOWED : - (code == 416) ? Response.Status.RANGE_NOT_SATISFIABLE : - (code == 500) ? Response.Status.INTERNAL_ERROR : new Response.IStatus() { - @Override - public int getRequestStatus() { - return code; - } - - @Override - public String getDescription() { - return "Unknown"; - } - }; - return new Response(status, MIME_HTML, ""); - - } - - private Response getRedirectResponse(int times) { - if (times > 0) { - Response response = new Response(Response.Status.REDIRECT, MIME_HTML, "This resource has been moved permanently"); - response.addHeader("Location", "/redirect/" + times); - return response; - } else if (times == 0) { - return getOKResponse(); - } else { - return getInternalError(); - } - } - - private Response getUnauthorizedResponse() { - Response response = new Response(Response.Status.UNAUTHORIZED, MIME_HTML, ""); - response.addHeader("WWW-Authenticate", "Basic realm=\"Test Realm\""); - return response; - } - - private Response getOKResponse() { - return new Response(Response.Status.OK, MIME_HTML, ""); - } - - private Response getInternalError() { - return new Response(Response.Status.INTERNAL_ERROR, MIME_HTML, "The server encountered an internal error"); - } - - private Response getRangeNotSatisfiable() { - return new Response(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAIN, ""); - } - - private Response get404Error() { - return new Response(Response.Status.NOT_FOUND, MIME_HTML, "The requested URL was not found on this server"); - } -} diff --git a/src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java b/src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java deleted file mode 100644 index 916a2af6c..000000000 --- a/src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java +++ /dev/null @@ -1,1420 +0,0 @@ -package instrumentationTest.de.test.antennapod.util.service.download; - -import java.io.*; -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.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.TimeZone; - -/** - * 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: - *

- *

- *

- * How to use: - *

- *

- * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence) - */ -public abstract class NanoHTTPD { - /** - * 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"; - private final String hostname; - private final int myPort; - private ServerSocket myServerSocket; - private Set openConnections = new HashSet(); - private Thread myThread; - /** - * Pluggable strategy for asynchronously executing requests. - */ - private 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); - } - - /** - * 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()); - } - - private static final void safeClose(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (IOException e) { - } - } - } - - private static final void safeClose(Socket closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (IOException e) { - } - } - } - - private static final void safeClose(ServerSocket closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (IOException e) { - } - } - } - - /** - * Start the server. - * - * @throws IOException if the socket is in use. - */ - public void start() throws IOException { - myServerSocket = new ServerSocket(); - myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); - - myThread = new Thread(new Runnable() { - @Override - public void run() { - do { - try { - final Socket finalAccept = myServerSocket.accept(); - registerConnection(finalAccept); - finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT); - final InputStream inputStream = finalAccept.getInputStream(); - asyncRunner.exec(new Runnable() { - @Override - public void run() { - OutputStream outputStream = null; - try { - outputStream = finalAccept.getOutputStream(); - TempFileManager tempFileManager = tempFileManagerFactory.create(); - HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress()); - while (!finalAccept.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 (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) { - e.printStackTrace(); - } - } finally { - safeClose(outputStream); - safeClose(inputStream); - safeClose(finalAccept); - unRegisterConnection(finalAccept); - } - } - }); - } catch (IOException e) { - } - } while (!myServerSocket.isClosed()); - } - }); - myThread.setDaemon(true); - myThread.setName("NanoHttpd Main Listener"); - myThread.start(); - } - - /** - * Stop the server. - */ - public void stop() { - try { - safeClose(myServerSocket); - closeAllConnections(); - if (myThread != null) { - myThread.join(); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Registers that a new connection has been set up. - * - * @param socket the {@link Socket} for the connection. - */ - public synchronized void registerConnection(Socket socket) { - openConnections.add(socket); - } - - /** - * Registers that a connection has been closed - * - * @param socket - * the {@link Socket} for the connection. - */ - public synchronized void unRegisterConnection(Socket socket) { - openConnections.remove(socket); - } - - /** - * Forcibly closes all connections that are open. - */ - public synchronized void closeAllConnections() { - for (Socket socket : openConnections) { - safeClose(socket); - } - } - - public final int getListeningPort() { - return myServerSocket == null ? -1 : myServerSocket.getLocalPort(); - } - - public final boolean wasStarted() { - return myServerSocket != null && myThread != null; - } - - public final boolean isAlive() { - return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive(); - } - - /** - * Override this to customize the server. - *

- *

- * (By default, this delegates to serveFile() and allows directory listing.) - * - * @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 new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); - } - - /** - * Override this to customize the server. - *

- *

- * (By default, this delegates to serveFile() and allows directory listing.) - * - * @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 new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - } catch (ResponseException re) { - return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); - } - } - - Map parms = session.getParms(); - parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString()); - return serve(session.getUri(), method, session.getHeaders(), parms, files); - } - - /** - * 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 String decodePercent(String str) { - String decoded = null; - try { - decoded = URLDecoder.decode(str, "UTF8"); - } catch (UnsupportedEncodingException ignored) { - } - return decoded; - } - - /** - * 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 Map> decodeParameters(Map parms) { - return this.decodeParameters(parms.get(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 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; - } - - // ------------------------------------------------------------------------------- // - // - // Threading Strategy. - // - // ------------------------------------------------------------------------------- // - - /** - * Pluggable strategy for asynchronously executing requests. - * - * @param asyncRunner new strategy for handling threads. - */ - public void setAsyncRunner(AsyncRunner asyncRunner) { - this.asyncRunner = asyncRunner; - } - - // ------------------------------------------------------------------------------- // - // - // Temp file handling strategy. - // - // ------------------------------------------------------------------------------- // - - /** - * 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; - } - - /** - * HTTP Request methods, with the ability to decode a String back to its enum value. - */ - public enum Method { - GET, PUT, POST, DELETE, HEAD, OPTIONS; - - static Method lookup(String method) { - for (Method m : Method.values()) { - if (m.toString().equalsIgnoreCase(method)) { - return m; - } - } - return null; - } - } - - /** - * Pluggable strategy for asynchronously executing requests. - */ - public interface AsyncRunner { - void exec(Runnable code); - } - - /** - * Factory to create temp file managers. - */ - public interface TempFileManagerFactory { - TempFileManager create(); - } - - // ------------------------------------------------------------------------------- // - - /** - * 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 { - TempFile createTempFile() throws Exception; - - void clear(); - } - - /** - * A temp file. - *

- *

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

- */ - public interface TempFile { - OutputStream open() throws Exception; - - void delete() throws Exception; - - String getName(); - } - - /** - * 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; - - @Override - public void exec(Runnable code) { - ++requestCount; - Thread t = new Thread(code); - t.setDaemon(true); - t.setName("NanoHttpd Request Processor (#" + requestCount + ")"); - t.start(); - } - } - - /** - * 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() { - tmpdir = System.getProperty("java.io.tmpdir"); - tempFiles = new ArrayList(); - } - - @Override - public TempFile createTempFile() throws Exception { - DefaultTempFile tempFile = new DefaultTempFile(tmpdir); - tempFiles.add(tempFile); - return tempFile; - } - - @Override - public void clear() { - for (TempFile file : tempFiles) { - try { - file.delete(); - } catch (Exception ignored) { - } - } - tempFiles.clear(); - } - } - - /** - * 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 File file; - private OutputStream fstream; - - public DefaultTempFile(String tempdir) throws IOException { - file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); - fstream = new FileOutputStream(file); - } - - @Override - public OutputStream open() throws Exception { - return fstream; - } - - @Override - public void delete() throws Exception { - safeClose(fstream); - file.delete(); - } - - @Override - public String getName() { - return file.getAbsolutePath(); - } - } - - /** - * HTTP response. Return one of these from serve(). - */ - public static class Response { - /** - * HTTP status code after processing, e.g. "200 OK", HTTP_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; - /** - * Headers for the HTTP response. Use addHeader() to add lines. - */ - private Map header = new HashMap(); - /** - * The request method that spawned this response. - */ - private Method requestMethod; - /** - * Use chunkedTransfer - */ - private boolean chunkedTransfer; - - /** - * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message - */ - public Response(String msg) { - this(Status.OK, MIME_HTML, msg); - } - - /** - * Basic constructor. - */ - public Response(IStatus status, String mimeType, InputStream data) { - this.status = status; - this.mimeType = mimeType; - this.data = data; - } - - /** - * Convenience method that makes an InputStream out of given text. - */ - public Response(IStatus status, String mimeType, String txt) { - this.status = status; - this.mimeType = mimeType; - try { - this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null; - } catch (java.io.UnsupportedEncodingException uee) { - uee.printStackTrace(); - } - } - - /** - * Adds given line to the header. - */ - public void addHeader(String name, String value) { - header.put(name, value); - } - - public String getHeader(String name) { - return header.get(name); - } - - /** - * Sends given response to the socket. - */ - protected void send(OutputStream outputStream) { - String mime = mimeType; - SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); - gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); - - try { - if (status == null) { - throw new Error("sendResponse(): Status can't be null."); - } - PrintWriter pw = new PrintWriter(outputStream); - pw.print("HTTP/1.1 " + status.getDescription() + " \r\n"); - - if (mime != null) { - pw.print("Content-Type: " + mime + "\r\n"); - } - - if (header == null || header.get("Date") == null) { - pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); - } - - if (header != null) { - for (String key : header.keySet()) { - String value = header.get(key); - pw.print(key + ": " + value + "\r\n"); - } - } - - sendConnectionHeaderIfNotAlreadyPresent(pw, header); - - if (requestMethod != Method.HEAD && chunkedTransfer) { - sendAsChunked(outputStream, pw); - } else { - int pending = data != null ? data.available() : 0; - sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending); - pw.print("\r\n"); - pw.flush(); - sendAsFixedLength(outputStream, pending); - } - outputStream.flush(); - safeClose(data); - } catch (IOException ioe) { - // Couldn't write? No can do. - } - } - - protected void sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map header, int size) { - if (!headerAlreadySent(header, "content-length")) { - pw.print("Content-Length: "+ size +"\r\n"); - } - } - - protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map header) { - if (!headerAlreadySent(header, "connection")) { - pw.print("Connection: keep-alive\r\n"); - } - } - - private boolean headerAlreadySent(Map header, String name) { - boolean alreadySent = false; - for (String headerName : header.keySet()) { - alreadySent |= headerName.equalsIgnoreCase(name); - } - return alreadySent; - } - - private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException { - pw.print("Transfer-Encoding: chunked\r\n"); - pw.print("\r\n"); - pw.flush(); - int BUFFER_SIZE = 16 * 1024; - byte[] CRLF = "\r\n".getBytes(); - byte[] buff = new byte[BUFFER_SIZE]; - int read; - while ((read = data.read(buff)) > 0) { - outputStream.write(String.format("%x\r\n", read).getBytes()); - outputStream.write(buff, 0, read); - outputStream.write(CRLF); - } - outputStream.write(String.format("0\r\n\r\n").getBytes()); - } - - private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException { - if (requestMethod != Method.HEAD && data != null) { - int BUFFER_SIZE = 16 * 1024; - byte[] buff = new byte[BUFFER_SIZE]; - while (pending > 0) { - int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending)); - if (read <= 0) { - break; - } - outputStream.write(buff, 0, read); - pending -= read; - } - } - } - - public IStatus getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; - } - - public String getMimeType() { - return mimeType; - } - - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } - - public InputStream getData() { - return data; - } - - public void setData(InputStream data) { - this.data = data; - } - - public Method getRequestMethod() { - return requestMethod; - } - - public void setRequestMethod(Method requestMethod) { - this.requestMethod = requestMethod; - } - - public void setChunkedTransfer(boolean chunkedTransfer) { - this.chunkedTransfer = chunkedTransfer; - } - - public interface IStatus { - int getRequestStatus(); - String getDescription(); - } - - /** - * 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"), RANGE_NOT_SATISFIABLE(416, - "Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error"); - private final int requestStatus; - private final String description; - - Status(int requestStatus, String description) { - this.requestStatus = requestStatus; - this.description = description; - } - - @Override - public int getRequestStatus() { - return this.requestStatus; - } - - @Override - public String getDescription() { - return "" + this.requestStatus + " " + description; - } - } - } - - public static final class ResponseException extends Exception { - - 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 status; - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - */ - private class DefaultTempFileManagerFactory implements TempFileManagerFactory { - @Override - public TempFileManager create() { - return new DefaultTempFileManager(); - } - } - - /** - * Handles one session, i.e. parses the HTTP request and returns the response. - */ - public interface IHTTPSession { - void execute() throws IOException; - - Map getParms(); - - Map getHeaders(); - - /** - * @return the path part of the URL. - */ - String getUri(); - - String getQueryParameterString(); - - Method getMethod(); - - InputStream getInputStream(); - - CookieHandler getCookies(); - - /** - * Adds the files in the request body to the files map. - * @arg files - map to modify - */ - void parseBody(Map files) throws IOException, ResponseException; - } - - protected class HTTPSession implements IHTTPSession { - public static final int BUFSIZE = 8192; - private final TempFileManager tempFileManager; - private final OutputStream outputStream; - private PushbackInputStream 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; - - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { - this.tempFileManager = tempFileManager; - this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); - this.outputStream = outputStream; - } - - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { - this.tempFileManager = tempFileManager; - this.inputStream = new PushbackInputStream(inputStream, BUFSIZE); - this.outputStream = outputStream; - String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); - headers = new HashMap(); - - headers.put("remote-addr", remoteIp); - headers.put("http-client-ip", remoteIp); - } - - @Override - public void execute() throws IOException { - 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[BUFSIZE]; - splitbyte = 0; - rlen = 0; - { - int read = -1; - try { - read = inputStream.read(buf, 0, BUFSIZE); - } catch (Exception e) { - safeClose(inputStream); - safeClose(outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - if (read == -1) { - // socket was been closed - safeClose(inputStream); - safeClose(outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - while (read > 0) { - rlen += read; - splitbyte = findHeaderEnd(buf, rlen); - if (splitbyte > 0) - break; - read = inputStream.read(buf, rlen, BUFSIZE - rlen); - } - } - - if (splitbyte < rlen) { - inputStream.unread(buf, splitbyte, rlen - splitbyte); - } - - parms = new HashMap(); - if(null == headers) { - headers = new HashMap(); - } - - // Create a BufferedReader for parsing the header. - BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen))); - - // Decode the header into parms and header java properties - Map pre = new HashMap(); - decodeHeader(hin, pre, parms, headers); - - method = Method.lookup(pre.get("method")); - if (method == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); - } - - uri = pre.get("uri"); - - cookies = new CookieHandler(headers); - - // Ok, now do the serve() - Response r = serve(this); - if (r == null) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); - } else { - cookies.unloadQueue(r); - r.setRequestMethod(method); - r.send(outputStream); - } - } catch (SocketException e) { - // throw it out to close socket object (finalAccept) - throw e; - } catch (SocketTimeoutException ste) { - throw ste; - } catch (IOException ioe) { - Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - r.send(outputStream); - safeClose(outputStream); - } catch (ResponseException re) { - Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); - r.send(outputStream); - safeClose(outputStream); - } finally { - tempFileManager.clear(); - } - } - - @Override - public void parseBody(Map files) throws IOException, ResponseException { - RandomAccessFile randomAccessFile = null; - BufferedReader in = null; - try { - - randomAccessFile = getTmpBucket(); - - long size; - if (headers.containsKey("content-length")) { - size = Integer.parseInt(headers.get("content-length")); - } else if (splitbyte < rlen) { - size = rlen - splitbyte; - } else { - size = 0; - } - - // Now read all the body and write it to f - byte[] buf = new byte[512]; - while (rlen >= 0 && size > 0) { - rlen = inputStream.read(buf, 0, (int)Math.min(size, 512)); - size -= rlen; - if (rlen > 0) { - randomAccessFile.write(buf, 0, rlen); - } - } - - // Get the raw body as a byte [] - ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); - randomAccessFile.seek(0); - - // Create a BufferedReader for easily reading it as string. - InputStream bin = new FileInputStream(randomAccessFile.getFD()); - in = new BufferedReader(new InputStreamReader(bin)); - - // If the method is POST, there may be parameters - // in data section, too, read it: - if (Method.POST.equals(method)) { - String contentType = ""; - String contentTypeHeader = 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); - } - - decodeMultipartData(boundary, fbuf, in, parms, files); - } else { - String postLine = ""; - StringBuilder postLineBuffer = new StringBuilder(); - char pbuf[] = new char[512]; - int read = in.read(pbuf); - while (read >= 0 && !postLine.endsWith("\r\n")) { - postLine = String.valueOf(pbuf, 0, read); - postLineBuffer.append(postLine); - read = in.read(pbuf); - } - postLine = postLineBuffer.toString().trim(); - // Handle application/x-www-form-urlencoded - if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { - decodeParms(postLine, 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(method)) { - files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); - } - } finally { - safeClose(randomAccessFile); - safeClose(in); - } - } - - /** - * 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, it's protocol version, - // followed by HTTP headers. Ignore version but parse headers. - // NOTE: this now forces header names lowercase since they are - // case insensitive and vary by client. - if (st.hasMoreTokens()) { - 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 decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map parms, - Map files) throws ResponseException { - try { - int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes()); - int boundarycount = 1; - String mpline = in.readLine(); - while (mpline != null) { - if (!mpline.contains(boundary)) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html"); - } - boundarycount++; - Map item = new HashMap(); - mpline = in.readLine(); - while (mpline != null && mpline.trim().length() > 0) { - int p = mpline.indexOf(':'); - if (p != -1) { - item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim()); - } - mpline = in.readLine(); - } - if (mpline != null) { - String contentDisposition = item.get("content-disposition"); - if (contentDisposition == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html"); - } - StringTokenizer st = new StringTokenizer(contentDisposition, ";"); - Map disposition = new HashMap(); - while (st.hasMoreTokens()) { - String token = st.nextToken().trim(); - int p = token.indexOf('='); - if (p != -1) { - disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim()); - } - } - String pname = disposition.get("name"); - pname = pname.substring(1, pname.length() - 1); - - String value = ""; - if (item.get("content-type") == null) { - while (mpline != null && !mpline.contains(boundary)) { - mpline = in.readLine(); - if (mpline != null) { - int d = mpline.indexOf(boundary); - if (d == -1) { - value += mpline; - } else { - value += mpline.substring(0, d - 2); - } - } - } - } else { - if (boundarycount > bpositions.length) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request"); - } - int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]); - String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4); - files.put(pname, path); - value = disposition.get("filename"); - value = value.substring(1, value.length() - 1); - do { - mpline = in.readLine(); - } while (mpline != null && !mpline.contains(boundary)); - } - parms.put(pname, value); - } - } - } catch (IOException ioe) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); - } - } - - /** - * 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. - */ - private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { - int matchcount = 0; - int matchbyte = -1; - List matchbytes = new ArrayList(); - for (int i = 0; i < b.limit(); i++) { - if (b.get(i) == boundary[matchcount]) { - if (matchcount == 0) - matchbyte = i; - matchcount++; - if (matchcount == boundary.length) { - matchbytes.add(matchbyte); - matchcount = 0; - matchbyte = -1; - } - } else { - i -= matchcount; - matchcount = 0; - matchbyte = -1; - } - } - int[] ret = new int[matchbytes.size()]; - for (int i = 0; i < ret.length; i++) { - ret[i] = matchbytes.get(i); - } - return ret; - } - - /** - * 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 path = ""; - if (len > 0) { - FileOutputStream fileOutputStream = null; - try { - TempFile tempFile = tempFileManager.createTempFile(); - 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; - } - - private RandomAccessFile getTmpBucket() { - try { - TempFile tempFile = tempFileManager.createTempFile(); - return new RandomAccessFile(tempFile.getName(), "rw"); - } catch (Exception e) { - throw new Error(e); // we won't recover, so throw an error - } - } - - /** - * It returns the offset separating multipart file headers from the file's data. - */ - private int stripMultipartHeaders(ByteBuffer b, int offset) { - int i; - for (i = offset; i < b.limit(); i++) { - if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') { - break; - } - } - return i + 1; - } - - /** - * 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) { - queryParameterString = ""; - return; - } - - 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 final Map getParms() { - return parms; - } - - public String getQueryParameterString() { - return queryParameterString; - } - - @Override - public final Map getHeaders() { - return headers; - } - - @Override - public final String getUri() { - return uri; - } - - @Override - public final Method getMethod() { - return method; - } - - @Override - public final InputStream getInputStream() { - return inputStream; - } - - @Override - public CookieHandler getCookies() { - return cookies; - } - } - - public static class Cookie { - private String n, v, e; - - public Cookie(String name, String value, String expires) { - n = name; - v = value; - e = expires; - } - - public Cookie(String name, String value) { - this(name, value, 30); - } - - public Cookie(String name, String value, int numDays) { - n = name; - v = value; - e = getHTTPTime(numDays); - } - - public String getHTTPHeader() { - String fmt = "%s=%s; expires=%s"; - return String.format(fmt, n, v, e); - } - - 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()); - } - } - - /** - * 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 HashMap cookies = new HashMap(); - private 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) { - cookies.put(data[0], data[1]); - } - } - } - } - - @Override public Iterator iterator() { - return 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 cookies.get(name); - } - - /** - * 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) { - queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); - } - - public void set(Cookie cookie) { - queue.add(cookie); - } - - /** - * 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); - } - - /** - * 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 : queue) { - response.addHeader("Set-Cookie", cookie.getHTTPHeader()); - } - } - } -} -- cgit v1.2.3