PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java

https://gitlab.com/paresh/fdroidclient
Java | 361 lines | 269 code | 61 blank | 31 comment | 41 complexity | d8c132270feae2fd80206a732a26f1b7 MD5 | raw file
  1. package org.fdroid.fdroid.net.bluetooth;
  2. import android.annotation.TargetApi;
  3. import android.bluetooth.BluetoothAdapter;
  4. import android.bluetooth.BluetoothServerSocket;
  5. import android.bluetooth.BluetoothSocket;
  6. import android.util.Log;
  7. import android.webkit.MimeTypeMap;
  8. import org.fdroid.fdroid.Utils;
  9. import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
  10. import org.fdroid.fdroid.net.bluetooth.httpish.Request;
  11. import org.fdroid.fdroid.net.bluetooth.httpish.Response;
  12. import java.io.File;
  13. import java.io.FileInputStream;
  14. import java.io.IOException;
  15. import java.io.InputStream;
  16. import java.util.ArrayList;
  17. import java.util.HashMap;
  18. import java.util.List;
  19. import java.util.Map;
  20. import fi.iki.elonen.NanoHTTPD;
  21. /**
  22. * Act as a layer on top of LocalHTTPD server, by forwarding requests served
  23. * over bluetooth to that server.
  24. */
  25. public class BluetoothServer extends Thread {
  26. private static final String TAG = "BluetoothServer";
  27. private BluetoothServerSocket serverSocket;
  28. private final List<ClientConnection> clients = new ArrayList<>();
  29. private final File webRoot;
  30. private final BluetoothSwap swap;
  31. private boolean isRunning;
  32. public BluetoothServer(BluetoothSwap swap, File webRoot) {
  33. this.webRoot = webRoot;
  34. this.swap = swap;
  35. start();
  36. }
  37. public boolean isRunning() {
  38. return isRunning;
  39. }
  40. public void close() {
  41. for (ClientConnection clientConnection : clients) {
  42. clientConnection.interrupt();
  43. }
  44. interrupt();
  45. if (serverSocket != null) {
  46. Utils.closeQuietly(serverSocket);
  47. }
  48. }
  49. @TargetApi(10)
  50. @Override
  51. public void run() {
  52. isRunning = true;
  53. BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
  54. try {
  55. serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
  56. } catch (IOException e) {
  57. Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now", e);
  58. swap.stop();
  59. isRunning = false;
  60. return;
  61. }
  62. while (true) {
  63. if (isInterrupted()) {
  64. Utils.debugLog(TAG, "Server stopped so will terminate loop looking for client connections.");
  65. break;
  66. }
  67. try {
  68. BluetoothSocket clientSocket = serverSocket.accept();
  69. if (clientSocket != null) {
  70. if (isInterrupted()) {
  71. Utils.debugLog(TAG, "Server stopped after socket accepted from client, but before initiating connection.");
  72. break;
  73. }
  74. ClientConnection client = new ClientConnection(clientSocket, webRoot);
  75. client.start();
  76. clients.add(client);
  77. }
  78. } catch (IOException e) {
  79. Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients", e);
  80. }
  81. }
  82. isRunning = false;
  83. }
  84. private static class ClientConnection extends Thread {
  85. private final BluetoothSocket socket;
  86. private final File webRoot;
  87. ClientConnection(BluetoothSocket socket, File webRoot) {
  88. this.socket = socket;
  89. this.webRoot = webRoot;
  90. }
  91. @Override
  92. public void run() {
  93. Utils.debugLog(TAG, "Listening for incoming Bluetooth requests from client");
  94. BluetoothConnection connection;
  95. try {
  96. connection = new BluetoothConnection(socket);
  97. connection.open();
  98. } catch (IOException e) {
  99. Log.e(TAG, "Error listening for incoming connections over bluetooth", e);
  100. return;
  101. }
  102. while (true) {
  103. try {
  104. Utils.debugLog(TAG, "Listening for new Bluetooth request from client.");
  105. Request incomingRequest = Request.listenForRequest(connection);
  106. handleRequest(incomingRequest).send(connection);
  107. } catch (IOException e) {
  108. Log.e(TAG, "Error receiving incoming connection over bluetooth", e);
  109. break;
  110. }
  111. if (isInterrupted()) {
  112. break;
  113. }
  114. }
  115. connection.closeQuietly();
  116. }
  117. private Response handleRequest(Request request) throws IOException {
  118. Utils.debugLog(TAG, "Received Bluetooth request from client, will process it now.");
  119. Response.Builder builder = null;
  120. try {
  121. int statusCode = 404;
  122. int totalSize = -1;
  123. if (request.getMethod().equals(Request.Methods.HEAD)) {
  124. builder = new Response.Builder();
  125. } else {
  126. HashMap<String, String> headers = new HashMap<>();
  127. Response resp = respond(headers, "/" + request.getPath());
  128. builder = new Response.Builder(resp.toContentStream());
  129. statusCode = resp.getStatusCode();
  130. totalSize = resp.getFileSize();
  131. }
  132. // TODO: At this stage, will need to download the file to get this info.
  133. // However, should be able to make totalDownloadSize and getCacheTag work without downloading.
  134. return builder
  135. .setStatusCode(statusCode)
  136. .setFileSize(totalSize)
  137. .build();
  138. } catch (Exception e) {
  139. /*
  140. if (Build.VERSION.SDK_INT <= 9) {
  141. // Would like to use the specific IOException below with a "cause", but it is
  142. // only supported on SDK 9, so I guess this is the next most useful thing.
  143. throw e;
  144. } else {
  145. throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e);
  146. }*/
  147. Log.e(TAG, "error processing request; sending 500 response", e);
  148. if (builder == null) {
  149. builder = new Response.Builder();
  150. }
  151. return builder
  152. .setStatusCode(500)
  153. .setFileSize(0)
  154. .build();
  155. }
  156. }
  157. private Response respond(Map<String, String> headers, String uri) {
  158. // Remove URL arguments
  159. uri = uri.trim().replace(File.separatorChar, '/');
  160. if (uri.indexOf('?') >= 0) {
  161. uri = uri.substring(0, uri.indexOf('?'));
  162. }
  163. // Prohibit getting out of current directory
  164. if (uri.contains("../")) {
  165. return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
  166. "FORBIDDEN: Won't serve ../ for security reasons.");
  167. }
  168. File f = new File(webRoot, uri);
  169. if (!f.exists()) {
  170. return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
  171. "Error 404, file not found.");
  172. }
  173. // Browsers get confused without '/' after the directory, send a
  174. // redirect.
  175. if (f.isDirectory() && !uri.endsWith("/")) {
  176. uri += "/";
  177. Response res = createResponse(NanoHTTPD.Response.Status.REDIRECT, NanoHTTPD.MIME_HTML,
  178. "<html><body>Redirected: <a href=\"" +
  179. uri + "\">" + uri + "</a></body></html>");
  180. res.addHeader("Location", uri);
  181. return res;
  182. }
  183. if (f.isDirectory()) {
  184. // First look for index files (index.html, index.htm, etc) and if
  185. // none found, list the directory if readable.
  186. String indexFile = findIndexFileInDirectory(f);
  187. if (indexFile == null) {
  188. if (f.canRead()) {
  189. // No index file, list the directory if it is readable
  190. return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, "");
  191. }
  192. return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
  193. "FORBIDDEN: No directory listing.");
  194. }
  195. return respond(headers, uri + indexFile);
  196. }
  197. Response response = serveFile(uri, headers, f, getMimeTypeForFile(uri));
  198. return response != null ? response :
  199. createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
  200. "Error 404, file not found.");
  201. }
  202. /**
  203. * Serves file from homeDir and its' subdirectories (only). Uses only URI,
  204. * ignores all headers and HTTP parameters.
  205. */
  206. Response serveFile(String uri, Map<String, String> header, File file, String mime) {
  207. Response res;
  208. try {
  209. // Calculate etag
  210. String etag = Integer
  211. .toHexString((file.getAbsolutePath() + file.lastModified() + String.valueOf(file.length()))
  212. .hashCode());
  213. // Support (simple) skipping:
  214. long startFrom = 0;
  215. long endAt = -1;
  216. String range = header.get("range");
  217. if (range != null && range.startsWith("bytes=")) {
  218. range = range.substring("bytes=".length());
  219. int minus = range.indexOf('-');
  220. try {
  221. if (minus > 0) {
  222. startFrom = Long.parseLong(range.substring(0, minus));
  223. endAt = Long.parseLong(range.substring(minus + 1));
  224. }
  225. } catch (NumberFormatException ignored) {
  226. }
  227. }
  228. // Change return code and add Content-Range header when skipping is
  229. // requested
  230. long fileLen = file.length();
  231. if (range != null && startFrom >= 0) {
  232. if (startFrom >= fileLen) {
  233. res = createResponse(NanoHTTPD.Response.Status.RANGE_NOT_SATISFIABLE,
  234. NanoHTTPD.MIME_PLAINTEXT, "");
  235. res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
  236. res.addHeader("ETag", etag);
  237. } else {
  238. if (endAt < 0) {
  239. endAt = fileLen - 1;
  240. }
  241. long newLen = endAt - startFrom + 1;
  242. if (newLen < 0) {
  243. newLen = 0;
  244. }
  245. final long dataLen = newLen;
  246. FileInputStream fis = new FileInputStream(file) {
  247. @Override
  248. public int available() throws IOException {
  249. return (int) dataLen;
  250. }
  251. };
  252. fis.skip(startFrom);
  253. res = createResponse(NanoHTTPD.Response.Status.PARTIAL_CONTENT, mime, fis);
  254. res.addHeader("Content-Length", String.valueOf(dataLen));
  255. res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/"
  256. + fileLen);
  257. res.addHeader("ETag", etag);
  258. }
  259. } else {
  260. if (etag.equals(header.get("if-none-match"))) {
  261. res = createResponse(NanoHTTPD.Response.Status.NOT_MODIFIED, mime, "");
  262. } else {
  263. res = createResponse(NanoHTTPD.Response.Status.OK, mime, new FileInputStream(file));
  264. res.addHeader("Content-Length", String.valueOf(fileLen));
  265. res.addHeader("ETag", etag);
  266. }
  267. }
  268. } catch (IOException ioe) {
  269. res = createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
  270. "FORBIDDEN: Reading file failed.");
  271. }
  272. return res;
  273. }
  274. // Announce that the file server accepts partial content requests
  275. private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, String content) {
  276. return new Response(status.getRequestStatus(), mimeType, content);
  277. }
  278. // Announce that the file server accepts partial content requests
  279. private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, InputStream content) {
  280. return new Response(status.getRequestStatus(), mimeType, content);
  281. }
  282. public static String getMimeTypeForFile(String uri) {
  283. String type = null;
  284. String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
  285. if (extension != null) {
  286. MimeTypeMap mime = MimeTypeMap.getSingleton();
  287. type = mime.getMimeTypeFromExtension(extension);
  288. }
  289. return type;
  290. }
  291. private String findIndexFileInDirectory(File directory) {
  292. String indexFileName = "index.html";
  293. File indexFile = new File(directory, indexFileName);
  294. if (indexFile.exists()) {
  295. return indexFileName;
  296. }
  297. return null;
  298. }
  299. }
  300. }