1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-11-26 22:31:16 +00:00

Replaced deprecated http APIs

This commit is contained in:
Felix Ableitner 2016-09-22 22:23:12 +09:00
parent b38a152f6a
commit 63e8c22129
21 changed files with 332 additions and 632 deletions

View file

@ -28,7 +28,6 @@ dependencies {
compile 'org.mindrot:jbcrypt:0.3m'
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.1.2'
androidTestCompile 'com.squareup.okhttp:mockwebserver:2.4.0'
}
project.archivesBaseName = 'syncthing'

View file

@ -6,12 +6,13 @@ import android.content.Context;
import android.support.annotation.NonNull;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class MockRestApi extends RestApi {
public MockRestApi(Context context, String url, String apiKey,
public MockRestApi(Context context, URL url, String apiKey,
OnApiAvailableListener listener) {
super(context, url, apiKey, listener, null);
}
@ -76,7 +77,7 @@ public class MockRestApi extends RestApi {
}
@Override
public boolean editFolder(Folder folder, boolean create, Activity activity) {
public void editFolder(Folder folder, boolean create, Activity activity) {
throw new UnsupportedOperationException();
}

View file

@ -6,6 +6,7 @@ import android.os.IBinder;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import java.net.URL;
import java.util.LinkedList;
public class MockSyncthingService extends SyncthingService {
@ -67,7 +68,7 @@ public class MockSyncthingService extends SyncthingService {
}
@Override
public String getWebGuiUrl() {
public URL getWebGuiUrl() {
throw new UnsupportedOperationException();
}

View file

@ -1,71 +0,0 @@
package com.nutomic.syncthingandroid.test.syncthing;
import android.net.Uri;
import android.test.AndroidTestCase;
import com.nutomic.syncthingandroid.syncthing.GetTask;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
import java.io.IOException;
public class GetTaskTest extends AndroidTestCase {
private MockWebServer mServer;
private static final String RESPONSE = "the response";
private static final String API_KEY = "the key";
private static final String PARAM_KEY_ONE = "first-param";
private static final String PARAM_VALUE_ONE = "first param value";
@Override
protected void setUp() throws Exception {
super.setUp();
mServer = new MockWebServer();
mServer.enqueue(new MockResponse().setBody(RESPONSE));
mServer.play();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
// TODO: causes problems, see https://github.com/square/okhttp/issues/1033
//mServer.shutdown();
}
public void testGetNoParams() throws IOException, InterruptedException {
new GetTask("") {
@Override
protected void onPostExecute(String s) {
assertEquals(RESPONSE, s);
}
}.execute(mServer.getUrl("").toString(), GetTask.URI_CONFIG, API_KEY);
RecordedRequest request = mServer.takeRequest();
assertEquals(API_KEY, request.getHeader(RestApi.HEADER_API_KEY));
Uri uri = Uri.parse(request.getPath());
assertEquals(GetTask.URI_CONFIG, uri.getPath());
}
public void testGetParams() throws IOException, InterruptedException {
new GetTask("") {
@Override
protected void onPostExecute(String s) {
assertEquals(RESPONSE, s);
}
}.execute(mServer.getUrl("").toString(), GetTask.URI_CONFIG, API_KEY, PARAM_KEY_ONE,
PARAM_VALUE_ONE);
RecordedRequest request = mServer.takeRequest();
assertEquals(API_KEY, request.getHeader(RestApi.HEADER_API_KEY));
Uri uri = Uri.parse(request.getPath());
assertEquals(GetTask.URI_CONFIG, uri.getPath());
assertEquals(PARAM_VALUE_ONE, uri.getQueryParameter(PARAM_KEY_ONE));
}
}

View file

@ -1,48 +0,0 @@
package com.nutomic.syncthingandroid.test.syncthing;
import android.test.AndroidTestCase;
import com.nutomic.syncthingandroid.syncthing.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.syncthing.SyncthingRunnable;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import com.nutomic.syncthingandroid.test.MockContext;
import com.nutomic.syncthingandroid.util.ConfigXml;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class PollWebGuiAvailableTaskTest extends AndroidTestCase {
private ConfigXml mConfig;
@Override
protected void setUp() throws Exception {
super.setUp();
mConfig = new ConfigXml(new MockContext(getContext()));
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
ConfigXml.getConfigFile(new MockContext(getContext())).delete();
}
public void testPolling() throws InterruptedException {
new SyncthingRunnable(new MockContext(getContext()), SyncthingRunnable.Command.main);
String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
final CountDownLatch latch = new CountDownLatch(1);
new PollWebGuiAvailableTask(httpsCertPath) {
@Override
protected void onPostExecute(Void aVoid) {
latch.countDown();
}
}.execute(mConfig.getWebGuiUrl());
latch.await(1, TimeUnit.SECONDS);
// TODO: Unit tests fail when Syncthing is killed SyncthingRunnable.killSyncthing();
}
}

View file

@ -2,7 +2,7 @@ package com.nutomic.syncthingandroid.test.syncthing;
import android.test.AndroidTestCase;
import com.nutomic.syncthingandroid.syncthing.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingRunnable;
import com.nutomic.syncthingandroid.syncthing.SyncthingService;
@ -28,13 +28,13 @@ public class RestApiTest extends AndroidTestCase {
String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE;
final CountDownLatch latch = new CountDownLatch(2);
new PollWebGuiAvailableTask(httpsCertPath) {
new PollWebGuiAvailableTask(config.getWebGuiUrl(), httpsCertPath, config.getApiKey()) {
@Override
protected void onPostExecute(Void aVoid) {
mApi.onWebGuiAvailable();
latch.countDown();
}
}.execute(config.getWebGuiUrl());
}.execute();
mApi = new RestApi(getContext(), config.getWebGuiUrl(), config.getApiKey(),
latch::countDown, null);
latch.await(1, TimeUnit.SECONDS);

View file

@ -29,7 +29,7 @@ public class ConfigXmlTest extends AndroidTestCase {
}
public void testGetWebGuiUrl() {
assertTrue(mConfig.getWebGuiUrl().startsWith("https://127.0.0.1:"));
assertTrue(mConfig.getWebGuiUrl().toString().startsWith("https://127.0.0.1:"));
}
}

View file

@ -129,7 +129,7 @@ public class WebGuiActivity extends SyncthingActivity
@Override
public void onWebGuiAvailable() {
mWebView.loadUrl(getService().getWebGuiUrl());
mWebView.loadUrl(getService().getWebGuiUrl().toString());
}
/**

View file

@ -0,0 +1,57 @@
package com.nutomic.syncthingandroid.http;
import android.util.Log;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
/**
* Performs a GET request to the Syncthing API
* Performs a GET request with no parameters to the URL in uri[0] with the path in uri[1] and
* returns the result as a String.
*/
public class GetTask extends RestTask<String, Void, String> {
private static final String TAG = "GetTask";
public static final String URI_CONFIG = "/rest/system/config";
public static final String URI_VERSION = "/rest/system/version";
public static final String URI_SYSTEM = "/rest/system/status";
public static final String URI_CONNECTIONS = "/rest/system/connections";
public static final String URI_MODEL = "/rest/db/status";
public static final String URI_DEVICEID = "/rest/svc/deviceid";
public static final String URI_REPORT = "/rest/svc/report";
public static final String URI_EVENTS = "/rest/events";
public GetTask(URL url, String path, String httpsCertPath, String apiKey) {
super(url, path, httpsCertPath, apiKey);
}
/**
* @param params Keys and values for the query string.
*/
@Override
protected String doInBackground(String... params) {
try {
HttpsURLConnection connection = openConnection(params);
Log.v(TAG, "Calling Rest API at " + connection.getURL());
connection.connect();
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String line;
String result = "";
while ((line = br.readLine()) != null) {
result += line;
}
br.close();
Log.v(TAG, "API call result: " + result);
return result;
} catch (IOException e) {
Log.w(TAG, "Failed to call rest api", e);
return null;
}
}
}

View file

@ -0,0 +1,47 @@
package com.nutomic.syncthingandroid.http;
import android.util.Log;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.net.URL;
/**
* Polls to load the web interface, until we receive http status 200.
*/
public abstract class PollWebGuiAvailableTask extends RestTask<Void, Void, Void> {
private static final String TAG = "PollWebGuiAvailableTask";
/**
* Interval in ms, at which connections to the web gui are performed on first start
* to find out if it's online.
*/
private static final long WEB_GUI_POLL_INTERVAL = 100;
public PollWebGuiAvailableTask(URL url, String httpsCertPath, String apiKey) {
super(url, "", httpsCertPath, apiKey);
}
@Override
protected Void doInBackground(Void... aVoid) {
int status = 0;
do {
try {
HttpsURLConnection connection = openConnection();
connection.connect();
status = connection.getResponseCode();
} catch (IOException e) {
// We catch this in every call, as long as the service is not online, so we ignore and continue.
try {
Thread.sleep(WEB_GUI_POLL_INTERVAL);
} catch (InterruptedException e2) {
Log.w(TAG, "Failed to sleep", e2);
}
}
} while (status != HttpsURLConnection.HTTP_OK);
return null;
}
}

View file

@ -0,0 +1,60 @@
package com.nutomic.syncthingandroid.http;
import android.util.Log;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
/**
* Sends a POST request to the Syncthing API.
*/
public class PostTask extends RestTask<String, Void, Boolean> {
private static final String TAG = "PostTask";
public static final String URI_CONFIG = "/rest/system/config";
public static final String URI_SCAN = "/rest/db/scan";
public PostTask(URL url, String path, String httpsCertPath, String apiKey) {
super(url, path, httpsCertPath, apiKey);
}
/**
* For {@link #URI_CONFIG}, params[0] must contain the config.
*
* For {@link #URI_SCAN}, params[0] must contain the folder, and params[1] the subfolder.
*/
@Override
protected Boolean doInBackground(String... params) {
try {
HttpsURLConnection connection = (mPath.equals(URI_SCAN))
? openConnection("folder", params[0], "sub", params[1])
: openConnection();
connection.setRequestMethod("POST");
Log.v(TAG, "Calling Rest API at " + connection.getURL());
if (mPath.equals(URI_CONFIG)) {
OutputStream os = connection.getOutputStream();
os.write(params[0].getBytes("UTF-8"));
}
connection.connect();
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String line;
String result = "";
while ((line = br.readLine()) != null) {
result += line;
}
br.close();
Log.v(TAG, "API call result: " + result);
return true;
} catch (IOException e) {
Log.w(TAG, "Failed to call rest api", e);
return false;
}
}
}

View file

@ -0,0 +1,111 @@
package com.nutomic.syncthingandroid.http;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public abstract class RestTask<A, B, C> extends AsyncTask<A, B, C> {
private static final String TAG = "RestTask";
private final URL mUrl;
protected final String mPath;
private final String mHttpsCertPath;
private final String mApiKey;
public RestTask(URL url, String path, String httpsCertPath, String apiKey) {
mUrl = url;
mPath = path;
mHttpsCertPath = httpsCertPath;
mApiKey = apiKey;
}
protected HttpsURLConnection openConnection(String... params) throws IOException {
Uri.Builder uriBuilder = Uri.parse(mUrl.toString())
.buildUpon()
.path(mPath);
for (int paramCounter = 0; paramCounter + 1 < params.length; ) {
uriBuilder.appendQueryParameter(params[paramCounter++], params[paramCounter++]);
}
URL url = new URL(uriBuilder.build().toString());
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setRequestProperty(RestApi.HEADER_API_KEY, mApiKey);
connection.setHostnameVerifier((h, s) -> true);
connection.setSSLSocketFactory(getSslSocketFactory());
return connection;
}
private SSLSocketFactory getSslSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new SyncthingTrustManager()},
new SecureRandom());
return sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
Log.w(TAG, e);
return null;
}
}
/*
* TrustManager checking against the local Syncthing instance's https public key.
*
* Based on http://stackoverflow.com/questions/16719959#16759793
*/
private class SyncthingTrustManager implements X509TrustManager {
private static final String TAG = "SyncthingTrustManager";
@Override
@SuppressLint("TrustAllX509TrustManager")
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
/**
* Verifies certs against public key of the local syncthing instance
*/
@Override
public void checkServerTrusted(X509Certificate[] certs,
String authType) throws CertificateException {
InputStream is = null;
try {
is = new FileInputStream(mHttpsCertPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate ca = (X509Certificate) cf.generateCertificate(is);
for (X509Certificate cert : certs) {
cert.verify(ca.getPublicKey());
}
} catch (FileNotFoundException | NoSuchAlgorithmException | InvalidKeyException |
NoSuchProviderException | SignatureException e) {
throw new CertificateException("Untrusted Certificate!", e);
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}

View file

@ -1,107 +0,0 @@
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.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.LinkedList;
/**
* Performs a GET request with no parameters to the URL in uri[0] with the path in uri[1] and
* returns the result as a String.
*/
public class GetTask extends AsyncTask<String, Void, String> {
private static final String TAG = "GetTask";
public static final String URI_CONFIG = "/rest/system/config";
public static final String URI_VERSION = "/rest/system/version";
public static final String URI_SYSTEM = "/rest/system/status";
public static final String URI_CONNECTIONS = "/rest/system/connections";
public static final String URI_MODEL = "/rest/db/status";
public static final String URI_DEVICEID = "/rest/svc/deviceid";
public static final String URI_REPORT = "/rest/svc/report";
public static final String URI_EVENTS = "/rest/events";
private final String mHttpsCertPath;
public GetTask(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
/**
* params[0] Syncthing hostname
* params[1] URI to call
* params[2] Syncthing API key
* params[3] optional parameter key
* params[4] optional parameter value
*/
@Override
protected String doInBackground(String... params) {
String fullUri = params[0] + params[1];
Log.v(TAG, "Calling Rest API at " + fullUri);
if (params.length >= 5) {
LinkedList<NameValuePair> urlParams = new LinkedList<>();
for (int paramCounter = 3; paramCounter + 1 < params.length; ) {
urlParams.add(new BasicNameValuePair(params[paramCounter++], params[paramCounter++]));
}
fullUri += "?" + URLEncodedUtils.format(urlParams, HTTP.UTF_8);
}
// Retry at most 5 times before failing
for (int i = 0; i < 5; i++) {
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
HttpGet get = new HttpGet(fullUri);
get.addHeader(new BasicHeader(RestApi.HEADER_API_KEY, params[2]));
if (isCancelled())
return null;
try {
HttpResponse response = httpclient.execute(get);
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream is = entity.getContent();
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line;
String result = "";
while ((line = br.readLine()) != null) {
result += line;
}
br.close();
Log.v(TAG, "API call result: " + result);
return result;
}
} catch (IOException|IllegalArgumentException e) {
Log.w(TAG, "Failed to call Rest API at " + fullUri);
}
try {
// Don't push the API too hard
Thread.sleep(500 * i);
} catch (InterruptedException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Retrying GetTask Rest API call (" + (i + 1) + "/5)");
}
return null;
}
}

View file

@ -1,61 +0,0 @@
package com.nutomic.syncthingandroid.syncthing;
import android.os.AsyncTask;
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 java.io.IOException;
/**
* Polls SYNCTHING_URL until it returns HTTP status OK, then calls all listeners
* in mOnWebGuiAvailableListeners and clears it.
*/
public abstract class PollWebGuiAvailableTask extends AsyncTask<String, Void, Void> {
private static final String TAG = "PollWebGuiAvailableTask";
/**
* Interval in ms, at which connections to the web gui are performed on first start
* to find out if it's online.
*/
private static final long WEB_GUI_POLL_INTERVAL = 100;
private final String mHttpsCertPath;
public PollWebGuiAvailableTask(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
/**
* @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 = Https.createHttpsClient(mHttpsCertPath);
HttpHead head = new HttpHead(url[0]);
do {
try {
Thread.sleep(WEB_GUI_POLL_INTERVAL);
HttpResponse response = httpclient.execute(head);
// NOTE: status is not really needed, as HttpHostConnectException is thrown
// earlier.
status = response.getStatusLine().getStatusCode();
} 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|IllegalArgumentException e) {
//Log.w(TAG, "Failed to poll for web interface", e);
}
} while (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED);
return null;
}
}

View file

@ -1,56 +0,0 @@
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.message.BasicHeader;
import org.apache.http.protocol.HTTP;
import java.io.IOException;
/**
* Sends a new config to {@link #URI_CONFIG}.
*/
public class PostConfigTask extends AsyncTask<String, Void, Boolean> {
private static final String TAG = "PostConfigTask";
public static final String URI_CONFIG = "/rest/system/config";
private final String mHttpsCertPath;
public PostConfigTask(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
/**
* params[0] Syncthing hostname
* params[1] Syncthing API key
* params[2] The new config
*/
@Override
protected Boolean doInBackground(String... params) {
String fullUri = params[0] + URI_CONFIG;
Log.v(TAG, "Calling Rest API at " + fullUri);
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
HttpPost post = new HttpPost(fullUri);
post.addHeader(new BasicHeader(RestApi.HEADER_API_KEY, params[1]));
try {
post.setEntity(new StringEntity(params[2], HTTP.UTF_8));
Log.v(TAG, "API call parameters: " + params[2]);
httpclient.execute(post);
} catch (IOException|IllegalArgumentException e) {
Log.w(TAG, "Failed to call Rest API at " + fullUri, e);
return false;
}
return true;
}
}

View file

@ -1,78 +0,0 @@
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.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import java.io.IOException;
import java.util.LinkedList;
/**
* Performs a POST request to {@link #URI_SCAN} to notify Syncthing of a changed file or folder.
*/
public class PostScanTask extends AsyncTask<String, Void, Void> {
private static final String TAG = "PostScanTask";
public static final String URI_SCAN = "/rest/db/scan";
private final String mHttpsCertPath;
public PostScanTask(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
/**
* params[0] Syncthing hostname
* params[1] Syncthing API key
* params[2] folder parameter (the Syncthing folder to update)
* params[3] sub parameter (the subfolder to update
*/
@Override
protected Void doInBackground(String... params) {
String fullUri = params[0] + URI_SCAN;
LinkedList<NameValuePair> urlParams = new LinkedList<>();
urlParams.add(new BasicNameValuePair("folder", params[2]));
urlParams.add(new BasicNameValuePair("sub", params[3]));
fullUri += "?" + URLEncodedUtils.format(urlParams, HTTP.UTF_8);
Log.v(TAG, "Calling Rest API at " + fullUri);
// Retry at most 5 times before failing
for (int i = 0; i < 5; i++) {
HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath);
HttpPost post = new HttpPost(fullUri);
post.addHeader(new BasicHeader(RestApi.HEADER_API_KEY, params[1]));
if (isCancelled())
return null;
try {
HttpResponse response = httpclient.execute(post);
if (response.getEntity() != null)
return null;
} catch (IOException | IllegalArgumentException e) {
Log.w(TAG, "Failed to call Rest API at " + fullUri);
}
try {
// Don't push the API too hard
Thread.sleep(500 * i);
} catch (InterruptedException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Retrying GetTask Rest API call (" + (i + 1) + "/5)");
}
return null;
}
}

View file

@ -11,26 +11,23 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import com.nutomic.syncthingandroid.BuildConfig;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.activities.RestartActivity;
import com.nutomic.syncthingandroid.http.GetTask;
import com.nutomic.syncthingandroid.http.PostTask;
import com.nutomic.syncthingandroid.util.FolderObserver;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
@ -169,7 +166,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
private String mVersion;
private final String mUrl;
private final URL mUrl;
private final String mApiKey;
@ -203,7 +200,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
private final Map<String, String> mCacheFolderPathLookup = new HashMap<>();
public RestApi(Context context, String url, String apiKey, OnApiAvailableListener apiListener,
public RestApi(Context context, URL url, String apiKey, OnApiAvailableListener apiListener,
OnConfigChangedListener configListener) {
mContext = context;
mUrl = url;
@ -237,7 +234,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
@Override
public void onWebGuiAvailable() {
mAvailableCount.set(0);
new GetTask(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_VERSION, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -253,8 +250,8 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
Log.w(TAG, "Failed to parse config", e);
}
}
}.execute(mUrl, GetTask.URI_VERSION, mApiKey);
new GetTask(mHttpsCertPath) {
}.execute();
new GetTask(mUrl, GetTask.URI_CONFIG, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String config) {
try {
@ -264,7 +261,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
Log.w(TAG, "Failed to parse config", e);
}
}
}.execute(mUrl, GetTask.URI_CONFIG, mApiKey);
}.execute();
getSystemInfo(info -> {
mLocalDeviceId = info.myID;
tryIsAvailable();
@ -367,7 +364,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
public void requireRestart(Activity activity) {
if (mRestartPostponed) {
new PostConfigTask(mHttpsCertPath).execute(mUrl, mApiKey, mConfig.toString());
new PostTask(mUrl, PostTask.URI_CONFIG, mHttpsCertPath, mApiKey).execute(mConfig.toString());
} else {
activity.startActivity(new Intent(mContext, RestartActivity.class));
}
@ -380,13 +377,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* This executes a restart immediately, and does not show a dialog.
*/
public void updateConfig() {
new PostConfigTask(mHttpsCertPath) {
new PostTask(mUrl, PostTask.URI_CONFIG, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(Boolean aBoolean) {
protected void onPostExecute(Boolean b) {
mContext.startService(new Intent(mContext, SyncthingService.class)
.setAction(SyncthingService.ACTION_RESTART));
}
}.execute(mUrl, mApiKey, mConfig.toString());
}.execute(mConfig.toString());
}
@ -445,7 +442,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* @param listener Callback invoked when the result is received.
*/
public void getSystemInfo(final OnReceiveSystemInfoListener listener) {
new GetTask(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_SYSTEM, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -472,7 +469,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
Log.w(TAG, "Failed to read system info", e);
}
}
}.execute(mUrl, GetTask.URI_SYSTEM, mApiKey);
}.execute();
}
/**
@ -481,7 +478,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* @param listener Callback invoked when the result is received.
*/
public void getSystemVersion(final OnReceiveSystemVersionListener listener) {
new GetTask(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_VERSION, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String response) {
if (response == null) {
@ -495,7 +492,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
Log.w(TAG, "Failed to read system info", e);
}
}
}.execute(mUrl, GetTask.URI_VERSION, mApiKey);
}.execute();
}
/**
@ -594,7 +591,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* Use the key {@link #TOTAL_STATS} to get connection info for the local device.
*/
public void getConnections(final OnReceiveConnectionsListener listener) {
new GetTask(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_CONNECTIONS, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -650,7 +647,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
Log.w(TAG, "Failed to parse connections", e);
}
}
}.execute(mUrl, GetTask.URI_CONNECTIONS, mApiKey);
}.execute();
}
/**
@ -720,7 +717,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(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_MODEL, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -748,7 +745,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
Log.w(TAG, "Failed to read folder info", e);
}
}
}.execute(mUrl, GetTask.URI_MODEL, mApiKey, "folder", folderId);
}.execute("folder", folderId);
}
/**
@ -779,7 +776,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* The OnReceiveEventListeners onEvent method is called for each event.
*/
public final void getEvents(final long sinceId, final long limit, final OnReceiveEventListener listener) {
new GetTask(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_EVENTS, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String s) {
if (s == null)
@ -815,7 +812,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
Log.w(TAG, "Failed to read events", e);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mUrl, GetTask.URI_EVENTS, mApiKey,
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
"since", String.valueOf(sinceId), "limit", String.valueOf(limit));
}
@ -998,7 +995,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* Normalizes a given device ID.
*/
public void normalizeDeviceId(final String id, final OnDeviceIdNormalizedListener listener) {
new GetTask(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_DEVICEID, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
@ -1016,7 +1013,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
}
listener.onDeviceIdNormalized(normalized, error);
}
}.execute(mUrl, GetTask.URI_DEVICEID, mApiKey, "id", id);
}.execute("id", id);
}
/**
@ -1050,7 +1047,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
*/
@Override
public void onFolderFileChange(String folderId, String relativePath) {
new PostScanTask(mHttpsCertPath).execute(mUrl, mApiKey, folderId, relativePath);
new PostTask(mUrl, PostTask.URI_SCAN, mHttpsCertPath, mApiKey).execute(folderId, relativePath);
}
/**
@ -1109,7 +1106,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
* Returns prettyfied usage report.
*/
public void getUsageReport(final OnReceiveUsageReportListener listener) {
new GetTask(mHttpsCertPath) {
new GetTask(mUrl, GetTask.URI_REPORT, mHttpsCertPath, mApiKey) {
@Override
protected void onPostExecute(String s) {
try {
@ -1121,7 +1118,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
throw new RuntimeException("Failed to prettify usage report", e);
}
}
}.execute(mUrl, GetTask.URI_REPORT, mApiKey);
}.execute();
}
/**

View file

@ -22,6 +22,7 @@ import android.widget.Toast;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.activities.MainActivity;
import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.util.ConfigXml;
import com.nutomic.syncthingandroid.util.FolderObserver;
import com.nutomic.syncthingandroid.util.PRNGFixes;
@ -30,6 +31,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Iterator;
@ -226,8 +228,8 @@ public class SyncthingService extends Service implements
registerOnWebGuiAvailableListener(mApi);
if (mEventProcessor != null)
registerOnWebGuiAvailableListener(mEventProcessor);
new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE)
.execute(mConfig.getWebGuiUrl());
new PollWebGuiAvailableTaskImpl(getWebGuiUrl(), getFilesDir() + "/" + HTTPS_CERT_FILE, mConfig.getApiKey())
.execute();
mRunnable = new SyncthingRunnable(this, SyncthingRunnable.Command.main);
new Thread(mRunnable).start();
updateNotification();
@ -321,10 +323,10 @@ public class SyncthingService extends Service implements
* version, and reads syncthing URL and API key (these are passed internally as
* {@code Pair<String, String>}.
*/
private class StartupTask extends AsyncTask<Void, Void, Pair<String, String>> {
private class StartupTask extends AsyncTask<Void, Void, Pair<URL, String>> {
@Override
protected Pair<String, String> doInBackground(Void... voids) {
protected Pair<URL, String> doInBackground(Void... voids) {
try {
mConfig = new ConfigXml(SyncthingService.this);
return new Pair<>(mConfig.getWebGuiUrl(), mConfig.getApiKey());
@ -334,7 +336,7 @@ public class SyncthingService extends Service implements
}
@Override
protected void onPostExecute(Pair<String, String> urlAndKey) {
protected void onPostExecute(Pair<URL, String> urlAndKey) {
if (urlAndKey == null) {
Toast.makeText(SyncthingService.this, R.string.config_create_failed,
Toast.LENGTH_LONG).show();
@ -478,8 +480,8 @@ public class SyncthingService extends Service implements
private class PollWebGuiAvailableTaskImpl extends PollWebGuiAvailableTask {
public PollWebGuiAvailableTaskImpl(String httpsCertPath) {
super(httpsCertPath);
public PollWebGuiAvailableTaskImpl(URL url, String httpsCertPath, String apiKey) {
super(url, httpsCertPath, apiKey);
}
/**
@ -528,7 +530,7 @@ public class SyncthingService extends Service implements
}
}
public String getWebGuiUrl() {
public URL getWebGuiUrl() {
return mConfig.getWebGuiUrl();
}

View file

@ -18,6 +18,8 @@ import org.xml.sax.SAXException;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Locale;
@ -96,8 +98,12 @@ public class ConfigXml {
return new File(context.getFilesDir(), CONFIG_FILE);
}
public String getWebGuiUrl() {
return "https://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent();
public URL getWebGuiUrl() {
try {
return new URL("https://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent());
} catch (MalformedURLException e) {
throw new RuntimeException("Failed to parse web interface URL", e);
}
}
public String getApiKey() {

View file

@ -1,107 +0,0 @@
package com.nutomic.syncthingandroid.util;
import android.annotation.SuppressLint;
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 final String mHttpsCertPath;
public CustomX509TrustManager(String httpsCertPath) {
mHttpsCertPath = httpsCertPath;
}
@Override
@SuppressLint("TrustAllX509TrustManager")
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;
}
}

View file

@ -1,53 +0,0 @@
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;
}
}