Bug 1405081 - Add partial HttpBin implementation for local HTTP tests r=esawin

This was derived from work done by Andrew Gaul <andrew@gaul.org> for
the Chaos HTTP proxy.

Differential Revision: https://phabricator.services.mozilla.com/D9130
This commit is contained in:
James Willcox 2018-10-18 12:14:31 -05:00
Родитель a464ab21e5
Коммит 3b0bbc9bfa
3 изменённых файлов: 335 добавлений и 0 удалений

Просмотреть файл

@ -226,6 +226,8 @@ dependencies {
androidTestImplementation 'com.android.support.test:rules:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
androidTestImplementation "com.android.support:support-annotations:$support_library_version"
androidTestImplementation 'org.eclipse.jetty:jetty-server:7.6.21.v20160908'
}
apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"

Просмотреть файл

@ -0,0 +1,155 @@
/*
* Copyright 2015-2016 Bounce Storage, Inc. <info@bouncestorage.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.geckoview.test.util;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.util.Log;
import java.net.URI;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.log.AbstractLogger;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
/**
* Reimplementation of HttpBin http://httpbin.org/ suitable for offline unit
* tests.
*/
public final class HttpBin {
private static final String LOGTAG = "HttpBin";
private final Server mServer;
static {
org.eclipse.jetty.util.log.Log.setLog(new AndroidLogger());
}
public HttpBin(@NonNull URI endpoint) {
this(endpoint, new HttpBinHandler());
}
public HttpBin(@NonNull URI endpoint, @NonNull HttpBinHandler handler) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder(
StrictMode.getThreadPolicy())
.permitNetwork()
.build());
mServer = new Server(endpoint.getPort());
mServer.setHandler(handler);
}
public void start() throws Exception {
mServer.start();
}
public void stop() throws Exception {
mServer.stop();
}
public int getPort() {
return mServer.getConnectors()[0].getLocalPort();
}
// We need this because the default Logger tries to use some things
// that don't exist in older versions of Android
private static class AndroidLogger extends AbstractLogger {
private boolean mDebugEnabled;
@Override
protected Logger newLogger(String fullname) {
return this;
}
@Override
public String getName() {
return null;
}
@Override
public void warn(String msg, Object... args) {
Log.w(LOGTAG, String.format(msg, args));
}
@Override
public void warn(Throwable thrown) {
Log.w(LOGTAG, thrown);
}
@Override
public void warn(String msg, Throwable thrown) {
Log.w(LOGTAG, msg, thrown);
}
@Override
public void info(String msg, Object... args) {
Log.i(LOGTAG, String.format(msg, args));
}
@Override
public void info(Throwable thrown) {
Log.i(LOGTAG, thrown.getMessage(), thrown);
}
@Override
public void info(String msg, Throwable thrown) {
Log.i(LOGTAG, msg, thrown);
}
@Override
public boolean isDebugEnabled() {
return mDebugEnabled;
}
@Override
public void setDebugEnabled(boolean enabled) {
mDebugEnabled = enabled;
}
@Override
public void debug(String msg, Object... args) {
if (mDebugEnabled) {
Log.d(LOGTAG, String.format(msg, args));
}
}
@Override
public void debug(Throwable thrown) {
if (mDebugEnabled) {
Log.d(LOGTAG, thrown.getMessage(), thrown);
}
}
@Override
public void debug(String msg, Throwable thrown) {
if (mDebugEnabled) {
Log.d(LOGTAG, msg, thrown);
}
}
@Override
public void ignore(Throwable ignored) {
// This is pretty spammy
if (mDebugEnabled) {
Log.w(LOGTAG, "Ignored Exception: ", ignored);
}
}
};
}

Просмотреть файл

@ -0,0 +1,178 @@
/*
* Copyright 2015-2016 Bounce Storage, Inc. <info@bouncestorage.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.geckoview.test.util;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.json.JSONException;
import org.json.JSONObject;
class HttpBinHandler extends AbstractHandler {
private static final String LOGTAG = "HttpBinHandler";
private static final int BUFSIZE = 4096;
private static void pipe(final @NonNull InputStream is) throws IOException {
pipe(is, null);
}
private static void pipe(final @NonNull InputStream is, final @Nullable OutputStream os)
throws IOException {
final byte[] buf = new byte[BUFSIZE];
int count = 0;
while ((count = is.read(buf)) > 0) {
if (os != null) {
os.write(buf, 0, count);
}
}
}
private void respondJSON(HttpServletResponse response, OutputStream os, JSONObject obj)
throws IOException {
final byte[] body = obj.toString().getBytes();
response.setContentLength(body.length);
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
os.write(body);
os.flush();
}
private void redirectTo(HttpServletResponse response, String location) {
response.setHeader("Location", location);
response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
}
@Override
@SuppressWarnings("unchecked")
public void handle(String target, Request baseRequest,
HttpServletRequest request, HttpServletResponse servletResponse)
throws IOException {
String method = request.getMethod();
String uri = request.getRequestURI();
try (InputStream is = request.getInputStream();
OutputStream os = servletResponse.getOutputStream()) {
if (method.equals("GET") && uri.startsWith("/status/")) {
pipe(is);
int status = Integer.parseInt(uri.substring(
"/status/".length()));
servletResponse.setStatus(status);
baseRequest.setHandled(true);
} else if (uri.equals("/redirect-to")) {
pipe(is);
redirectTo(servletResponse, request.getParameter("url"));
baseRequest.setHandled(true);
} else if (uri.startsWith("/redirect/")) {
pipe(is);
int count = Integer.parseInt(uri.substring("/redirect/".length())) - 1;
if (count > 0) {
redirectTo(servletResponse, "/redirect/" + count);
} else {
servletResponse.setStatus(HttpServletResponse.SC_OK);
}
baseRequest.setHandled(true);
} else if (uri.equals("/cookies")) {
pipe(is);
final JSONObject cookies = new JSONObject();
if (request.getCookies() != null) {
for (final Cookie cookie : request.getCookies()) {
cookies.put(cookie.getName(), cookie.getValue());
}
}
final JSONObject response = new JSONObject();
response.put("cookies", cookies);
respondJSON(servletResponse, os, response);
baseRequest.setHandled(true);
} else if (uri.startsWith("/cookies/set/")) {
pipe(is);
final String[] parts = uri.substring("/cookies/set/".length()).split("/");
servletResponse.addHeader("Set-Cookie",
String.format("%s=%s; Path=/", parts[0], parts[1]));
servletResponse.setHeader("Location", "/cookies");
servletResponse.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
baseRequest.setHandled(true);
} else if (uri.startsWith("/basic-auth")) {
pipe(is);
// FIXME: we don't actually check the username/password here
servletResponse.addHeader("WWW-Authenticate",
"Basic realm=\"Fake Realm\"");
servletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
baseRequest.setHandled(true);
} else if (uri.equals("/anything")) {
servletResponse.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
final JSONObject response = new JSONObject();
// Method
response.put("method", method);
// Headers
final JSONObject headers = new JSONObject();
response.put("headers", headers);
for (Enumeration<String> names = request.getHeaderNames(); names.hasMoreElements();) {
final String name = names.nextElement();
headers.put(name, request.getHeader(name));
}
// Body data
final ByteArrayOutputStream data = new ByteArrayOutputStream();
pipe(is, data);
response.put("data", data.toString("UTF-8"));
respondJSON(servletResponse, os, response);
baseRequest.setHandled(true);
}
if (!baseRequest.isHandled()) {
servletResponse.setStatus(501);
baseRequest.setHandled(true);
}
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error while handling response", e);
servletResponse.setStatus(500);
baseRequest.setHandled(true);
}
}
}