Merge branch 'make-swap-great-again' into 'master'
make swap great again ;-) Closes #248 See merge request fdroid/fdroidclient!726
This commit is contained in:
commit
73388ddf8c
@ -38,10 +38,10 @@ public class SignatureBlockGenerator {
|
|||||||
|
|
||||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
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());
|
ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.getPrivateKey());
|
||||||
|
|
||||||
JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("SC");
|
JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("BC");
|
||||||
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
|
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
|
||||||
|
|
||||||
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
|
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
|
||||||
|
@ -17,6 +17,7 @@ import android.util.Log;
|
|||||||
import org.fdroid.fdroid.FDroidApp;
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
import org.fdroid.fdroid.Hasher;
|
import org.fdroid.fdroid.Hasher;
|
||||||
import org.fdroid.fdroid.Preferences;
|
import org.fdroid.fdroid.Preferences;
|
||||||
|
import org.fdroid.fdroid.RepoUpdater;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.Apk;
|
import org.fdroid.fdroid.data.Apk;
|
||||||
import org.fdroid.fdroid.data.App;
|
import org.fdroid.fdroid.data.App;
|
||||||
@ -103,7 +104,7 @@ public final class LocalRepoManager {
|
|||||||
repoDir = new SanitizedFile(fdroidDir, "repo");
|
repoDir = new SanitizedFile(fdroidDir, "repo");
|
||||||
repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO");
|
repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO");
|
||||||
iconsDir = new SanitizedFile(repoDir, "icons");
|
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");
|
xmlIndexJarUnsigned = new SanitizedFile(repoDir, "index.unsigned.jar");
|
||||||
|
|
||||||
if (!fdroidDir.exists() && !fdroidDir.mkdir()) {
|
if (!fdroidDir.exists() && !fdroidDir.mkdir()) {
|
||||||
@ -481,7 +482,7 @@ public final class LocalRepoManager {
|
|||||||
public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException {
|
public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException {
|
||||||
BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
|
BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
|
||||||
JarOutputStream jo = new JarOutputStream(bo);
|
JarOutputStream jo = new JarOutputStream(bo);
|
||||||
JarEntry je = new JarEntry("index.xml");
|
JarEntry je = new JarEntry(RepoUpdater.DATA_FILE_NAME);
|
||||||
jo.putNextEntry(je);
|
jo.putNextEntry(je);
|
||||||
new IndexXmlBuilder().build(context, apps, jo);
|
new IndexXmlBuilder().build(context, apps, jo);
|
||||||
jo.close();
|
jo.close();
|
||||||
|
@ -1,22 +1,52 @@
|
|||||||
package org.fdroid.fdroid.net;
|
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.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
import fi.iki.elonen.NanoHTTPD;
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
import fi.iki.elonen.NanoHTTPD.Response.IStatus;
|
||||||
import org.fdroid.fdroid.BuildConfig;
|
import org.fdroid.fdroid.BuildConfig;
|
||||||
import org.fdroid.fdroid.Utils;
|
|
||||||
import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
|
import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
|
||||||
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
|
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
|
||||||
|
|
||||||
import javax.net.ssl.SSLServerSocketFactory;
|
import javax.net.ssl.SSLServerSocketFactory;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -27,314 +57,104 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.StringTokenizer;
|
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.
|
||||||
|
* <p>
|
||||||
|
* This is mostly just synced from {@code SimpleWebServer.java} from NanoHTTPD.
|
||||||
|
*
|
||||||
|
* @see <a href="https://github.com/NanoHttpd/nanohttpd/blob/nanohttpd-project-2.3.1/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java">webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java</a>
|
||||||
|
*/
|
||||||
public class LocalHTTPD extends NanoHTTPD {
|
public class LocalHTTPD extends NanoHTTPD {
|
||||||
private static final String TAG = "LocalHTTPD";
|
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<File> 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) {
|
public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
|
||||||
super(hostname, port);
|
super(hostname, port);
|
||||||
this.webRoot = webRoot;
|
rootDirs = Collections.singletonList(webRoot);
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
if (useHttps) {
|
if (useHttps) {
|
||||||
enableHTTPS();
|
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'
|
* URL-encodes everything between "/"-characters. Encodes spaces as '%20'
|
||||||
* instead of '+'.
|
* instead of '+'.
|
||||||
*/
|
*/
|
||||||
private String encodeUriBetweenSlashes(String uri) {
|
private String encodeUri(String uri) {
|
||||||
String newUri = "";
|
String newUri = "";
|
||||||
StringTokenizer st = new StringTokenizer(uri, "/ ", true);
|
StringTokenizer st = new StringTokenizer(uri, "/ ", true);
|
||||||
while (st.hasMoreTokens()) {
|
while (st.hasMoreTokens()) {
|
||||||
String tok = st.nextToken();
|
String tok = st.nextToken();
|
||||||
switch (tok) {
|
if ("/".equals(tok)) {
|
||||||
case "/":
|
newUri += "/";
|
||||||
newUri += "/";
|
} else if (" ".equals(tok)) {
|
||||||
break;
|
newUri += "%20";
|
||||||
case " ":
|
} else {
|
||||||
newUri += "%20";
|
try {
|
||||||
break;
|
newUri += URLEncoder.encode(tok, "UTF-8");
|
||||||
default:
|
} catch (UnsupportedEncodingException ignored) {
|
||||||
try {
|
}
|
||||||
newUri += URLEncoder.encode(tok, "UTF-8");
|
|
||||||
} catch (UnsupportedEncodingException ignored) {
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newUri;
|
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<String, String>());
|
|
||||||
} 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<String, String> header = session.getHeaders();
|
|
||||||
Map<String, String> parms = session.getParms();
|
|
||||||
String uri = session.getUri();
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Utils.debugLog(TAG, session.getMethod() + " '" + uri + "' ");
|
|
||||||
|
|
||||||
Iterator<String> 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<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(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,
|
|
||||||
"<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(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<String, String> 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) {
|
private String findIndexFileInDirectory(File directory) {
|
||||||
String indexFileName = "index.html";
|
for (String fileName : LocalHTTPD.INDEX_FILE_NAMES) {
|
||||||
File indexFile = new File(directory, indexFileName);
|
File indexFile = new File(directory, fileName);
|
||||||
if (indexFile.exists()) {
|
if (indexFile.isFile()) {
|
||||||
return indexFileName;
|
return fileName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
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;
|
String heading = "Directory " + uri;
|
||||||
StringBuilder msg = new StringBuilder("<html><head><title>" + heading
|
StringBuilder msg =
|
||||||
+ "</title><style><!--\n" +
|
new StringBuilder("<html><head><title>" + heading + "</title><style><!--\n" + "span.dirname { font-weight: bold; }\n" + "span.filesize { font-size: 75%; }\n"
|
||||||
"span.dirname { font-weight: bold; }\n" +
|
+ "// -->\n" + "</style>" + "</head><body><h1>" + heading + "</h1>");
|
||||||
"span.filesize { font-size: 75%; }\n" +
|
|
||||||
"// -->\n" +
|
|
||||||
"</style>" +
|
|
||||||
"</head><body><h1>" + heading + "</h1>");
|
|
||||||
|
|
||||||
String up = null;
|
String up = null;
|
||||||
if (uri.length() > 1) {
|
if (uri.length() > 1) {
|
||||||
@ -346,6 +166,7 @@ public class LocalHTTPD extends NanoHTTPD {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> files = Arrays.asList(f.list(new FilenameFilter() {
|
List<String> files = Arrays.asList(f.list(new FilenameFilter() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean accept(File dir, String name) {
|
public boolean accept(File dir, String name) {
|
||||||
return new File(dir, name).isFile();
|
return new File(dir, name).isFile();
|
||||||
@ -353,6 +174,7 @@ public class LocalHTTPD extends NanoHTTPD {
|
|||||||
}));
|
}));
|
||||||
Collections.sort(files);
|
Collections.sort(files);
|
||||||
List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
|
List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean accept(File dir, String name) {
|
public boolean accept(File dir, String name) {
|
||||||
return new File(dir, name).isDirectory();
|
return new File(dir, name).isDirectory();
|
||||||
@ -364,35 +186,27 @@ public class LocalHTTPD extends NanoHTTPD {
|
|||||||
if (up != null || directories.size() > 0) {
|
if (up != null || directories.size() > 0) {
|
||||||
msg.append("<section class=\"directories\">");
|
msg.append("<section class=\"directories\">");
|
||||||
if (up != null) {
|
if (up != null) {
|
||||||
msg.append("<li><a rel=\"directory\" href=\"").append(up)
|
msg.append("<li><a rel=\"directory\" href=\"").append(up).append("\"><span class=\"dirname\">..</span></a></li>");
|
||||||
.append("\"><span class=\"dirname\">..</span></a></b></li>");
|
|
||||||
}
|
}
|
||||||
for (String directory : directories) {
|
for (String directory : directories) {
|
||||||
String dir = directory + "/";
|
String dir = directory + "/";
|
||||||
msg.append("<li><a rel=\"directory\" href=\"")
|
msg.append("<li><a rel=\"directory\" href=\"").append(encodeUri(uri + dir)).append("\"><span class=\"dirname\">").append(dir).append("</span></a></li>");
|
||||||
.append(encodeUriBetweenSlashes(uri + dir))
|
|
||||||
.append("\"><span class=\"dirname\">").append(dir)
|
|
||||||
.append("</span></a></b></li>");
|
|
||||||
}
|
}
|
||||||
msg.append("</section>");
|
msg.append("</section>");
|
||||||
}
|
}
|
||||||
if (files.size() > 0) {
|
if (files.size() > 0) {
|
||||||
msg.append("<section class=\"files\">");
|
msg.append("<section class=\"files\">");
|
||||||
for (String file : files) {
|
for (String file : files) {
|
||||||
msg.append("<li><a href=\"").append(encodeUriBetweenSlashes(uri + file))
|
msg.append("<li><a href=\"").append(encodeUri(uri + file)).append("\"><span class=\"filename\">").append(file).append("</span></a>");
|
||||||
.append("\"><span class=\"filename\">").append(file)
|
|
||||||
.append("</span></a>");
|
|
||||||
File curFile = new File(f, file);
|
File curFile = new File(f, file);
|
||||||
long len = curFile.length();
|
long len = curFile.length();
|
||||||
msg.append(" <span class=\"filesize\">(");
|
msg.append(" <span class=\"filesize\">(");
|
||||||
if (len < 1024) {
|
if (len < 1024) {
|
||||||
msg.append(len).append(" bytes");
|
msg.append(len).append(" bytes");
|
||||||
} else if (len < 1024 * 1024) {
|
} else if (len < 1024 * 1024) {
|
||||||
msg.append(len / 1024).append('.').append(len % 1024 / 10 % 100)
|
msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100).append(" KB");
|
||||||
.append(" KB");
|
|
||||||
} else {
|
} else {
|
||||||
msg.append(len / (1024 * 1024)).append('.')
|
msg.append(len / (1024 * 1024)).append(".").append(len % (1024 * 1024) / 10000 % 100).append(" MB");
|
||||||
.append(len % (1024 * 1024) / 10 % 100).append(" MB");
|
|
||||||
}
|
}
|
||||||
msg.append(")</span></li>");
|
msg.append(")</span></li>");
|
||||||
}
|
}
|
||||||
@ -403,4 +217,252 @@ public class LocalHTTPD extends NanoHTTPD {
|
|||||||
msg.append("</body></html>");
|
msg.append("</body></html>");
|
||||||
return msg.toString();
|
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<String, String> headers, IHTTPSession session, String uri) {
|
||||||
|
return defaultRespond(headers, session, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response defaultRespond(Map<String, String> 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, "<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 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<String, String> header = session.getHeaders();
|
||||||
|
Map<String, String> parms = session.getParms();
|
||||||
|
String uri = session.getUri();
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
System.out.println(session.getMethod() + " '" + uri + "' ");
|
||||||
|
|
||||||
|
Iterator<String> 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<String, String>());
|
||||||
|
} 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<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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<String, String> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,16 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
private LocalBroadcastManager localBroadcastManager;
|
private LocalBroadcastManager localBroadcastManager;
|
||||||
private WifiManager wifiManager;
|
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
|
@NonNull
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -83,6 +83,9 @@ import java.util.jar.JarFile;
|
|||||||
public class RepoUpdater {
|
public class RepoUpdater {
|
||||||
private static final String TAG = "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;
|
final String indexUrl;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -205,7 +208,7 @@ public class RepoUpdater {
|
|||||||
FDroidApp.disableBouncyCastleOnLollipop();
|
FDroidApp.disableBouncyCastleOnLollipop();
|
||||||
|
|
||||||
JarFile jarFile = new JarFile(downloadedFile, true);
|
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),
|
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
|
||||||
processIndexListener, repo.address, (int) indexEntry.getSize());
|
processIndexListener, repo.address, (int) indexEntry.getSize());
|
||||||
|
|
||||||
|
@ -382,7 +382,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir);
|
SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir);
|
||||||
app.installedApk = new Apk();
|
app.installedApk = new Apk();
|
||||||
if (apkFile.canRead()) {
|
if (apkFile.canRead()) {
|
||||||
String hashType = "SHA-256";
|
String hashType = "sha256";
|
||||||
String hash = Utils.getBinaryHash(apkFile, hashType);
|
String hash = Utils.getBinaryHash(apkFile, hashType);
|
||||||
if (TextUtils.isEmpty(hash)) {
|
if (TextUtils.isEmpty(hash)) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -53,7 +53,7 @@ import java.net.URL;
|
|||||||
public class HttpDownloader extends Downloader {
|
public class HttpDownloader extends Downloader {
|
||||||
private static final String TAG = "HttpDownloader";
|
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 username;
|
||||||
private final String password;
|
private final String password;
|
||||||
@ -167,6 +167,7 @@ public class HttpDownloader extends Downloader {
|
|||||||
if (isSwapUrl(sourceUrl)) {
|
if (isSwapUrl(sourceUrl)) {
|
||||||
// swap never works with a proxy, its unrouted IP on the same subnet
|
// swap never works with a proxy, its unrouted IP on the same subnet
|
||||||
connection = (HttpURLConnection) sourceUrl.openConnection();
|
connection = (HttpURLConnection) sourceUrl.openConnection();
|
||||||
|
connection.setRequestProperty("Connection", "Close"); // avoid keep-alive
|
||||||
} else {
|
} else {
|
||||||
if (queryString != null) {
|
if (queryString != null) {
|
||||||
connection = NetCipher.getHttpURLConnection(new URL(urlString + "?" + queryString));
|
connection = NetCipher.getHttpURLConnection(new URL(urlString + "?" + queryString));
|
||||||
|
@ -55,6 +55,7 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import org.fdroid.fdroid.FDroidApp;
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.RepoUpdater;
|
||||||
import org.fdroid.fdroid.UpdateService;
|
import org.fdroid.fdroid.UpdateService;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.compat.CursorAdapterCompat;
|
import org.fdroid.fdroid.compat.CursorAdapterCompat;
|
||||||
@ -567,7 +568,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
return addressWithoutIndex;
|
return addressWithoutIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uri uri = builder.appendPath("index.jar").build();
|
final Uri uri = builder.appendPath(RepoUpdater.SIGNED_FILE_NAME).build();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (checkForRepository(uri)) {
|
if (checkForRepository(uri)) {
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
449
app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java
Normal file
449
app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java
Normal file
@ -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 <a href="https://github.com/NanoHttpd/nanohttpd/blob/nanohttpd-project-2.3.1/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java">webserver/src/test/java/fi/iki/elonen/LocalHTTPDTest.java</a>
|
||||||
|
*/
|
||||||
|
@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("<html>\n<head>\n<title>dummy</title>\n</head>\n<body>\n\t<h1>it works</h1>\n</body>\n</html>", 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("<head>")));
|
||||||
|
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("</head>"));
|
||||||
|
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("<head>")));
|
||||||
|
Assert.assertThat("The data from the end of the file should have been skipped as specified in the 'range' header",
|
||||||
|
responseString, not(containsString("</head>")));
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/testFull/resources/icon.png
Normal file
BIN
app/src/testFull/resources/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
5
app/src/testFull/resources/index.html
Normal file
5
app/src/testFull/resources/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Simple index file</title>
|
||||||
|
</head>
|
||||||
|
</html>
|
8
app/src/testFull/resources/test.html
Normal file
8
app/src/testFull/resources/test.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>dummy</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>it works</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
app/src/testFull/resources/urzip.apk
Normal file
BIN
app/src/testFull/resources/urzip.apk
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user