package org.fdroid.fdroid.net.bluetooth.httpish; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.HashMap; import java.util.Locale; import java.util.Map; public final class Request { private static final String TAG = "bluetooth.Request"; public interface Methods { String HEAD = "HEAD"; String GET = "GET"; } private String method; private String path; private Map<String, String> headers; private final BluetoothConnection connection; private final Writer output; private final InputStream input; private Request(String method, String path, BluetoothConnection connection) { this.method = method; this.path = path; this.connection = connection; output = new OutputStreamWriter(connection.getOutputStream()); input = connection.getInputStream(); } public static Request createHEAD(String path, BluetoothConnection connection) { return new Request(Methods.HEAD, path, connection); } public static Request createGET(String path, BluetoothConnection connection) { return new Request(Methods.GET, path, connection); } public String getHeaderValue(String header) { return headers.containsKey(header) ? headers.get(header) : null; } public Response send() throws IOException { Utils.debugLog(TAG, "Sending request to server (" + path + ")"); output.write(method); output.write(' '); output.write(path); output.write("\n\n"); output.flush(); Utils.debugLog(TAG, "Finished sending request, now attempting to read response status code..."); int responseCode = readResponseCode(); Utils.debugLog(TAG, "Read response code " + responseCode + " from server, now reading headers..."); Map<String, String> headers = readHeaders(); Utils.debugLog(TAG, "Read " + headers.size() + " headers"); if (method.equals(Methods.HEAD)) { Utils.debugLog(TAG, "Request was a " + Methods.HEAD + " request, not including anything other than headers and status..."); return new Response(responseCode, headers); } Utils.debugLog(TAG, "Request was a " + Methods.GET + " request, so including content stream in response..."); return new Response(responseCode, headers, connection.getInputStream()); } /** * Helper function used by listenForRequest(). * The reason it is here is because the listenForRequest() is a static function, which would * need to instantiate it's own InputReaders from the bluetooth connection. However, we already * have that happening in a Request, so it is in some ways simpler to delegate to a member * method like this. */ private boolean listen() throws IOException { String requestLine = readLine(); if (requestLine == null || requestLine.trim().length() == 0) { return false; } String[] parts = requestLine.split("\\s+"); // First part is the method (GET/HEAD), second is the path (/fdroid/repo/index.jar) if (parts.length < 2) { return false; } method = parts[0].toUpperCase(Locale.ENGLISH); path = parts[1]; headers = readHeaders(); return true; } /** * This is a blocking method, which will wait until a full Request is received. */ public static Request listenForRequest(BluetoothConnection connection) throws IOException { Request request = new Request("", "", connection); return request.listen() ? request : null; } /** * First line of a HTTP 1.1 response is the status line: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 * The first part is the HTTP version, followed by a space, then the status code, then * a space, and then the status label (which may contain spaces). */ private int readResponseCode() throws IOException { String line = readLine(); // TODO: Error handling int firstSpace = line.indexOf(' '); int secondSpace = line.indexOf(' ', firstSpace + 1); String status = line.substring(firstSpace + 1, secondSpace); return Integer.parseInt(status); } private String readLine() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); String line = null; while (line == null) { while (input.available() > 0) { int b = input.read(); if (((char) b) == '\n') { if (baos.size() > 0) { line = new String(baos.toByteArray()); } return line; } baos.write(b); } try { Thread.sleep(100); } catch (Exception e) { // ignore } } return line; } /** * Subsequent lines (after the status line) represent the headers, which are case * insensitive and may be multi-line. We don't deal with multi-line headers in * our HTTP-ish implementation. */ private Map<String, String> readHeaders() throws IOException { Map<String, String> headers = new HashMap<>(); String responseLine = readLine(); while (responseLine != null) { // TODO: Error handling String[] parts = responseLine.split(":"); if (parts.length > 1) { String header = parts[0].trim(); String value = parts[1].trim(); headers.put(header, value); } if (input.available() > 0) { responseLine = readLine(); } else { break; } } return headers; } public String getPath() { return path; } public String getMethod() { return method; } }