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<String, String> headers = new HashMap<String, String>(); + 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<String, String> 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, + "<html><body>Redirected: <a href=\"" + + uri + "\">" + uri + "</a></body></html>"); + 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<String, String> 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<String,String>(); + this.contentStream = new StringBufferInputStream(content); + } + + public Response(int statusCode, String mimeType, InputStream contentStream) { + this.statusCode = statusCode; + this.headers = new HashMap<String,String>(); + 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(); }