diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0414349 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "nanohttpd"] + path = nanohttpd + url = https://github.com/NanoHttpd/nanohttpd.git diff --git a/README.md b/README.md index d3dca8a..8a612da 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,15 @@ that we can just write PC side script to write UIAutomator tests. - Install Ant if you have not. - Run command: - - $ ant build # compile - $ ant install # install jar file to device via adb + $ git submodule init + $ git submodule update + $ ant build # compile + $ ant install # install jar file to device via adb # Run the jsonrcp server on Android device - $ adb shell uiautomator runtest uiautomator-stub.jar bundle.jar -c com.github.uiautomatorstub.Stub - $ adb forward tcp:9008 tcp:9008 # tcp forward + $ adb shell uiautomator runtest uiautomator-stub.jar bundle.jar -c com.github.uiautomatorstub.Stub + $ adb forward tcp:9008 tcp:9008 # tcp forward # How to use diff --git a/ant.properties b/ant.properties new file mode 100644 index 0000000..8cce186 --- /dev/null +++ b/ant.properties @@ -0,0 +1 @@ +source.dir=src;nanohttpd/core/src/main/java diff --git a/nanohttpd b/nanohttpd new file mode 160000 index 0000000..8523184 --- /dev/null +++ b/nanohttpd @@ -0,0 +1 @@ +Subproject commit 852318439539b54ee6b4ce048df63b6c12cf0417 diff --git a/src/com/github/uiautomatorstub/AutomatorService.java b/src/com/github/uiautomatorstub/AutomatorService.java index e420dd9..6e45828 100644 --- a/src/com/github/uiautomatorstub/AutomatorService.java +++ b/src/com/github/uiautomatorstub/AutomatorService.java @@ -805,4 +805,20 @@ public interface AutomatorService { */ @JsonRpcErrors({@JsonRpcError(exception=UiObjectNotFoundException.class, code=ERROR_CODE_BASE-2)}) boolean waitUntilGone (String obj, long timeout) throws UiObjectNotFoundException; + + /** + * Get Configurator + * @return Configurator information. + * @throws NotImplementedException + */ + @JsonRpcErrors({@JsonRpcError(exception=NotImplementedException.class, code=ERROR_CODE_BASE-3)}) + ConfiguratorInfo getConfigurator() throws NotImplementedException; + + /** + * Set Configurator. + * @param info the configurator information to be set. + * @throws NotImplementedException + */ + @JsonRpcErrors({@JsonRpcError(exception=NotImplementedException.class, code=ERROR_CODE_BASE-3)}) + ConfiguratorInfo setConfigurator(ConfiguratorInfo info) throws NotImplementedException; } diff --git a/src/com/github/uiautomatorstub/AutomatorServiceImpl.java b/src/com/github/uiautomatorstub/AutomatorServiceImpl.java index dea0844..d9b248e 100644 --- a/src/com/github/uiautomatorstub/AutomatorServiceImpl.java +++ b/src/com/github/uiautomatorstub/AutomatorServiceImpl.java @@ -69,7 +69,7 @@ static void setAsVerticalList(UiScrollable obj) { */ @Override public String ping() { - new UiObject(new UiSelector()).exists(); // here we call the method just for checking if the UiAutomationService is ok, else it will throw IllegalStateException. + //new UiObject(new UiSelector()).exists(); // here we call the method just for checking if the UiAutomationService is ok, else it will throw IllegalStateException. return "pong"; } @@ -1530,4 +1530,33 @@ public boolean waitForExists(String obj, long timeout) throws UiObjectNotFoundEx public boolean waitUntilGone(String obj, long timeout) throws UiObjectNotFoundException { return getUiObject(obj).waitUntilGone(timeout); } + + /** + * Get Configurator + * + * @return Configurator information. + * @throws com.github.uiautomatorstub.NotImplementedException + */ + @Override + public ConfiguratorInfo getConfigurator() throws NotImplementedException { + if (Build.VERSION.SDK_INT < 18) + throw new NotImplementedException(); + + return new ConfiguratorInfo(); + } + + /** + * Set Configurator. + * + * @param info the configurator information to be set. + * @throws com.github.uiautomatorstub.NotImplementedException + */ + @Override + public ConfiguratorInfo setConfigurator(ConfiguratorInfo info) throws NotImplementedException { + if (Build.VERSION.SDK_INT < 18) + throw new NotImplementedException(); + + ConfiguratorInfo.setConfigurator(info); + return new ConfiguratorInfo(); + } } diff --git a/src/com/github/uiautomatorstub/ConfiguratorInfo.java b/src/com/github/uiautomatorstub/ConfiguratorInfo.java new file mode 100644 index 0000000..b374622 --- /dev/null +++ b/src/com/github/uiautomatorstub/ConfiguratorInfo.java @@ -0,0 +1,95 @@ +package com.github.uiautomatorstub; + +import java.lang.reflect.InvocationTargetException; + +/** + * Created by b036 on 12/26/13. + */ +public class ConfiguratorInfo { + + public ConfiguratorInfo() { + try { + Class clz = Class.forName("com.android.uiautomator.core.Configurator"); + Object conf = clz.getMethod("getInstance").invoke(null); + this._actionAcknowledgmentTimeout = (Long)clz.getMethod("getActionAcknowledgmentTimeout").invoke(conf); + this._keyInjectionDelay = (Long)clz.getMethod("getKeyInjectionDelay").invoke(conf); + this._scrollAcknowledgmentTimeout = (Long)clz.getMethod("getScrollAcknowledgmentTimeout").invoke(conf); + this._waitForIdleTimeout = (Long)clz.getMethod("getWaitForIdleTimeout").invoke(conf); + this._waitForSelectorTimeout = (Long)clz.getMethod("getWaitForSelectorTimeout").invoke(conf); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + public long getActionAcknowledgmentTimeout() { + return _actionAcknowledgmentTimeout; + } + + public void setActionAcknowledgmentTimeout(long _actionAcknowledgmentTimeout) { + this._actionAcknowledgmentTimeout = _actionAcknowledgmentTimeout; + } + + public long getKeyInjectionDelay() { + return _keyInjectionDelay; + } + + public void setKeyInjectionDelay(long _keyInjectionDelay) { + this._keyInjectionDelay = _keyInjectionDelay; + } + + public long getScrollAcknowledgmentTimeout() { + return _scrollAcknowledgmentTimeout; + } + + public void setScrollAcknowledgmentTimeout(long _scrollAcknowledgmentTimeout) { + this._scrollAcknowledgmentTimeout = _scrollAcknowledgmentTimeout; + } + + public long getWaitForIdleTimeout() { + return _waitForIdleTimeout; + } + + public void setWaitForIdleTimeout(long _waitForIdleTimeout) { + this._waitForIdleTimeout = _waitForIdleTimeout; + } + + public long getWaitForSelectorTimeout() { + return _waitForSelectorTimeout; + } + + public void setWaitForSelectorTimeout(long _waitForSelectorTimeout) { + this._waitForSelectorTimeout = _waitForSelectorTimeout; + } + + public static void setConfigurator(ConfiguratorInfo info) { + try { + Class clz = Class.forName("com.android.uiautomator.core.Configurator"); + Object conf = clz.getMethod("getInstance").invoke(null); + clz.getMethod("setActionAcknowledgmentTimeout", Long.TYPE).invoke(conf, info.getActionAcknowledgmentTimeout()); + clz.getMethod("setKeyInjectionDelay", Long.TYPE).invoke(conf, info.getKeyInjectionDelay()); + clz.getMethod("setScrollAcknowledgmentTimeout", Long.TYPE).invoke(conf, info.getScrollAcknowledgmentTimeout()); + clz.getMethod("setWaitForIdleTimeout", Long.TYPE).invoke(conf, info.getWaitForIdleTimeout()); + clz.getMethod("setWaitForSelectorTimeout", Long.TYPE).invoke(conf, info.getWaitForSelectorTimeout()); + } catch (IllegalAccessException e) { + Log.d(e.getMessage()); + } catch (InvocationTargetException e) { + Log.d(e.getMessage()); + } catch (NoSuchMethodException e) { + Log.d(e.getMessage()); + } catch (ClassNotFoundException e) { + Log.d(e.getMessage()); + } + } + + private long _actionAcknowledgmentTimeout; + private long _keyInjectionDelay; + private long _scrollAcknowledgmentTimeout; + private long _waitForIdleTimeout; + private long _waitForSelectorTimeout; +} diff --git a/src/fi/iki/elonen/NanoHTTPD.java b/src/fi/iki/elonen/NanoHTTPD.java deleted file mode 100644 index 7f848ff..0000000 --- a/src/fi/iki/elonen/NanoHTTPD.java +++ /dev/null @@ -1,1401 +0,0 @@ -package fi.iki.elonen; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.RandomAccessFile; -import java.io.SequenceInputStream; -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.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(ServerSocket serverSocket) { - if (serverSocket != null) { - try { - serverSocket.close(); - } catch (IOException e) { - } - } - } - - private static final void safeClose(Socket socket) { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - } - } - } - - private static final void safeClose(Closeable 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(); - if (inputStream == null) { - safeClose(finalAccept); - unRegisterConnection(finalAccept); - } else { - 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(); - 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 Status 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(Status 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(Status 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); - } - - /** - * Sends given response to the socket. - */ - private 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"); - } - } - - pw.print("Connection: keep-alive\r\n"); - - if (requestMethod != Method.HEAD && chunkedTransfer) { - sendAsChunked(outputStream, pw); - } else { - sendAsFixedLength(outputStream, pw); - } - outputStream.flush(); - safeClose(data); - } catch (IOException ioe) { - // Couldn't write? No can do. - } - } - - 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, PrintWriter pw) throws IOException { - int pending = data != null ? data.available() : 0; // This is to support partial sends, see serveFile() - pw.print("Content-Length: "+pending+"\r\n"); - - pw.print("\r\n"); - pw.flush(); - - 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 Status 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; - } - - /** - * Some HTTP response status codes - */ - public enum Status { - 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; - } - - public int getRequestStatus() { - return this.requestStatus; - } - - 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 InputStream 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 = inputStream; - this.outputStream = outputStream; - } - - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { - this.tempFileManager = tempFileManager; - this.inputStream = inputStream; - 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 (SocketException e) { - throw new SocketException("NanoHttpd Shutdown"); - } - if (read == -1) { - // socket was been closed - 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) { - ByteArrayInputStream splitInputStream = new ByteArrayInputStream(buf, splitbyte, rlen - splitbyte); - SequenceInputStream sequenceInputStream = new SequenceInputStream(splitInputStream, inputStream); - inputStream = sequenceInputStream; - } - - 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, 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 { - // Handle application/x-www-form-urlencoded - String postLine = ""; - char pbuf[] = new char[512]; - int read = in.read(pbuf); - while (read >= 0 && !postLine.endsWith("\r\n")) { - postLine += String.valueOf(pbuf, 0, read); - read = in.read(pbuf); - } - postLine = postLine.trim(); - decodeParms(postLine, parms); - } - } 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(); - 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 - System.err.println("Error: " + e.getMessage()); - } finally { - safeClose(fileOutputStream); - } - } - return path; - } - - private RandomAccessFile getTmpBucket() { - try { - TempFile tempFile = tempFileManager.createTempFile(); - return new RandomAccessFile(tempFile.getName(), "rw"); - } catch (Exception e) { - System.err.println("Error: " + e.getMessage()); - } - return null; - } - - /** - * 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()); - } - } - } -} \ No newline at end of file