/*
* Copyright (c) 2013, Psiphon Inc.
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package ca.psiphon.ploggy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.util.Pair;
/**
* Component tests.
*
* Covered (by end-to-end request from one peer to another through Tor):
* - TorWrapper
* - Identity
* - X509
* - HiddenService
* - WebClient
* - WebServer
*/
public class Tests {
private static final String LOG_TAG = "Tests";
private static Timer mTimer = new Timer();
public static void scheduleComponentTests() {
mTimer.schedule(
new TimerTask() {
@Override
public void run() {
Tests.runComponentTests();
}
},
2000);
}
private static class MockRequestHandler implements WebServer.RequestHandler {
private final ExecutorService mThreadPool = Executors.newCachedThreadPool();
private final Date mMockTimestamp;
private final double mMockLatitude;
private final double mMockLongitude;
private final String mMockAddress;
MockRequestHandler() {
mMockTimestamp = new Date();
mMockLatitude = Math.random()*100.0 - 50.0;
mMockLongitude = Math.random()*100.0 - 50.0;
mMockAddress = "301 Front St W, Toronto, ON M5V 2T6";
}
public void stop() {
Utils.shutdownExecutorService(mThreadPool);
}
@Override
public void submitWebRequestTask(Runnable task) {
mThreadPool.execute(task);
}
public Data.Status getMockStatus() {
return new Data.Status(
Arrays.asList(new Data.Message(mMockTimestamp, "", null)),
new Data.Location(
mMockTimestamp,
mMockLatitude,
mMockLongitude,
10,
mMockAddress));
}
@Override
public Data.Status handlePullStatusRequest(String friendId) throws Utils.ApplicationError {
Log.addEntry(LOG_TAG, "handle pull status request...");
return getMockStatus();
}
@Override
public void handlePushStatusRequest(String friendId, Data.Status status) throws Utils.ApplicationError {
Log.addEntry(LOG_TAG, "handle push status request...");
}
@Override
public DownloadResponse handleDownloadRequest(
String friendCertificate, String resourceId, Pair<Long, Long> range) throws Utils.ApplicationError {
Log.addEntry(LOG_TAG, "handle download request...");
return null;
}
}
public static void runComponentTests() {
WebServer selfWebServer = null;
MockRequestHandler selfRequestHandler = null;
WebServer friendWebServer = null;
MockRequestHandler friendRequestHandler = null;
TorWrapper selfTor = null;
TorWrapper friendTor = null;
try {
Log.addEntry(LOG_TAG, "Make self...");
String selfNickname = "Me";
HiddenService.KeyMaterial selfHiddenServiceKeyMaterial = HiddenService.generateKeyMaterial();
X509.KeyMaterial selfX509KeyMaterial = X509.generateKeyMaterial(selfHiddenServiceKeyMaterial.mHostname);
Data.Self self = new Data.Self(
Identity.makeSignedPublicIdentity(
selfNickname,
selfX509KeyMaterial,
selfHiddenServiceKeyMaterial),
Identity.makePrivateIdentity(
selfX509KeyMaterial,
selfHiddenServiceKeyMaterial),
new Date());
Log.addEntry(LOG_TAG, "Make friend...");
String friendNickname = "My Friend";
HiddenService.KeyMaterial friendHiddenServiceKeyMaterial = HiddenService.generateKeyMaterial();
X509.KeyMaterial friendX509KeyMaterial = X509.generateKeyMaterial(friendHiddenServiceKeyMaterial.mHostname);
Data.Self friendSelf = new Data.Self(
Identity.makeSignedPublicIdentity(
friendNickname,
friendX509KeyMaterial,
friendHiddenServiceKeyMaterial),
Identity.makePrivateIdentity(
friendX509KeyMaterial,
friendHiddenServiceKeyMaterial),
new Date());
Data.Friend friend = new Data.Friend(friendSelf.mPublicIdentity, new Date());
// Not running hidden service for other friend: this is to test multiple client certs in the web server
Log.addEntry(LOG_TAG, "Make other friend...");
HiddenService.KeyMaterial otherFriendHiddenServiceKeyMaterial = HiddenService.generateKeyMaterial();
X509.KeyMaterial otherFriendX509KeyMaterial = X509.generateKeyMaterial(otherFriendHiddenServiceKeyMaterial.mHostname);
Log.addEntry(LOG_TAG, "Make unfriendly key material...");
HiddenService.KeyMaterial unfriendlyHiddenServiceKeyMaterial = HiddenService.generateKeyMaterial();
X509.KeyMaterial unfriendlyX509KeyMaterial = X509.generateKeyMaterial(unfriendlyHiddenServiceKeyMaterial.mHostname);
Log.addEntry(LOG_TAG, "Start self web server...");
List<String> selfPeerCertificates = new ArrayList<String>();
selfPeerCertificates.add(friend.mPublicIdentity.mX509Certificate);
selfPeerCertificates.add(otherFriendX509KeyMaterial.mCertificate);
selfRequestHandler = new MockRequestHandler();
selfWebServer = new WebServer(selfRequestHandler, selfX509KeyMaterial, selfPeerCertificates);
try {
selfWebServer.start();
} catch (IOException e) {
throw new Utils.ApplicationError(LOG_TAG, e);
}
Log.addEntry(LOG_TAG, "Start friend web server...");
List<String> friendPeerCertificates = new ArrayList<String>();
friendPeerCertificates.add(self.mPublicIdentity.mX509Certificate);
friendPeerCertificates.add(otherFriendX509KeyMaterial.mCertificate);
friendRequestHandler = new MockRequestHandler();
friendWebServer = new WebServer(friendRequestHandler, friendX509KeyMaterial, friendPeerCertificates);
try {
friendWebServer.start();
} catch (IOException e) {
throw new Utils.ApplicationError(LOG_TAG, e);
}
// Test direct web request (not through Tor)
// Repeat multiple times to exercise keep-alive connection
String response;
String expectedResponse = Json.toJson(selfRequestHandler.getMockStatus());
for (int i = 0; i < 4; i++) {
Log.addEntry(LOG_TAG, "Direct GET request from valid friend...");
response = WebClient.makeGetRequest(
friendX509KeyMaterial,
self.mPublicIdentity.mX509Certificate,
WebClient.UNTUNNELED_REQUEST,
"127.0.0.1",
selfWebServer.getListeningPort(),
Protocol.PULL_STATUS_REQUEST_PATH);
Protocol.validateStatus(Json.fromJson(response, Data.Status.class));
if (!response.equals(expectedResponse)) {
throw new Utils.ApplicationError(LOG_TAG, "unexpected status response value");
}
Log.addEntry(LOG_TAG, "Direct POST request from valid friend...");
WebClient.makeJsonPostRequest(
friendX509KeyMaterial,
self.mPublicIdentity.mX509Certificate,
WebClient.UNTUNNELED_REQUEST,
"127.0.0.1",
selfWebServer.getListeningPort(),
Protocol.PUSH_STATUS_REQUEST_PATH,
expectedResponse);
}
Log.addEntry(LOG_TAG, "Run self Tor...");
List<TorWrapper.HiddenServiceAuth> selfHiddenServiceAuths = new ArrayList<TorWrapper.HiddenServiceAuth>();
selfHiddenServiceAuths.add(
new TorWrapper.HiddenServiceAuth(
friendSelf.mPublicIdentity.mHiddenServiceHostname,
friendSelf.mPublicIdentity.mHiddenServiceAuthCookie));
selfTor = new TorWrapper(
TorWrapper.Mode.MODE_RUN_SERVICES,
"runComponentTests-self",
selfHiddenServiceAuths,
selfHiddenServiceKeyMaterial,
selfWebServer.getListeningPort());
selfTor.start();
Log.addEntry(LOG_TAG, "Run friend Tor...");
List<TorWrapper.HiddenServiceAuth> friendHiddenServiceAuths = new ArrayList<TorWrapper.HiddenServiceAuth>();
friendHiddenServiceAuths.add(
new TorWrapper.HiddenServiceAuth(
self.mPublicIdentity.mHiddenServiceHostname,
self.mPublicIdentity.mHiddenServiceAuthCookie));
friendTor = new TorWrapper(
TorWrapper.Mode.MODE_RUN_SERVICES,
"runComponentTests-friend",
friendHiddenServiceAuths,
friendHiddenServiceKeyMaterial,
friendWebServer.getListeningPort());
friendTor.start();
selfTor.start();
selfTor.awaitStarted();
friendTor.awaitStarted();
// TODO: monitor publication state via Tor control interface?
int publishWaitMilliseconds = 30000;
Log.addEntry(LOG_TAG, String.format("Wait %d ms. while hidden service is published...", publishWaitMilliseconds));
try {
Thread.sleep(publishWaitMilliseconds);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
for (int i = 0; i < 4; i++) {
Log.addEntry(LOG_TAG, "request from valid friend...");
response = WebClient.makeGetRequest(
friendX509KeyMaterial,
self.mPublicIdentity.mX509Certificate,
friendTor.getSocksProxyPort(),
self.mPublicIdentity.mHiddenServiceHostname,
Protocol.WEB_SERVER_VIRTUAL_PORT,
Protocol.PULL_STATUS_REQUEST_PATH);
Protocol.validateStatus(Json.fromJson(response, Data.Status.class));
if (!response.equals(expectedResponse)) {
throw new Utils.ApplicationError(LOG_TAG, "unexpected status response value");
}
}
Log.addEntry(LOG_TAG, "Request from invalid friend...");
boolean failed = false;
try {
WebClient.makeGetRequest(
unfriendlyX509KeyMaterial,
self.mPublicIdentity.mX509Certificate,
friendTor.getSocksProxyPort(),
self.mPublicIdentity.mHiddenServiceHostname,
Protocol.WEB_SERVER_VIRTUAL_PORT,
Protocol.PULL_STATUS_REQUEST_PATH);
} catch (Utils.ApplicationError e) {
if (!e.getMessage().contains("No peer certificate")) {
throw e;
}
failed = true;
}
if (!failed) {
throw new Utils.ApplicationError(LOG_TAG, "unexpected success");
}
// Re-run friend's Tor with an invalid hidden service auth cookie
Log.addEntry(LOG_TAG, "Request from friend with invalid hidden service auth cookie...");
friendTor.stop();
friendHiddenServiceAuths = new ArrayList<TorWrapper.HiddenServiceAuth>();
byte[] badAuthCookie = new byte[16]; // 128-bit value, as per spec
new Random().nextBytes(badAuthCookie);
friendHiddenServiceAuths.add(
new TorWrapper.HiddenServiceAuth(
self.mPublicIdentity.mHiddenServiceHostname,
Utils.encodeBase64(badAuthCookie).substring(0, 22)));
friendTor = new TorWrapper(
TorWrapper.Mode.MODE_RUN_SERVICES,
"runComponentTests-friend",
friendHiddenServiceAuths,
friendHiddenServiceKeyMaterial,
friendWebServer.getListeningPort());
friendTor.start();
friendTor.awaitStarted();
failed = false;
try {
response = WebClient.makeGetRequest(
friendX509KeyMaterial,
self.mPublicIdentity.mX509Certificate,
friendTor.getSocksProxyPort(),
self.mPublicIdentity.mHiddenServiceHostname,
Protocol.WEB_SERVER_VIRTUAL_PORT,
Protocol.PULL_STATUS_REQUEST_PATH);
} catch (Utils.ApplicationError e) {
if (!e.getMessage().contains("SOCKS4a connect failed")) {
throw e;
}
failed = true;
}
if (!failed) {
throw new Utils.ApplicationError(LOG_TAG, "unexpected success");
}
Log.addEntry(LOG_TAG, "Component test run success");
} catch (Utils.ApplicationError e) {
Log.addEntry(LOG_TAG, "Test failed");
} finally {
if (selfTor != null) {
selfTor.stop();
}
if (friendTor != null) {
friendTor.stop();
}
if (selfWebServer != null) {
selfWebServer.stop();
}
if (selfRequestHandler != null) {
selfRequestHandler.stop();
}
if (friendWebServer != null) {
friendWebServer.stop();
}
if (friendRequestHandler != null) {
friendRequestHandler.stop();
}
}
}
}