package de.test.antennapod.util.service.download;
import android.util.Base64;
import android.util.Log;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.GZIPOutputStream;
import de.danoeh.antennapod.BuildConfig;
/**
* Http server for testing purposes
*
* Supported features:
*
* /status/code: Returns HTTP response with the given status code
* /redirect/n: Redirects n times
* /delay/n: Delay response for n seconds
* /basic-auth/username/password: Basic auth with username and password
* /gzip/n: Send gzipped data of size n bytes
* /files/id: Accesses the file with the specified ID (this has to be added first via serveFile).
*/
public class HTTPBin extends NanoHTTPD {
private static final String TAG = "HTTPBin";
public static final int PORT = 8124;
public static final String BASE_URL = "http://127.0.0.1:" + HTTPBin.PORT;
private static final String MIME_HTML = "text/html";
private static final String MIME_PLAIN = "text/plain";
private List servedFiles;
public HTTPBin() {
super(PORT);
this.servedFiles = new ArrayList<>();
}
/**
* Adds the given file to the server.
*
* @return The ID of the file or -1 if the file could not be added to the server.
*/
public synchronized int serveFile(File file) {
if (file == null) throw new IllegalArgumentException("file = null");
if (!file.exists()) {
return -1;
}
for (int i = 0; i < servedFiles.size(); i++) {
if (servedFiles.get(i).getAbsolutePath().equals(file.getAbsolutePath())) {
return i;
}
}
servedFiles.add(file);
return servedFiles.size() - 1;
}
/**
* Removes the file with the given ID from the server.
*
* @return True if a file was removed, false otherwise
*/
public synchronized boolean removeFile(int id) {
if (id < 0) throw new IllegalArgumentException("ID < 0");
if (id >= servedFiles.size()) {
return false;
} else {
return servedFiles.remove(id) != null;
}
}
public synchronized File accessFile(int id) {
if (id < 0 || id >= servedFiles.size()) {
return null;
} else {
return servedFiles.get(id);
}
}
@Override
public Response serve(IHTTPSession session) {
if (BuildConfig.DEBUG) Log.d(TAG, "Requested url: " + session.getUri());
String[] segments = session.getUri().split("/");
if (segments.length < 3) {
Log.w(TAG, String.format("Invalid number of URI segments: %d %s", segments.length, Arrays.toString(segments)));
get404Error();
}
final String func = segments[1];
final String param = segments[2];
final Map headers = session.getHeaders();
if (func.equalsIgnoreCase("status")) {
try {
int code = Integer.parseInt(param);
return getStatus(code);
} catch (NumberFormatException e) {
e.printStackTrace();
return getInternalError();
}
} else if (func.equalsIgnoreCase("redirect")) {
try {
int times = Integer.parseInt(param);
if (times < 0) {
throw new NumberFormatException("times <= 0: " + times);
}
return getRedirectResponse(times - 1);
} catch (NumberFormatException e) {
e.printStackTrace();
return getInternalError();
}
} else if (func.equalsIgnoreCase("delay")) {
try {
int sec = Integer.parseInt(param);
if (sec <= 0) {
throw new NumberFormatException("sec <= 0: " + sec);
}
Thread.sleep(sec * 1000L);
return getOKResponse();
} catch (NumberFormatException e) {
e.printStackTrace();
return getInternalError();
} catch (InterruptedException e) {
e.printStackTrace();
return getInternalError();
}
} else if (func.equalsIgnoreCase("basic-auth")) {
if (!headers.containsKey("authorization")) {
Log.w(TAG, "No credentials provided");
return getUnauthorizedResponse();
}
try {
String credentials = new String(Base64.decode(headers.get("authorization").split(" ")[1], 0), "UTF-8");
String[] credentialParts = credentials.split(":");
if (credentialParts.length != 2) {
Log.w(TAG, "Unable to split credentials: " + Arrays.toString(credentialParts));
return getInternalError();
}
if (credentialParts[0].equals(segments[2])
&& credentialParts[1].equals(segments[3])) {
Log.i(TAG, "Credentials accepted");
return getOKResponse();
} else {
Log.w(TAG, String.format("Invalid credentials. Expected %s, %s, but was %s, %s",
segments[2], segments[3], credentialParts[0], credentialParts[1]));
return getUnauthorizedResponse();
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return getInternalError();
}
} else if (func.equalsIgnoreCase("gzip")) {
try {
int size = Integer.parseInt(param);
if (size <= 0) {
Log.w(TAG, "Invalid size for gzipped data: " + size);
throw new NumberFormatException();
}
return getGzippedResponse(size);
} catch (NumberFormatException e) {
e.printStackTrace();
return getInternalError();
} catch (IOException e) {
e.printStackTrace();
return getInternalError();
}
} else if (func.equalsIgnoreCase("files")) {
try {
int id = Integer.parseInt(param);
if (id < 0) {
Log.w(TAG, "Invalid ID: " + id);
throw new NumberFormatException();
}
return getFileAccessResponse(id, headers);
} catch (NumberFormatException e) {
e.printStackTrace();
return getInternalError();
}
}
return get404Error();
}
private synchronized Response getFileAccessResponse(int id, Map header) {
File file = accessFile(id);
if (file == null || !file.exists()) {
Log.w(TAG, "File not found: " + id);
return get404Error();
}
InputStream inputStream = null;
String contentRange = null;
Response.Status status;
boolean successful = false;
try {
inputStream = new FileInputStream(file);
if (header.containsKey("range")) {
// read range header field
final String value = header.get("range");
final String[] segments = value.split("=");
if (segments.length != 2) {
Log.w(TAG, "Invalid segment length: " + Arrays.toString(segments));
return getInternalError();
}
final String type = StringUtils.substringBefore(value, "=");
if (!type.equalsIgnoreCase("bytes")) {
Log.w(TAG, "Range is not specified in bytes: " + value);
return getInternalError();
}
try {
long start = Long.parseLong(StringUtils.substringBefore(segments[1], "-"));
if (start >= file.length()) {
return getRangeNotSatisfiable();
}
// skip 'start' bytes
IOUtils.skipFully(inputStream, start);
contentRange = "bytes " + start + (file.length() - 1) + "/" + file.length();
} catch (NumberFormatException e) {
e.printStackTrace();
return getInternalError();
} catch (IOException e) {
e.printStackTrace();
return getInternalError();
}
status = Response.Status.PARTIAL_CONTENT;
} else {
// request did not contain range header field
status = Response.Status.OK;
}
successful = true;
} catch (FileNotFoundException e) {
e.printStackTrace();
return getInternalError();
} finally {
if (!successful && inputStream != null) {
IOUtils.closeQuietly(inputStream);
}
}
Response response = new Response(status, URLConnection.guessContentTypeFromName(file.getAbsolutePath()), inputStream);
response.addHeader("Accept-Ranges", "bytes");
if (contentRange != null) {
response.addHeader("Content-Range", contentRange);
}
response.addHeader("Content-Length", String.valueOf(file.length()));
return response;
}
private Response getGzippedResponse(int size) throws IOException {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
final byte[] buffer = new byte[size];
Random random = new Random(System.currentTimeMillis());
random.nextBytes(buffer);
ByteArrayOutputStream compressed = new ByteArrayOutputStream(buffer.length);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(compressed);
gzipOutputStream.write(buffer);
gzipOutputStream.close();
InputStream inputStream = new ByteArrayInputStream(compressed.toByteArray());
Response response = new Response(Response.Status.OK, MIME_PLAIN, inputStream);
response.addHeader("Content-Encoding", "gzip");
response.addHeader("Content-Length", String.valueOf(compressed.size()));
return response;
}
private Response getStatus(final int code) {
Response.IStatus status = (code == 200) ? Response.Status.OK :
(code == 201) ? Response.Status.CREATED :
(code == 206) ? Response.Status.PARTIAL_CONTENT :
(code == 301) ? Response.Status.REDIRECT :
(code == 304) ? Response.Status.NOT_MODIFIED :
(code == 400) ? Response.Status.BAD_REQUEST :
(code == 401) ? Response.Status.UNAUTHORIZED :
(code == 403) ? Response.Status.FORBIDDEN :
(code == 404) ? Response.Status.NOT_FOUND :
(code == 405) ? Response.Status.METHOD_NOT_ALLOWED :
(code == 416) ? Response.Status.RANGE_NOT_SATISFIABLE :
(code == 500) ? Response.Status.INTERNAL_ERROR : new Response.IStatus() {
@Override
public int getRequestStatus() {
return code;
}
@Override
public String getDescription() {
return "Unknown";
}
};
return new Response(status, MIME_HTML, "");
}
private Response getRedirectResponse(int times) {
if (times > 0) {
Response response = new Response(Response.Status.REDIRECT, MIME_HTML, "This resource has been moved permanently");
response.addHeader("Location", "/redirect/" + times);
return response;
} else if (times == 0) {
return getOKResponse();
} else {
return getInternalError();
}
}
private Response getUnauthorizedResponse() {
Response response = new Response(Response.Status.UNAUTHORIZED, MIME_HTML, "");
response.addHeader("WWW-Authenticate", "Basic realm=\"Test Realm\"");
return response;
}
private Response getOKResponse() {
return new Response(Response.Status.OK, MIME_HTML, "");
}
private Response getInternalError() {
return new Response(Response.Status.INTERNAL_ERROR, MIME_HTML, "The server encountered an internal error");
}
private Response getRangeNotSatisfiable() {
return new Response(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAIN, "");
}
private Response get404Error() {
return new Response(Response.Status.NOT_FOUND, MIME_HTML, "The requested URL was not found on this server");
}
}