Skip to content

Commit

Permalink
Adding beginning error handling, updating documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
joevandeventer committed Sep 4, 2017
1 parent 98e7795 commit 38fad3b
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 34 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@

SSHTunnel is a framework for development tools to allow secure remote access to your server resources. Frequently a tool for development, such as a MySQL client, might need access to a service that isn't accessible to the outside world for security reasons. If you're creating such an app, integrating SSHTunnel allows the user to create a secure tunnel to their server, accessing it as though they're logged in locally. You simply give it the address of the server, the port you'd like to connect to, and your SSH authentication data, and SSHTunnel will return a port number. Tell your app to connect to that port on `localhost`, and presto! You have an encrypted tunnel to your service.

A typical use case looks like:

```
var tunnel = SSHTunnel(toHostname: "dev.nuptunes.com", port: 6379, username: "fritter")
tunnel.delegate = self
tunnel.connect()
```

The delegate will then be called to interact with the server for session negotiation - particularly authentication.

This code is obviously very early, and any bug fixes/gaping security holes/code improvements are appreciated.

#### Known bugs/planned fixes

- Error handling
- Account for non-fatal error handling (fixes issues with authentication)
- Add separate call/response for fingerprint checking to allow for user interaction
- Fix inevitable memory leaks that I missed
- Get rid of `select()`, fix multithreaded connection code
- Refactor more C code into C `struct` extensions
Expand Down
66 changes: 41 additions & 25 deletions SSHTunnel/SSHTunnel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ internal typealias SSH2Channel = OpaquePointer
public enum SSHTunnelError: Error {
case HostnameLookupError
case SSH2ConnectionError
case SSH2FingerprintError
case SSH2AuthenticationError
case SSH2ListenError
case SSH2RemoteDisconnectError
}

public class SSHTunnel: SSHTunnelProtocol {
Expand Down Expand Up @@ -50,13 +53,17 @@ public class SSHTunnel: SSHTunnelProtocol {
connectOperations.isSuspended = true

self.queue.async {
self.bindSocketToServer()
self.openSSHConnection()
self.beginSSHAuthentication()
do {
try self.bindSocketToServer()
try self.openSSHConnection()
try self.beginSSHAuthentication()
} catch {
self.disconnect(withError: error)
}
}
}

private func bindSocketToServer() {
private func bindSocketToServer() throws {
// First, create a pointer to an empty addrinfo struct to hold the results linked list
var servInfoPtr:UnsafeMutablePointer<addrinfo>? = UnsafeMutablePointer<addrinfo>.allocate(capacity: 1)

Expand All @@ -73,21 +80,20 @@ public class SSHTunnel: SSHTunnelProtocol {

// Now, perform the hostname lookup
if getaddrinfo(self.hostname, String(self.sshPort), &hints, &servInfoPtr) != 0 {
// Throw error. Hmmm.
self.disconnect()
self.disconnect(withError: SSHTunnelError.HostnameLookupError)
return
}

guard let firstAddr = servInfoPtr else {
self.disconnect()
self.disconnect(withError: SSHTunnelError.HostnameLookupError)
return
}

// Now, iterate through the linked list to find valid addresses
for addr in sequence(first: firstAddr, next: {$0.pointee.ai_next}) {
let address = addr.pointee
let sock:Socket = socket(address.ai_family, address.ai_socktype, address.ai_protocol)
if sock < 1 {
if sock < 0 {
continue
}

Expand All @@ -99,50 +105,50 @@ public class SSHTunnel: SSHTunnelProtocol {
return
}

self.disconnect()
self.disconnect(withError: SSHTunnelError.HostnameLookupError)
return
}

private func openSSHConnection() {
private func openSSHConnection() throws {
self.sshSession = libssh2_session_init_ex(nil, nil, nil, nil)
guard
let session = self.sshSession,
let socket = self.sshHostSocket,
let delegate = self.delegate
else {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ConnectionError)
return
}

if libssh2_session_handshake(session, socket) != 0 {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ConnectionError)
return
}

guard let fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1) else {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ConnectionError)
return
}

DispatchQueue.main.async {
if delegate.sshTunnel(self, returnedFingerprint: String(cString: fingerprint)) == false {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2FingerprintError)
return
}
}
}

private func beginSSHAuthentication() {
private func beginSSHAuthentication() throws {
guard
let session = self.sshSession,
let delegate = self.delegate
else {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ConnectionError)
return
}

guard let userAuthList = libssh2_userauth_list(session, self.username, UInt32(self.username.characters.count)) else {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2AuthenticationError)
return
}

Expand All @@ -162,7 +168,7 @@ public class SSHTunnel: SSHTunnelProtocol {

public func sendAuthenticationData(_ authenticationData: AuthenticationData) {
guard let session = self.sshSession else {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ConnectionError)
return
}
self.queue.async {
Expand All @@ -185,7 +191,7 @@ public class SSHTunnel: SSHTunnelProtocol {
}

if result != 0 {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ConnectionError)
return
}

Expand All @@ -195,7 +201,7 @@ public class SSHTunnel: SSHTunnelProtocol {

private func startListeningLocally() {
guard let session = self.sshSession else {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ConnectionError)
return
}
// Since this is just a local connection, hardcoding to IPv4 is fine.
Expand All @@ -206,8 +212,8 @@ public class SSHTunnel: SSHTunnelProtocol {
address.sin_port = in_port_t(0.bigEndian)
inet_aton("127.0.0.1", &address.sin_addr)
let sockfd:Socket = socket(PF_INET, SOCK_STREAM, 0)
if sockfd < 1 {
self.disconnect()
if sockfd < 0 {
self.disconnect(withError: SSHTunnelError.SSH2ListenError)
return
}

Expand All @@ -232,20 +238,21 @@ public class SSHTunnel: SSHTunnelProtocol {
$0.withMemoryRebound(to: sockaddr_in.self, capacity: 1) {
let port = Int($0.pointee.sin_port.bigEndian)
self.localPort = port
self.isConnected = true
DispatchQueue.main.async {
self.delegate?.sshTunnel(self, beganListeningOn: port)
}
}
}

// This loop will just run till we die, blocking on accept
ACCEPT_LOOP: while true {
while true {
var connectedAddrInfo = sockaddr.zeroed()
var addrInfoSize:socklen_t = socklen_t(MemoryLayout<sockaddr>.size)
let requestDescriptor = accept(sockfd, &connectedAddrInfo, &addrInfoSize)

if requestDescriptor == -1 {
self.disconnect()
self.disconnect(withError: SSHTunnelError.SSH2ListenError)
break
}

Expand All @@ -254,13 +261,14 @@ public class SSHTunnel: SSHTunnelProtocol {
}
}

public func disconnect() {
public func disconnect(withError error: Error? = nil) {
for oper in self.connections.operations {
oper.cancel()
}

if let sshSession = self.sshSession {
libssh2_session_disconnect_ex(sshSession, SSH_DISCONNECT_BY_APPLICATION, "", nil)
libssh2_session_free(sshSession)
}

if let hostSocket = self.sshHostSocket {
Expand All @@ -271,7 +279,15 @@ public class SSHTunnel: SSHTunnelProtocol {
close(listenSocket)
}

libssh2_exit()

self.isConnected = false

if let myError = error, let delegate = self.delegate {
DispatchQueue.main.async {
delegate.sshTunnel(self, didFailWithError: myError)
}
}
}

deinit {
Expand Down
52 changes: 45 additions & 7 deletions SSHTunnel/SSHTunnelDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,56 @@

import Foundation

/**
An SSHTunnelDelegate-compatible object interacts with an SSHTunnel object to handle the process of authenticating and
maintaining a connection with an SSH server.
*/
public protocol SSHTunnelDelegate: class {
// Determine if the host is who it says it is. Application is responsible for maintaining a database of
// previously seen hostkeys - SOP is to approve if the fingerprints match, prompt the user to save if the
// host has never been seen before, and present a warning STRONGLY urging the user not to connect if the
// two fingerprints don't match.
//
// Returning false will stop the connection process.
/**
When negotiating the SSH connection, the server returns a fingerprint hash based on the hostname used to connect
and the given username. The delegate should then check an existing list of hash to determine whether the host's
fingerprint is already in the list (it won't be if it's a new connection), and if so, whether the given hash
matches the one already in the list. The delegate should return `true` if the fingerprint matches, and `false`
if it doesn't.


- parameter sshTunnel: The SSHTunnelProtocol-compliant caller.
- parameter fingerprintData: The fingerprint hash returned by the SSH server.
- returns: Bool declaring whether fingerprint is safe and authentication should continue.
*/

// FIXME: An unknown hash needs to be presented to the user & approved. Break this out into a separate call/response.

func sshTunnel(_ sshTunnel: SSHTunnelProtocol, returnedFingerprint fingerprintData: String) -> Bool

/**
During authentication, the SSHTunnel object will notify the delegate that authentication data is needed, and
provide a list of compatible `AuthenticationMethods`. If necessary, the delegate can then prompt the user before
calling `sendAuthenticationData` to send the user's credentials.


- parameter sshTunnel: The SSHTunnelProtocol-compilant caller.
- parameter methods: The AuthenticationMethods reported by the server as being supported.
*/

func sshTunnel(_ sshTunnel: SSHTunnelProtocol, requestsAuthentication methods: [AuthenticationMethods])

/**
After a connection is successfully established, the delegate will be notified and given a port on localhost
where communication can take place.


- parameter sshTunnel: The SSHTunnelProtocol-compilant caller.
- parameter port: The port on localhost where the app can connect.
*/
func sshTunnel(_ sshTunnel: SSHTunnelProtocol, beganListeningOn port: Int)

func sshTunnel(_ sshTunnel: SSHTunnelProtocol, didFailWithError: Error)
/**
If the connection experiences a fatal error, the delegate will be notified.


- parameter sshTunnel: The SSHTunnelProtocol-compliant caller.
- parameter error: The cause of the disconnect
*/
func sshTunnel(_ sshTunnel: SSHTunnelProtocol, didFailWithError error: Error)
}
23 changes: 22 additions & 1 deletion SSHTunnel/SSHTunnelProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,29 @@ import Foundation

public typealias SSHTunnelConnectCallback = (_ success: Bool, _ error: Error) -> ()

/// Protocol for an SSHTunnel-compatible object.
public protocol SSHTunnelProtocol {
/// Tell SSHTunnel object to begin attempting to connect. Requires host, port, and delegate to be set.
func connect()

/**
Attempt to send a user's authentication data to SSH server. Requires a prior call to `requestsAuthentication`.

- Parameters:
- An `AuthenticationData` enum supported by the SSH server.
*/

// FIXME: Invalid `AuthenticationData` will currently cause a full disconnect from the server.

func sendAuthenticationData(_ authenticationData: AuthenticationData)
func disconnect()

/**
Disconnect all SSH channels and tear down the server connection. If called with an error parameter,
`didFailWithError` will be sent to the delegate. Can also be called intentionally without an error
object, in which case, the delegate isn't notified.

- Parameters:
- An optional `Error` object explaining why the disconnect occurred.
*/
func disconnect(withError error: Error?)
}

0 comments on commit 38fad3b

Please sign in to comment.