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;