Added basic ssh auth in rust without error handling

This commit is contained in:
Abdelilah El Aissaoui 2023-09-20 10:26:02 +02:00
parent bb49e9482c
commit 6dee4fbc93
No known key found for this signature in database
GPG key ID: 7587FC860F594869
15 changed files with 239 additions and 267 deletions

View file

@ -205,7 +205,7 @@ fun generateKotlinFromUdl() {
workingDir = File(project.projectDir, "rs") workingDir = File(project.projectDir, "rs")
commandLine = listOf( commandLine = listOf(
"cargo", "run", "--features=uniffi/cli", "cargo", "run", "--features=uniffi/cli",
"--bin", "uniffi-bindgen", "generate", "src/repository_watcher.udl", "--bin", "uniffi-bindgen", "generate", "src/gitnuro.udl",
"--language", "kotlin", "--language", "kotlin",
"--out-dir", rustGeneratedSource "--out-dir", rustGeneratedSource
) )

View file

@ -13,6 +13,7 @@ name = "gitnuro_rs"
uniffi = { version = "0.24.1" } uniffi = { version = "0.24.1" }
notify = "6.0.1" notify = "6.0.1"
thiserror = "1.0.43" thiserror = "1.0.43"
libssh-rs = "0.2.1"
[build-dependencies] [build-dependencies]
uniffi = { version = "0.24.1", features = [ "build" ] } uniffi = { version = "0.24.1", features = [ "build" ] }

View file

@ -1,3 +1,3 @@
fn main() { fn main() {
uniffi::generate_scaffolding("src/repository_watcher.udl").unwrap(); uniffi::generate_scaffolding("src/gitnuro.udl").unwrap();
} }

46
rs/src/gitnuro.udl Normal file
View file

@ -0,0 +1,46 @@
namespace gitnuro {
[Throws=WatcherInitError]
void watch_directory(string path, WatchDirectoryNotifier checker);
};
callback interface WatchDirectoryNotifier {
boolean should_keep_looping();
void detected_change(sequence<string> paths);
};
[Error]
interface WatcherInitError {
Generic(string error);
Io(string error);
PathNotFound();
WatchNotFound();
InvalidConfig();
MaxFilesWatch();
};
interface Session {
constructor();
void setup(string host, string? user, i32 port);
void public_key_auth();
void password_auth(string password);
void disconnect();
};
interface Channel {
constructor(Session session);
void open_session();
boolean is_open();
void close();
void request_exec(string command);
boolean poll_has_bytes(boolean is_stderr);
ReadResult read(boolean is_stderr, u64 len);
void write_byte(i32 byte);
void write_bytes(bytes data);
};
dictionary ReadResult {
u64 read_count;
bytes data;
};

View file

@ -1,13 +1,18 @@
extern crate notify; extern crate notify;
use std::fmt::Debug; use std::fmt::Debug;
use std::io::{Write};
use std::path::Path; use std::path::Path;
use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::mpsc::{channel, RecvTimeoutError};
use std::sync::{Arc, RwLock};
use std::time::Duration; use std::time::Duration;
use libssh_rs::{PollStatus, SshOption};
use notify::{Config, Error, ErrorKind, Event, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, Error, ErrorKind, Event, RecommendedWatcher, RecursiveMode, Watcher};
uniffi::include_scaffolding!("repository_watcher"); uniffi::include_scaffolding!("gitnuro");
const ACCEPTED_SSH_TYPES: &str = "ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-rsa,rsa-sha2-512,rsa-sha2-256,ssh-dss";
fn watch_directory( fn watch_directory(
path: String, path: String,
@ -114,3 +119,125 @@ impl WatcherInitErrorConverter for ErrorKind {
} }
} }
} }
struct Session {
pub session: RwLock<libssh_rs::Session>,
}
impl Session {
fn new() -> Self {
let session = libssh_rs::Session::new().unwrap();
Session {
session: RwLock::new(session)
}
}
fn setup(&self, host: String, user: Option<String>, port: i32) {
let session = self.session.write().unwrap();
session.set_option(SshOption::Hostname(host)).unwrap();
if let Some(user) = user {
session.set_option(SshOption::User(Some(user))).unwrap();
}
if let Ok(port) = port.try_into() {
session.set_option(SshOption::Port(port)).unwrap();
}
session.set_option(SshOption::PublicKeyAcceptedTypes(ACCEPTED_SSH_TYPES.to_string())).unwrap();
session.options_parse_config(None).unwrap();
session.connect().unwrap();
}
fn public_key_auth(&self) {
let session = self.session.write().unwrap();
session.userauth_public_key_auto(None, Some("")).unwrap();
}
fn password_auth(&self, password: String) {
let session = self.session.write().unwrap();
session.userauth_password(None, Some(&password)).unwrap();
}
fn disconnect(&self) {
let session = self.session.write().unwrap();
session.disconnect()
}
}
struct Channel {
channel: RwLock<libssh_rs::Channel>,
}
unsafe impl Send for Channel {}
unsafe impl Sync for Channel {}
impl Channel {
fn new(session: Arc<Session>) -> Self {
let session = session.session.write().unwrap();
let channel = session.new_channel().unwrap();
Channel {
channel: RwLock::new(channel)
}
}
fn open_session(&self) {
let channel = self.channel.write().unwrap();
channel.open_session().unwrap();
}
fn is_open(&self) -> bool {
let channel = self.channel.write().unwrap();
channel.is_open()
}
fn close(&self) {
let channel = self.channel.write().unwrap();
channel.close().unwrap();
}
fn request_exec(&self, command: String) {
let channel = self.channel.write().unwrap();
channel.request_exec(&command).unwrap();
}
fn poll_has_bytes(&self, is_stderr: bool) -> bool {
let channel = self.channel.write().unwrap();
let poll_timeout = channel.poll_timeout(is_stderr, None).unwrap();
match poll_timeout {
PollStatus::AvailableBytes(count) => count > 0,
PollStatus::EndOfFile => false
}
}
fn read(&self, is_stderr: bool, len: u64) -> ReadResult {
let ulen = len as usize;
let channel = self.channel.write().unwrap();
let mut buffer = vec![0; ulen];
let read = channel.read_timeout(&mut buffer, is_stderr, None).unwrap();
ReadResult {
read_count: read as u64,
data: buffer,
}
}
fn write_byte(&self, byte: i32) {
let channel = self.channel.write().unwrap();
channel.stdin().write_all(&byte.to_ne_bytes()).unwrap();
}
fn write_bytes(&self, data: Vec<u8>) {
let channel = self.channel.write().unwrap();
channel.stdin().write_all(&data).unwrap();
}
}
pub struct ReadResult {
read_count: u64,
data: Vec<u8>,
}

View file

@ -1,20 +0,0 @@
namespace gitnuro {
[Throws=WatcherInitError]
void watch_directory(string path, WatchDirectoryNotifier checker);
};
callback interface WatchDirectoryNotifier {
boolean should_keep_looping();
void detected_change(sequence<string> paths);
};
[Error]
interface WatcherInitError {
Generic(string error);
Io(string error);
PathNotFound();
WatchNotFound();
InvalidConfig();
MaxFilesWatch();
};

View file

@ -1,13 +1,14 @@
package com.jetpackduba.gitnuro.credentials package com.jetpackduba.gitnuro.credentials
import com.jetpackduba.gitnuro.ssh.libssh.LibSshChannel import com.jetpackduba.gitnuro.ssh.libssh.LibSshChannel
import com.jetpackduba.gitnuro.ssh.libssh.LibSshSession import uniffi.gitnuro.Channel
import uniffi.gitnuro.Session
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
class GProcessLibSsh : Process() { class GProcessLibSsh : Process() {
private lateinit var channel: LibSshChannel private lateinit var channel: LibSshChannel
private lateinit var session: LibSshSession private lateinit var session: Session
private val outputStream by lazy { private val outputStream by lazy {
channel.outputStream channel.outputStream
@ -42,7 +43,9 @@ class GProcessLibSsh : Process() {
check(!isRunning()) check(!isRunning())
println("exitValue called") println("exitValue called")
return channel.close() channel.close()
return 0
} }
override fun destroy() { override fun destroy() {
@ -57,8 +60,8 @@ class GProcessLibSsh : Process() {
return channel.isOpen() return channel.isOpen()
} }
fun setup(session: LibSshSession, commandName: String) { fun setup(session: Session, commandName: String) {
val channel = session.createChannel() val channel = LibSshChannel(session)
channel.openSession() channel.openSession()
channel.requestExec(commandName) channel.requestExec(commandName)

View file

@ -1,21 +1,17 @@
package com.jetpackduba.gitnuro.credentials package com.jetpackduba.gitnuro.credentials
import com.jetpackduba.gitnuro.ssh.libssh.LibSshOptions
import com.jetpackduba.gitnuro.ssh.libssh.LibSshSession
import kotlinx.coroutines.CancellationException
import org.eclipse.jgit.transport.RemoteSession import org.eclipse.jgit.transport.RemoteSession
import org.eclipse.jgit.transport.URIish import org.eclipse.jgit.transport.URIish
import uniffi.gitnuro.Session
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
private const val DEFAULT_SSH_PORT = 22 private const val DEFAULT_SSH_PORT = 22
class GRemoteSession @Inject constructor( class GRemoteSession @Inject constructor(
private val processSession: Provider<LibSshSession>,
private val credentialsStateManager: CredentialsStateManager, private val credentialsStateManager: CredentialsStateManager,
) : RemoteSession { ) : RemoteSession {
private var session: LibSshSession? = null private var session: Session? = null
override fun exec(commandName: String, timeout: Int): Process { override fun exec(commandName: String, timeout: Int): Process {
println("Running command $commandName") println("Running command $commandName")
@ -32,44 +28,33 @@ class GRemoteSession @Inject constructor(
} }
fun setup(uri: URIish) { fun setup(uri: URIish) {
val session = processSession.get() val session = Session()
session.setOptions(LibSshOptions.SSH_OPTIONS_HOST, uri.host) session.setup(uri.host, uri.user, uri.port)
uri.user?.let { var result = session.publicKeyAuth()
session.setOptions(LibSshOptions.SSH_OPTIONS_USER, uri.user)
}
session.setOptions( // if (result == 1) {
LibSshOptions.SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, // credentialsStateManager.updateState(CredentialsRequested.SshCredentialsRequested)
"ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-rsa,rsa-sha2-512,rsa-sha2-256,ssh-dss" //
) // var credentials = credentialsStateManager.currentCredentialsState
session.loadOptionsFromConfig() // while (credentials is CredentialsRequested) {
// credentials = credentialsStateManager.currentCredentialsState
session.connect() // }
var result = session.userAuthPublicKeyAuto(null, "") //
// val password = if (credentials !is CredentialsAccepted.SshCredentialsAccepted)
if (result == 1) { // throw CancellationException("Credentials cancelled")
credentialsStateManager.updateState(CredentialsRequested.SshCredentialsRequested) // else
// credentials.password
var credentials = credentialsStateManager.currentCredentialsState //
while (credentials is CredentialsRequested) { // result = session.userAuthPublicKeyAuto(null, password)
credentials = credentialsStateManager.currentCredentialsState //
} // if (result != 0) {
// result = session.userAuthPassword(password)
val password = if (credentials !is CredentialsAccepted.SshCredentialsAccepted) // }
throw CancellationException("Credentials cancelled") // }
else //
credentials.password // if (result != 0)
// throw Exception("Something went wrong with authentication. Code $result")
result = session.userAuthPublicKeyAuto(null, password)
if (result != 0) {
result = session.userAuthPassword(password)
}
}
if (result != 0)
throw Exception("Something went wrong with authentication. Code $result")
this.session = session this.session = session
} }

View file

@ -3,30 +3,29 @@ package com.jetpackduba.gitnuro.ssh.libssh
import com.jetpackduba.gitnuro.ssh.libssh.streams.LibSshChannelInputErrStream import com.jetpackduba.gitnuro.ssh.libssh.streams.LibSshChannelInputErrStream
import com.jetpackduba.gitnuro.ssh.libssh.streams.LibSshChannelInputStream import com.jetpackduba.gitnuro.ssh.libssh.streams.LibSshChannelInputStream
import com.jetpackduba.gitnuro.ssh.libssh.streams.LibSshChannelOutputStream import com.jetpackduba.gitnuro.ssh.libssh.streams.LibSshChannelOutputStream
import uniffi.gitnuro.Channel
import uniffi.gitnuro.Session
class LibSshChannel internal constructor(sshSession: ssh_session) { class LibSshChannel internal constructor(sshSession: Session) {
private val sshLib = SSHLibrary.INSTANCE private var channel = Channel(sshSession)
private var channel: ssh_channel = sshLib.ssh_channel_new(sshSession)
val outputStream = LibSshChannelOutputStream(channel) val outputStream = LibSshChannelOutputStream(channel)
val inputStream = LibSshChannelInputStream(channel) val inputStream = LibSshChannelInputStream(channel)
val errorOutputStream = LibSshChannelInputErrStream(channel) val errorOutputStream = LibSshChannelInputErrStream(channel)
fun openSession() { fun openSession() {
sshLib.ssh_channel_open_session(channel) channel.openSession()
} }
fun requestExec(commandName: String) { fun requestExec(commandName: String) {
sshLib.ssh_channel_request_exec(channel, commandName) channel.requestExec(commandName)
} }
fun isOpen(): Boolean { fun isOpen(): Boolean {
return sshLib.ssh_channel_is_open(channel) == 1 return channel.isOpen()
} }
fun close(): Int { fun close() {
return sshLib.ssh_channel_close(channel) channel.close()
} }
} }

View file

@ -1,50 +0,0 @@
package com.jetpackduba.gitnuro.ssh.libssh
/**
* Enum based on the enum "ssh_options_e" of libssh/libssh.h
*/
enum class LibSshOptions {
SSH_OPTIONS_HOST,
SSH_OPTIONS_PORT,
SSH_OPTIONS_PORT_STR,
SSH_OPTIONS_FD,
SSH_OPTIONS_USER,
SSH_OPTIONS_SSH_DIR,
SSH_OPTIONS_IDENTITY,
SSH_OPTIONS_ADD_IDENTITY,
SSH_OPTIONS_KNOWNHOSTS,
SSH_OPTIONS_TIMEOUT,
SSH_OPTIONS_TIMEOUT_USEC,
SSH_OPTIONS_SSH1,
SSH_OPTIONS_SSH2,
SSH_OPTIONS_LOG_VERBOSITY,
SSH_OPTIONS_LOG_VERBOSITY_STR,
SSH_OPTIONS_CIPHERS_C_S,
SSH_OPTIONS_CIPHERS_S_C,
SSH_OPTIONS_COMPRESSION_C_S,
SSH_OPTIONS_COMPRESSION_S_C,
SSH_OPTIONS_PROXYCOMMAND,
SSH_OPTIONS_BINDADDR,
SSH_OPTIONS_STRICTHOSTKEYCHECK,
SSH_OPTIONS_COMPRESSION,
SSH_OPTIONS_COMPRESSION_LEVEL,
SSH_OPTIONS_KEY_EXCHANGE,
SSH_OPTIONS_HOSTKEYS,
SSH_OPTIONS_GSSAPI_SERVER_IDENTITY,
SSH_OPTIONS_GSSAPI_CLIENT_IDENTITY,
SSH_OPTIONS_GSSAPI_DELEGATE_CREDENTIALS,
SSH_OPTIONS_HMAC_C_S,
SSH_OPTIONS_HMAC_S_C,
SSH_OPTIONS_PASSWORD_AUTH,
SSH_OPTIONS_PUBKEY_AUTH,
SSH_OPTIONS_KBDINT_AUTH,
SSH_OPTIONS_GSSAPI_AUTH,
SSH_OPTIONS_GLOBAL_KNOWNHOSTS,
SSH_OPTIONS_NODELAY,
SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES,
SSH_OPTIONS_PROCESS_CONFIG,
SSH_OPTIONS_REKEY_DATA,
SSH_OPTIONS_REKEY_TIME,
SSH_OPTIONS_RSA_MIN_SIZE,
SSH_OPTIONS_IDENTITY_AGENT
}

View file

@ -1,62 +0,0 @@
package com.jetpackduba.gitnuro.ssh.libssh
import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.ssh.libssh.streams.checkValidResult
import javax.inject.Inject
private const val TAG = "LibSshSession"
class LibSshSession @Inject constructor() {
private val sshLib = SSHLibrary.INSTANCE
private var session: ssh_session = sshLib.ssh_new()
private var channel: LibSshChannel? = null
fun setOptions(option: LibSshOptions, value: String) {
sshLib.ssh_options_set(session, option.ordinal, value)
}
fun loadOptionsFromConfig() {
checkValidResult("loadOptionsFromConfig", sshLib.ssh_options_parse_config(session, null))
}
fun connect() {
sshLib.ssh_connect(session)
}
fun userAuthPublicKeyAuto(username: String?, password: String?): Int {
val result = sshLib.ssh_userauth_publickey_auto(session, username, password)
if (result != 0)
printError(TAG, "Result is: $result.\nError is: ${getError()}")
return result
}
fun userAuthPassword(password: String): Int {
val result = sshLib.ssh_userauth_password(session, null, password)
if (result != 0)
printError(TAG, "Result is: $result.\nError is: ${getError()}")
return result
}
fun createChannel(): LibSshChannel {
val newChannel = LibSshChannel(session)
this.channel = newChannel
return newChannel
}
private fun getError(): String {
return sshLib.ssh_get_error(session)
}
fun disconnect() {
sshLib.ssh_disconnect(session)
}
}

View file

@ -1,44 +0,0 @@
package com.jetpackduba.gitnuro.ssh.libssh
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.PointerType
class ssh_session : PointerType()
class ssh_channel : PointerType()
@Suppress("FunctionName")
interface SSHLibrary : Library {
fun ssh_new(): ssh_session
fun ssh_disconnect(session: ssh_session): ssh_session
fun ssh_options_set(session: ssh_session, enumValue: Int, value: String)
fun ssh_options_parse_config(session: ssh_session, fileName: String?): Int
fun ssh_connect(session: ssh_session): Int
fun ssh_userauth_publickey_auto(session: ssh_session, username: String?, password: String?): Int
fun ssh_userauth_password(session: ssh_session, username: String?, password: String?): Int
fun ssh_get_error(session: ssh_session): String
fun ssh_channel_new(sshSession: ssh_session): ssh_channel
fun ssh_channel_open_session(sshChannel: ssh_channel): Int
fun ssh_channel_request_exec(sshChannel: ssh_channel, command: String): Int
fun ssh_channel_read(sshChannel: ssh_channel, buffer: ByteArray, count: Int, isStderr: Int): Int
fun ssh_channel_poll(sshChannel: ssh_channel, isStderr: Int): Int
fun ssh_channel_write(sshChannel: ssh_channel, data: ByteArray, len: Int): Int
fun ssh_channel_close(sshChannel: ssh_channel): Int
fun ssh_channel_is_open(sshChannel: ssh_channel): Int
companion object {
val INSTANCE = Native.load("ssh", SSHLibrary::class.java) as SSHLibrary
}
}

View file

@ -1,20 +1,17 @@
package com.jetpackduba.gitnuro.ssh.libssh.streams package com.jetpackduba.gitnuro.ssh.libssh.streams
import com.jetpackduba.gitnuro.ssh.libssh.SSHLibrary import uniffi.gitnuro.Channel
import com.jetpackduba.gitnuro.ssh.libssh.ssh_channel
import java.io.InputStream import java.io.InputStream
class LibSshChannelInputErrStream(private val sshChannel: ssh_channel) : InputStream() { class LibSshChannelInputErrStream(private val sshChannel: Channel) : InputStream() {
private var cancelled = false private var cancelled = false
private val sshLib = SSHLibrary.INSTANCE
override fun read(): Int { override fun read(): Int {
val buffer = ByteArray(1) return if (sshChannel.pollHasBytes(true)) {
val read = sshChannel.read(true, 1u)
val byteArray = read.data
return if (sshLib.ssh_channel_poll(sshChannel, 1) > 0) { val first = byteArray.first()
sshLib.ssh_channel_read(sshChannel, buffer, 1, 1)
val first = buffer.first()
first.toInt() first.toInt()
} else } else

View file

@ -1,28 +1,26 @@
package com.jetpackduba.gitnuro.ssh.libssh.streams package com.jetpackduba.gitnuro.ssh.libssh.streams
import com.jetpackduba.gitnuro.ssh.libssh.SSHLibrary import uniffi.gitnuro.Channel
import com.jetpackduba.gitnuro.ssh.libssh.ssh_channel
import java.io.InputStream import java.io.InputStream
class LibSshChannelInputStream(private val sshChannel: ssh_channel) : InputStream() { class LibSshChannelInputStream(private val sshChannel: Channel) : InputStream() {
private val sshLib = SSHLibrary.INSTANCE
override fun read(b: ByteArray, off: Int, len: Int): Int { override fun read(b: ByteArray, off: Int, len: Int): Int {
val byteArray = ByteArray(len) val result = sshChannel.read(false, len.toULong())
val result = sshLib.ssh_channel_read(sshChannel, byteArray, len, 0) val byteArray = result.data
val read = result.readCount
for (i in 0 until len) { for (i in 0 until len) {
b[off + i] = byteArray[i] b[off + i] = byteArray[i]
} }
return result return read.toInt()
} }
override fun read(): Int { override fun read(): Int {
val buffer = ByteArray(1)
sshLib.ssh_channel_read(sshChannel, buffer, 1, 0) val result = sshChannel.read(false, 1u)
val first = buffer.first() val first = result.data.first()
return first.toInt() return first.toInt()
} }

View file

@ -1,26 +1,18 @@
package com.jetpackduba.gitnuro.ssh.libssh.streams package com.jetpackduba.gitnuro.ssh.libssh.streams
import com.jetpackduba.gitnuro.ssh.libssh.SSHLibrary import uniffi.gitnuro.Channel
import com.jetpackduba.gitnuro.ssh.libssh.ssh_channel
import java.io.OutputStream import java.io.OutputStream
class LibSshChannelOutputStream(private val sshChannel: ssh_channel) : OutputStream() { class LibSshChannelOutputStream(private val sshChannel: Channel) : OutputStream() {
private val sshLib = SSHLibrary.INSTANCE
override fun write(b: Int) { override fun write(b: Int) {
val byteArrayData = byteArrayOf(b.toByte()) val byteArrayData = byteArrayOf(b.toByte())
write(byteArrayData) write(byteArrayData)
} }
override fun write(b: ByteArray) { override fun write(b: ByteArray) {
sshLib.ssh_channel_write(sshChannel, b, b.size) sshChannel.writeBytes(b)
} }
override fun close() { override fun close() {
} }
} }
fun checkValidResult(tag: String, result: Int) {
if (result != 0)
throw Exception("$tag - Result is $result")
}