Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Git via SSH: Replace Jsch transport with Apache #948

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,11 @@ dependencies {
implementation "io.github.rburgst:okhttp-digest:$versions.okhttp_digest"

implementation "org.eclipse.jgit:org.eclipse.jgit:$versions.jgit"
implementation "org.eclipse.jgit:org.eclipse.jgit.ssh.jsch:$versions.jgit"
implementation("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:$versions.jgit") {
// Resolves DuplicatePlatformClasses lint error
exclude group: 'org.apache.sshd', module: 'sshd-osgi'
}

}

repositories {
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/orgzly/android/AppIntent.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public class AppIntent {

public static final String ACTION_SHOW_SNACKBAR = "com.orgzly.intent.action.SHOW_SNACKBAR";

public static final String ACTION_REJECT_REMOTE_HOST_KEY = "com.orgzly.intent.action.REJECT_REMOTE_HOST_KEY";
public static final String ACTION_ACCEPT_REMOTE_HOST_KEY = "com.orgzly.intent.action.ACCEPT_REMOTE_HOST_KEY";
public static final String ACTION_ACCEPT_AND_STORE_REMOTE_HOST_KEY = "com.orgzly.intent.action.ACCEPT_AND_STORE_REMOTE_HOST_KEY";

public static final String EXTRA_MESSAGE = "com.orgzly.intent.extra.MESSAGE";
public static final String EXTRA_BOOK_ID = "com.orgzly.intent.extra.BOOK_ID";
public static final String EXTRA_BOOK_PREFACE = "com.orgzly.intent.extra.BOOK_PREFACE";
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/com/orgzly/android/NotificationChannels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ object NotificationChannels {
const val REMINDERS = "reminders"
const val SYNC_PROGRESS = "sync-progress"
const val SYNC_FAILED = "sync-failed"
const val SYNC_PROMPT = "sync-prompt"

@JvmStatic
fun createAll(context: Context) {
Expand All @@ -28,6 +29,7 @@ object NotificationChannels {
createForReminders(context)
createForSyncProgress(context)
createForSyncFailed(context)
createForSyncPrompt(context)
}
}

Expand Down Expand Up @@ -111,4 +113,25 @@ object NotificationChannels {

context.getNotificationManager().createNotificationChannel(channel)
}

@RequiresApi(Build.VERSION_CODES.O)
private fun createForSyncPrompt(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}

val id = SYNC_PROMPT
val name = "Sync prompt"
val description = "Display sync prompt"
val importance = NotificationManager.IMPORTANCE_HIGH

val channel = NotificationChannel(id, name, importance)

channel.description = description

channel.setShowBadge(false)

val mNotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotificationManager.createNotificationChannel(channel)
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,74 @@
package com.orgzly.android.git;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import android.net.Uri;
import android.os.Build;

import androidx.annotation.RequiresApi;

import com.orgzly.android.App;

import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.TransportCommand;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig;
import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
import org.eclipse.jgit.transport.sshd.SshdSessionFactory;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;

public class GitSSHKeyTransportSetter implements GitTransportSetter {
private String sshKeyPath;
private SshSessionFactory sshSessionFactory;
private TransportConfigCallback configCallback;
private final TransportConfigCallback configCallback;

public GitSSHKeyTransportSetter(String pathToSSHKey) {
sshKeyPath = pathToSSHKey;
sshSessionFactory = new JschConfigSessionFactory() {

SshSessionFactory factory = new SshdSessionFactory(null, null) {

@Override
protected void configure(OpenSshConfig.Host host, Session session ) {
session.setConfig("StrictHostKeyChecking", "no");
public File getHomeDirectory() {
return App.getAppContext().getFilesDir();
}

@RequiresApi(api = Build.VERSION_CODES.O)
@Override
protected JSch createDefaultJSch(FS fs) throws JSchException {
JSch defaultJSch = super.createDefaultJSch(fs);
defaultJSch.addIdentity(sshKeyPath);
return defaultJSch;
protected List<Path> getDefaultIdentities(File sshDir) {
return Collections.singletonList(Paths.get(Uri.decode(pathToSSHKey)));
}

@Override
protected String getDefaultPreferredAuthentications() {
return "publickey";
}

};
configCallback = new TransportConfigCallback() {
@Override
public void configure(Transport transport) {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(sshSessionFactory);
protected ServerKeyDatabase createServerKeyDatabase(@NonNull File homeDir,
@NonNull File sshDir) {
// We override this method because we want to set "askAboutNewFile" to False.
return new OpenSshServerKeyDatabase(false,
getDefaultKnownHostsFiles(sshDir));
}
};

SshSessionFactory.setInstance(factory);

// org.apache.sshd.common.config.keys.IdentityUtils freaks out if user.home is not set
System.setProperty("user.home", App.getAppContext().getFilesDir().toString());

configCallback = transport -> {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(factory);

};
}

public TransportCommand setTransport(TransportCommand tc) {
tc.setTransportConfigCallback(configCallback);
tc.setCredentialsProvider(new SshCredentialsProvider());
return tc;
}
}
135 changes: 135 additions & 0 deletions app/src/main/java/com/orgzly/android/git/SshCredentialsProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.orgzly.android.git;

import static com.orgzly.android.AppIntent.ACTION_ACCEPT_AND_STORE_REMOTE_HOST_KEY;
import static com.orgzly.android.AppIntent.ACTION_ACCEPT_REMOTE_HOST_KEY;
import static com.orgzly.android.AppIntent.ACTION_REJECT_REMOTE_HOST_KEY;
import static com.orgzly.android.ui.notifications.Notifications.SYNC_SSH_REMOTE_HOST_KEY;

import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;

import com.orgzly.android.App;
import com.orgzly.android.ui.notifications.Notifications;

import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;

import java.util.ArrayList;
import java.util.List;

public class SshCredentialsProvider extends CredentialsProvider {

private static final Object monitor = new Object();

public static final String DENY = "Reject";
public static final String ALLOW = "Accept";
public static final String ALLOW_AND_STORE = "Accept and store";

@Override
public boolean isInteractive() {
return true;
}

@Override
public boolean supports(CredentialItem... items) {
for (CredentialItem i : items) {
if (i instanceof CredentialItem.YesNoType) {
continue;
}
if (i instanceof CredentialItem.InformationalMessage) {
continue;
}
return false;
}
return true;
}

@Override
public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
List<CredentialItem.YesNoType> questions = new ArrayList<>();
for (CredentialItem item : items) {
if (item instanceof CredentialItem.InformationalMessage) {
continue;
}
if (item instanceof CredentialItem.YesNoType) {
questions.add((CredentialItem.YesNoType) item);
continue;
}
throw new UnsupportedCredentialItem(uri, item.getClass().getName()
+ ":" + item.getPromptText()); //$NON-NLS-1$
}

if (questions.isEmpty()) {
return true;
} else {
// We need to prompt the user via a notification;
// set up a broadcast receiver for this purpose.
Context context = App.getAppContext();
final Boolean[] userHasResponded = {false};
final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// Remove the notification
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(SYNC_SSH_REMOTE_HOST_KEY);
// Save the user response
switch (intent.getAction()) {
case ACTION_REJECT_REMOTE_HOST_KEY:
questions.get(0).setValue(false);
break;
case ACTION_ACCEPT_REMOTE_HOST_KEY:
questions.get(0).setValue(true);
break;
case ACTION_ACCEPT_AND_STORE_REMOTE_HOST_KEY:
questions.get(0).setValue(true);
if (questions.size() == 2) {
questions.get(1).setValue(true);
}
}
userHasResponded[0] = true;
synchronized (monitor) {
monitor.notify();
}
}
};
// Create intent filter and register receiver
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_REJECT_REMOTE_HOST_KEY);
intentFilter.addAction(ACTION_ACCEPT_REMOTE_HOST_KEY);
intentFilter.addAction(ACTION_ACCEPT_AND_STORE_REMOTE_HOST_KEY);
context.registerReceiver(broadcastReceiver, intentFilter);

// Send the notification and wait up to 30 seconds for the user to respond
Notifications.showSshRemoteHostKeyPrompt(context, uri, items);
synchronized (monitor) {
if (!userHasResponded[0]) {
try {
monitor.wait(30000);
} catch (InterruptedException e) {
e.printStackTrace();
if (!userHasResponded[0]) {
return false;
}
}
}
}
// Remove the broadcast receiver and its intent filters
context.unregisterReceiver(broadcastReceiver);
// Update the original list objects
int questionCounter = 0;
for (CredentialItem item : items) {
if (item instanceof CredentialItem.YesNoType) {
((CredentialItem.YesNoType) item).setValue(questions.get(questionCounter).getValue());
questionCounter++;
}
}
return questions.get(0).getValue();
}
}
}
Loading