diff --git a/.gitignore b/.gitignore
index 330a8f6d7..bff42afdd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
*~
+*#
bin
gen
local.properties
diff --git a/res/layout/dialog_ssl_confirm.xml b/res/layout/dialog_ssl_confirm.xml
new file mode 100644
index 000000000..43312e9fd
--- /dev/null
+++ b/res/layout/dialog_ssl_confirm.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7115b38cc..af8cafc26 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -148,4 +148,10 @@
%d file(s) selected
Empty folder
Choose a file
+ SSL check failed
+ The ssl certificate of %s is not trusted. Do you want to continue?
+ Untrusted Connection
+ Yes
+ No
+ Remember my choice
diff --git a/src/com/seafile/seadroid2/AccountDetailActivity.java b/src/com/seafile/seadroid2/AccountDetailActivity.java
index 7b96fc153..e1a7fed05 100644
--- a/src/com/seafile/seadroid2/AccountDetailActivity.java
+++ b/src/com/seafile/seadroid2/AccountDetailActivity.java
@@ -22,6 +22,7 @@
import com.seafile.seadroid2.account.Account;
import com.seafile.seadroid2.account.AccountManager;
+import com.seafile.seadroid2.ui.SslConfirmDialog;
public class AccountDetailActivity extends FragmentActivity {
@@ -225,8 +226,32 @@ protected String doInBackground(Void... params) {
return doLogin();
}
+ private void resend() {
+ ConcurrentAsyncTask.execute(new LoginTask(loginAccount));
+ }
+
@Override
- protected void onPostExecute(String result) {
+ protected void onPostExecute(final String result) {
+ if (err == SeafException.sslException) {
+ SslConfirmDialog dialog = new SslConfirmDialog(loginAccount,
+ new SslConfirmDialog.Listener() {
+ @Override
+ public void onAccepted(boolean rememberChoice) {
+ CertsManager.instance().saveCertForAccount(loginAccount, rememberChoice);
+ resend();
+ }
+
+ @Override
+ public void onRejected() {
+ Log.d("SeafileHTTPS", "the user rejectes the ssl certificate");
+ statusView.setText(result);
+ loginButton.setEnabled(true);
+ }
+ });
+ dialog.show(getSupportFragmentManager(), SslConfirmDialog.FRAGMENT_TAG);
+ return;
+ }
+
if (result != null && result.equals("Success")) {
if (isFromEdit) {
accountManager.updateAccount(account, loginAccount);
@@ -236,10 +261,7 @@ protected void onPostExecute(String result) {
}
startFilesActivity(loginAccount);
} else {
- if (err != null && err == SeafException.sslException) {
- statusView.setText("SSL Error or Certification Error. You may try again.");
- } else
- statusView.setText(result);
+ statusView.setText(result);
}
loginButton.setEnabled(true);
}
@@ -253,6 +275,9 @@ private String doLogin() {
return "Success";
} catch (SeafException e) {
err = e;
+ if (e == SeafException.sslException) {
+ return getString(R.string.ssl_error);
+ }
switch (e.getCode()) {
case 400:
return getString(R.string.err_wrong_user_or_passwd);
diff --git a/src/com/seafile/seadroid2/BrowserActivity.java b/src/com/seafile/seadroid2/BrowserActivity.java
index 6a7391094..a29047620 100644
--- a/src/com/seafile/seadroid2/BrowserActivity.java
+++ b/src/com/seafile/seadroid2/BrowserActivity.java
@@ -57,9 +57,9 @@
import com.seafile.seadroid2.fileschooser.MultiFileChooserActivity;
import com.seafile.seadroid2.gallery.MultipleImageSelectionActivity;
import com.seafile.seadroid2.monitor.FileMonitorService;
-import com.seafile.seadroid2.transfer.TransferService;
import com.seafile.seadroid2.transfer.TransferManager.DownloadTaskInfo;
import com.seafile.seadroid2.transfer.TransferManager.UploadTaskInfo;
+import com.seafile.seadroid2.transfer.TransferService;
import com.seafile.seadroid2.transfer.TransferService.TransferBinder;
import com.seafile.seadroid2.ui.AppChoiceDialog;
import com.seafile.seadroid2.ui.AppChoiceDialog.CustomAction;
@@ -71,6 +71,7 @@
import com.seafile.seadroid2.ui.PasswordDialog;
import com.seafile.seadroid2.ui.RenameFileDialog;
import com.seafile.seadroid2.ui.ReposFragment;
+import com.seafile.seadroid2.ui.SslConfirmDialog;
import com.seafile.seadroid2.ui.StarredFragment;
import com.seafile.seadroid2.ui.TabsFragment;
import com.seafile.seadroid2.ui.TaskDialog;
@@ -230,6 +231,7 @@ protected void onCreate(Bundle savedInstanceState) {
unsetRefreshing();
if (savedInstanceState != null) {
+ Log.d(DEBUG_TAG, "savedInstanceState is not null");
tabsFragment = (TabsFragment)
getSupportFragmentManager().findFragmentByTag(TABS_FRAGMENT_TAG);
uploadTasksFragment = (UploadTasksFragment)
@@ -247,6 +249,18 @@ protected void onCreate(Bundle savedInstanceState) {
ft.commit();
}
+ SslConfirmDialog sslConfirmDlg = (SslConfirmDialog)
+ getSupportFragmentManager().findFragmentByTag(SslConfirmDialog.FRAGMENT_TAG);
+
+ if (sslConfirmDlg != null) {
+ Log.d(DEBUG_TAG, "sslConfirmDlg is not null");
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ ft.detach(sslConfirmDlg);
+ ft.commit();
+ } else {
+ Log.d(DEBUG_TAG, "sslConfirmDlg is null");
+ }
+
String repoID = savedInstanceState.getString("repoID");
String repoName = savedInstanceState.getString("repoName");
String path = savedInstanceState.getString("path");
@@ -257,6 +271,7 @@ protected void onCreate(Bundle savedInstanceState) {
navContext.setDir(path, dirID);
}
} else {
+ Log.d(DEBUG_TAG, "savedInstanceState is null");
tabsFragment = new TabsFragment();
uploadTasksFragment = new UploadTasksFragment();
getSupportFragmentManager().beginTransaction().add(R.id.content_frame, tabsFragment, TABS_FRAGMENT_TAG).commit();
@@ -371,7 +386,6 @@ public void onStart() {
mTransferReceiver = new TransferReceiver();
}
-
IntentFilter filter = new IntentFilter(TransferService.BROADCAST_ACTION);
LocalBroadcastManager.getInstance(this).registerReceiver(mTransferReceiver, filter);
diff --git a/src/com/seafile/seadroid2/CertsManager.java b/src/com/seafile/seadroid2/CertsManager.java
new file mode 100644
index 000000000..faa6b3241
--- /dev/null
+++ b/src/com/seafile/seadroid2/CertsManager.java
@@ -0,0 +1,199 @@
+package com.seafile.seadroid2;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Map;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Base64;
+import android.util.Log;
+
+import com.google.common.collect.Maps;
+import com.seafile.seadroid2.account.Account;
+
+/**
+ * Save the ssl certificates the user has confirmed to trust
+ */
+public class CertsManager {
+
+ private static final String DEBUG_TAG = "CertsManager";
+
+ private DBHelper db = DBHelper.getDatabaseHelper();
+
+ private static CertsManager instance;
+
+ private static Map cachedCerts = Maps.newHashMap();
+
+ public static synchronized CertsManager instance() {
+ if (instance == null) {
+ instance = new CertsManager();
+ }
+
+ return instance;
+ }
+
+ public void saveCertForAccount(Account account, boolean rememberChoice) {
+ List certs = SSLTrustManager.instance().getCertsChainForAccount(account);
+ if (certs == null || certs.size() == 0) {
+ return;
+ }
+
+ X509Certificate cert = certs.get(0);
+ cachedCerts.put(account, cert);
+
+ if (rememberChoice) {
+ db.saveCertificate(account.server, cert);
+ }
+
+ Log.d(DEBUG_TAG, "saved cert for account " + account);
+ }
+
+ X509Certificate getCertificate(Account account) {
+ X509Certificate cert = cachedCerts.get(account);
+ if (cert != null) {
+ return cert;
+ }
+ return db.getCertificate(account.server);
+ }
+
+ static class DBHelper extends SQLiteOpenHelper {
+ // If you change the database schema, you must increment the database version.
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "certs.db";
+
+ private static final String TABLE_NAME = "Certs";
+
+ private static final String COLUMN_URL = "url";
+ private static final String COLUMN_CERT = "cert";
+
+ private static final String CREATE_TABLE_SQL = "CREATE TABLE " + TABLE_NAME + " ("
+ + COLUMN_URL + " VARCHAR(255) PRIMARY KEY, " + COLUMN_CERT + " TEXT " + ")";
+
+ private static DBHelper dbHelper = null;
+ private SQLiteDatabase database = null;
+
+ public static DBHelper getDatabaseHelper() {
+ if (dbHelper != null)
+ return dbHelper;
+ dbHelper = new DBHelper(SeadroidApplication.getAppContext());
+ dbHelper.database = dbHelper.getWritableDatabase();
+ return dbHelper;
+ }
+
+ public DBHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_SQL);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
+
+ public X509Certificate getCertificate(String url) {
+ String[] projection = {COLUMN_CERT};
+
+ Cursor c = database.query(TABLE_NAME,
+ projection,
+ "url=?",
+ new String[] {url},
+ null, // don't group the rows
+ null, // don't filter by row groups
+ null); // The sort order
+
+ if (c.moveToFirst() == false) {
+ c.close();
+ return null;
+ }
+
+ X509Certificate cert = cursorToCert(c);
+
+ c.close();
+ return cert;
+ }
+
+ private X509Certificate cursorToCert(Cursor cursor) {
+ X509Certificate cert = null;
+ String text = cursor.getString(0);
+
+ ByteArrayInputStream bis = null;
+ ObjectInputStream ois = null;
+ byte[] data = null;
+
+ data = Base64.decode(text, Base64.DEFAULT);
+
+ try {
+ bis = new ByteArrayInputStream(data);
+ ois = new ObjectInputStream(bis);
+ cert = (X509Certificate) ois.readObject();
+ return cert;
+ } catch (ClassNotFoundException e) {
+ return null;
+ } catch (IOException e) {
+ return null;
+ } finally {
+ if (bis != null) {
+ try {
+ bis.close();
+ } catch (IOException e) {
+ }
+ }
+
+ if (ois != null) {
+ try {
+ ois.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ public void saveCertificate(String url, X509Certificate cert) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream out = null;
+ String text = null;
+ try {
+ out = new ObjectOutputStream(bos);
+ out.writeObject(cert);
+ byte[] data = bos.toByteArray();
+ text = Base64.encodeToString(data, Base64.DEFAULT);
+ } catch (IOException e) {
+ return;
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ }
+ } catch (IOException ex) {
+ // ignore close exception
+ }
+ try {
+ bos.close();
+ } catch (IOException ex) {
+ // ignore close exception
+ }
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_URL, url);
+ values.put(COLUMN_CERT, text);
+
+ database.replace(TABLE_NAME, null, values);
+ }
+ }
+}
diff --git a/src/com/seafile/seadroid2/SSLTrustManager.java b/src/com/seafile/seadroid2/SSLTrustManager.java
new file mode 100644
index 000000000..1f2bc5f77
--- /dev/null
+++ b/src/com/seafile/seadroid2/SSLTrustManager.java
@@ -0,0 +1,226 @@
+package com.seafile.seadroid2;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import android.util.Log;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.seafile.seadroid2.account.Account;
+
+public class SSLTrustManager {
+
+ private static final String DEBUG_TAG = "SSLTrustManager";
+
+ private X509TrustManager defaultTrustManager;
+
+ private SSLTrustManager() {}
+
+ private static SSLTrustManager instance;
+
+ public static synchronized SSLTrustManager instance() {
+ if (instance == null) {
+ instance = new SSLTrustManager();
+ instance.init();
+ }
+
+ return instance;
+ }
+
+ private void init() {
+ java.io.InputStream fis = null;
+ try {
+ javax.net.ssl.TrustManagerFactory tmf;
+ TrustManager[] tms;
+ tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
+ tmf.init((KeyStore) null);
+ tms = tmf.getTrustManagers();
+ if (tms != null) {
+ for (TrustManager tm : tms) {
+ if (tm instanceof X509TrustManager) {
+ defaultTrustManager = (X509TrustManager) tm;
+ break;
+ }
+ }
+ }
+
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(DEBUG_TAG, "Unable to get X509 Trust Manager ", e);
+ } catch (KeyStoreException e) {
+ Log.e(DEBUG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
+ } finally {
+ try {
+ if (fis != null)
+ fis.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+
+ public synchronized TrustManager[] getTrustManagers(Account account) {
+ SecureX509TrustManager mgr = managers.get(account);
+ if (mgr == null) {
+ mgr = new SecureX509TrustManager(account);
+ managers.put(account, mgr);
+ }
+
+ return new TrustManager[] {mgr};
+ }
+
+ private Map managers =
+ new HashMap();
+
+ // TODO: cache SSLSocketFactory for each account
+ public SSLSocketFactory getSSLSocketFactory(Account account) {
+ try {
+ SSLContext context = SSLContext.getInstance("TLS");
+ TrustManager[] mgrs = getTrustManagers(account);
+ context.init(null, mgrs, new SecureRandom());
+ SSLSocketFactory factory = context.getSocketFactory();
+ return factory;
+ } catch (Exception e) {
+ Log.d("SeafileHTTPS", Utils.getStackTrace(e));
+ return null;
+ }
+ }
+
+ public List getCertsChainForAccount(Account account) {
+ SecureX509TrustManager mgr = managers.get(account);
+ if (mgr == null) {
+ return null;
+ }
+
+ return mgr.getServerCertsChain();
+ }
+
+ /**
+ * Reorder the certificates chain, since it may not be in the right order when passed to us
+ * @see http://stackoverflow.com/questions/7822381/need-help-understanding-certificate-chains
+ */
+ public List orderCerts(X509Certificate[] certificates) {
+ if (certificates == null || certificates.length == 0) {
+ return ImmutableList.of();
+ }
+
+ List certs = Lists.newArrayList(certificates);
+ // certs.addAll(Arrays.asList(certificates));
+ X509Certificate certChain = certs.get(0);
+ certs.remove(certChain);
+ LinkedList chainList= new LinkedList();
+ chainList.add(certChain);
+ Principal certIssuer = certChain.getIssuerDN();
+ Principal certSubject = certChain.getSubjectDN();
+ while(!certs.isEmpty()){
+ List tempcerts = ImmutableList.copyOf(certs);
+ for (X509Certificate cert : tempcerts) {
+ if(cert.getIssuerDN().equals(certSubject)){
+ chainList.addFirst(cert);
+ certSubject = cert.getSubjectDN();
+ certs.remove(cert);
+ continue;
+ }
+
+ if(cert.getSubjectDN().equals(certIssuer)){
+ chainList.addLast(cert);
+ certIssuer = cert.getIssuerDN();
+ certs.remove(cert);
+ continue;
+ }
+ }
+ }
+
+ return chainList;
+ }
+
+ private class SecureX509TrustManager implements X509TrustManager {
+
+ private Account account;
+
+ private volatile List certsChain = ImmutableList.of();
+
+ public SecureX509TrustManager(Account account) {
+ this.account = account;
+ Log.d("SeafileHTTPS", "a SecureX509TrustManager is created:" + hashCode());
+ }
+
+ public List getServerCertsChain() {
+ return certsChain;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ defaultTrustManager.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ if (chain == null || chain.length == 0) {
+ defaultTrustManager.checkServerTrusted(chain, authType);
+ return;
+ }
+
+ Log.d("SeafileHTTPS", ">>>>> authType is " + authType);
+
+ try {
+ // First try to do default check
+ defaultTrustManager.checkServerTrusted(chain, authType);
+ } catch (CertificateException e) {
+ List orderedChain = orderCerts(chain);
+ customCheck(orderedChain, authType);
+ }
+ }
+
+ private void customCheck(List chain, String authType)
+ throws CertificateException {
+
+ certsChain = ImmutableList.copyOf(chain);
+
+ X509Certificate cert = chain.get(0);
+
+ X509Certificate savedCert = CertsManager.instance().getCertificate(account);
+ if (savedCert == null) {
+ Log.d(DEBUG_TAG, "no saved cert for " + account.server);
+ throw new CertificateException();
+ } else if (savedCert.equals(cert)) {
+ // The user has confirmed to trust this certificate
+ Log.d(DEBUG_TAG, "the cert of " + account.server + " is trusted");
+ return;
+ } else {
+ // The certificate is different from the one user confirmed to trust,
+ // This may be either:
+ // 1. The server admin has changed its cert
+ // 2. The user is under security attak
+ Log.d(DEBUG_TAG, "the cert of " + account.server + " has changed");
+ throw new CertificateException();
+ }
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return defaultTrustManager.getAcceptedIssuers();
+ }
+
+ @Override
+ public void finalize() {
+ Log.d("SeafileHTTPS", "a SecureX509TrustManager is finalized:" + hashCode());
+ }
+ }
+}
diff --git a/src/com/seafile/seadroid2/SeafConnection.java b/src/com/seafile/seadroid2/SeafConnection.java
index e8a7aee28..1cce9764b 100644
--- a/src/com/seafile/seadroid2/SeafConnection.java
+++ b/src/com/seafile/seadroid2/SeafConnection.java
@@ -7,10 +7,15 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
import java.net.URLEncoder;
+import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.Map;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLHandshakeException;
+
import org.json.JSONException;
import org.json.JSONObject;
@@ -52,30 +57,39 @@ private HttpRequest prepareApiGetRequest(String apiPath, Map params)
return req;
}
- private void setRequestCommon(HttpRequest req) {
- req.trustAllCerts().trustAllHosts().
- readTimeout(30000).connectTimeout(15000).
- header("Authorization", "Token " + account.token);
- }
-
private HttpRequest prepareApiPutRequest(String apiPath, Map params) throws IOException {
HttpRequest req = HttpRequest.put(account.server + apiPath, params, false);
setRequestCommon(req);
return req;
}
+ private void setRequestCommon(HttpRequest req) {
+ req.readTimeout(30000)
+ .connectTimeout(15000)
+ .header("Authorization", "Token " + account.token);
+
+ prepareHttpsCheck(req);
+ }
+
+ private HttpRequest prepareHttpsCheck(HttpRequest req) {
+ HttpURLConnection conn = req.getConnection();
+ if (conn instanceof HttpsURLConnection) {
+ req.trustAllHosts();
+ HttpsURLConnection sconn = (HttpsURLConnection)conn;
+ sconn.setSSLSocketFactory(SSLTrustManager.instance().getSSLSocketFactory(account));
+ }
+
+ return req;
+ }
+
private HttpRequest prepareApiGetRequest(String apiPath) throws IOException {
return prepareApiGetRequest(apiPath, null);
}
private HttpRequest prepareApiFileGetRequest(String url) throws HttpRequestException {
- return HttpRequest.get(url).
- trustAllCerts().trustAllHosts().
- connectTimeout(15000);
- }
+ HttpRequest req = HttpRequest.get(url).connectTimeout(15000);
- private HttpRequest prepareApiPostRequest(String apiPath, boolean withToken) {
- return prepareApiPostRequest(apiPath, withToken, null);
+ return prepareHttpsCheck(req);
}
/** Prepare a post request.
@@ -86,15 +100,14 @@ private HttpRequest prepareApiPostRequest(String apiPath, boolean withToken) {
*/
private HttpRequest prepareApiPostRequest(String apiPath, boolean withToken, Map params)
throws HttpRequestException {
- HttpRequest req = HttpRequest.post(account.server + apiPath, params, true).
- trustAllCerts().trustAllHosts().
- connectTimeout(15000);
+ HttpRequest req = HttpRequest.post(account.server + apiPath, params, true)
+ .connectTimeout(15000);
if (withToken) {
req.header("Authorization", "Token " + account.token);
}
- return req;
+ return prepareHttpsCheck(req);
}
/**
@@ -103,8 +116,9 @@ private HttpRequest prepareApiPostRequest(String apiPath, boolean withToken, Map
* @throws SeafException
*/
private boolean realLogin() throws SeafException {
+ HttpRequest req = null;
try {
- HttpRequest req = prepareApiPostRequest("api2/auth-token/", false, null);
+ req = prepareApiPostRequest("api2/auth-token/", false, null);
Log.d(DEBUG_TAG, "Login to " + account.server + "api2/auth-token/");
req.form("username", account.email);
@@ -146,7 +160,11 @@ private boolean realLogin() throws SeafException {
} catch (SeafException e) {
throw e;
} catch (HttpRequestException e) {
- throw SeafException.networkException;
+ if (e.getCause() instanceof SSLHandshakeException) {
+ throw SeafException.sslException;
+ } else {
+ throw SeafException.networkException;
+ }
} catch (IOException e) {
e.printStackTrace();
throw SeafException.networkException;
@@ -165,8 +183,9 @@ public boolean doLogin() throws SeafException {
}
public String getRepos() throws SeafException {
+ HttpRequest req = null;
try {
- HttpRequest req = prepareApiGetRequest("api2/repos/");
+ req = prepareApiGetRequest("api2/repos/");
if (req.code() != 200) {
if (req.message() == null) {
throw SeafException.networkException;
@@ -180,7 +199,11 @@ public String getRepos() throws SeafException {
} catch (SeafException e) {
throw e;
} catch (HttpRequestException e) {
- throw SeafException.networkException;
+ if (e.getCause() instanceof SSLHandshakeException) {
+ throw SeafException.sslException;
+ } else {
+ throw SeafException.networkException;
+ }
} catch (IOException e) {
throw SeafException.networkException;
}
@@ -264,7 +287,11 @@ public Pair getDirents(String repoID, String path, String cached
} catch (UnsupportedEncodingException e) {
throw SeafException.encodingException;
} catch (HttpRequestException e) {
- throw SeafException.networkException;
+ if (e.getCause() instanceof SSLHandshakeException) {
+ throw SeafException.sslException;
+ } else {
+ throw SeafException.networkException;
+ }
} catch (IOException e) {
throw SeafException.networkException;
}
@@ -518,9 +545,10 @@ private String uploadFileCommon(String link, String repoID, String dir,
HttpRequest req = HttpRequest.post(link).
- trustAllCerts().trustAllHosts().
connectTimeout(15000);
+ prepareHttpsCheck(req);
+
/**
* We have to set the content-length header, otherwise the whole
* request would be buffered by android. So we have to format the
@@ -625,13 +653,14 @@ public Pair createNewDir(String repoID,
String parentDir,
String dirName) throws SeafException {
+ HttpRequest req = null;
try {
String fullPath = Utils.pathJoin(parentDir, dirName);
Map params = new HashMap();
params.put("p", fullPath);
params.put("reloaddir", "true");
- HttpRequest req = prepareApiPostRequest("api2/repos/" + repoID + "/dir/", true, params);
+ req = prepareApiPostRequest("api2/repos/" + repoID + "/dir/", true, params);
req.form("operation", "mkdir");
@@ -660,6 +689,20 @@ public Pair createNewDir(String repoID,
} catch (UnsupportedEncodingException e) {
throw SeafException.encodingException;
} catch (HttpRequestException e) {
+ if (req != null) {
+ IOException exception = e.getCause();
+ if (exception instanceof SSLHandshakeException) {
+ HttpsURLConnection conn = (HttpsURLConnection) req.getConnection();
+ try {
+ Certificate[] certs = conn.getServerCertificates();
+ for (Certificate cert : certs) {
+ Log.d("SeafileHTTPS", cert.toString());
+ }
+ } catch (Exception e1) {
+ }
+ }
+ }
+
throw SeafException.networkException;
}
}
diff --git a/src/com/seafile/seadroid2/TrustManagerFactory.java b/src/com/seafile/seadroid2/TrustManagerFactory.java
deleted file mode 100644
index c1531cd0a..000000000
--- a/src/com/seafile/seadroid2/TrustManagerFactory.java
+++ /dev/null
@@ -1,196 +0,0 @@
-package com.seafile.seadroid2;
-
-import android.util.Log;
-
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
-import com.seafile.seadroid2.SeadroidApplication;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-
-
-public class TrustManagerFactory {
-
- private static final String LOG_TAG = "TrustManagerFactory";
-
- private static X509TrustManager defaultTrustManager;
- private static X509TrustManager localTrustManager;
-
- private static X509Certificate[] lastCertChain = null;
-
- private static File keyStoreFile;
- private static KeyStore keyStore;
-
-
- private static class SimpleX509TrustManager implements X509TrustManager {
-
- public void checkClientTrusted(X509Certificate[] chain, String authType)
- throws CertificateException {
- }
-
- public void checkServerTrusted(X509Certificate[] chain, String authType)
- throws CertificateException {
- }
-
- public X509Certificate[] getAcceptedIssuers() {
- return null;
- }
- }
-
- private static class SecureX509TrustManager implements X509TrustManager {
-
- public void checkClientTrusted(X509Certificate[] chain, String authType)
- throws CertificateException {
- defaultTrustManager.checkClientTrusted(chain, authType);
- }
-
- public void checkServerTrusted(X509Certificate[] chain, String authType)
- throws CertificateException {
- TrustManagerFactory.setLastCertChain(chain);
- try {
- defaultTrustManager.checkServerTrusted(chain, authType);
- } catch (CertificateException e) {
- localTrustManager.checkServerTrusted(new X509Certificate[] {chain[0]}, authType);
- }
- }
-
- public X509Certificate[] getAcceptedIssuers() {
- return defaultTrustManager.getAcceptedIssuers();
- }
-
- }
-
- public static TrustManager[] getTrustManagers() {
- return new TrustManager[] { new SecureX509TrustManager() };
- }
-
- public static TrustManager[] getUnsecureTrustManagers() {
- return new TrustManager[] { new SimpleX509TrustManager() };
- }
-
- static {
- java.io.InputStream fis = null;
- try {
- javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
-
- keyStoreFile = new File(SeadroidApplication.getAppContext().getFilesDir(), "KeyStore.bks");
- keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
- try {
- fis = new java.io.FileInputStream(keyStoreFile);
- } catch (FileNotFoundException e1) {
- fis = null;
- }
- try {
- keyStore.load(fis, null);
- } catch (IOException e) {
- Log.e(LOG_TAG, "KeyStore IOException while initializing TrustManagerFactory ", e);
- keyStore = null;
- } catch (CertificateException e) {
- Log.e(LOG_TAG, "KeyStore CertificateException while initializing TrustManagerFactory ", e);
- keyStore = null;
- }
- tmf.init(keyStore);
- TrustManager[] tms = tmf.getTrustManagers();
- if (tms != null) {
- for (TrustManager tm : tms) {
- if (tm instanceof X509TrustManager) {
- localTrustManager = (X509TrustManager)tm;
- break;
- }
- }
- }
- tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
- tmf.init((KeyStore)null);
- tms = tmf.getTrustManagers();
- if (tms != null) {
- for (TrustManager tm : tms) {
- if (tm instanceof X509TrustManager) {
- defaultTrustManager = (X509TrustManager) tm;
- break;
- }
- }
- }
-
- } catch (NoSuchAlgorithmException e) {
- Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
- } catch (KeyStoreException e) {
- Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
- } finally {
- try {
- if (fis != null)
- fis.close();
- } catch (IOException e) {
- // ignore
- }
- }
- }
-
- private TrustManagerFactory() {
- }
-
- public static KeyStore getKeyStore() {
- return keyStore;
- }
-
- public static void setLastCertChain(X509Certificate[] chain) {
- lastCertChain = chain;
- }
-
- public static X509Certificate[] getLastCertChain() {
- return lastCertChain;
- }
-
- public static void addCertificateChain(X509Certificate[] chain) throws CertificateException {
- if (chain == null)
- return;
- try {
- javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
- for (X509Certificate element : chain) {
- keyStore.setCertificateEntry
- (element.getSubjectDN().toString(), element);
- }
-
- tmf.init(keyStore);
- TrustManager[] tms = tmf.getTrustManagers();
- if (tms != null) {
- for (TrustManager tm : tms) {
- if (tm instanceof X509TrustManager) {
- localTrustManager = (X509TrustManager) tm;
- break;
- }
- }
- }
-
- java.io.OutputStream keyStoreStream = null;
- try {
- keyStoreStream = new java.io.FileOutputStream(keyStoreFile);
- keyStore.store(keyStoreStream, null);
- } catch (FileNotFoundException e) {
- throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
- } catch (CertificateException e) {
- throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
- } catch (IOException e) {
- throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
- } finally {
- try {
- keyStoreStream.close();
- } catch (IOException e) {
- // ignore
- }
- }
- } catch (NoSuchAlgorithmException e) {
- Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
- } catch (KeyStoreException e) {
- Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
- }
- }
-
-}
diff --git a/src/com/seafile/seadroid2/Utils.java b/src/com/seafile/seadroid2/Utils.java
index 509720e96..34a389222 100644
--- a/src/com/seafile/seadroid2/Utils.java
+++ b/src/com/seafile/seadroid2/Utils.java
@@ -10,7 +10,9 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
+import java.io.PrintWriter;
import java.io.Reader;
+import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.text.DecimalFormat;
@@ -33,8 +35,8 @@
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.net.Uri;
import android.net.NetworkInfo.DetailedState;
+import android.net.Uri;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -481,4 +483,11 @@ else if ("file".equalsIgnoreCase(uri.getScheme())) {
return null;
}
+
+ public static String getStackTrace(Exception e) {
+ StringWriter buffer = new StringWriter();
+ PrintWriter writer = new PrintWriter(buffer);
+ e.printStackTrace(writer);
+ return buffer.toString();
+ }
}
diff --git a/src/com/seafile/seadroid2/account/Account.java b/src/com/seafile/seadroid2/account/Account.java
index 6e2cedf9e..f78ecc4bd 100644
--- a/src/com/seafile/seadroid2/account/Account.java
+++ b/src/com/seafile/seadroid2/account/Account.java
@@ -4,51 +4,53 @@
import android.os.Parcelable;
import android.util.Log;
+import com.google.common.base.Objects;
+
public class Account implements Parcelable {
private static final String DEBUG_TAG = "Account";
-
+
// The full URL of the server, like 'http://gonggeng.org/seahub/' or 'http://gonggeng.org/'
public String server;
-
+
public String email;
public String token;
public String passwd;
-
+
public Account() {
-
+
}
-
+
public Account(String server, String email) {
this.server = server;
this.email = email;
}
-
+
public Account(String server, String email, String passwd) {
this.server = server;
this.email = email;
this.passwd = passwd;
}
-
+
public Account(String server, String email, String passwd, String token) {
this.server = server;
this.email = email;
this.passwd = passwd;
this.token = token;
}
-
+
public String getServerHost() {
String s = server.substring(server.indexOf("://") + 3);
return s.substring(0, s.indexOf('/'));
}
-
+
public String getEmail() {
return email;
}
-
+
public String getServer() {
return server;
}
-
+
public String getServerNoProtocol() {
String result = server.substring(server.indexOf("://") + 3);
if (result.endsWith("/"))
@@ -59,28 +61,27 @@ public String getServerNoProtocol() {
public String getToken() {
return token;
}
-
+
public boolean isHttps() {
return server.startsWith("https");
}
-
@Override
public int hashCode() {
- return server.hashCode() + email.hashCode();
+ return Objects.hashCode(server, email);
}
-
+
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || (obj.getClass() != this.getClass()))
return false;
-
+
Account a = (Account)obj;
if (a.server == null || a.email == null)
return false;
-
+
return a.server.equals(this.server) && a.email.equals(this.email);
}
@@ -89,35 +90,44 @@ public String getSignature() {
}
@Override
- public int describeContents() {
- return 0;
- }
+ public int describeContents() {
+ return 0;
+ }
@Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeString(server);
- out.writeString(email);
- out.writeString(passwd);
- out.writeString(token);
- }
-
- public static final Parcelable.Creator CREATOR
- = new Parcelable.Creator() {
- public Account createFromParcel(Parcel in) {
- return new Account(in);
- }
-
- public Account[] newArray(int size) {
- return new Account[size];
- }
- };
-
- private Account(Parcel in) {
- server = in.readString();
- email = in.readString();
- passwd = in.readString();
- token = in.readString();
-
- Log.d(DEBUG_TAG, String.format("%s %s %s %s", server, email, passwd, token));
- }
-}
\ No newline at end of file
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(server);
+ out.writeString(email);
+ out.writeString(passwd);
+ out.writeString(token);
+ }
+
+ public static final Parcelable.Creator CREATOR
+ = new Parcelable.Creator() {
+ public Account createFromParcel(Parcel in) {
+ return new Account(in);
+ }
+
+ public Account[] newArray(int size) {
+ return new Account[size];
+ }
+ };
+
+ private Account(Parcel in) {
+ server = in.readString();
+ email = in.readString();
+ passwd = in.readString();
+ token = in.readString();
+
+ Log.d(DEBUG_TAG, String.format("%s %s %s %s", server, email, passwd, token));
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("server", server)
+ .add("user", email)
+ .toString();
+ }
+
+}
diff --git a/src/com/seafile/seadroid2/account/AccountManager.java b/src/com/seafile/seadroid2/account/AccountManager.java
index 7837cd2ce..416f2c015 100644
--- a/src/com/seafile/seadroid2/account/AccountManager.java
+++ b/src/com/seafile/seadroid2/account/AccountManager.java
@@ -103,7 +103,7 @@ public void saveAccount(Account account) {
values.put(AccountDbHelper.COLUMN_TOKEN, account.token);
// Insert the new row, returning the primary key value of the new row
- db.insert(AccountDbHelper.TABLE_NAME, null, values);
+ db.replace(AccountDbHelper.TABLE_NAME, null, values);
db.close();
}
diff --git a/src/com/seafile/seadroid2/ui/ReposFragment.java b/src/com/seafile/seadroid2/ui/ReposFragment.java
index 69fd49d16..a3f243a22 100644
--- a/src/com/seafile/seadroid2/ui/ReposFragment.java
+++ b/src/com/seafile/seadroid2/ui/ReposFragment.java
@@ -19,11 +19,13 @@
import com.actionbarsherlock.app.SherlockListFragment;
import com.seafile.seadroid2.BrowserActivity;
+import com.seafile.seadroid2.CertsManager;
import com.seafile.seadroid2.ConcurrentAsyncTask;
import com.seafile.seadroid2.NavContext;
import com.seafile.seadroid2.R;
import com.seafile.seadroid2.SeafException;
import com.seafile.seadroid2.Utils;
+import com.seafile.seadroid2.account.Account;
import com.seafile.seadroid2.data.DataManager;
import com.seafile.seadroid2.data.SeafDirent;
import com.seafile.seadroid2.data.SeafGroup;
@@ -314,6 +316,27 @@ protected List doInBackground(Void... params) {
}
}
+ private void displaySSLError() {
+ if (mActivity == null)
+ return;
+
+ if (getNavContext().inRepo()) {
+ return;
+ }
+
+ showError(R.string.ssl_error);
+ }
+
+ private void resend() {
+ if (mActivity == null)
+ return;
+
+ if (getNavContext().inRepo()) {
+ return;
+ }
+ ConcurrentAsyncTask.execute(new LoadTask(dataManager));
+ }
+
// onPostExecute displays the results of the AsyncTask.
@Override
protected void onPostExecute(List rs) {
@@ -326,6 +349,27 @@ protected void onPostExecute(List rs) {
return;
}
+ // Prompt the user to accept the ssl certificate
+ if (err == SeafException.sslException) {
+ SslConfirmDialog dialog = new SslConfirmDialog(dataManager.getAccount(),
+ new SslConfirmDialog.Listener() {
+ @Override
+ public void onAccepted(boolean rememberChoice) {
+ Account account = dataManager.getAccount();
+ CertsManager.instance().saveCertForAccount(account, rememberChoice);
+ resend();
+ }
+
+ @Override
+ public void onRejected() {
+ Log.d("SeafileHTTPS", "the user rejectes the ssl certificate");
+ displaySSLError();
+ }
+ });
+ dialog.show(getFragmentManager(), SslConfirmDialog.FRAGMENT_TAG);
+ return;
+ }
+
if (err != null) {
err.printStackTrace();
Log.i(DEBUG_TAG, "failed to load repos: " + err.getMessage());
@@ -342,7 +386,6 @@ protected void onPostExecute(List rs) {
showError(R.string.error_when_load_repos);
}
}
-
}
private void showError(int strID) {
@@ -413,6 +456,28 @@ protected List doInBackground(String... params) {
}
+ private void resend() {
+ if (mActivity == null)
+ return;
+ NavContext nav = mActivity.getNavContext();
+ if (!myRepoID.equals(nav.getRepoID()) || !myPath.equals(nav.getDirPath())) {
+ return;
+ }
+
+ ConcurrentAsyncTask.execute(new LoadDirTask(dataManager), myRepoName, myRepoID, myPath);
+ }
+
+ private void displaySSLError() {
+ if (mActivity == null)
+ return;
+
+ NavContext nav = mActivity.getNavContext();
+ if (!myRepoID.equals(nav.getRepoID()) || !myPath.equals(nav.getDirPath())) {
+ return;
+ }
+ showError(R.string.ssl_error);
+ }
+
// onPostExecute displays the results of the AsyncTask.
@Override
protected void onPostExecute(List dirents) {
@@ -425,6 +490,26 @@ protected void onPostExecute(List dirents) {
return;
}
+ if (err == SeafException.sslException) {
+ SslConfirmDialog dialog = new SslConfirmDialog(dataManager.getAccount(),
+ new SslConfirmDialog.Listener() {
+ @Override
+ public void onAccepted(boolean rememberChoice) {
+ Account account = dataManager.getAccount();
+ CertsManager.instance().saveCertForAccount(account, rememberChoice);
+ resend();
+ }
+
+ @Override
+ public void onRejected() {
+ Log.d("SeafileHTTPS", "the user rejectes the ssl certificate");
+ displaySSLError();
+ }
+ });
+ dialog.show(getFragmentManager(), SslConfirmDialog.FRAGMENT_TAG);
+ return;
+ }
+
if (err != null) {
if (err.getCode() == 440) {
showPasswordDialog();
diff --git a/src/com/seafile/seadroid2/ui/SslConfirmDialog.java b/src/com/seafile/seadroid2/ui/SslConfirmDialog.java
new file mode 100644
index 000000000..9d2daf49a
--- /dev/null
+++ b/src/com/seafile/seadroid2/ui/SslConfirmDialog.java
@@ -0,0 +1,91 @@
+package com.seafile.seadroid2.ui;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.seafile.seadroid2.R;
+import com.seafile.seadroid2.account.Account;
+
+
+public class SslConfirmDialog extends DialogFragment {
+ public interface Listener {
+ void onAccepted(boolean rememberChoice);
+ void onRejected();
+ }
+
+ public static final String FRAGMENT_TAG = "SslConfirmDialog";
+ public static final String DEBUG_TAG = "SslConfirmDialog";
+
+ private Account account;
+ private Listener listener;
+ private TextView messageText;
+ private CheckBox rememberChoiceCheckbox;
+
+ public SslConfirmDialog() {
+ }
+
+ public SslConfirmDialog(Account account, Listener listener) {
+ this.listener = listener;
+ this.account = account;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ LinearLayout view = (LinearLayout)inflater.inflate(R.layout.dialog_ssl_confirm, null);
+
+ messageText = (TextView)view.findViewById(R.id.message);
+ rememberChoiceCheckbox = (CheckBox)view.findViewById(R.id.remember_choice);
+
+ String host = null;
+
+ try {
+ host = new URL(account.server).getHost();
+ } catch (MalformedURLException e) {
+ // ignore
+ }
+
+ String msg = String.format(getActivity().getString(R.string.ssl_confirm), host);
+ messageText.setText(msg);
+
+ builder.setTitle(R.string.ssl_confirm_title);
+ builder.setView(view);
+
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Log.d(DEBUG_TAG, "listener.onAccepted is called");
+ listener.onAccepted(rememberChoiceCheckbox.isChecked());
+ }
+ });
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Log.d(DEBUG_TAG, "listener.onRejected is called");
+ listener.onRejected();
+ }
+ });
+
+ Dialog dialog = builder.create();
+
+ return dialog;
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ Log.d(DEBUG_TAG, "listener.onRejected is called");
+ listener.onRejected();
+ }
+}
diff --git a/src/com/seafile/seadroid2/ui/TabsFragment.java b/src/com/seafile/seadroid2/ui/TabsFragment.java
index fd3fcc7e7..a72ebc58e 100644
--- a/src/com/seafile/seadroid2/ui/TabsFragment.java
+++ b/src/com/seafile/seadroid2/ui/TabsFragment.java
@@ -1,6 +1,5 @@
package com.seafile.seadroid2.ui;
-import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@@ -8,14 +7,12 @@
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
-import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.actionbarsherlock.app.SherlockFragment;
-import com.seafile.seadroid2.BrowserActivity;
import com.seafile.seadroid2.R;
import com.viewpagerindicator.IconPagerAdapter;
import com.viewpagerindicator.TabPageIndicator;