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.
This commit is contained in:
n8fr8 2015-07-13 16:45:53 -04:00
parent e930e03378
commit 2a6b514232
3 changed files with 235 additions and 14 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -331,7 +331,7 @@ public class SwapWorkflowActivity extends ActionBarActivity {
if (!state.isEnabled()) {
state.enableSwapping();
}
new BluetoothServer(this).start();
new BluetoothServer(this,getFilesDir()).start();
showBluetoothDeviceList();
}