diff --git a/app/src/full/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java b/app/src/full/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java
index 197cefa44..b7b89d6b2 100644
--- a/app/src/full/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java
+++ b/app/src/full/java/kellinwood/security/zipsigner/optional/SignatureBlockGenerator.java
@@ -38,10 +38,10 @@ public class SignatureBlockGenerator {
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
- JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.getSignatureAlgorithm()).setProvider("SC");
+ JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.getSignatureAlgorithm()).setProvider("BC");
ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.getPrivateKey());
- JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("SC");
+ JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("BC");
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index 7ea5905cc..6256b6209 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -17,6 +17,7 @@ import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.RepoUpdater;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
@@ -103,7 +104,7 @@ public final class LocalRepoManager {
repoDir = new SanitizedFile(fdroidDir, "repo");
repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO");
iconsDir = new SanitizedFile(repoDir, "icons");
- xmlIndexJar = new SanitizedFile(repoDir, "index.jar");
+ xmlIndexJar = new SanitizedFile(repoDir, RepoUpdater.SIGNED_FILE_NAME);
xmlIndexJarUnsigned = new SanitizedFile(repoDir, "index.unsigned.jar");
if (!fdroidDir.exists() && !fdroidDir.mkdir()) {
@@ -481,7 +482,7 @@ public final class LocalRepoManager {
public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException {
BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
JarOutputStream jo = new JarOutputStream(bo);
- JarEntry je = new JarEntry("index.xml");
+ JarEntry je = new JarEntry(RepoUpdater.DATA_FILE_NAME);
jo.putNextEntry(je);
new IndexXmlBuilder().build(context, apps, jo);
jo.close();
diff --git a/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java b/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
index 4ba03df04..4d4aade69 100644
--- a/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
+++ b/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
@@ -1,22 +1,52 @@
package org.fdroid.fdroid.net;
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
import android.content.Context;
-import android.content.Intent;
import android.net.Uri;
-import android.util.Log;
-import android.webkit.MimeTypeMap;
import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
import org.fdroid.fdroid.BuildConfig;
-import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import javax.net.ssl.SSLServerSocketFactory;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
-import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
@@ -27,314 +57,104 @@ import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
+/**
+ * A HTTP server for serving the files that are being swapped via WiFi, etc.
+ * The only changes were to remove unneeded extras like {@code main()}, the
+ * plugin interface, and custom CORS header manipulation.
+ *
+ * This is mostly just synced from {@code SimpleWebServer.java} from NanoHTTPD.
+ *
+ * @see webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java
+ */
public class LocalHTTPD extends NanoHTTPD {
private static final String TAG = "LocalHTTPD";
- private final Context context;
- private final File webRoot;
+ /**
+ * Default Index file names.
+ */
+ public static final String[] INDEX_FILE_NAMES = {"index.html"};
+ private final Context context;
+
+ protected List rootDirs;
+
+ /**
+ * Configure and start the webserver. This also sets the MIME Types only
+ * for files that should be downloadable when a browser is used to display
+ * the swap repo, rather than the F-Droid client. The other file types
+ * should not be added because it could expose exploits to the browser.
+ */
public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
super(hostname, port);
- this.webRoot = webRoot;
+ rootDirs = Collections.singletonList(webRoot);
this.context = context.getApplicationContext();
if (useHttps) {
enableHTTPS();
}
+ MIME_TYPES = new HashMap<>(); // ignore nanohttpd's list
+ MIME_TYPES.put("apk", "application/vnd.android.package-archive");
+ MIME_TYPES.put("html", "text/html");
+ MIME_TYPES.put("png", "image/png");
+ MIME_TYPES.put("xml", "application/xml");
+ }
+
+ private boolean canServeUri(String uri, File homeDir) {
+ boolean canServeUri;
+ File f = new File(homeDir, uri);
+ canServeUri = f.exists();
+ return canServeUri;
}
/**
* URL-encodes everything between "/"-characters. Encodes spaces as '%20'
* instead of '+'.
*/
- private String encodeUriBetweenSlashes(String uri) {
+ private String encodeUri(String uri) {
String newUri = "";
StringTokenizer st = new StringTokenizer(uri, "/ ", true);
while (st.hasMoreTokens()) {
String tok = st.nextToken();
- switch (tok) {
- case "/":
- newUri += "/";
- break;
- case " ":
- newUri += "%20";
- break;
- default:
- try {
- newUri += URLEncoder.encode(tok, "UTF-8");
- } catch (UnsupportedEncodingException ignored) {
- }
- break;
+ if ("/".equals(tok)) {
+ newUri += "/";
+ } else if (" ".equals(tok)) {
+ newUri += "%20";
+ } else {
+ try {
+ newUri += URLEncoder.encode(tok, "UTF-8");
+ } catch (UnsupportedEncodingException ignored) {
+ }
}
}
return newUri;
}
- private void requestSwap(String repo) {
- Utils.debugLog(TAG, "Received request to swap with " + repo);
- Utils.debugLog(TAG, "Showing confirm screen to check whether that is okay with the user.");
-
- Uri repoUri = Uri.parse(repo);
- Intent intent = new Intent(context, SwapWorkflowActivity.class);
- intent.setData(repoUri);
- intent.putExtra(SwapWorkflowActivity.EXTRA_CONFIRM, true);
- intent.putExtra(SwapWorkflowActivity.EXTRA_PREVENT_FURTHER_SWAP_REQUESTS, true);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- }
-
- @Override
- public Response serve(IHTTPSession session) {
-
- if (session.getMethod() == Method.POST) {
- try {
- session.parseBody(new HashMap());
- } catch (IOException e) {
- Log.e(TAG, "An error occured while parsing the POST body", e);
- return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
- "Internal server error, check logcat on server for details.");
- } catch (ResponseException re) {
- return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
- }
-
- return handlePost(session);
- }
- return handleGet(session);
- }
-
- private Response handlePost(IHTTPSession session) {
- Uri uri = Uri.parse(session.getUri());
- switch (uri.getPath()) {
- case "/request-swap":
- if (!session.getParms().containsKey("repo")) {
- Log.e(TAG, "Malformed /request-swap request to local repo HTTP server."
- + " Should have posted a 'repo' parameter.");
- return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT,
- "Requires 'repo' parameter to be posted.");
- }
- requestSwap(session.getParms().get("repo"));
- return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "Swap request received.");
- }
- return newFixedLengthResponse("");
- }
-
- private Response handleGet(IHTTPSession session) {
-
- Map header = session.getHeaders();
- Map parms = session.getParms();
- String uri = session.getUri();
-
- if (BuildConfig.DEBUG) {
- Utils.debugLog(TAG, session.getMethod() + " '" + uri + "' ");
-
- Iterator e = header.keySet().iterator();
- while (e.hasNext()) {
- String value = e.next();
- Utils.debugLog(TAG, " HDR: '" + value + "' = '" + header.get(value) + "'");
- }
- e = parms.keySet().iterator();
- while (e.hasNext()) {
- String value = e.next();
- Utils.debugLog(TAG, " PRM: '" + value + "' = '" + parms.get(value) + "'");
- }
- }
-
- if (!webRoot.isDirectory()) {
- return createResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
- "INTERNAL ERRROR: given path is not a directory (" + webRoot + ").");
- }
-
- return respond(Collections.unmodifiableMap(header), uri);
- }
-
- private void enableHTTPS() {
- try {
- LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
- SSLServerSocketFactory factory = NanoHTTPD.makeSSLSocketFactory(
- localRepoKeyStore.getKeyStore(),
- localRepoKeyStore.getKeyManagers());
- makeSecure(factory, null);
- } catch (LocalRepoKeyStore.InitException | IOException e) {
- Log.e(TAG, "Could not enable HTTPS", e);
- }
- }
-
- 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(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
- "FORBIDDEN: Won't serve ../ for security reasons.");
- }
-
- File f = new File(webRoot, uri);
- if (!f.exists()) {
- return createResponse(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(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(Response.Status.OK, NanoHTTPD.MIME_HTML,
- listDirectory(uri, f));
- } else {
- return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
- "FORBIDDEN: No directory listing.");
- }
- } else {
- return respond(headers, uri + indexFile);
- }
- }
-
- Response response = serveFile(headers, f, getAndroidMimeTypeForFile(uri));
- return response != null ? response :
- createResponse(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.
- */
- private Response serveFile(Map header, File file, String mime) {
- Response res;
- try {
- // Calculate etag
- String etag = Integer
- .toHexString((file.getAbsolutePath() + file.lastModified() + String.valueOf(file.length()))
- .hashCode());
-
- // Support (simple) skipping:
- long startFrom = 0;
- long endAt = -1;
- String range = header.get("range");
- if (range != null && 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(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;
- }
- };
- long skipped = fis.skip(startFrom);
- if (skipped != startFrom) {
- throw new IOException("unable to skip the required " + startFrom + " bytes.");
- }
-
- res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis);
- res.addHeader("Content-Length", String.valueOf(dataLen));
- res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/"
- + fileLen);
- res.addHeader("ETag", etag);
- }
- } else {
- if (etag.equals(header.get("if-none-match"))) {
- res = createResponse(Response.Status.NOT_MODIFIED, mime, "");
- } else {
- res = createResponse(Response.Status.OK, mime, new FileInputStream(file));
- res.addHeader("Content-Length", String.valueOf(fileLen));
- res.addHeader("ETag", etag);
- }
- }
- } catch (IOException ioe) {
- res = createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
- "FORBIDDEN: Reading file failed.");
- }
-
- return res;
- }
-
- // Announce that the file server accepts partial content requests
- private Response createResponse(Response.Status status, String mimeType, InputStream message) {
- Response res = newChunkedResponse(status, mimeType, message);
- res.addHeader("Accept-Ranges", "bytes");
- return res;
- }
-
- // Announce that the file server accepts partial content requests
- private Response createResponse(Response.Status status, String mimeType, String message) {
- Response res = newFixedLengthResponse(status, mimeType, message);
- res.addHeader("Accept-Ranges", "bytes");
- return res;
- }
-
- private static String getAndroidMimeTypeForFile(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;
+ for (String fileName : LocalHTTPD.INDEX_FILE_NAMES) {
+ File indexFile = new File(directory, fileName);
+ if (indexFile.isFile()) {
+ return fileName;
+ }
}
return null;
}
- private String listDirectory(String uri, File f) {
+ protected Response getForbiddenResponse(String s) {
+ return newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "FORBIDDEN: " + s);
+ }
+
+ protected Response getInternalErrorResponse(String s) {
+ return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "INTERNAL ERROR: " + s);
+ }
+
+ protected Response getNotFoundResponse() {
+ return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Error 404, file not found.");
+ }
+
+ protected String listDirectory(String uri, File f) {
String heading = "Directory " + uri;
- StringBuilder msg = new StringBuilder("" + heading
- + "" +
- "" + heading + "
");
+ StringBuilder msg =
+ new StringBuilder("" + heading + "" + "" + heading + "
");
String up = null;
if (uri.length() > 1) {
@@ -346,6 +166,7 @@ public class LocalHTTPD extends NanoHTTPD {
}
List files = Arrays.asList(f.list(new FilenameFilter() {
+
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isFile();
@@ -353,6 +174,7 @@ public class LocalHTTPD extends NanoHTTPD {
}));
Collections.sort(files);
List directories = Arrays.asList(f.list(new FilenameFilter() {
+
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isDirectory();
@@ -364,35 +186,27 @@ public class LocalHTTPD extends NanoHTTPD {
if (up != null || directories.size() > 0) {
msg.append("");
}
if (files.size() > 0) {
msg.append("");
for (String file : files) {
- msg.append("").append(file)
- .append("");
+ msg.append("").append(file).append("");
File curFile = new File(f, file);
long len = curFile.length();
msg.append(" (");
if (len < 1024) {
msg.append(len).append(" bytes");
} else if (len < 1024 * 1024) {
- msg.append(len / 1024).append('.').append(len % 1024 / 10 % 100)
- .append(" KB");
+ msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
} else {
- msg.append(len / (1024 * 1024)).append('.')
- .append(len % (1024 * 1024) / 10 % 100).append(" MB");
+ msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB");
}
msg.append(")");
}
@@ -403,4 +217,252 @@ public class LocalHTTPD extends NanoHTTPD {
msg.append("");
return msg.toString();
}
+
+ public static Response newFixedLengthResponse(IStatus status, String mimeType, String message) {
+ Response response = NanoHTTPD.newFixedLengthResponse(status, mimeType, message);
+ response.addHeader("Accept-Ranges", "bytes");
+ return response;
+ }
+
+ private Response respond(Map headers, IHTTPSession session, String uri) {
+ return defaultRespond(headers, session, uri);
+ }
+
+ private Response defaultRespond(Map headers, IHTTPSession session, 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 getForbiddenResponse("Won't serve ../ for security reasons.");
+ }
+
+ boolean canServeUri = false;
+ File homeDir = null;
+ for (int i = 0; !canServeUri && i < this.rootDirs.size(); i++) {
+ homeDir = this.rootDirs.get(i);
+ canServeUri = canServeUri(uri, homeDir);
+ }
+ if (!canServeUri) {
+ return getNotFoundResponse();
+ }
+
+ // Browsers get confused without '/' after the directory, send a
+ // redirect.
+ File f = new File(homeDir, uri);
+ if (f.isDirectory() && !uri.endsWith("/")) {
+ uri += "/";
+ Response res =
+ newFixedLengthResponse(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 newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, listDirectory(uri, f));
+ } else {
+ return getForbiddenResponse("No directory listing.");
+ }
+ } else {
+ return respond(headers, session, uri + indexFile);
+ }
+ }
+ String mimeTypeForFile = getMimeTypeForFile(uri);
+ Response response = serveFile(uri, headers, f, mimeTypeForFile);
+ return response != null ? response : getNotFoundResponse();
+ }
+
+ @Override
+ public Response serve(IHTTPSession session) {
+ Map header = session.getHeaders();
+ Map parms = session.getParms();
+ String uri = session.getUri();
+
+ if (BuildConfig.DEBUG) {
+ System.out.println(session.getMethod() + " '" + uri + "' ");
+
+ Iterator e = header.keySet().iterator();
+ while (e.hasNext()) {
+ String value = e.next();
+ System.out.println(" HDR: '" + value + "' = '" + header.get(value) + "'");
+ }
+ e = parms.keySet().iterator();
+ while (e.hasNext()) {
+ String value = e.next();
+ System.out.println(" PRM: '" + value + "' = '" + parms.get(value) + "'");
+ }
+ }
+
+ if (session.getMethod() == Method.POST) {
+ try {
+ session.parseBody(new HashMap());
+ } catch (IOException e) {
+ return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
+ "Internal server error, check logcat on server for details.");
+ } catch (ResponseException re) {
+ return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
+ }
+
+ return handlePost(session);
+ }
+
+ for (File homeDir : this.rootDirs) {
+ // Make sure we won't die of an exception later
+ if (!homeDir.isDirectory()) {
+ return getInternalErrorResponse("given path is not a directory (" + homeDir + ").");
+ }
+ }
+ return respond(Collections.unmodifiableMap(header), session, uri);
+ }
+
+ private Response handlePost(IHTTPSession session) {
+ Uri uri = Uri.parse(session.getUri());
+ switch (uri.getPath()) {
+ case "/request-swap":
+ if (!session.getParms().containsKey("repo")) {
+ return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT,
+ "Requires 'repo' parameter to be posted.");
+ }
+ SwapWorkflowActivity.requestSwap(context, session.getParms().get("repo"));
+ return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "Swap request received.");
+ }
+ return newFixedLengthResponse("");
+ }
+
+ /**
+ * 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) {
+ }
+ }
+ }
+
+ // get if-range header. If present, it must match etag or else we
+ // should ignore the range request
+ String ifRange = header.get("if-range");
+ boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange));
+
+ String ifNoneMatch = header.get("if-none-match");
+ boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag));
+
+ // Change return code and add Content-Range header when skipping is
+ // requested
+ long fileLen = file.length();
+ if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) {
+ // range request that matches current etag
+ // and the startFrom of the range is satisfiable
+ if (headerIfNoneMatchPresentAndMatching) {
+ // range request that matches current etag
+ // and the startFrom of the range is satisfiable
+ // would return range from file
+ // respond with not-modified
+ res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
+ res.addHeader("ETag", etag);
+ } else {
+ if (endAt < 0) {
+ endAt = fileLen - 1;
+ }
+ long newLen = endAt - startFrom + 1;
+ if (newLen < 0) {
+ newLen = 0;
+ }
+
+ FileInputStream fis = new FileInputStream(file);
+ fis.skip(startFrom);
+
+ res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen);
+ res.addHeader("Accept-Ranges", "bytes");
+ res.addHeader("Content-Length", "" + newLen);
+ res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
+ res.addHeader("ETag", etag);
+ }
+ } else {
+
+ if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) {
+ // return the size of the file
+ // 4xx responses are not trumped by if-none-match
+ res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
+ res.addHeader("Content-Range", "bytes */" + fileLen);
+ res.addHeader("ETag", etag);
+ } else if (range == null && headerIfNoneMatchPresentAndMatching) {
+ // full-file-fetch request
+ // would return entire file
+ // respond with not-modified
+ res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
+ res.addHeader("ETag", etag);
+ } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) {
+ // range request that doesn't match current etag
+ // would return entire (different) file
+ // respond with not-modified
+
+ res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
+ res.addHeader("ETag", etag);
+ } else {
+ // supply the file
+ res = newFixedFileResponse(file, mime);
+ res.addHeader("Content-Length", "" + fileLen);
+ res.addHeader("ETag", etag);
+ }
+ }
+ } catch (IOException ioe) {
+ res = getForbiddenResponse("Reading file failed.");
+ }
+
+ return res;
+ }
+
+ private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException {
+ Response res;
+ res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length());
+ res.addHeader("Accept-Ranges", "bytes");
+ return res;
+ }
+
+ private void enableHTTPS() {
+ try {
+ LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
+ SSLServerSocketFactory factory = NanoHTTPD.makeSSLSocketFactory(
+ localRepoKeyStore.getKeyStore(),
+ localRepoKeyStore.getKeyManagers());
+ makeSecure(factory, null);
+ } catch (LocalRepoKeyStore.InitException | IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ protected Response addCORSHeaders(Map queryHeaders, Response resp, String cors) {
+ resp.addHeader("Access-Control-Allow-Credentials", "false");
+ resp.addHeader("Access-Control-Allow-Methods", "GET, POST, HEAD");
+ // TODO add HTTP Content Security Policy headers
+ return resp;
+ }
}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index 71f86e287..fd8616fd3 100644
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -123,6 +123,16 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private LocalBroadcastManager localBroadcastManager;
private WifiManager wifiManager;
+ public static void requestSwap(Context context, String repo) {
+ Uri repoUri = Uri.parse(repo);
+ Intent intent = new Intent(context, SwapWorkflowActivity.class);
+ intent.setData(repoUri);
+ intent.putExtra(EXTRA_CONFIRM, true);
+ intent.putExtra(EXTRA_PREVENT_FURTHER_SWAP_REQUESTS, true);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
@NonNull
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
diff --git a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java
index 92d78bbc1..6432cd261 100644
--- a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java
+++ b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java
@@ -83,6 +83,9 @@ import java.util.jar.JarFile;
public class RepoUpdater {
private static final String TAG = "RepoUpdater";
+ public static final String SIGNED_FILE_NAME = "index.jar";
+ public static final String DATA_FILE_NAME = "index.xml";
+
final String indexUrl;
@NonNull
@@ -205,7 +208,7 @@ public class RepoUpdater {
FDroidApp.disableBouncyCastleOnLollipop();
JarFile jarFile = new JarFile(downloadedFile, true);
- JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml");
+ JarEntry indexEntry = (JarEntry) jarFile.getEntry(RepoUpdater.DATA_FILE_NAME);
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
processIndexListener, repo.address, (int) indexEntry.getSize());
diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java
index a1b02e49e..a10d03512 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -382,7 +382,7 @@ public class App extends ValueObject implements Comparable, Parcelable {
SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir);
app.installedApk = new Apk();
if (apkFile.canRead()) {
- String hashType = "SHA-256";
+ String hashType = "sha256";
String hash = Utils.getBinaryHash(apkFile, hashType);
if (TextUtils.isEmpty(hash)) {
return null;
diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java
index 75f9416e5..63bfe99b2 100644
--- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java
+++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java
@@ -53,7 +53,7 @@ import java.net.URL;
public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
- private static final String HEADER_FIELD_ETAG = "ETag";
+ static final String HEADER_FIELD_ETAG = "ETag";
private final String username;
private final String password;
@@ -167,6 +167,7 @@ public class HttpDownloader extends Downloader {
if (isSwapUrl(sourceUrl)) {
// swap never works with a proxy, its unrouted IP on the same subnet
connection = (HttpURLConnection) sourceUrl.openConnection();
+ connection.setRequestProperty("Connection", "Close"); // avoid keep-alive
} else {
if (queryString != null) {
connection = NetCipher.getHttpURLConnection(new URL(urlString + "?" + queryString));
diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java
index dbb5e7b47..85b30525a 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java
@@ -55,6 +55,7 @@ import android.widget.TextView;
import android.widget.Toast;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.RepoUpdater;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.CursorAdapterCompat;
@@ -567,7 +568,7 @@ public class ManageReposActivity extends AppCompatActivity
return addressWithoutIndex;
}
- final Uri uri = builder.appendPath("index.jar").build();
+ final Uri uri = builder.appendPath(RepoUpdater.SIGNED_FILE_NAME).build();
try {
if (checkForRepository(uri)) {
diff --git a/app/src/testFull/java/org/fdroid/fdroid/localrepo/LocalRepoKeyStoreTest.java b/app/src/testFull/java/org/fdroid/fdroid/localrepo/LocalRepoKeyStoreTest.java
new file mode 100644
index 000000000..e45d48783
--- /dev/null
+++ b/app/src/testFull/java/org/fdroid/fdroid/localrepo/LocalRepoKeyStoreTest.java
@@ -0,0 +1,57 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.content.Context;
+import android.text.TextUtils;
+import org.apache.commons.io.IOUtils;
+import org.fdroid.fdroid.RepoUpdater;
+import org.fdroid.fdroid.Utils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(RobolectricTestRunner.class)
+public class LocalRepoKeyStoreTest {
+
+ @Test
+ public void testSignZip() throws IOException, LocalRepoKeyStore.InitException, RepoUpdater.SigningException {
+ Context context = RuntimeEnvironment.application;
+
+ File xmlIndexJarUnsigned = File.createTempFile(getClass().getName(), "unsigned.jar");
+ BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
+ JarOutputStream jo = new JarOutputStream(bo);
+ JarEntry je = new JarEntry(RepoUpdater.DATA_FILE_NAME);
+ jo.putNextEntry(je);
+ InputStream inputStream = getClass().getClassLoader().getResourceAsStream("smallRepo.xml");
+ IOUtils.copy(inputStream, jo);
+ jo.close();
+ bo.close();
+
+ LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
+ Certificate localCert = localRepoKeyStore.getCertificate();
+ assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
+
+ File xmlIndexJar = File.createTempFile(getClass().getName(), RepoUpdater.SIGNED_FILE_NAME);
+ localRepoKeyStore.signZip(xmlIndexJarUnsigned, xmlIndexJar);
+
+ JarFile jarFile = new JarFile(xmlIndexJar, true);
+ JarEntry indexEntry = (JarEntry) jarFile.getEntry(RepoUpdater.DATA_FILE_NAME);
+ byte[] data = IOUtils.toByteArray(jarFile.getInputStream(indexEntry));
+ assertEquals(17187, data.length);
+ assertNotNull(RepoUpdater.getSigningCertFromJar(indexEntry));
+ }
+}
diff --git a/app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java b/app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java
new file mode 100644
index 000000000..373eaad10
--- /dev/null
+++ b/app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java
@@ -0,0 +1,449 @@
+package org.fdroid.fdroid.net;
+
+/*
+ * #%L
+ * NanoHttpd-Webserver
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the nanohttpd nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+import android.content.Context;
+import android.text.TextUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowLog;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Synced from NanoHTTPD's {@code TestHttpServer.java}
+ *
+ * @see webserver/src/test/java/fi/iki/elonen/LocalHTTPDTest.java
+ */
+@SuppressWarnings("LineLength")
+@RunWith(RobolectricTestRunner.class)
+public class LocalHTTPDTest {
+
+ private static ClassLoader classLoader;
+ private static LocalHTTPD localHttpd;
+ private static Thread serverStartThread;
+ private static File webRoot;
+
+ @Before
+ public void setUp() throws Exception {
+ ShadowLog.stream = System.out;
+ classLoader = getClass().getClassLoader();
+
+ final Context context = RuntimeEnvironment.application.getApplicationContext();
+ webRoot = context.getFilesDir();
+ FileUtils.deleteDirectory(webRoot);
+ assertTrue(webRoot.mkdir());
+ assertTrue(webRoot.isDirectory());
+
+ final File testdir = new File(webRoot, "testdir");
+ assertTrue(testdir.mkdir());
+ IOUtils.copy(classLoader.getResourceAsStream("test.html"),
+ new FileOutputStream(new File(testdir, "test.html")));
+
+ serverStartThread = new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ localHttpd = new LocalHTTPD(
+ context,
+ "localhost",
+ 8888,
+ webRoot,
+ false);
+ try {
+ localHttpd.start();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ assertTrue(localHttpd.isAlive());
+ }
+ });
+ serverStartThread.start();
+ // give the server some tine to start.
+ Thread.sleep(100);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ localHttpd.stop();
+ serverStartThread.join(5000);
+ assertFalse(localHttpd.isAlive());
+ assertFalse(serverStartThread.isAlive());
+ }
+
+ @Test
+ public void doTest404() throws Exception {
+ URL url = new URL("http://localhost:8888/xxx/yyy.html");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setReadTimeout(5000);
+ connection.connect();
+ Assert.assertEquals(404, connection.getResponseCode());
+ connection.disconnect();
+ }
+
+ @Test
+ public void doSomeBasicTest() throws Exception {
+ URL url = new URL("http://localhost:8888/testdir/test.html");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ assertEquals(200, connection.getResponseCode());
+ String string = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ Assert.assertEquals("\n\ndummy\n\n\n\tit works
\n\n", string);
+ connection.disconnect();
+
+ url = new URL("http://localhost:8888/");
+ connection = (HttpURLConnection) url.openConnection();
+ assertEquals(200, connection.getResponseCode());
+ string = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ System.out.println("REPLY: " + string);
+ assertTrue(string.indexOf("testdir") > 0);
+ connection.disconnect();
+
+ url = new URL("http://localhost:8888/testdir");
+ connection = (HttpURLConnection) url.openConnection();
+ assertEquals(200, connection.getResponseCode());
+ string = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ assertTrue(string.indexOf("test.html") > 0);
+ connection.disconnect();
+
+ IOUtils.copy(classLoader.getResourceAsStream("index.microg.jar"),
+ new FileOutputStream(new File(webRoot, "index.microg.jar")));
+ url = new URL("http://localhost:8888/index.microg.jar");
+ connection = (HttpURLConnection) url.openConnection();
+ assertEquals(200, connection.getResponseCode());
+ byte[] actual = IOUtils.toByteArray(connection.getInputStream());
+ byte[] expected = IOUtils.toByteArray(classLoader.getResourceAsStream("index.microg.jar"));
+ Assert.assertArrayEquals(expected, actual);
+ connection.disconnect();
+
+ IOUtils.copy(classLoader.getResourceAsStream("extendedPerms.xml"),
+ new FileOutputStream(new File(webRoot, "extendedPerms.xml")));
+ url = new URL("http://localhost:8888/extendedPerms.xml");
+ connection = (HttpURLConnection) url.openConnection();
+ assertEquals(200, connection.getResponseCode());
+ actual = IOUtils.toByteArray(connection.getInputStream());
+ expected = IOUtils.toByteArray(classLoader.getResourceAsStream("extendedPerms.xml"));
+ Assert.assertArrayEquals(expected, actual);
+ connection.disconnect();
+ }
+
+ @Test
+ public void testAPKMimeType() throws IOException {
+ String fileName = "urzip.apk";
+ String mimeType = "application/vnd.android.package-archive";
+ IOUtils.copy(classLoader.getResourceAsStream(fileName),
+ new FileOutputStream(new File(webRoot, fileName)));
+ URL url = new URL("http://localhost:8888/" + fileName);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("HEAD");
+ assertEquals(200, connection.getResponseCode());
+ Assert.assertEquals(mimeType, connection.getContentType());
+ connection.disconnect();
+ }
+
+ @Test
+ public void testHeadRequest() throws IOException {
+ File indexFile = new File(webRoot, "index.html");
+ IOUtils.copy(classLoader.getResourceAsStream("index.html"),
+ new FileOutputStream(indexFile));
+
+ URL url = new URL("http://localhost:8888/");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("HEAD");
+ String mimeType = "text/html";
+ System.out.println(mimeType + " " + connection.getContentType());
+ assertEquals(mimeType, connection.getContentType());
+ assertEquals(indexFile.length(), connection.getContentLength());
+ assertNotEquals(0, connection.getContentLength());
+
+ String etag = connection.getHeaderField(HttpDownloader.HEADER_FIELD_ETAG);
+ assertFalse(TextUtils.isEmpty(etag));
+
+ assertEquals(200, connection.getResponseCode());
+ connection.disconnect();
+ }
+
+ @Test
+ public void testPostRequest() throws IOException {
+ URL url = new URL("http://localhost:8888/request-swap");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoInput(true);
+ connection.setDoOutput(true);
+
+ OutputStream outputStream = connection.getOutputStream();
+ OutputStreamWriter writer = new OutputStreamWriter(outputStream);
+ writer.write("repo=http://localhost:8888");
+ writer.flush();
+ writer.close();
+ outputStream.close();
+
+ assertEquals(200, connection.getResponseCode());
+ connection.disconnect();
+ }
+
+ @Test
+ public void testBadPostRequest() throws IOException {
+ URL url = new URL("http://localhost:8888/request-swap");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoInput(true);
+ connection.setDoOutput(true);
+ OutputStream outputStream = connection.getOutputStream();
+ OutputStreamWriter writer = new OutputStreamWriter(outputStream);
+ writer.write("repolkasdfkjhttp://localhost:8888");
+ writer.flush();
+ writer.close();
+ outputStream.close();
+ assertEquals(400, connection.getResponseCode());
+ connection.disconnect();
+ }
+
+ @Test
+ public void doArgumentTest() throws InterruptedException, UnsupportedEncodingException, IOException {
+ final int testPort = 9458;
+ Thread testServer = new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ LocalHTTPD localHttpd = new LocalHTTPD(
+ RuntimeEnvironment.application,
+ "localhost",
+ testPort,
+ webRoot,
+ false);
+ try {
+ localHttpd.start();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ assertTrue(localHttpd.isAlive());
+ }
+ });
+
+ testServer.start();
+ Thread.sleep(200);
+
+ URL url = new URL("http://localhost:" + testPort + "/");
+
+ HttpURLConnection connection = null;
+ try {
+ connection = (HttpURLConnection) url.openConnection();
+ assertEquals(200, connection.getResponseCode());
+ String str = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ assertTrue("The response entity didn't contain the string 'testdir'", str.contains("testdir"));
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ testServer.join(5000);
+ }
+ }
+
+ @Test
+ public void testURLContainsParentDirectory() throws IOException {
+ HttpURLConnection connection = null;
+ URL url = new URL("http://localhost:8888/testdir/../index.html");
+ try {
+ connection = (HttpURLConnection) url.openConnection();
+ Assert.assertEquals("The response status should be 403(Forbidden), " + "since the server won't serve requests with '../' due to security reasons",
+ 403, connection.getResponseCode());
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ @Test
+ public void testIndexFileIsShownWhenURLEndsWithDirectory() throws IOException {
+ HttpURLConnection connection = null;
+ try {
+ String dirName = "indexDir";
+ File indexDir = new File(webRoot, dirName);
+ assertTrue(indexDir.mkdir());
+ IOUtils.copy(classLoader.getResourceAsStream("index.html"),
+ new FileOutputStream(new File(indexDir, "index.html")));
+ URL url = new URL("http://localhost:8888/" + dirName);
+ connection = (HttpURLConnection) url.openConnection();
+ String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ Assert.assertThat("When the URL ends with a directory, and if an index.html file is present in that directory," + " the server should respond with that file",
+ responseString, containsString("Simple index file"));
+
+ IOUtils.copy(classLoader.getResourceAsStream("index.html"),
+ new FileOutputStream(new File(webRoot, "index.html")));
+ url = new URL("http://localhost:8888/");
+ connection = (HttpURLConnection) url.openConnection();
+ responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ Assert.assertThat("When the URL ends with a directory, and if an index.html file is present in that directory,"
+ + " the server should respond with that file",
+ responseString, containsString("Simple index file"));
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ @Test
+ public void testRangeHeaderWithStartPositionOnly() throws IOException {
+ HttpURLConnection connection = null;
+ try {
+ URL url = new URL("http://localhost:8888/testdir/test.html");
+ connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestProperty("Connection", "close");
+ connection.addRequestProperty("range", "bytes=10-");
+ connection.setReadTimeout(5000);
+ String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ System.out.println("responseString " + responseString);
+ Assert.assertThat("The data from the beginning of the file should have been skipped as specified in the 'range' header", responseString,
+ not(containsString("")));
+ Assert.assertThat("The response should contain the data from the end of the file since end position was not given in the 'range' header", responseString,
+ containsString(""));
+ Assert.assertEquals("The content length should be the length starting from the requested byte", "74", connection.getHeaderField("Content-Length"));
+ Assert.assertEquals("The 'Content-Range' header should contain the correct lengths and offsets based on the range served", "bytes 10-83/84",
+ connection.getHeaderField("Content-Range"));
+ Assert.assertEquals("Response status for a successful range request should be PARTIAL_CONTENT(206)",
+ 206, connection.getResponseCode());
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ @Test
+ public void testRangeStartGreaterThanFileLength() throws IOException {
+ HttpURLConnection connection = null;
+ try {
+ URL url = new URL("http://localhost:8888/testdir/test.html");
+ connection = (HttpURLConnection) url.openConnection();
+ connection.addRequestProperty("range", "bytes=1000-");
+ connection.connect();
+ Assert.assertEquals("Response status for a request with 'range' header value which exceeds file length should be RANGE_NOT_SATISFIABLE(416)",
+ 416, connection.getResponseCode());
+ Assert.assertEquals("The 'Content-Range' header should contain the correct lengths and offsets based on the range served",
+ "bytes */84", connection.getHeaderField("Content-Range"));
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ @Test
+ public void testRangeHeaderWithStartAndEndPosition() throws IOException {
+ HttpURLConnection connection = null;
+ try {
+ URL url = new URL("http://localhost:8888/testdir/test.html");
+ connection = (HttpURLConnection) url.openConnection();
+ connection.addRequestProperty("range", "bytes=10-40");
+ String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
+ Assert.assertThat("The data from the beginning of the file should have been skipped as specified in the 'range' header",
+ responseString, not(containsString("")));
+ Assert.assertThat("The data from the end of the file should have been skipped as specified in the 'range' header",
+ responseString, not(containsString("")));
+ Assert.assertEquals("The 'Content-Length' should be the length from the requested start position to end position",
+ "31", connection.getHeaderField("Content-Length"));
+ Assert.assertEquals("The 'Contnet-Range' header should contain the correct lengths and offsets based on the range served",
+ "bytes 10-40/84", connection.getHeaderField("Content-Range"));
+ Assert.assertEquals("Response status for a successful request with 'range' header should be PARTIAL_CONTENT(206)",
+ 206, connection.getResponseCode());
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ @Test
+ public void testIfNoneMatchHeader() throws IOException {
+ HttpURLConnection connection = null;
+ int status = -1;
+ while (status == -1) {
+ System.out.println("testIfNoneMatchHeader connect attempt");
+ try {
+ URL url = new URL("http://localhost:8888/testdir/test.html");
+ connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestProperty("if-none-match", "*");
+ connection.setRequestProperty("Connection", "close");
+ connection.connect();
+ status = connection.getResponseCode();
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+ Assert.assertEquals("The response status to a reqeuest with 'if-non-match=*' header should be NOT_MODIFIED(304), if the file exists",
+ 304, status);
+ }
+
+ @Test
+ public void testRangeHeaderAndIfNoneMatchHeader() throws IOException {
+ HttpURLConnection connection = null;
+ try {
+ URL url = new URL("http://localhost:8888/testdir/test.html");
+ connection = (HttpURLConnection) url.openConnection();
+ connection.addRequestProperty("range", "bytes=10-20");
+ connection.addRequestProperty("if-none-match", "*");
+ Assert.assertEquals("The response status to a reqeuest with 'if-non-match=*' header and 'range' header should be NOT_MODIFIED(304),"
+ + " if the file exists, because 'if-non-match' header should be given priority", 304, connection.getResponseCode());
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+}
diff --git a/app/src/testFull/resources/icon.png b/app/src/testFull/resources/icon.png
new file mode 100644
index 000000000..21439b7b8
Binary files /dev/null and b/app/src/testFull/resources/icon.png differ
diff --git a/app/src/testFull/resources/index.html b/app/src/testFull/resources/index.html
new file mode 100644
index 000000000..d6405a434
--- /dev/null
+++ b/app/src/testFull/resources/index.html
@@ -0,0 +1,5 @@
+
+
+Simple index file
+
+
\ No newline at end of file
diff --git a/app/src/testFull/resources/test.html b/app/src/testFull/resources/test.html
new file mode 100644
index 000000000..4cb157cc3
--- /dev/null
+++ b/app/src/testFull/resources/test.html
@@ -0,0 +1,8 @@
+
+
+dummy
+
+
+ it works
+
+
\ No newline at end of file
diff --git a/app/src/testFull/resources/urzip.apk b/app/src/testFull/resources/urzip.apk
new file mode 100644
index 000000000..ee5e5cba8
Binary files /dev/null and b/app/src/testFull/resources/urzip.apk differ