From 2a6b5142324fca408760807a7db926fbdd635c8b Mon Sep 17 00:00:00 2001 From: n8fr8 Date: Mon, 13 Jul 2015 16:45:53 -0400 Subject: [PATCH] change BT request handling to operate directly on files We don't want to depend on HTTP being active in case there is no wifi that exists at all. In some cases, local 127.0.0.1 does not exist if the Wifi is not connected. This commit takes the code from LocalHTTPD and repurposes it for the BluetoothServer needs. --- .../fdroid/net/bluetooth/BluetoothServer.java | 229 +++++++++++++++++- .../net/bluetooth/httpish/Response.java | 18 ++ .../views/swap/SwapWorkflowActivity.java | 2 +- 3 files changed, 235 insertions(+), 14 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java index 08b5264bf..9066ba5f5 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java +++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java @@ -6,15 +6,27 @@ import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Build; import android.util.Log; +import android.webkit.MimeTypeMap; + import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.net.HttpDownloader; import org.fdroid.fdroid.net.bluetooth.httpish.Request; import org.fdroid.fdroid.net.bluetooth.httpish.Response; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import fi.iki.elonen.NanoHTTPD; /** * Act as a layer on top of LocalHTTPD server, by forwarding requests served @@ -31,9 +43,11 @@ public class BluetoothServer extends Thread { private String deviceBluetoothName = null; public final static String BLUETOOTH_NAME_TAG = "FDroid:"; + private final File webRoot; - public BluetoothServer(Context context) { + public BluetoothServer(Context context, File webRoot) { this.context = context.getApplicationContext(); + this.webRoot = webRoot; } public void close() { @@ -75,7 +89,7 @@ public class BluetoothServer extends Thread { try { BluetoothSocket clientSocket = serverSocket.accept(); if (clientSocket != null && !isInterrupted()) { - Connection client = new Connection(context, clientSocket); + Connection client = new Connection(context, clientSocket, webRoot); client.start(); clients.add(client); } else { @@ -88,15 +102,16 @@ public class BluetoothServer extends Thread { } - private static class Connection extends Thread - { + private static class Connection extends Thread { private final Context context; private final BluetoothSocket socket; + private final File webRoot; - public Connection(Context context, BluetoothSocket socket) { + public Connection(Context context, BluetoothSocket socket, File webRoot) { this.context = context.getApplicationContext(); this.socket = socket; + this.webRoot = webRoot; } @Override @@ -121,6 +136,8 @@ public class BluetoothServer extends Thread { handleRequest(incomingRequest).send(connection); } catch (IOException e) { Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage()); + + } if (isInterrupted()) @@ -134,34 +151,220 @@ public class BluetoothServer extends Thread { Log.d(TAG, "Received Bluetooth request from client, will process it now."); - try { - HttpDownloader downloader = new HttpDownloader("http://127.0.0.1:" + ( FDroidApp.port + 1 ) + "/" + request.getPath(), context); + Response.Builder builder = null; - Response.Builder builder; + try { +// HttpDownloader downloader = new HttpDownloader("http://127.0.0.1:" + ( FDroidApp.port) + "/" + request.getPath(), context); + int statusCode = 404; + int totalSize = -1; if (request.getMethod().equals(Request.Methods.HEAD)) { builder = new Response.Builder(); } else { - builder = new Response.Builder(downloader.getInputStream()); + HashMap headers = new HashMap(); + Response resp = respond(headers, "/" + request.getPath()); + + builder = new Response.Builder(resp.toContentStream()); + statusCode = resp.getStatusCode(); + totalSize = resp.getFileSize(); } // TODO: At this stage, will need to download the file to get this info. // However, should be able to make totalDownloadSize and getCacheTag work without downloading. return builder - .setStatusCode(downloader.getStatusCode()) - .setFileSize(downloader.totalDownloadSize()) + .setStatusCode(statusCode) + .setFileSize(totalSize) .build(); - } catch (IOException e) { + } catch (Exception e) { + /* if (Build.VERSION.SDK_INT <= 9) { // Would like to use the specific IOException below with a "cause", but it is // only supported on SDK 9, so I guess this is the next most useful thing. throw e; } else { throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e); - } + }*/ + + Log.e(TAG, "error processing request; sending 500 response", e); + + if (builder == null) + builder = new Response.Builder(); + + return builder + .setStatusCode(500) + .setFileSize(0) + .build(); + } } + + + private Response respond(Map headers, String uri) { + // Remove URL arguments + uri = uri.trim().replace(File.separatorChar, '/'); + if (uri.indexOf('?') >= 0) { + uri = uri.substring(0, uri.indexOf('?')); + } + + // Prohibit getting out of current directory + if (uri.contains("../")) { + return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, + "FORBIDDEN: Won't serve ../ for security reasons."); + } + + File f = new File(webRoot, uri); + if (!f.exists()) { + return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, + "Error 404, file not found."); + } + + // Browsers get confused without '/' after the directory, send a + // redirect. + if (f.isDirectory() && !uri.endsWith("/")) { + uri += "/"; + Response res = createResponse(NanoHTTPD.Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, + "Redirected: " + uri + ""); + res.addHeader("Location", uri); + return res; + } + + if (f.isDirectory()) { + // First look for index files (index.html, index.htm, etc) and if + // none found, list the directory if readable. + String indexFile = findIndexFileInDirectory(f); + if (indexFile == null) { + if (f.canRead()) { + // No index file, list the directory if it is readable + return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, ""); + } else { + return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, + "FORBIDDEN: No directory listing."); + } + } else { + return respond(headers, uri + indexFile); + } + } + + Response response = serveFile(uri, headers, f, getMimeTypeForFile(uri)); + return response != null ? response : + createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, + "Error 404, file not found."); + } + + /** + * Serves file from homeDir and its' subdirectories (only). Uses only URI, + * ignores all headers and HTTP parameters. + */ + Response serveFile(String uri, Map header, File file, String mime) { + Response res; + try { + // Calculate etag + String etag = Integer + .toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()) + .hashCode()); + + // Support (simple) skipping: + long startFrom = 0; + long endAt = -1; + String range = header.get("range"); + if (range != null) { + if (range.startsWith("bytes=")) { + range = range.substring("bytes=".length()); + int minus = range.indexOf('-'); + try { + if (minus > 0) { + startFrom = Long.parseLong(range.substring(0, minus)); + endAt = Long.parseLong(range.substring(minus + 1)); + } + } catch (NumberFormatException ignored) { + } + } + } + + // Change return code and add Content-Range header when skipping is + // requested + long fileLen = file.length(); + if (range != null && startFrom >= 0) { + if (startFrom >= fileLen) { + res = createResponse(NanoHTTPD.Response.Status.RANGE_NOT_SATISFIABLE, + NanoHTTPD.MIME_PLAINTEXT, ""); + res.addHeader("Content-Range", "bytes 0-0/" + fileLen); + res.addHeader("ETag", etag); + } else { + if (endAt < 0) { + endAt = fileLen - 1; + } + long newLen = endAt - startFrom + 1; + if (newLen < 0) { + newLen = 0; + } + + final long dataLen = newLen; + FileInputStream fis = new FileInputStream(file) { + @Override + public int available() throws IOException { + return (int) dataLen; + } + }; + fis.skip(startFrom); + + res = createResponse(NanoHTTPD.Response.Status.PARTIAL_CONTENT, mime, fis); + res.addHeader("Content-Length", "" + dataLen); + res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + + fileLen); + res.addHeader("ETag", etag); + } + } else { + if (etag.equals(header.get("if-none-match"))) + res = createResponse(NanoHTTPD.Response.Status.NOT_MODIFIED, mime, ""); + else { + res = createResponse(NanoHTTPD.Response.Status.OK, mime, new FileInputStream(file)); + res.addHeader("Content-Length", "" + fileLen); + res.addHeader("ETag", etag); + } + } + } catch (IOException ioe) { + res = createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, + "FORBIDDEN: Reading file failed."); + } + + return res; + } + + // Announce that the file server accepts partial content requests + private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, String content) { + Response res = new Response(status.getRequestStatus(), mimeType, content); + return res; + } + + // Announce that the file server accepts partial content requests + private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, InputStream content) { + Response res = new Response(status.getRequestStatus(), mimeType, content); + return res; + } + + public static String getMimeTypeForFile(String uri) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(uri); + if (extension != null) { + MimeTypeMap mime = MimeTypeMap.getSingleton(); + type = mime.getMimeTypeFromExtension(extension); + } + return type; + } + + private String findIndexFileInDirectory(File directory) { + String indexFileName = "index.html"; + File indexFile = new File(directory, indexFileName); + if (indexFile.exists()) { + return indexFileName; + } + return null; + } } + + } diff --git a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java index 3e5b274b3..1f1fc5ad5 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java +++ b/F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Response.java @@ -10,6 +10,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; +import java.io.StringBufferInputStream; import java.io.Writer; import java.util.HashMap; import java.util.Map; @@ -38,6 +39,23 @@ public class Response { this.contentStream = contentStream; } + public Response(int statusCode, String mimeType, String content) { + this.statusCode = statusCode; + this.headers = new HashMap(); + this.contentStream = new StringBufferInputStream(content); + } + + public Response(int statusCode, String mimeType, InputStream contentStream) { + this.statusCode = statusCode; + this.headers = new HashMap(); + this.contentStream = contentStream; + } + + public void addHeader (String key, String value) + { + headers.put(key, value); + } + public int getStatusCode() { return statusCode; } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 432a6180f..f61702c51 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -331,7 +331,7 @@ public class SwapWorkflowActivity extends ActionBarActivity { if (!state.isEnabled()) { state.enableSwapping(); } - new BluetoothServer(this).start(); + new BluetoothServer(this,getFilesDir()).start(); showBluetoothDeviceList(); }