mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-11-23 04:41:16 +00:00
Implement TLS support (fixes #19)
This commit is contained in:
parent
7ddee2f953
commit
c7cdfa1ff2
12 changed files with 225 additions and 27 deletions
|
@ -43,7 +43,7 @@ public class GetTaskTest extends AndroidTestCase {
|
|||
|
||||
@MediumTest
|
||||
public void testGetNoParams() throws IOException, InterruptedException {
|
||||
new GetTask() {
|
||||
new GetTask("") {
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
assertEquals(RESPONSE, s);
|
||||
|
@ -57,7 +57,7 @@ public class GetTaskTest extends AndroidTestCase {
|
|||
|
||||
@MediumTest
|
||||
public void testGetParams() throws IOException, InterruptedException {
|
||||
new GetTask() {
|
||||
new GetTask("") {
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
assertEquals(RESPONSE, s);
|
||||
|
|
|
@ -33,8 +33,10 @@ public class PollWebGuiAvailableTaskTest extends AndroidTestCase {
|
|||
new SyncthingRunnable(new MockContext(null),
|
||||
getContext().getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME);
|
||||
|
||||
String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
new PollWebGuiAvailableTask() {
|
||||
new PollWebGuiAvailableTask(httpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
latch.countDown();
|
||||
|
|
|
@ -23,6 +23,8 @@ public class RestApiTest extends AndroidTestCase {
|
|||
|
||||
private RestApi mApi;
|
||||
|
||||
private String mHttpsCertPath;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
@ -33,8 +35,10 @@ public class RestApiTest extends AndroidTestCase {
|
|||
mConfig = new ConfigXml(new MockContext(getContext()));
|
||||
mConfig.changeDefaultFolder();
|
||||
|
||||
mHttpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(2);
|
||||
new PollWebGuiAvailableTask() {
|
||||
new PollWebGuiAvailableTask(mHttpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
mApi.onWebGuiAvailable();
|
||||
|
|
|
@ -36,7 +36,7 @@ public class ConfigXmlTest extends AndroidTestCase {
|
|||
|
||||
@SmallTest
|
||||
public void testGetWebGuiUrl() {
|
||||
assertTrue(mConfig.getWebGuiUrl().startsWith("http://127.0.0.1:"));
|
||||
assertTrue(mConfig.getWebGuiUrl().startsWith("https://127.0.0.1:"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,13 +3,14 @@ package com.nutomic.syncthingandroid.syncthing;
|
|||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nutomic.syncthingandroid.util.Https;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.protocol.HTTP;
|
||||
|
@ -40,6 +41,12 @@ public class GetTask extends AsyncTask<String, Void, String> {
|
|||
|
||||
public static final String URI_DEVICEID = "/rest/deviceid";
|
||||
|
||||
private String mHttpsCertPath;
|
||||
|
||||
public GetTask(String httpsCertPath) {
|
||||
mHttpsCertPath = httpsCertPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* params[0] Syncthing hostname
|
||||
* params[1] URI to call
|
||||
|
@ -52,7 +59,7 @@ public class GetTask extends AsyncTask<String, Void, String> {
|
|||
// Retry at most 10 times before failing
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String fullUri = params[0] + params[1];
|
||||
HttpClient httpclient = new DefaultHttpClient();
|
||||
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
|
||||
if (params.length == 5) {
|
||||
LinkedList<NameValuePair> urlParams = new LinkedList<>();
|
||||
urlParams.add(new BasicNameValuePair(params[3], params[4]));
|
||||
|
@ -77,7 +84,7 @@ public class GetTask extends AsyncTask<String, Void, String> {
|
|||
br.close();
|
||||
return result;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} catch (IOException|IllegalArgumentException e) {
|
||||
Log.w(TAG, "Failed to call Rest API at " + fullUri, e);
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -4,12 +4,13 @@ package com.nutomic.syncthingandroid.syncthing;
|
|||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nutomic.syncthingandroid.util.Https;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpHead;
|
||||
import org.apache.http.conn.HttpHostConnectException;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -27,13 +28,19 @@ public abstract class PollWebGuiAvailableTask extends AsyncTask<String, Void, Vo
|
|||
*/
|
||||
private static final long WEB_GUI_POLL_INTERVAL = 100;
|
||||
|
||||
private String mHttpsCertPath;
|
||||
|
||||
public PollWebGuiAvailableTask(String httpsCertPath) {
|
||||
mHttpsCertPath = httpsCertPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url The URL of the web GUI (eg 127.0.0.1:8384).
|
||||
* @param @url The URL of the web GUI (eg 127.0.0.1:8384).
|
||||
*/
|
||||
@Override
|
||||
protected Void doInBackground(String... url) {
|
||||
int status = 0;
|
||||
HttpClient httpclient = new DefaultHttpClient();
|
||||
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
|
||||
HttpHead head = new HttpHead(url[0]);
|
||||
do {
|
||||
try {
|
||||
|
@ -45,7 +52,7 @@ public abstract class PollWebGuiAvailableTask extends AsyncTask<String, Void, Vo
|
|||
} catch (HttpHostConnectException e) {
|
||||
// We catch this in every call, as long as the service is not online,
|
||||
// so we ignore and continue.
|
||||
} catch (IOException|InterruptedException e) {
|
||||
} catch (IOException|InterruptedException|IllegalArgumentException e) {
|
||||
Log.w(TAG, "Failed to poll for web interface", e);
|
||||
}
|
||||
} while (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED);
|
||||
|
|
|
@ -3,10 +3,11 @@ package com.nutomic.syncthingandroid.syncthing;
|
|||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nutomic.syncthingandroid.util.Https;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.apache.http.protocol.HTTP;
|
||||
|
||||
|
@ -23,6 +24,12 @@ public class PostTask extends AsyncTask<String, Void, Boolean> {
|
|||
|
||||
public static final String URI_SCAN = "/rest/scan";
|
||||
|
||||
private String mHttpsCertPath;
|
||||
|
||||
public PostTask(String httpsCertPath) {
|
||||
mHttpsCertPath = httpsCertPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* params[0] Syncthing hostname
|
||||
* params[1] URI to call
|
||||
|
@ -32,7 +39,7 @@ public class PostTask extends AsyncTask<String, Void, Boolean> {
|
|||
@Override
|
||||
protected Boolean doInBackground(String... params) {
|
||||
String fullUri = params[0] + params[1];
|
||||
HttpClient httpclient = new DefaultHttpClient();
|
||||
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
|
||||
HttpPost post = new HttpPost(fullUri);
|
||||
post.addHeader(new BasicHeader(RestApi.HEADER_API_KEY, params[2]));
|
||||
|
||||
|
@ -41,7 +48,7 @@ public class PostTask extends AsyncTask<String, Void, Boolean> {
|
|||
post.setEntity(new StringEntity(params[3], HTTP.UTF_8));
|
||||
}
|
||||
httpclient.execute(post);
|
||||
} catch (IOException e) {
|
||||
} catch (IOException|IllegalArgumentException e) {
|
||||
Log.w(TAG, "Failed to call Rest API at " + fullUri, e);
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -151,7 +151,9 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
|
||||
private final String mUrl;
|
||||
|
||||
private String mApiKey;
|
||||
private final String mApiKey;
|
||||
|
||||
private final String mHttpsCertPath;
|
||||
|
||||
private JSONObject mConfig;
|
||||
|
||||
|
@ -180,6 +182,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
mContext = context;
|
||||
mUrl = url;
|
||||
mApiKey = apiKey;
|
||||
mHttpsCertPath = mContext.getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
|
||||
mOnApiAvailableListener = listener;
|
||||
}
|
||||
|
||||
|
@ -197,7 +200,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
public void onApiAvailable();
|
||||
}
|
||||
|
||||
private OnApiAvailableListener mOnApiAvailableListener;
|
||||
private final OnApiAvailableListener mOnApiAvailableListener;
|
||||
|
||||
/**
|
||||
* Gets local device id, syncthing version and config, then calls all OnApiAvailableListeners.
|
||||
|
@ -205,7 +208,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
@Override
|
||||
public void onWebGuiAvailable() {
|
||||
mAvailableCount.set(0);
|
||||
new GetTask() {
|
||||
new GetTask(mHttpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
try {
|
||||
|
@ -219,7 +222,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
}
|
||||
}
|
||||
}.execute(mUrl, GetTask.URI_VERSION, mApiKey);
|
||||
new GetTask() {
|
||||
new GetTask(mHttpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(String config) {
|
||||
try {
|
||||
|
@ -340,7 +343,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
*/
|
||||
@TargetApi(11)
|
||||
public void requireRestart(Activity activity) {
|
||||
new PostTask().execute(mUrl, PostTask.URI_CONFIG, mApiKey, mConfig.toString());
|
||||
new PostTask(mHttpsCertPath).execute(mUrl, PostTask.URI_CONFIG, mApiKey, mConfig.toString());
|
||||
|
||||
if (mRestartPostponed)
|
||||
return;
|
||||
|
@ -438,7 +441,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
* @param listener Callback invoked when the result is received.
|
||||
*/
|
||||
public void getSystemInfo(final OnReceiveSystemInfoListener listener) {
|
||||
new GetTask() {
|
||||
new GetTask(mHttpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
if (s == null)
|
||||
|
@ -557,7 +560,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
* Use the key {@link #LOCAL_DEVICE_CONNECTIONS} to get connection info for the local device.
|
||||
*/
|
||||
public void getConnections(final OnReceiveConnectionsListener listener) {
|
||||
new GetTask() {
|
||||
new GetTask(mHttpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
if (s == null)
|
||||
|
@ -650,7 +653,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
* Returns status information about the folder with the given ID.
|
||||
*/
|
||||
public void getModel(final String folderId, final OnReceiveModelListener listener) {
|
||||
new GetTask() {
|
||||
new GetTask(mHttpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
if (s == null)
|
||||
|
@ -877,7 +880,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
* Normalizes a given device ID.
|
||||
*/
|
||||
public void normalizeDeviceId(String id, final OnDeviceIdNormalizedListener listener) {
|
||||
new GetTask() {
|
||||
new GetTask(mHttpsCertPath) {
|
||||
@Override
|
||||
protected void onPostExecute(String s) {
|
||||
super.onPostExecute(s);
|
||||
|
@ -934,7 +937,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
|
|||
*/
|
||||
@Override
|
||||
public void onFolderFileChange(String folderId, String relativePath) {
|
||||
new PostTask().execute(mUrl, PostTask.URI_SCAN, mApiKey, "folder", folderId, "sub",
|
||||
new PostTask(mHttpsCertPath).execute(mUrl, PostTask.URI_SCAN, mApiKey, "folder", folderId, "sub",
|
||||
relativePath);
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,11 @@ public class SyncthingService extends Service {
|
|||
*/
|
||||
public static final String PRIVATE_KEY_FILE = "key.pem";
|
||||
|
||||
/**
|
||||
* Name of the public HTTPS CA file in the data directory.
|
||||
*/
|
||||
public static final String HTTPS_CERT_FILE = "https-cert.pem";
|
||||
|
||||
/**
|
||||
* Directory where config is exported to and imported from.
|
||||
*/
|
||||
|
@ -195,7 +200,7 @@ public class SyncthingService extends Service {
|
|||
mConfig = new ConfigXml(SyncthingService.this);
|
||||
mCurrentState = State.STARTING;
|
||||
registerOnWebGuiAvailableListener(mApi);
|
||||
new PollWebGuiAvailableTaskImpl().execute(mConfig.getWebGuiUrl());
|
||||
new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE).execute(mConfig.getWebGuiUrl());
|
||||
new Thread(new SyncthingRunnable(
|
||||
this, getApplicationInfo().dataDir + "/" + BINARY_NAME)).start();
|
||||
Notification n = new NotificationCompat.Builder(this)
|
||||
|
@ -357,6 +362,11 @@ public class SyncthingService extends Service {
|
|||
}
|
||||
|
||||
private class PollWebGuiAvailableTaskImpl extends PollWebGuiAvailableTask {
|
||||
|
||||
public PollWebGuiAvailableTaskImpl(String httpsCertPath) {
|
||||
super(httpsCertPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
if (mStopScheduled) {
|
||||
|
|
|
@ -88,7 +88,7 @@ public class ConfigXml {
|
|||
}
|
||||
|
||||
public String getWebGuiUrl() {
|
||||
return "http://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent();
|
||||
return "https://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent();
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package com.nutomic.syncthingandroid.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
|
||||
/*
|
||||
* TrustManager allowing the Syncthing https.pem CA
|
||||
*
|
||||
* Based on http://stackoverflow.com/questions/16719959#16759793
|
||||
*
|
||||
*/
|
||||
public class CustomX509TrustManager implements X509TrustManager {
|
||||
|
||||
private static final String TAG = "CustomX509TrustManager";
|
||||
|
||||
/**
|
||||
* Taken from: http://janis.peisenieks.lv/en/76/english-making-an-ssl-connection-via-android/
|
||||
*
|
||||
*/
|
||||
public static class CustomSSLSocketFactory extends SSLSocketFactory {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
|
||||
public CustomSSLSocketFactory(SSLContext context)
|
||||
throws KeyManagementException, NoSuchAlgorithmException,
|
||||
KeyStoreException, UnrecoverableKeyException {
|
||||
super(null);
|
||||
sslContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket socket, String host, int port,
|
||||
boolean autoClose) throws IOException {
|
||||
return sslContext.getSocketFactory().createSocket(socket, host, port,
|
||||
autoClose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return sslContext.getSocketFactory().createSocket();
|
||||
}
|
||||
}
|
||||
|
||||
private String mHttpsCertPath;
|
||||
|
||||
public CustomX509TrustManager(String httpsCertPath) {
|
||||
mHttpsCertPath = httpsCertPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies certs against public key of the local syncthing instance
|
||||
*/
|
||||
@Override
|
||||
public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
|
||||
String authType) throws CertificateException {
|
||||
InputStream inStream = null;
|
||||
try {
|
||||
inStream = new FileInputStream(mHttpsCertPath);
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate ca = (X509Certificate)
|
||||
cf.generateCertificate(inStream);
|
||||
for (X509Certificate cert : certs) {
|
||||
cert.verify(ca.getPublicKey());
|
||||
}
|
||||
} catch (FileNotFoundException |NoSuchAlgorithmException|InvalidKeyException|
|
||||
NoSuchProviderException |SignatureException e) {
|
||||
throw new CertificateException("Untrusted Certificate!", e);
|
||||
} finally {
|
||||
try {
|
||||
if (inStream != null)
|
||||
inStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
}
|
53
src/main/java/com/nutomic/syncthingandroid/util/Https.java
Normal file
53
src/main/java/com/nutomic/syncthingandroid/util/Https.java
Normal file
|
@ -0,0 +1,53 @@
|
|||
package com.nutomic.syncthingandroid.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.conn.ClientConnectionManager;
|
||||
import org.apache.http.conn.scheme.Scheme;
|
||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
/*
|
||||
* Wrapper for HTTPS Clients allowing the Syncthing https.pem CA
|
||||
*
|
||||
*/
|
||||
public class Https {
|
||||
|
||||
private static final String TAG = "HTTPS";
|
||||
|
||||
/**
|
||||
* Create a HTTPClient that verifies a custom PEM certificate
|
||||
*
|
||||
* @param httpsCertPath refers to the filepath of a SSL/TLS PEM certificate.
|
||||
*/
|
||||
public static HttpClient createHttpsClient(String httpsCertPath) {
|
||||
try {
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(null, new TrustManager[] { new CustomX509TrustManager(httpsCertPath) },
|
||||
new SecureRandom());
|
||||
HttpClient client = new DefaultHttpClient();
|
||||
SSLSocketFactory ssf = new CustomX509TrustManager.CustomSSLSocketFactory(ctx);
|
||||
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
|
||||
ClientConnectionManager ccm = client.getConnectionManager();
|
||||
SchemeRegistry sr = ccm.getSchemeRegistry();
|
||||
sr.register(new Scheme("https", ssf, 443));
|
||||
return new DefaultHttpClient(ccm,
|
||||
client.getParams());
|
||||
} catch (NoSuchAlgorithmException|KeyManagementException|KeyStoreException|
|
||||
UnrecoverableKeyException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue