From c7cdfa1ff2beda8d75b9f5030ef43b7d578b10b1 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Fri, 27 Mar 2015 23:29:55 +0100 Subject: [PATCH 01/10] Implement TLS support (fixes #19) --- .../test/syncthing/GetTaskTest.java | 4 +- .../PollWebGuiAvailableTaskTest.java | 4 +- .../test/syncthing/RestApiTest.java | 6 +- .../test/util/ConfigXmlTest.java | 2 +- .../syncthingandroid/syncthing/GetTask.java | 13 ++- .../syncthing/PollWebGuiAvailableTask.java | 15 ++- .../syncthingandroid/syncthing/PostTask.java | 13 ++- .../syncthingandroid/syncthing/RestApi.java | 23 ++-- .../syncthing/SyncthingService.java | 12 +- .../syncthingandroid/util/ConfigXml.java | 2 +- .../util/CustomX509TrustManager.java | 105 ++++++++++++++++++ .../nutomic/syncthingandroid/util/Https.java | 53 +++++++++ 12 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/nutomic/syncthingandroid/util/CustomX509TrustManager.java create mode 100644 src/main/java/com/nutomic/syncthingandroid/util/Https.java diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java index 86e86a8d..8743cddd 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java @@ -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); diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java index 042ecbf9..9d42a95b 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java @@ -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(); diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java index 22d97e92..58b79a2a 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java @@ -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(); diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java index 94662cb7..a109d026 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java @@ -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:")); } /** diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java index 8695abd7..cb7b8a9d 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java @@ -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 { 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 { // 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 urlParams = new LinkedList<>(); urlParams.add(new BasicNameValuePair(params[3], params[4])); @@ -77,7 +84,7 @@ public class GetTask extends AsyncTask { br.close(); return result; } - } catch (IOException e) { + } catch (IOException|IllegalArgumentException e) { Log.w(TAG, "Failed to call Rest API at " + fullUri, e); } try { diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java index 829381e9..7f44c557 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java @@ -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 { 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 { @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 { 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; } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index b173f775..a8bb70dd 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -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); } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index 74e58f4f..4d9724f6 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -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) { diff --git a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java index 9154d8d7..719792d7 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java @@ -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() { diff --git a/src/main/java/com/nutomic/syncthingandroid/util/CustomX509TrustManager.java b/src/main/java/com/nutomic/syncthingandroid/util/CustomX509TrustManager.java new file mode 100644 index 00000000..2266b4de --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/util/CustomX509TrustManager.java @@ -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; + } +} diff --git a/src/main/java/com/nutomic/syncthingandroid/util/Https.java b/src/main/java/com/nutomic/syncthingandroid/util/Https.java new file mode 100644 index 00000000..17ec2d8c --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/util/Https.java @@ -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; + } +} From 8f2fe9955a862f7ae763f4a8237cd144ff9f4940 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Thu, 9 Apr 2015 20:03:50 +0200 Subject: [PATCH 02/10] Enable TLS for the WebGUI --- proguard-android.txt | 5 ++ .../activities/WebGuiActivity.java | 84 ++++++++++++++++++- .../syncthingandroid/syncthing/RestApi.java | 4 + 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/proguard-android.txt b/proguard-android.txt index fb4c375e..5629c4d0 100644 --- a/proguard-android.txt +++ b/proguard-android.txt @@ -1,3 +1,8 @@ # Fix appcompat-v7 v21.0.0 causing crash on Samsung devices with Android v4.2.2 (https://code.google.com/p/android/issues/detail?id=78377) -keep class !android.support.v7.internal.view.menu.MenuBuilder, !android.support.v7.internal.view.menu.SubMenuBuilder, android.support.v7.** { *; } -keep interface android.support.v7.** { *; } + +# Enable reflective access to mX509Certificate +-keepclassmembers class android.net.http.SslCertificate { + private final X509Certificate mX509Certificate; +} diff --git a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java index 636c72b1..0f5e4b81 100644 --- a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java @@ -2,30 +2,82 @@ package com.nutomic.syncthingandroid.activities; import android.annotation.SuppressLint; import android.content.ComponentName; +import android.graphics.Bitmap; +import android.net.http.SslCertificate; +import android.net.http.SslError; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; import android.view.View; +import android.webkit.SslErrorHandler; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; import com.nutomic.syncthingandroid.R; +import com.nutomic.syncthingandroid.syncthing.RestApi; import com.nutomic.syncthingandroid.syncthing.SyncthingService; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + /** * Holds a WebView that shows the web ui of the local syncthing instance. */ -public class WebGuiActivity extends SyncthingActivity implements SyncthingService.OnWebGuiAvailableListener { +public class WebGuiActivity extends SyncthingActivity + implements SyncthingService.OnWebGuiAvailableListener { + + private static final String TAG = "WebGuiActivity"; private WebView mWebView; private View mLoadingView; + private X509Certificate mCaCert; + /** * Hides the loading screen and shows the WebView once it is fully loaded. */ private final WebViewClient mWebViewClient = new WebViewClient() { + /** + * Catch (self-signed) SSL errors and test if they correspond to Syncthing's certificate. + */ + @Override + public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error) { + try { + // Uses reflection to access the private mX509Certificate field of SslCertificate + SslCertificate sslCert = error.getCertificate(); + Field f = sslCert.getClass().getDeclaredField("mX509Certificate"); + f.setAccessible(true); + X509Certificate cert = (X509Certificate)f.get(sslCert); + if (cert == null) { + Log.w(TAG, "X509Certificate reference invalid"); + handler.cancel(); + return; + } + cert.verify(mCaCert.getPublicKey()); + handler.proceed(); + } catch (NoSuchFieldException|IllegalAccessException|CertificateException| + NoSuchAlgorithmException|InvalidKeyException|NoSuchProviderException| + SignatureException e) { + Log.w(TAG, e); + handler.cancel(); + } + } + @Override public void onPageFinished(WebView view, String url) { mWebView.setVisibility(View.VISIBLE); @@ -50,6 +102,8 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic ProgressBar pb = (ProgressBar) mLoadingView.findViewById(R.id.progress); pb.setIndeterminate(true); + loadCaCert(); + mWebView = (WebView) findViewById(R.id.webview); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.setWebViewClient(mWebViewClient); @@ -63,10 +117,36 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic /** * Loads and shows WebView, hides loading view. + * + * Sets the X-API-Key (HEADER_API_KEY) header for authorization */ @Override public void onWebGuiAvailable() { - mWebView.loadUrl(getService().getWebGuiUrl()); + Map extraHeaders = new HashMap<>(); + extraHeaders.put(RestApi.HEADER_API_KEY, getService().getApi().getApiKey()); + mWebView.loadUrl(getService().getWebGuiUrl(), extraHeaders); } + /** + * Reads the SyncthingService.HTTPS_CERT_FILE Ca Cert key and loads it in memory + */ + private void loadCaCert() { + InputStream inStream = null; + try { + String httpsCertPath = getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; + inStream = new FileInputStream(httpsCertPath); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + mCaCert = (X509Certificate) + cf.generateCertificate(inStream); + } catch (FileNotFoundException|CertificateException e) { + throw new IllegalArgumentException("Untrusted Certificate"); + } finally { + try { + if (inStream != null) + inStream.close(); + } catch (IOException e) { + Log.w(TAG, e); + } + } + } } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index a8bb70dd..75094c57 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -953,4 +953,8 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, return new Device(); } + public String getApiKey() { + return mApiKey; + } + } From 5e0029e2085325827cc79187089b75b28e6d6ac5 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Thu, 23 Apr 2015 08:07:54 +0200 Subject: [PATCH 03/10] Update deprecated 8080 port to 8384 --- .../com/nutomic/syncthingandroid/util/ConfigXml.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java index 719792d7..37142879 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java @@ -142,6 +142,17 @@ public class ConfigXml { } } + // Update deprecated 8080 port to 8384 + NodeList addressList = gui.getElementsByTagName("address"); + for (int i = 0; i < addressList.getLength(); i++) { + Element g = (Element) addressList.item(i); + if (g.getTextContent().equals("127.0.0.1:8080")) { + Log.i(TAG, "Replacing 127.0.0.1:8080 address with 127.0.0.1:8384"); + g.setTextContent("127.0.0.1:8384"); + changed = true; + } + } + if (changed) { saveChanges(); } From bcbec555cd4c604b663839c3a11140a1b6b75572 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Thu, 9 Apr 2015 20:04:50 +0200 Subject: [PATCH 04/10] Update default HTTP configurations to use HTTPS --- .../com/nutomic/syncthingandroid/util/ConfigXml.java | 10 ++++++++++ src/main/res/xml/app_settings.xml | 6 ------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java index 37142879..061be894 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java @@ -142,6 +142,16 @@ public class ConfigXml { } } + // Enforce TLS. + Element gui = (Element) mConfig.getDocumentElement() + .getElementsByTagName("gui").item(0); + boolean tls = Boolean.parseBoolean(gui.getAttribute("tls")); + if (!tls) { + Log.i(TAG, "Enforce TLS"); + gui.setAttribute("tls", Boolean.toString(true)); + changed = true; + } + // Update deprecated 8080 port to 8384 NodeList addressList = gui.getElementsByTagName("address"); for (int i = 0; i < addressList.getLength(); i++) { diff --git a/src/main/res/xml/app_settings.xml b/src/main/res/xml/app_settings.xml index af872291..f64c5aab 100644 --- a/src/main/res/xml/app_settings.xml +++ b/src/main/res/xml/app_settings.xml @@ -100,12 +100,6 @@ android:key="gui_password" android:title="@string/gui_password" /> - - Date: Thu, 9 Apr 2015 20:14:14 +0200 Subject: [PATCH 05/10] Fix several lint warnings --- .../test/syncthing/RestApiTest.java | 22 ++++++------------- .../syncthingandroid/syncthing/GetTask.java | 4 +++- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java index 58b79a2a..193a21bb 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java @@ -17,35 +17,29 @@ import java.util.concurrent.TimeUnit; public class RestApiTest extends AndroidTestCase { - private SyncthingRunnable mSyncthing; - - private ConfigXml mConfig; - private RestApi mApi; - private String mHttpsCertPath; - @Override protected void setUp() throws Exception { super.setUp(); - mSyncthing = new SyncthingRunnable(new MockContext(null), + new SyncthingRunnable(new MockContext(null), getContext().getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME); - mConfig = new ConfigXml(new MockContext(getContext())); - mConfig.changeDefaultFolder(); + ConfigXml config = new ConfigXml(new MockContext(getContext())); + config.changeDefaultFolder(); - mHttpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; + String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; final CountDownLatch latch = new CountDownLatch(2); - new PollWebGuiAvailableTask(mHttpsCertPath) { + new PollWebGuiAvailableTask(httpsCertPath) { @Override protected void onPostExecute(Void aVoid) { mApi.onWebGuiAvailable(); latch.countDown(); } - }.execute(mConfig.getWebGuiUrl()); - mApi = new RestApi(getContext(), mConfig.getWebGuiUrl(), mConfig.getApiKey(), + }.execute(config.getWebGuiUrl()); + mApi = new RestApi(getContext(), config.getWebGuiUrl(), config.getApiKey(), new RestApi.OnApiAvailableListener() { @Override public void onApiAvailable() { @@ -58,8 +52,6 @@ public class RestApiTest extends AndroidTestCase { @Override protected void tearDown() throws Exception { super.tearDown(); - - final CountDownLatch latch = new CountDownLatch(1); SyncthingRunnable.killSyncthing(); ConfigXml.getConfigFile(new MockContext(getContext())).delete(); } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java index cb7b8a9d..9f9f8f87 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java @@ -90,7 +90,9 @@ public class GetTask extends AsyncTask { try { // Don't push the API too hard Thread.sleep(500 * i); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + Log.w(TAG, e); + } Log.w(TAG, "Retrying GetTask Rest API call ("+i+")"); } return null; From 4146970e649729c58e3712263ad2890560b41dc4 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Thu, 9 Apr 2015 20:45:00 +0200 Subject: [PATCH 06/10] Update Rest API to v0.11 Conflicts: src/main/java/com/nutomic/syncthingandroid/fragments/DeviceSettingsFragment.java --- .../test/util/DevicesAdapterTest.java | 16 +- .../test/util/FolderObserverTest.java | 4 +- .../test/util/FoldersAdapterTest.java | 20 +- .../fragments/DeviceSettingsFragment.java | 62 ++--- .../fragments/DevicesFragment.java | 2 +- .../fragments/DrawerFragment.java | 6 +- .../fragments/FolderSettingsFragment.java | 64 +++--- .../fragments/FoldersFragment.java | 4 +- .../fragments/SettingsFragment.java | 30 ++- .../syncthingandroid/syncthing/GetTask.java | 12 +- .../syncthingandroid/syncthing/PostTask.java | 4 +- .../syncthingandroid/syncthing/RestApi.java | 216 +++++++++--------- .../syncthing/SyncthingService.java | 8 +- .../syncthingandroid/util/DevicesAdapter.java | 12 +- .../syncthingandroid/util/FolderObserver.java | 14 +- .../syncthingandroid/util/FoldersAdapter.java | 14 +- src/main/res/xml/app_settings.xml | 20 +- 17 files changed, 254 insertions(+), 254 deletions(-) diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/DevicesAdapterTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/DevicesAdapterTest.java index 9137e883..cf87797f 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/DevicesAdapterTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/DevicesAdapterTest.java @@ -25,13 +25,13 @@ public class DevicesAdapterTest extends AndroidTestCase { super.setUp(); mAdapter = new DevicesAdapter(getContext()); - mDevice.Addresses = "127.0.0.1:12345"; - mDevice.Name = "the device"; - mDevice.DeviceID = "123-456-789"; + mDevice.addresses = "127.0.0.1:12345"; + mDevice.name = "the device"; + mDevice.deviceID = "123-456-789"; - mConnection.Completion = 100; - mConnection.InBits = 1048576; - mConnection.OutBits = 1073741824; + mConnection.completion = 100; + mConnection.inBits = 1048576; + mConnection.outBits = 1073741824; } @@ -40,7 +40,7 @@ public class DevicesAdapterTest extends AndroidTestCase { mAdapter.add(Arrays.asList(mDevice)); View v = mAdapter.getView(0, null, null); - assertEquals(mDevice.Name, ((TextView) v.findViewById(R.id.name)).getText()); + assertEquals(mDevice.name, ((TextView) v.findViewById(R.id.name)).getText()); assertEquals(getContext().getString(R.string.device_disconnected), ((TextView) v.findViewById(R.id.status)).getText().toString()); assertFalse(((TextView) v.findViewById(R.id.status)).getText().equals("")); @@ -52,7 +52,7 @@ public class DevicesAdapterTest extends AndroidTestCase { public void testGetViewConnections() { mAdapter.add(Arrays.asList(mDevice)); mAdapter.onReceiveConnections( - new HashMap() {{ put(mDevice.DeviceID, mConnection); }}); + new HashMap() {{ put(mDevice.deviceID, mConnection); }}); View v = mAdapter.getView(0, null, null); assertEquals(getContext().getString(R.string.device_up_to_date), diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FolderObserverTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FolderObserverTest.java index 8c9de1c9..0cedcd22 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FolderObserverTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FolderObserverTest.java @@ -44,8 +44,8 @@ public class FolderObserverTest extends AndroidTestCase private RestApi.Folder createFolder(String id) { RestApi.Folder r = new RestApi.Folder(); - r.Path = mTestFolder.getAbsolutePath(); - r.ID = id; + r.path = mTestFolder.getAbsolutePath(); + r.id = id; return r; } diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FoldersAdapterTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FoldersAdapterTest.java index 143559a8..f928c73d 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FoldersAdapterTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/FoldersAdapterTest.java @@ -25,12 +25,12 @@ public class FoldersAdapterTest extends AndroidTestCase { super.setUp(); mAdapter = new FoldersAdapter(getContext()); - mFolder.Path = "/my/dir/"; - mFolder.ID = "id 123"; - mFolder.Invalid = "all good"; - mFolder.DeviceIds = new ArrayList<>(); - mFolder.ReadOnly = false; - mFolder.Versioning = new RestApi.Versioning(); + mFolder.path = "/my/dir/"; + mFolder.id = "id 123"; + mFolder.invalid = "all good"; + mFolder.deviceIds = new ArrayList<>(); + mFolder.readOnly = false; + mFolder.versioning = new RestApi.Versioning(); mModel.state = "idle"; mModel.localFiles = 50; @@ -43,15 +43,15 @@ public class FoldersAdapterTest extends AndroidTestCase { public void testGetViewNoModel() { mAdapter.add(Arrays.asList(mFolder)); View v = mAdapter.getView(0, null, null); - assertEquals(mFolder.ID, ((TextView) v.findViewById(R.id.id)).getText()); - assertEquals(mFolder.Path, ((TextView) v.findViewById(R.id.directory)).getText()); - assertEquals(mFolder.Invalid, ((TextView) v.findViewById(R.id.invalid)).getText()); + assertEquals(mFolder.id, ((TextView) v.findViewById(R.id.id)).getText()); + assertEquals(mFolder.path, ((TextView) v.findViewById(R.id.directory)).getText()); + assertEquals(mFolder.invalid, ((TextView) v.findViewById(R.id.invalid)).getText()); } @MediumTest public void testGetViewModel() { mAdapter.add(Arrays.asList(mFolder)); - mAdapter.onReceiveModel(mFolder.ID, mModel); + mAdapter.onReceiveModel(mFolder.id, mModel); View v = mAdapter.getView(0, null, null); assertFalse(((TextView) v.findViewById(R.id.state)).getText().toString().equals("")); String items = ((TextView) v.findViewById(R.id.items)).getText().toString(); diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceSettingsFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceSettingsFragment.java index 93d68bb3..3168c383 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceSettingsFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceSettingsFragment.java @@ -99,12 +99,12 @@ public class DeviceSettingsFragment extends PreferenceFragment implements } if (mDevice == null) { mDevice = new RestApi.Device(); - mDevice.Name = ""; - mDevice.DeviceID = ""; - mDevice.Addresses = "dynamic"; - mDevice.Compression = "always"; - mDevice.Introducer = false; - ((EditTextPreference) mDeviceId).setText(mDevice.DeviceID); + mDevice.name = ""; + mDevice.deviceID = ""; + mDevice.addresses = "dynamic"; + mDevice.compression = "always"; + mDevice.introducer = false; + ((EditTextPreference) mDeviceId).setText(mDevice.deviceID); } } } @@ -140,7 +140,7 @@ public class DeviceSettingsFragment extends PreferenceFragment implements getActivity().setTitle(R.string.edit_device); List devices = mSyncthingService.getApi().getDevices(false); for (int i = 0; i < devices.size(); i++) { - if (devices.get(i).DeviceID.equals( + if (devices.get(i).deviceID.equals( getActivity().getIntent().getStringExtra(EXTRA_NODE_ID))) { device = devices.get(i); break; @@ -157,14 +157,14 @@ public class DeviceSettingsFragment extends PreferenceFragment implements mSyncthingService.getApi().getConnections(DeviceSettingsFragment.this); - mDeviceId.setSummary(mDevice.DeviceID); - mName.setText((mDevice.Name)); - mName.setSummary(mDevice.Name); - mAddresses.setText(mDevice.Addresses); - mAddresses.setSummary(mDevice.Addresses); - mCompression.setValue(mDevice.Compression); - mCompression.setSummary(mDevice.Compression); - mIntroducer.setChecked(mDevice.Introducer); + mDeviceId.setSummary(mDevice.deviceID); + mName.setText((mDevice.name)); + mName.setSummary(mDevice.name); + mAddresses.setText(mDevice.addresses); + mAddresses.setSummary(mDevice.addresses); + mCompression.setValue(mDevice.compression); + mCompression.setSummary(mDevice.compression); + mIntroducer.setChecked(mDevice.introducer); } @Override @@ -184,12 +184,12 @@ public class DeviceSettingsFragment extends PreferenceFragment implements public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.create: - if (mDevice.DeviceID.equals("")) { + if (mDevice.deviceID.equals("")) { Toast.makeText(getActivity(), R.string.device_id_required, Toast.LENGTH_LONG) .show(); return true; } - if (mDevice.Name.equals("")) { + if (mDevice.name.equals("")) { Toast.makeText(getActivity(), R.string.device_name_required, Toast.LENGTH_LONG) .show(); return true; @@ -197,7 +197,7 @@ public class DeviceSettingsFragment extends PreferenceFragment implements mSyncthingService.getApi().editDevice(mDevice, getActivity(), this); return true; case R.id.share_device_id: - RestApi.shareDeviceId(getActivity(), mDevice.DeviceID); + RestApi.shareDeviceId(getActivity(), mDevice.deviceID); return true; case R.id.delete: new AlertDialog.Builder(getActivity()) @@ -230,23 +230,23 @@ public class DeviceSettingsFragment extends PreferenceFragment implements pref.setSummary((String) o); } if (preference.equals(mDeviceId)) { - mDevice.DeviceID = (String) o; + mDevice.deviceID = (String) o; deviceUpdated(); return true; } else if (preference.equals(mName)) { - mDevice.Name = (String) o; + mDevice.name = (String) o; deviceUpdated(); return true; } else if (preference.equals(mAddresses)) { - mDevice.Addresses = (String) o; + mDevice.addresses = (String) o; deviceUpdated(); return true; } else if (preference.equals(mCompression)) { - mDevice.Compression = (String) o; + mDevice.compression = (String) o; deviceUpdated(); return true; } else if (preference.equals(mIntroducer)) { - mDevice.Introducer = (Boolean) o; + mDevice.introducer = (Boolean) o; deviceUpdated(); return true; } @@ -256,7 +256,7 @@ public class DeviceSettingsFragment extends PreferenceFragment implements @Override public boolean onPreferenceClick(Preference preference) { if (preference.equals(mDeviceId)) { - mSyncthingService.getApi().copyDeviceId(mDevice.DeviceID); + mSyncthingService.getApi().copyDeviceId(mDevice.deviceID); return true; } return false; @@ -272,9 +272,9 @@ public class DeviceSettingsFragment extends PreferenceFragment implements public void onReceiveConnections(Map connections) { if (mVersion == null || mCurrentAddress == null) return; - if (connections.containsKey(mDevice.DeviceID)) { - mVersion.setSummary(connections.get(mDevice.DeviceID).ClientVersion); - mCurrentAddress.setSummary(connections.get(mDevice.DeviceID).Address); + if (connections.containsKey(mDevice.deviceID)) { + mVersion.setSummary(connections.get(mDevice.deviceID).clientVersion); + mCurrentAddress.setSummary(connections.get(mDevice.deviceID).address); } } @@ -304,14 +304,14 @@ public class DeviceSettingsFragment extends PreferenceFragment implements } /** - * Receives value of scanned QR code and sets it as device ID. + * Receives value of scanned QR code and sets it as device id. */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == SCAN_QR_REQUEST_CODE && resultCode == Activity.RESULT_OK) { - mDevice.DeviceID = data.getStringExtra("SCAN_RESULT"); - ((EditTextPreference) mDeviceId).setText(mDevice.DeviceID); - mDeviceId.setSummary(mDevice.DeviceID); + mDevice.deviceID = data.getStringExtra("SCAN_RESULT"); + ((EditTextPreference) mDeviceId).setText(mDevice.deviceID); + mDeviceId.setSummary(mDevice.deviceID); } } diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/DevicesFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/DevicesFragment.java index b6f69b92..ba24e923 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/DevicesFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/DevicesFragment.java @@ -96,7 +96,7 @@ public class DevicesFragment extends ListFragment implements SyncthingService.On Intent intent = new Intent(getActivity(), SettingsActivity.class); intent.setAction(SettingsActivity.ACTION_NODE_SETTINGS_FRAGMENT); intent.putExtra(SettingsActivity.EXTRA_IS_CREATE, false); - intent.putExtra(DeviceSettingsFragment.EXTRA_NODE_ID, mAdapter.getItem(i).DeviceID); + intent.putExtra(DeviceSettingsFragment.EXTRA_NODE_ID, mAdapter.getItem(i).deviceID); startActivity(intent); } diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java index 66d59212..264a5be7 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java @@ -205,9 +205,9 @@ public class DrawerFragment extends Fragment implements RestApi.OnReceiveSystemI */ @Override public void onReceiveConnections(Map connections) { - RestApi.Connection c = connections.get(RestApi.LOCAL_DEVICE_CONNECTIONS); - mDownload.setText(RestApi.readableTransferRate(mActivity, c.InBits)); - mUpload.setText(RestApi.readableTransferRate(mActivity, c.OutBits)); + RestApi.Connection c = connections.get(RestApi.TOTAL_STATS); + mDownload.setText(RestApi.readableTransferRate(mActivity, c.inBits)); + mUpload.setText(RestApi.readableTransferRate(mActivity, c.outBits)); } /** diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/FolderSettingsFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/FolderSettingsFragment.java index 53b0de63..9f561440 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/FolderSettingsFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/FolderSettingsFragment.java @@ -101,11 +101,11 @@ public class FolderSettingsFragment extends PreferenceFragment } if (mFolder == null) { mFolder = new RestApi.Folder(); - mFolder.ID = ""; - mFolder.Path = ""; - mFolder.RescanIntervalS = 259200; // Scan every 3 days (in case inotify dropped some changes) - mFolder.DeviceIds = new ArrayList<>(); - mFolder.Versioning = new RestApi.Versioning(); + mFolder.id = ""; + mFolder.path = ""; + mFolder.rescanIntervalS = 259200; // Scan every 3 days (in case inotify dropped some changes) + mFolder.deviceIds = new ArrayList<>(); + mFolder.versioning = new RestApi.Versioning(); } } } @@ -136,7 +136,7 @@ public class FolderSettingsFragment extends PreferenceFragment getActivity().setTitle(R.string.edit_folder); List folders = mSyncthingService.getApi().getFolders(); for (int i = 0; i < folders.size(); i++) { - if (folders.get(i).ID.equals( + if (folders.get(i).id.equals( getActivity().getIntent().getStringExtra(EXTRA_REPO_ID))) { folder = folders.get(i); break; @@ -150,29 +150,29 @@ public class FolderSettingsFragment extends PreferenceFragment mFolder = folder; } - mFolderId.setText(mFolder.ID); - mFolderId.setSummary(mFolder.ID); - mDirectory.setSummary(mFolder.Path); - mFolderMaster.setChecked(mFolder.ReadOnly); + mFolderId.setText(mFolder.id); + mFolderId.setSummary(mFolder.id); + mDirectory.setSummary(mFolder.path); + mFolderMaster.setChecked(mFolder.readOnly); List devicesList = mSyncthingService.getApi().getDevices(false); for (RestApi.Device n : devicesList) { ExtendedCheckBoxPreference cbp = new ExtendedCheckBoxPreference(getActivity(), n); // Calling addPreference later causes it to change the checked state. mDevices.addPreference(cbp); - cbp.setTitle(n.Name); + cbp.setTitle(n.name); cbp.setKey(KEY_NODE_SHARED); cbp.setOnPreferenceChangeListener(FolderSettingsFragment.this); cbp.setChecked(false); - for (String n2 : mFolder.DeviceIds) { - if (n2.equals(n.DeviceID)) { + for (String n2 : mFolder.deviceIds) { + if (n2.equals(n.deviceID)) { cbp.setChecked(true); } } } - mVersioning.setChecked(mFolder.Versioning instanceof RestApi.SimpleVersioning); + mVersioning.setChecked(mFolder.versioning instanceof RestApi.SimpleVersioning); if (mVersioning.isChecked()) { - mVersioningKeep.setText(mFolder.Versioning.getParams().get("keep")); - mVersioningKeep.setSummary(mFolder.Versioning.getParams().get("keep")); + mVersioningKeep.setText(mFolder.versioning.getParams().get("keep")); + mVersioningKeep.setSummary(mFolder.versioning.getParams().get("keep")); mVersioningKeep.setEnabled(true); } else { mVersioningKeep.setEnabled(false); @@ -201,12 +201,12 @@ public class FolderSettingsFragment extends PreferenceFragment public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.create: - if (mFolder.ID.length() > 64 || !mFolder.ID.matches("[a-zA-Z0-9-_\\.]+")) { + if (mFolder.id.length() > 64 || !mFolder.id.matches("[a-zA-Z0-9-_\\.]+")) { Toast.makeText(getActivity(), R.string.folder_id_invalid, Toast.LENGTH_LONG) .show(); return true; } - if (mFolder.Path.equals("")) { + if (mFolder.path.equals("")) { Toast.makeText(getActivity(), R.string.folder_path_required, Toast.LENGTH_LONG) .show(); return true; @@ -249,27 +249,27 @@ public class FolderSettingsFragment extends PreferenceFragment } if (preference.equals(mFolderId)) { - mFolder.ID = (String) o; + mFolder.id = (String) o; folderUpdated(); return true; } else if (preference.equals(mDirectory)) { - mFolder.Path = (String) o; + mFolder.path = (String) o; folderUpdated(); return true; } else if (preference.equals(mFolderMaster)) { - mFolder.ReadOnly = (Boolean) o; + mFolder.readOnly = (Boolean) o; folderUpdated(); return true; } else if (preference.getKey().equals(KEY_NODE_SHARED)) { ExtendedCheckBoxPreference pref = (ExtendedCheckBoxPreference) preference; RestApi.Device device = (RestApi.Device) pref.getObject(); if ((Boolean) o) { - mFolder.DeviceIds.add(device.DeviceID); + mFolder.deviceIds.add(device.deviceID); } else { - Iterator it = mFolder.DeviceIds.iterator(); + Iterator it = mFolder.deviceIds.iterator(); while (it.hasNext()) { String n = it.next(); - if (n.equals(device.DeviceID)) { + if (n.equals(device.deviceID)) { it.remove(); } } @@ -280,23 +280,23 @@ public class FolderSettingsFragment extends PreferenceFragment mVersioningKeep.setEnabled((Boolean) o); if ((Boolean) o) { RestApi.SimpleVersioning v = new RestApi.SimpleVersioning(); - mFolder.Versioning = v; + mFolder.versioning = v; v.setParams(5); mVersioningKeep.setText("5"); mVersioningKeep.setSummary("5"); } else { - mFolder.Versioning = new RestApi.Versioning(); + mFolder.versioning = new RestApi.Versioning(); } folderUpdated(); return true; } else if (preference.equals(mVersioningKeep)) { try { - ((RestApi.SimpleVersioning) mFolder.Versioning) + ((RestApi.SimpleVersioning) mFolder.versioning) .setParams(Integer.parseInt((String) o)); folderUpdated(); return true; } catch (NumberFormatException e) { - Log.w(TAG, "Invalid versioning option: "+ o); + Log.w(TAG, "invalid versioning option: "+ o); } } @@ -307,8 +307,8 @@ public class FolderSettingsFragment extends PreferenceFragment public boolean onPreferenceClick(Preference preference) { if (preference.equals(mDirectory)) { Intent intent = new Intent(getActivity(), FolderPickerActivity.class); - if (mFolder.Path.length() > 0) { - intent.putExtra(FolderPickerActivity.EXTRA_INITIAL_DIRECTORY, mFolder.Path); + if (mFolder.path.length() > 0) { + intent.putExtra(FolderPickerActivity.EXTRA_INITIAL_DIRECTORY, mFolder.path); } startActivityForResult(intent, DIRECTORY_REQUEST_CODE); } else if (preference.equals(mDevices) && @@ -322,8 +322,8 @@ public class FolderSettingsFragment extends PreferenceFragment @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && requestCode == DIRECTORY_REQUEST_CODE) { - mFolder.Path = data.getStringExtra(FolderPickerActivity.EXTRA_RESULT_DIRECTORY); - mDirectory.setSummary(mFolder.Path); + mFolder.path = data.getStringExtra(FolderPickerActivity.EXTRA_RESULT_DIRECTORY); + mDirectory.setSummary(mFolder.path); folderUpdated(); } } diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/FoldersFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/FoldersFragment.java index 82efc0b8..523c706b 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/FoldersFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/FoldersFragment.java @@ -96,7 +96,7 @@ public class FoldersFragment extends ListFragment implements SyncthingService.On Intent intent = new Intent(getActivity(), SettingsActivity.class) .setAction(SettingsActivity.ACTION_REPO_SETTINGS_FRAGMENT) .putExtra(SettingsActivity.EXTRA_IS_CREATE, false) - .putExtra(FolderSettingsFragment.EXTRA_REPO_ID, mAdapter.getItem(i).ID); + .putExtra(FolderSettingsFragment.EXTRA_REPO_ID, mAdapter.getItem(i).id); startActivity(intent); } @@ -106,7 +106,7 @@ public class FoldersFragment extends ListFragment implements SyncthingService.On @Override public boolean onItemLongClick(AdapterView adapterView, View view, int i, long l) { Intent intent = new Intent(Intent.ACTION_VIEW); - Uri uri = Uri.parse(mAdapter.getItem(i).Path); + Uri uri = Uri.parse(mAdapter.getItem(i).path); intent.setDataAndType(uri, "*/*"); startActivity(intent); return true; diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java index 20ba64ec..cc76df7b 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java @@ -29,12 +29,11 @@ public class SettingsFragment extends PreferenceFragment private static final String SYNCTHING_OPTIONS_KEY = "syncthing_options"; private static final String SYNCTHING_GUI_KEY = "syncthing_gui"; - private static final String DEVICE_NAME_KEY = "DeviceName"; - private static final String USAGE_REPORT_ACCEPTED = "URAccepted"; - private static final String ADDRESS = "Address"; + private static final String DEVICE_NAME_KEY = "deviceName"; + private static final String USAGE_REPORT_ACCEPTED = "urAccepted"; + private static final String ADDRESS = "address"; private static final String GUI_USER = "gui_user"; private static final String GUI_PASSWORD = "gui_password"; - private static final String USER_TLS = "UseTLS"; private static final String EXPORT_CONFIG = "export_config"; private static final String IMPORT_CONFIG = "import_config"; private static final String STTRACE = "sttrace"; @@ -70,7 +69,7 @@ public class SettingsFragment extends PreferenceFragment String value; switch (pref.getKey()) { case DEVICE_NAME_KEY: - value = api.getLocalDevice().Name; + value = api.getLocalDevice().name; break; case USAGE_REPORT_ACCEPTED: String v = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey()); @@ -84,9 +83,6 @@ public class SettingsFragment extends PreferenceFragment Preference address = mGuiScreen.findPreference(ADDRESS); applyPreference(address, api.getValue(RestApi.TYPE_GUI, ADDRESS)); - - Preference tls = mGuiScreen.findPreference(USER_TLS); - applyPreference(tls, api.getValue(RestApi.TYPE_GUI, USER_TLS)); } } @@ -188,7 +184,7 @@ public class SettingsFragment extends PreferenceFragment o = Integer.parseInt((String) o); o = o.toString(); } catch (NumberFormatException e) { - Log.w(TAG, "Invalid number: " + o); + Log.w(TAG, "invalid number: " + o); return false; } } @@ -206,21 +202,21 @@ public class SettingsFragment extends PreferenceFragment } else if (preference.getKey().equals(DEVICE_NAME_KEY)) { RestApi.Device old = mSyncthingService.getApi().getLocalDevice(); RestApi.Device updated = new RestApi.Device(); - updated.Addresses = old.Addresses; - updated.Compression = old.Compression; - updated.DeviceID = old.DeviceID; - updated.Introducer = old.Introducer; - updated.Name = (String) o; + updated.addresses = old.addresses; + updated.compression = old.compression; + updated.deviceID = old.deviceID; + updated.introducer = old.introducer; + updated.name = (String) o; mSyncthingService.getApi().editDevice(updated, getActivity(), null); } else if (preference.getKey().equals(USAGE_REPORT_ACCEPTED)) { mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(), ((Boolean) o) ? 1 : 0, false, getActivity()); } else if (mOptionsScreen.findPreference(preference.getKey()) != null) { - boolean isArray = preference.getKey().equals("ListenAddress") || - preference.getKey().equals("GlobalAnnServers"); + boolean isArray = preference.getKey().equals("listenAddress") || + preference.getKey().equals("globalAnnServers"); mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(), o, isArray, getActivity()); - } else if (preference.getKey().equals(ADDRESS) || preference.getKey().equals(USER_TLS)) { + } else if (preference.getKey().equals(ADDRESS)) { mSyncthingService.getApi().setValue( RestApi.TYPE_GUI, preference.getKey(), o, false, getActivity()); } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java index 9f9f8f87..ef2b39d5 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java @@ -29,17 +29,17 @@ public class GetTask extends AsyncTask { private static final String TAG = "GetTask"; - public static final String URI_CONFIG = "/rest/config"; + public static final String URI_CONFIG = "/rest/system/config"; - public static final String URI_VERSION = "/rest/version"; + public static final String URI_VERSION = "/rest/system/version"; - public static final String URI_SYSTEM = "/rest/system"; + public static final String URI_SYSTEM = "/rest/system/status"; - public static final String URI_CONNECTIONS = "/rest/connections"; + public static final String URI_CONNECTIONS = "/rest/system/connections"; - public static final String URI_MODEL = "/rest/model"; + public static final String URI_MODEL = "/rest/db/status"; - public static final String URI_DEVICEID = "/rest/deviceid"; + public static final String URI_DEVICEID = "/rest/svc/deviceid"; private String mHttpsCertPath; diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java index 6bd2c51f..865dd196 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java @@ -20,9 +20,9 @@ public class PostTask extends AsyncTask { private static final String TAG = "PostTask"; - public static final String URI_CONFIG = "/rest/config"; + public static final String URI_CONFIG = "/rest/system/config"; - public static final String URI_SCAN = "/rest/scan"; + public static final String URI_SCAN = "/rest/db/scan"; private String mHttpsCertPath; diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index 75094c57..b7fe7022 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -11,7 +11,6 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.os.AsyncTask; import android.os.Build; import android.support.v4.app.NotificationCompat; import android.util.Log; @@ -32,9 +31,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -48,12 +44,12 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, /** * Parameter for {@link #getValue} or {@link #setValue} referring to "options" config item. */ - public static final String TYPE_OPTIONS = "Options"; + public static final String TYPE_OPTIONS = "options"; /** * Parameter for {@link #getValue} or {@link #setValue} referring to "gui" config item. */ - public static final String TYPE_GUI = "GUI"; + public static final String TYPE_GUI = "gui"; /** * The name of the HTTP header used for the syncthing API key. @@ -64,14 +60,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, * Key of the map element containing connection info for the local device, in the return * value of {@link #getConnections} */ - public static final String LOCAL_DEVICE_CONNECTIONS = "total"; + public static final String TOTAL_STATS = "total"; public static class Device implements Serializable { - public String Addresses; - public String Name; - public String DeviceID; - public String Compression; - public boolean Introducer; + public String addresses; + public String name; + public String deviceID; + public String compression; + public boolean introducer; } public static class SystemInfo { @@ -85,13 +81,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, } public static class Folder implements Serializable { - public String Path; - public String ID; - public String Invalid; - public List DeviceIds; - public boolean ReadOnly; - public int RescanIntervalS; - public Versioning Versioning; + public String path; + public String id; + public String invalid; + public List deviceIds; + public boolean readOnly; + public int rescanIntervalS; + public Versioning versioning; } public static class Versioning implements Serializable { @@ -118,14 +114,14 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, } public static class Connection { - public String At; - public long InBytesTotal; - public long OutBytesTotal; - public long InBits; - public long OutBits; - public String Address; - public String ClientVersion; - public int Completion; + public String at; + public long inBytesTotal; + public long outBytesTotal; + public long inBits; + public long outBits; + public String address; + public String clientVersion; + public int completion; } public static class Model { @@ -163,9 +159,9 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, /** * Stores the result of the last successful request to {@link GetTask#URI_CONNECTIONS}, - * or an empty HashMap. + * or an empty Map. */ - private HashMap mPreviousConnections = new HashMap<>(); + private Map mPreviousConnections = new HashMap<>(); /** * Stores the timestamp of the last successful request to {@link GetTask#URI_CONNECTIONS}. @@ -203,7 +199,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, private final OnApiAvailableListener mOnApiAvailableListener; /** - * Gets local device id, syncthing version and config, then calls all OnApiAvailableListeners. + * Gets local device ID, syncthing version and config, then calls all OnApiAvailableListeners. */ @Override public void onWebGuiAvailable() { @@ -407,17 +403,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, return new ArrayList<>(); try { - JSONArray devices = mConfig.getJSONArray("Devices"); + JSONArray devices = mConfig.getJSONArray("devices"); List ret = new ArrayList<>(devices.length()); for (int i = 0; i < devices.length(); i++) { JSONObject json = devices.getJSONObject(i); Device n = new Device(); - n.Addresses = json.optJSONArray("Addresses").join(" ").replace("\"", ""); - n.Name = json.getString("Name"); - n.DeviceID = json.getString("DeviceID"); - n.Compression = json.getString("Compression"); - n.Introducer = json.getBoolean("Introducer"); - if (includeLocal || !mLocalDeviceId.equals(n.DeviceID)) { + n.addresses = json.optJSONArray("addresses").join(" ").replace("\"", ""); + n.name = json.getString("name"); + n.deviceID = json.getString("deviceID"); + n.compression = json.getString("compression"); + n.introducer = json.getBoolean("introducer"); + if (includeLocal || !mLocalDeviceId.equals(n.deviceID)) { ret.add(n); } } @@ -482,31 +478,31 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, List ret; try { - JSONArray folders = mConfig.getJSONArray("Folders"); + JSONArray folders = mConfig.getJSONArray("folders"); ret = new ArrayList<>(folders.length()); for (int i = 0; i < folders.length(); i++) { JSONObject json = folders.getJSONObject(i); Folder r = new Folder(); - r.Path = json.getString("Path"); - r.ID = json.getString("ID"); - r.Invalid = json.getString("Invalid"); - r.DeviceIds = new ArrayList<>(); - JSONArray devices = json.getJSONArray("Devices"); + r.path = json.getString("path"); + r.id = json.getString("id"); + r.invalid = json.getString("invalid"); + r.deviceIds = new ArrayList<>(); + JSONArray devices = json.getJSONArray("devices"); for (int j = 0; j < devices.length(); j++) { JSONObject n = devices.getJSONObject(j); - r.DeviceIds.add(n.getString("DeviceID")); + r.deviceIds.add(n.getString("deviceID")); } - r.ReadOnly = json.getBoolean("ReadOnly"); - r.RescanIntervalS = json.getInt("RescanIntervalS"); - JSONObject versioning = json.getJSONObject("Versioning"); - if (versioning.getString("Type").equals("simple")) { + r.readOnly = json.getBoolean("readOnly"); + r.rescanIntervalS = json.getInt("rescanIntervalS"); + JSONObject versioning = json.getJSONObject("versioning"); + if (versioning.getString("type").equals("simple")) { SimpleVersioning sv = new SimpleVersioning(); - JSONObject params = versioning.getJSONObject("Params"); + JSONObject params = versioning.getJSONObject("params"); sv.setParams(params.getInt("keep")); - r.Versioning = sv; + r.versioning = sv; } else { - r.Versioning = new Versioning(); + r.versioning = new Versioning(); } ret.add(r); @@ -546,7 +542,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, public interface OnReceiveConnectionsListener { /** - * @param connections Map from Device ID to {@link Connection}. + * @param connections Map from Device id to {@link Connection}. *

* NOTE: The parameter connections is cached internally. Do not modify it or * any of its contents. @@ -557,7 +553,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, /** * Returns connection info for the local device and all connected devices. *

- * Use the key {@link #LOCAL_DEVICE_CONNECTIONS} to get connection info for the local device. + * Use the key {@link #TOTAL_STATS} to get connection info for the local device. */ public void getConnections(final OnReceiveConnectionsListener listener) { new GetTask(mHttpsCertPath) { @@ -575,27 +571,35 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, try { JSONObject json = new JSONObject(s); - String[] names = json.names().join(" ").replace("\"", "").split(" "); - HashMap connections = new HashMap(); - for (String deviceId : names) { + Map jsonConnections = new HashMap<>(); + jsonConnections.put(TOTAL_STATS, json.getJSONObject(TOTAL_STATS)); + JSONArray extConnections = json.getJSONObject("connections").names(); + if (extConnections != null) { + for (int i = 0; i < extConnections.length(); i++) { + String deviceId = extConnections.get(i).toString(); + jsonConnections.put(deviceId, json.getJSONObject("connections").getJSONObject(deviceId)); + } + } + Map connections = new HashMap<>(); + for (String deviceId : jsonConnections.keySet()) { Connection c = new Connection(); - JSONObject conn = json.getJSONObject(deviceId); - c.Address = deviceId; - c.At = conn.getString("At"); - c.InBytesTotal = conn.getLong("InBytesTotal"); - c.OutBytesTotal = conn.getLong("OutBytesTotal"); - c.Address = conn.getString("Address"); - c.ClientVersion = conn.getString("ClientVersion"); - c.Completion = getDeviceCompletion(deviceId); + JSONObject conn = jsonConnections.get(deviceId); + c.address = deviceId; + c.at = conn.getString("at"); + c.inBytesTotal = conn.getLong("inBytesTotal"); + c.outBytesTotal = conn.getLong("outBytesTotal"); + c.address = conn.getString("address"); + c.clientVersion = conn.getString("clientVersion"); + c.completion = getDeviceCompletion(deviceId); Connection prev = (mPreviousConnections.containsKey(deviceId)) ? mPreviousConnections.get(deviceId) : new Connection(); mPreviousConnectionTime = now; - c.InBits = Math.max(0, 8 * - (conn.getLong("InBytesTotal") - prev.InBytesTotal) / timeElapsed); - c.OutBits = Math.max(0, 8 * - (conn.getLong("OutBytesTotal") - prev.OutBytesTotal) / timeElapsed); + c.inBits = Math.max(0, 8 * + (conn.getLong("inBytesTotal") - prev.inBytesTotal) / timeElapsed); + c.outBits = Math.max(0, 8 * + (conn.getLong("outBytesTotal") - prev.outBytesTotal) / timeElapsed); connections.put(deviceId, c); @@ -619,7 +623,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, boolean isShared = false; outerloop: for (Folder r : getFolders()) { - for (String n : r.DeviceIds) { + for (String n : r.deviceIds) { if (n.equals(deviceId)) { isShared = true; break outerloop; @@ -650,7 +654,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, } /** - * Returns status information about the folder with the given ID. + * Returns status information about the folder with the given id. */ public void getModel(final String folderId, final OnReceiveModelListener listener) { new GetTask(mHttpsCertPath) { @@ -711,7 +715,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ public void editDevice(final Device device, final Activity activity, final OnDeviceIdNormalizedListener listener) { - normalizeDeviceId(device.DeviceID, + normalizeDeviceId(device.deviceID, new RestApi.OnDeviceIdNormalizedListener() { @Override public void onDeviceIdNormalized(String normalizedId, String error) { @@ -719,17 +723,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, if (normalizedId == null) return; - device.DeviceID = normalizedId; + device.deviceID = normalizedId; // If the device already exists, just update it. boolean create = true; for (RestApi.Device n : getDevices(true)) { - if (n.DeviceID.equals(device.DeviceID)) { + if (n.deviceID.equals(device.deviceID)) { create = false; } } try { - JSONArray devices = mConfig.getJSONArray("Devices"); + JSONArray devices = mConfig.getJSONArray("devices"); JSONObject n = null; if (create) { n = new JSONObject(); @@ -737,17 +741,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, } else { for (int i = 0; i < devices.length(); i++) { JSONObject json = devices.getJSONObject(i); - if (device.DeviceID.equals(json.getString("DeviceID"))) { + if (device.deviceID.equals(json.getString("deviceID"))) { n = devices.getJSONObject(i); break; } } } - n.put("DeviceID", device.DeviceID); - n.put("Name", device.Name); - n.put("Addresses", listToJson(device.Addresses.split(" "))); - n.put("Compression", device.Compression); - n.put("Introducer", device.Introducer); + n.put("deviceID", device.deviceID); + n.put("name", device.name); + n.put("addresses", listToJson(device.addresses.split(" "))); + n.put("compression", device.compression); + n.put("introducer", device.introducer); requireRestart(activity); } catch (JSONException e) { Log.w(TAG, "Failed to read devices", e); @@ -762,13 +766,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ public boolean deleteDevice(Device device, Activity activity) { try { - JSONArray devices = mConfig.getJSONArray("Devices"); + JSONArray devices = mConfig.getJSONArray("devices"); for (int i = 0; i < devices.length(); i++) { JSONObject json = devices.getJSONObject(i); - if (device.DeviceID.equals(json.getString("DeviceID"))) { - mConfig.remove("Devices"); - mConfig.put("Devices", delete(devices, devices.getJSONObject(i))); + if (device.deviceID.equals(json.getString("deviceID"))) { + mConfig.remove("devices"); + mConfig.put("devices", delete(devices, devices.getJSONObject(i))); break; } } @@ -785,7 +789,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ public boolean editFolder(Folder folder, boolean create, Activity activity) { try { - JSONArray folders = mConfig.getJSONArray("Folders"); + JSONArray folders = mConfig.getJSONArray("folders"); JSONObject r = null; if (create) { r = new JSONObject(); @@ -793,35 +797,35 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, } else { for (int i = 0; i < folders.length(); i++) { JSONObject json = folders.getJSONObject(i); - if (folder.ID.equals(json.getString("ID"))) { + if (folder.id.equals(json.getString("id"))) { r = folders.getJSONObject(i); break; } } } - r.put("Path", folder.Path); - r.put("ID", folder.ID); - r.put("IgnorePerms", true); - r.put("ReadOnly", folder.ReadOnly); + r.put("path", folder.path); + r.put("id", folder.id); + r.put("ignorePerms", true); + r.put("readOnly", folder.readOnly); JSONArray devices = new JSONArray(); - for (String n : folder.DeviceIds) { + for (String n : folder.deviceIds) { JSONObject element = new JSONObject(); - element.put("DeviceID", n); + element.put("deviceID", n); devices.put(element); } - r.put("Devices", devices); + r.put("devices", devices); JSONObject versioning = new JSONObject(); - versioning.put("Type", folder.Versioning.getType()); + versioning.put("type", folder.versioning.getType()); JSONObject params = new JSONObject(); - versioning.put("Params", params); - for (String key : folder.Versioning.getParams().keySet()) { - params.put(key, folder.Versioning.getParams().get(key)); + versioning.put("params", params); + for (String key : folder.versioning.getParams().keySet()) { + params.put(key, folder.versioning.getParams().get(key)); } - r.put("RescanIntervalS", folder.RescanIntervalS); - r.put("Versioning", versioning); + r.put("rescanIntervalS", folder.rescanIntervalS); + r.put("versioning", versioning); requireRestart(activity); } catch (JSONException e) { - Log.w(TAG, "Failed to edit folder " + folder.ID + " at " + folder.Path, e); + Log.w(TAG, "Failed to edit folder " + folder.id + " at " + folder.path, e); return false; } return true; @@ -832,13 +836,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ public boolean deleteFolder(Folder folder, Activity activity) { try { - JSONArray folders = mConfig.getJSONArray("Folders"); + JSONArray folders = mConfig.getJSONArray("folders"); for (int i = 0; i < folders.length(); i++) { JSONObject json = folders.getJSONObject(i); - if (folder.ID.equals(json.getString("ID"))) { - mConfig.remove("Folders"); - mConfig.put("Folders", delete(folders, folders.getJSONObject(i))); + if (folder.id.equals(json.getString("id"))) { + mConfig.remove("folders"); + mConfig.put("folders", delete(folders, folders.getJSONObject(i))); break; } } @@ -899,7 +903,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, } /** - * Shares the given device id via Intent. Must be called from an Activity. + * Shares the given device ID via Intent. Must be called from an Activity. */ public static void shareDeviceId(Context context, String id) { Intent shareIntent = new Intent(); @@ -946,7 +950,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ public Device getLocalDevice() { for (Device d : getDevices(true)) { - if (d.DeviceID.equals(mLocalDeviceId)) { + if (d.deviceID.equals(mLocalDeviceId)) { return d; } } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index 4d9724f6..d15ff648 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -56,17 +56,17 @@ public class SyncthingService extends Service { public static final int GUI_UPDATE_INTERVAL = 1000; /** - * Name of the public key file in the data directory. + * name of the public key file in the data directory. */ public static final String PUBLIC_KEY_FILE = "cert.pem"; /** - * Name of the private key file in the data directory. + * name of the private key file in the data directory. */ public static final String PRIVATE_KEY_FILE = "key.pem"; /** - * Name of the public HTTPS CA file in the data directory. + * name of the public HTTPS CA file in the data directory. */ public static final String HTTPS_CERT_FILE = "https-cert.pem"; @@ -77,7 +77,7 @@ public class SyncthingService extends Service { new File(Environment.getExternalStorageDirectory(), "backups/syncthing"); /** - * Path to the native, integrated syncthing binary, relative to the data folder + * path to the native, integrated syncthing binary, relative to the data folder */ public static final String BINARY_NAME = "lib/libsyncthing.so"; diff --git a/src/main/java/com/nutomic/syncthingandroid/util/DevicesAdapter.java b/src/main/java/com/nutomic/syncthingandroid/util/DevicesAdapter.java index 32fa1610..0162a49b 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/DevicesAdapter.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/DevicesAdapter.java @@ -42,22 +42,22 @@ public class DevicesAdapter extends ArrayAdapter TextView download = (TextView) convertView.findViewById(R.id.download); TextView upload = (TextView) convertView.findViewById(R.id.upload); - String deviceId = getItem(position).DeviceID; + String deviceId = getItem(position).deviceID; RestApi.Connection conn = mConnections.get(deviceId); - name.setText(getItem(position).Name); + name.setText(getItem(position).name); Resources r = getContext().getResources(); if (conn != null) { - if (conn.Completion == 100) { + if (conn.completion == 100) { status.setText(r.getString(R.string.device_up_to_date)); status.setTextColor(r.getColor(R.color.text_green)); } else { - status.setText(r.getString(R.string.device_syncing, conn.Completion)); + status.setText(r.getString(R.string.device_syncing, conn.completion)); status.setTextColor(r.getColor(R.color.text_blue)); } - download.setText(RestApi.readableTransferRate(getContext(), conn.InBits)); - upload.setText(RestApi.readableTransferRate(getContext(), conn.OutBits)); + download.setText(RestApi.readableTransferRate(getContext(), conn.inBits)); + upload.setText(RestApi.readableTransferRate(getContext(), conn.outBits)); } else { download.setText("0 " + r.getStringArray(R.array.transfer_rate_units)[0]); diff --git a/src/main/java/com/nutomic/syncthingandroid/util/FolderObserver.java b/src/main/java/com/nutomic/syncthingandroid/util/FolderObserver.java index 6e9b625b..6ce8606a 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/FolderObserver.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/FolderObserver.java @@ -42,7 +42,7 @@ public class FolderObserver extends FileObserver { @Override public String getMessage() { - return "Path " + mPath + " does not exist, aborting file observer"; + return "path " + mPath + " does not exist, aborting file observer"; } } @@ -51,19 +51,19 @@ public class FolderObserver extends FileObserver { * * @param listener The listener where changes should be sent to. * @param folder The folder where this folder belongs to. - * @param path Path to the monitored folder, relative to folder root. + * @param path path to the monitored folder, relative to folder root. */ private FolderObserver(OnFolderFileChangeListener listener, RestApi.Folder folder, String path) { - super(folder.Path + "/" + path, + super(folder.path + "/" + path, ATTRIB | CLOSE_WRITE | CREATE | DELETE | DELETE_SELF | MODIFY | MOVED_FROM | MOVED_TO | MOVE_SELF); mListener = listener; mFolder = folder; mPath = path; - Log.v(TAG, "observer created for " + path + " in " + folder.ID); + Log.v(TAG, "observer created for " + path + " in " + folder.id); startWatching(); - File currentFolder = new File(folder.Path, path); + File currentFolder = new File(folder.path, path); if (!currentFolder.exists()) { throw new FolderNotExistingException(currentFolder.getAbsolutePath()); } @@ -107,7 +107,7 @@ public class FolderObserver extends FileObserver { break; } } - mListener.onFolderFileChange(mFolder.ID, fullPath.getPath()); + mListener.onFolderFileChange(mFolder.id, fullPath.getPath()); break; case MOVED_TO: // fall through @@ -117,7 +117,7 @@ public class FolderObserver extends FileObserver { } // fall through default: - mListener.onFolderFileChange(mFolder.ID, fullPath.getPath()); + mListener.onFolderFileChange(mFolder.id, fullPath.getPath()); } } diff --git a/src/main/java/com/nutomic/syncthingandroid/util/FoldersAdapter.java b/src/main/java/com/nutomic/syncthingandroid/util/FoldersAdapter.java index 242f3957..6ee2a414 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/FoldersAdapter.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/FoldersAdapter.java @@ -42,10 +42,10 @@ public class FoldersAdapter extends ArrayAdapter TextView invalid = (TextView) convertView.findViewById(R.id.invalid); RestApi.Folder folder = getItem(position); - RestApi.Model model = mModels.get(folder.ID); - id.setText(folder.ID); + RestApi.Model model = mModels.get(folder.id); + id.setText(folder.id); state.setTextColor(getContext().getResources().getColor(R.color.text_green)); - directory.setText((folder.Path)); + directory.setText((folder.path)); if (model != null) { int percentage = (model.globalBytes != 0) ? (int) Math.floor(100 * model.inSyncBytes / model.globalBytes) @@ -57,13 +57,13 @@ public class FoldersAdapter extends ArrayAdapter .getString(R.string.files, model.inSyncFiles, model.globalFiles)); size.setText(RestApi.readableFileSize(getContext(), model.inSyncBytes) + " / " + RestApi.readableFileSize(getContext(), model.globalBytes)); - if (folder.Invalid.equals("")) { + if (folder.invalid.equals("")) { invalid.setText(model.invalid); invalid.setVisibility((model.invalid.equals("")) ? View.GONE : View.VISIBLE); } } else { - invalid.setText(folder.Invalid); - invalid.setVisibility((folder.Invalid.equals("")) ? View.GONE : View.VISIBLE); + invalid.setText(folder.invalid); + invalid.setVisibility((folder.invalid.equals("")) ? View.GONE : View.VISIBLE); } return convertView; @@ -85,7 +85,7 @@ public class FoldersAdapter extends ArrayAdapter for (int i = 0; i < getCount(); i++) { if (i >= listView.getFirstVisiblePosition() && i <= listView.getLastVisiblePosition()) { - api.getModel(getItem(i).ID, this); + api.getModel(getItem(i).id, this); } } } diff --git a/src/main/res/xml/app_settings.xml b/src/main/res/xml/app_settings.xml index f64c5aab..6e1eb20a 100644 --- a/src/main/res/xml/app_settings.xml +++ b/src/main/res/xml/app_settings.xml @@ -35,49 +35,49 @@ android:title="@string/syncthing_options"> @@ -88,7 +88,7 @@ android:title="@string/syncthing_gui"> From dccbbbbd41b53e7c342effaf94c74045d15b0b8f Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Sun, 12 Apr 2015 22:33:07 +0200 Subject: [PATCH 07/10] Support HTTP Auth in Webview --- .../activities/WebGuiActivity.java | 14 ++++++-------- .../syncthingandroid/syncthing/RestApi.java | 17 ++++++++++++++++- .../syncthing/SyncthingService.java | 11 ++++++++++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java index 0f5e4b81..f0714e8c 100644 --- a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java @@ -9,6 +9,7 @@ import android.os.Bundle; import android.os.IBinder; import android.util.Log; import android.view.View; +import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -78,6 +79,10 @@ public class WebGuiActivity extends SyncthingActivity } } + public void onReceivedHttpAuthRequest (WebView view, HttpAuthHandler handler, String host, String realm) { + handler.proceed(getService().getApi().getGuiUser(), getService().getApi().getGuiPassword()); + } + @Override public void onPageFinished(WebView view, String url) { mWebView.setVisibility(View.VISIBLE); @@ -115,16 +120,9 @@ public class WebGuiActivity extends SyncthingActivity getService().registerOnWebGuiAvailableListener(WebGuiActivity.this); } - /** - * Loads and shows WebView, hides loading view. - * - * Sets the X-API-Key (HEADER_API_KEY) header for authorization - */ @Override public void onWebGuiAvailable() { - Map extraHeaders = new HashMap<>(); - extraHeaders.put(RestApi.HEADER_API_KEY, getService().getApi().getApiKey()); - mWebView.loadUrl(getService().getWebGuiUrl(), extraHeaders); + mWebView.loadUrl(getService().getWebGuiUrl()); } /** diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index b7fe7022..74f7c57d 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -149,6 +149,10 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, private final String mApiKey; + private final String mGuiUser; + + private final String mGuiPassword; + private final String mHttpsCertPath; private JSONObject mConfig; @@ -174,10 +178,13 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ private HashMap mCachedModelInfo = new HashMap<>(); - public RestApi(Context context, String url, String apiKey, OnApiAvailableListener listener) { + public RestApi(Context context, String url, String apiKey, String guiUser, String guiPassword, + OnApiAvailableListener listener) { mContext = context; mUrl = url; mApiKey = apiKey; + mGuiUser = guiUser; + mGuiPassword = guiPassword; mHttpsCertPath = mContext.getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; mOnApiAvailableListener = listener; } @@ -961,4 +968,12 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, return mApiKey; } + public String getGuiUser() { + return mGuiUser; + } + + public String getGuiPassword() { + return mGuiPassword; + } + } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index d15ff648..35ba0c07 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -260,7 +260,7 @@ public class SyncthingService extends Service { mDeviceStateHolder = new DeviceStateHolder(SyncthingService.this); registerReceiver(mDeviceStateHolder, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - new StartupTask().execute(); + new StartupTask(sp.getString("gui_user",""), sp.getString("gui_password","")).execute(); } /** @@ -269,6 +269,14 @@ public class SyncthingService extends Service { * {@code Pair}. */ private class StartupTask extends AsyncTask> { + String mGuiUser; + String mGuiPassword; + + public StartupTask(String guiUser, String guiPassword) { + mGuiUser = guiUser; + mGuiPassword = guiPassword; + } + @Override protected Pair doInBackground(Void... voids) { mConfig = new ConfigXml(SyncthingService.this); @@ -278,6 +286,7 @@ public class SyncthingService extends Service { @Override protected void onPostExecute(Pair urlAndKey) { mApi = new RestApi(SyncthingService.this, urlAndKey.first, urlAndKey.second, + mGuiUser, mGuiPassword, new RestApi.OnApiAvailableListener() { @Override public void onApiAvailable() { From 10cad9ae37bad2b7a8e8bcd512aed976ced92a94 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Sun, 12 Apr 2015 22:34:09 +0200 Subject: [PATCH 08/10] Print stacktrace on error --- .../com/nutomic/syncthingandroid/activities/WebGuiActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java index f0714e8c..39c418af 100644 --- a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java @@ -137,7 +137,7 @@ public class WebGuiActivity extends SyncthingActivity mCaCert = (X509Certificate) cf.generateCertificate(inStream); } catch (FileNotFoundException|CertificateException e) { - throw new IllegalArgumentException("Untrusted Certificate"); + throw new IllegalArgumentException("Untrusted Certificate", e); } finally { try { if (inStream != null) From 1caf5fee2082a53a831789612422d2bb81138caf Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Thu, 23 Apr 2015 08:21:25 +0200 Subject: [PATCH 09/10] Update to Syncthing v0.11 --- ext/syncthing/src/github.com/syncthing/syncthing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/syncthing/src/github.com/syncthing/syncthing b/ext/syncthing/src/github.com/syncthing/syncthing index 3cc4cb0a..b35958d0 160000 --- a/ext/syncthing/src/github.com/syncthing/syncthing +++ b/ext/syncthing/src/github.com/syncthing/syncthing @@ -1 +1 @@ -Subproject commit 3cc4cb0a0b71908ae2d6392f14457e7ca6712278 +Subproject commit b35958d024175609a9e07934cdb1bedd3243939c From da4d6547218d4360fcd58e12360c23965f61cf04 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Wed, 29 Apr 2015 19:21:52 +0200 Subject: [PATCH 10/10] Support webgui for devices = Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // The mX509Certificate field is not available for ICS- devices + Log.w(TAG, "Skipping certificate check for devices