Added username & password fields to the REPO table, increased DB version to 52.

Extended DownloaderFactory to support optional username & password parameters.

Extended HttpDownloader to check for HTTP 401 Authorization Required status code
and send a simple HTTP Basic Authentication header with all requests.

Extended ManageReposActivity to support repositories that use HTTP Basic
Authentication, added a dialog to prompt for username and password.

Extended RepoDetailsActivity to be able to display and modify the authentication
credentials.
This commit is contained in:
Christian Morgner 2015-11-14 18:10:08 +01:00
parent 31313bc9ee
commit 34838fd0dc
11 changed files with 320 additions and 24 deletions

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="24dp"
android:paddingStart="24dp"
android:paddingRight="24dp"
android:paddingEnd="24dp"
android:paddingTop="20dp">
<LinearLayout
android:id="@+id/login_form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_name" />
<EditText
android:id="@+id/edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:maxLines="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_password" />
<EditText
android:id="@+id/edit_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:maxLines="1" />
<TextView
android:id="@+id/overwrite_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
tools:text="This repo is already setup, this will add new key information."/>
</LinearLayout>
<TextView
android:padding="10dp"
android:textSize="16sp"
android:id="@+id/text_searching_for_repo"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Searching for repository at\nhttps://www.example.com/fdroid/repo/" />
</RelativeLayout>

View File

@ -75,6 +75,25 @@
android:layout_height="wrap_content"
style="@style/RepoDetailsBody"/>
<!-- The credentials used to access this repo (optional) -->
<TextView
android:id="@+id/label_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_name"
style="@style/RepoDetailsCaption"/>
<TextView
android:id="@+id/text_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/RepoDetailsBody"/>
<Button
android:id="@+id/button_edit_credentials"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/repo_edit_credentials"
android:onClick="showChangePasswordDialog" />
<!-- Signature (or "unsigned" if none) -->
<TextView
android:id="@+id/label_repo_fingerprint"

View File

@ -37,6 +37,12 @@
<string name="local_repo_https">Use Private Connection</string>
<string name="local_repo_https_on">Use encrypted HTTPS:// connection for local repo</string>
<string name="login_title">Authentication required</string>
<string name="login_name">Username</string>
<string name="login_password">Password</string>
<string name="repo_edit_credentials">Change Password</string>
<string name="repo_error_empty_username">Empty username, credentials not changed</string>
<string name="search_results">Search Results</string>
<string name="app_details">App Details</string>
<string name="no_such_app">No such app found.</string>

View File

@ -95,7 +95,10 @@ public class RepoUpdater {
Downloader downloader = null;
try {
downloader = DownloaderFactory.create(context,
getIndexAddress(), File.createTempFile("index-", "-downloaded", context.getCacheDir()));
getIndexAddress(), File.createTempFile("index-", "-downloaded", context.getCacheDir()),
repo.username,
repo.password
);
downloader.setCacheTag(repo.lastetag);
downloader.downloadUninterrupted();

View File

@ -34,7 +34,9 @@ public class DBHelper extends SQLiteOpenHelper {
+ "maxage integer not null default 0, "
+ "version integer not null default 0, "
+ "lastetag text, lastUpdated string,"
+ "isSwap integer boolean default 0);";
+ "isSwap integer boolean default 0,"
+ "username string, password string"
+ ");";
private static final String CREATE_TABLE_APK =
"CREATE TABLE " + TABLE_APK + " ( "
@ -102,7 +104,7 @@ public class DBHelper extends SQLiteOpenHelper {
+ " );";
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + TABLE_INSTALLED_APP + ";";
private static final int DB_VERSION = 51;
private static final int DB_VERSION = 52;
private final Context context;
@ -167,12 +169,14 @@ public class DBHelper extends SQLiteOpenHelper {
+ "maxage integer not null default 0, "
+ "version integer not null default 0, "
+ "lastetag text, "
+ "lastUpdated string);";
+ "lastUpdated string,"
+ "username string, password string"
+ ");";
db.execSQL(createTableDdl);
String nonIdFields = "address, name, description, inuse, priority, " +
"pubkey, fingerprint, maxage, version, lastetag, lastUpdated";
"pubkey, fingerprint, maxage, version, lastetag, lastUpdated, username, password";
String insertSql = "INSERT INTO " + TABLE_REPO +
"(_id, " + nonIdFields + " ) " +
@ -281,6 +285,7 @@ public class DBHelper extends SQLiteOpenHelper {
populateRepoNames(db, oldVersion);
if (oldVersion < 43) createInstalledApp(db);
addIsSwapToRepo(db, oldVersion);
addCredentialsToRepo(db, oldVersion);
addChangelogToApp(db, oldVersion);
addIconUrlLargeToApp(db, oldVersion);
updateIconUrlLarge(db, oldVersion);
@ -419,6 +424,15 @@ public class DBHelper extends SQLiteOpenHelper {
}
}
private void addCredentialsToRepo(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 52 && !columnExists(db, TABLE_REPO, "username") && !columnExists(db, TABLE_REPO, "password")) {
Utils.debugLog(TAG, "Adding username field to " + TABLE_REPO + " table in db.");
db.execSQL("alter table " + TABLE_REPO + " add column username string;");
Utils.debugLog(TAG, "Adding password field to " + TABLE_REPO + " table in db.");
db.execSQL("alter table " + TABLE_REPO + " add column password string;");
}
}
private void addChangelogToApp(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 48 && !columnExists(db, TABLE_APP, "changelogURL")) {
Utils.debugLog(TAG, "Adding changelogURL column to " + TABLE_APP);

View File

@ -37,6 +37,9 @@ public class Repo extends ValueObject {
public Date lastUpdated;
public boolean isSwap;
public String username;
public String password;
public Repo() {
}
@ -85,6 +88,12 @@ public class Repo extends ValueObject {
case RepoProvider.DataColumns.IS_SWAP:
isSwap = cursor.getInt(i) == 1;
break;
case RepoProvider.DataColumns.USERNAME:
username = cursor.getString(i);
break;
case RepoProvider.DataColumns.PASSWORD:
password = cursor.getString(i);
break;
}
}
}
@ -192,5 +201,13 @@ public class Repo extends ValueObject {
if (values.containsKey(RepoProvider.DataColumns.IS_SWAP)) {
isSwap = toInt(values.getAsInteger(RepoProvider.DataColumns.IS_SWAP)) == 1;
}
if (values.containsKey(RepoProvider.DataColumns.USERNAME)) {
username = values.getAsString(RepoProvider.DataColumns.USERNAME);
}
if (values.containsKey(RepoProvider.DataColumns.PASSWORD)) {
password = values.getAsString(RepoProvider.DataColumns.PASSWORD);
}
}
}

View File

@ -223,10 +223,13 @@ public class RepoProvider extends FDroidProvider {
String LAST_UPDATED = "lastUpdated";
String VERSION = "version";
String IS_SWAP = "isSwap";
String USERNAME = "username";
String PASSWORD = "password";
String[] ALL = {
_ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, PUBLIC_KEY,
FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP,
USERNAME, PASSWORD,
};
}
@ -319,7 +322,7 @@ public class RepoProvider extends FDroidProvider {
// to be present.
if (!values.containsKey(DataColumns.IN_USE)) {
values.put(DataColumns.IN_USE, 1);
values.put(DataColumns.IN_USE, true);
}
if (!values.containsKey(DataColumns.PRIORITY)) {
@ -376,5 +379,4 @@ public class RepoProvider extends FDroidProvider {
getContext().getContentResolver().notifyChange(uri, null);
return numRows;
}
}

View File

@ -45,6 +45,11 @@ public class DownloaderFactory {
public static Downloader create(Context context, URL url, File destFile)
throws IOException {
return create(context, url, destFile, null, null);
}
public static Downloader create(Context context, URL url, File destFile, final String username, final String password)
throws IOException {
if (isBluetoothAddress(url)) {
String macAddress = url.getHost().replace("-", ":");
return new BluetoothDownloader(context, macAddress, url, destFile);
@ -53,7 +58,7 @@ public class DownloaderFactory {
} else if (isLocalFile(url)) {
return new LocalFileDownloader(context, url, destFile);
}
return new HttpDownloader(context, url, destFile);
return new HttpDownloader(context, url, destFile, username, password);
}
private static boolean isBluetoothAddress(URL url) {

View File

@ -29,11 +29,21 @@ public class HttpDownloader extends Downloader {
protected static final String HEADER_FIELD_ETAG = "ETag";
protected HttpURLConnection connection;
private int statusCode = -1;
private final String username;
private final String password;
private int statusCode = -1;
HttpDownloader(Context context, URL url, File destFile)
throws FileNotFoundException, MalformedURLException {
this(context, url, destFile, null, null);
}
HttpDownloader(Context context, URL url, File destFile, final String username, final String password)
throws FileNotFoundException, MalformedURLException {
super(context, url, destFile);
this.username = username;
this.password = password;
}
/**
@ -44,7 +54,7 @@ public class HttpDownloader extends Downloader {
* same one twice, bail with an exception).
* @throws IOException
*/
@Override
protected InputStream getDownloadersInputStream() throws IOException {
setupConnection();
return new BufferedInputStream(connection.getInputStream());
@ -87,10 +97,33 @@ public class HttpDownloader extends Downloader {
Proxy proxy = new Proxy(Proxy.Type.HTTP, sa);
connection = (HttpURLConnection) sourceUrl.openConnection(proxy);
} else {
// send HEAD request first, then GET afterwards
connection = (HttpURLConnection) sourceUrl.openConnection();
final String userInfo = sourceUrl.getUserInfo();
if (userInfo != null) {
connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(userInfo.getBytes()));
connection.setRequestMethod("HEAD");
// fetch HTTP status code and check for authentication
statusCode = connection.getResponseCode();
connection.disconnect();
// reset connection
connection = (HttpURLConnection) sourceUrl.openConnection();
// handle status codes
switch (statusCode) {
case 401:
final String userInfo = sourceUrl.getUserInfo();
if (userInfo != null) {
// add authorization header from user info in URL if present
connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(userInfo.getBytes()));
} else {
// add authorization header from username / password
connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String((username + ":" + password).getBytes()));
}
break;
default:
break;
}
}
}

View File

@ -54,9 +54,6 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.impl.client.DefaultHttpClient;
import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
@ -68,11 +65,13 @@ import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Locale;
import org.apache.commons.net.util.Base64;
public class ManageReposActivity extends ActionBarActivity {
private static final String TAG = "ManageReposActivity";
@ -451,6 +450,8 @@ public class ManageReposActivity extends ActionBarActivity {
final AsyncTask<String, String, String> checker = new AsyncTask<String, String, String>() {
private int statusCode = -1;
@Override
protected String doInBackground(String... params) {
@ -485,9 +486,27 @@ public class ManageReposActivity extends ActionBarActivity {
}
private boolean checkForRepository(Uri indexUri) throws IOException {
HttpClient client = new DefaultHttpClient();
HttpHead head = new HttpHead(indexUri.toString());
return client.execute(head).getStatusLine().getStatusCode() == 200;
final URL url = new URL(indexUri.toString());
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
// support discovery of Basic Auth repository URLs without login prompt
final String userInfo = url.getUserInfo();
if (userInfo != null) {
// authorize request
connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(userInfo.getBytes()));
}
statusCode = connection.getResponseCode();
switch (statusCode) {
case 401:
case 200:
return true;
}
return false;
}
@Override
@ -497,9 +516,45 @@ public class ManageReposActivity extends ActionBarActivity {
}
@Override
protected void onPostExecute(String newAddress) {
protected void onPostExecute(final String newAddress) {
if (addRepoDialog.isShowing()) {
createNewRepo(newAddress, fingerprint);
if (statusCode == 401) {
final View view = getLayoutInflater().inflate(R.layout.login, null);
final AlertDialog credentialsDialog = new AlertDialog.Builder(context).setView(view).create();
final EditText nameInput = (EditText) view.findViewById(R.id.edit_name);
final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password);
credentialsDialog.setTitle(R.string.login_title);
credentialsDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
getString(R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
// cancel parent dialog, don't add repo
addRepoDialog.cancel();
}
});
credentialsDialog.setButton(DialogInterface.BUTTON_POSITIVE,
getString(R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
createNewRepo(newAddress, fingerprint, nameInput.getText().toString(), passwordInput.getText().toString());
}
});
credentialsDialog.show();
} else {
// create repo without username/password
createNewRepo(newAddress, fingerprint, null, null);
}
}
}
};
@ -515,7 +570,7 @@ public class ManageReposActivity extends ActionBarActivity {
// or their internet is playing up, then you'd have to wait for several
// connection timeouts before being able to proceed.
createNewRepo(originalAddress, fingerprint);
createNewRepo(originalAddress, fingerprint, null, null);
checker.cancel(false);
}
});
@ -551,17 +606,23 @@ public class ManageReposActivity extends ActionBarActivity {
path, uri.getQuery(), uri.getFragment()).toString();
}
private void createNewRepo(String address, String fingerprint) {
private void createNewRepo(String address, String fingerprint, final String username, final String password) {
try {
address = normalizeUrl(address);
} catch (URISyntaxException e) {
// Leave address as it was.
}
ContentValues values = new ContentValues(2);
ContentValues values = new ContentValues(4);
values.put(RepoProvider.DataColumns.ADDRESS, address);
if (!TextUtils.isEmpty(fingerprint)) {
values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH));
}
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
values.put(RepoProvider.DataColumns.USERNAME, username);
values.put(RepoProvider.DataColumns.PASSWORD, password);
}
RepoProvider.Helper.insert(context, values);
finishedAddingRepo();
Toast.makeText(ManageReposActivity.this, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show();

View File

@ -2,6 +2,7 @@ package org.fdroid.fdroid.views;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@ -21,6 +22,8 @@ import android.text.format.DateUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
@ -54,6 +57,9 @@ public class RepoDetailsActivity extends ActionBarActivity {
R.id.text_num_apps,
R.id.label_last_update,
R.id.text_last_update,
R.id.label_username,
R.id.text_username,
R.id.button_edit_credentials,
R.id.label_repo_fingerprint,
R.id.text_repo_fingerprint,
R.id.text_repo_fingerprint_description,
@ -271,6 +277,25 @@ public class RepoDetailsActivity extends ActionBarActivity {
repoFingerprintView.setText(repoFingerprint);
}
private void setupCredentials(View parent, Repo repo) {
TextView usernameLabel = (TextView) parent.findViewById(R.id.label_username);
TextView username = (TextView) parent.findViewById(R.id.text_username);
Button changePassword = (Button) parent.findViewById(R.id.button_edit_credentials);
if (TextUtils.isEmpty(repo.username)) {
usernameLabel.setVisibility(View.GONE);
username.setVisibility(View.GONE);
username.setText("");
changePassword.setVisibility(View.GONE);
} else {
usernameLabel.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
username.setText(repo.username);
changePassword.setVisibility(View.VISIBLE);
}
}
private void updateRepoView() {
if (repo.hasBeenUpdated()) {
@ -301,6 +326,7 @@ public class RepoDetailsActivity extends ActionBarActivity {
setupDescription(repoView, repo);
setupRepoFingerprint(repoView, repo);
setupCredentials(repoView, repo);
// Repos that existed before this feature was supported will have an
// "Unknown" last update until next time they update...
@ -335,4 +361,54 @@ public class RepoDetailsActivity extends ActionBarActivity {
).show();
}
public void showChangePasswordDialog(final View parentView) {
final View view = getLayoutInflater().inflate(R.layout.login, null);
final AlertDialog credentialsDialog = new AlertDialog.Builder(this).setView(view).create();
final EditText nameInput = (EditText) view.findViewById(R.id.edit_name);
final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password);
nameInput.setText(repo.username);
passwordInput.requestFocus();
credentialsDialog.setTitle(R.string.repo_edit_credentials);
credentialsDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
getString(R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
credentialsDialog.setButton(DialogInterface.BUTTON_POSITIVE,
getString(R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String name = nameInput.getText().toString();
final String password = passwordInput.getText().toString();
if (name != null && !name.isEmpty()) {
final ContentValues values = new ContentValues(2);
values.put(RepoProvider.DataColumns.USERNAME, name);
values.put(RepoProvider.DataColumns.PASSWORD, password);
RepoProvider.Helper.update(RepoDetailsActivity.this, repo, values);
updateRepoView();
dialog.dismiss();
} else {
Toast.makeText(RepoDetailsActivity.this, R.string.repo_error_empty_username, Toast.LENGTH_LONG).show();
}
}
});
credentialsDialog.show();
}
}