
SwiftNIO SSH è un'implementazione programmatica di SSH che utilizza SwiftNIO
Questo progetto contiene il supporto SSH utilizzando SwiftNIO.
SwiftNIO SSH è un'implementazione programmatica di SSH: una raccolta di API che permettono ai programmatori di implementare endpoint che parlano SSH. Ciò significa che è più simile a libssh2 che a openssh. SwiftNIO SSH non fornisce client e server SSH pronti per la produzione, ma offre invece i mattoni per costruire questo tipo di client e server.
Ci sono diverse ragioni per fornire un'implementazione programmatica di SSH. Una è che SSH ha una relazione unica con l'interattività dell'utente. Gli utenti tecnici sono abituati a interagire con SSH in modo interattivo, sia per eseguire comandi su macchine remote sia per avviare shell interattive. Avere la possibilità di rispondere a queste richieste in modo programmatico permette modalità di interazione alternative interessanti. Come esempi precedenti, possiamo citare Twisted's Manhole, che utilizza un'implementazione programmatica di SSH chiamata conch per fornire un interprete Python interattivo all'interno di un server Python in esecuzione, o ssh-chat, un server SSH che fornisce una chat room invece della normale funzionalità di shell. Si possono immaginare anche usi innovativi per il forwarding TCP.
Un'altra buona ragione per fornire SSH programmatico è che non è raro che i servizi debbano interagire con altri servizi eseguendo comandi. Mentre Process risolve questo problema per il caso locale, a volte i comandi da invocare sono remoti. Sebbene Process possa avviare un client ssh come sottoprocesso per eseguire questa invocazione, può essere molto più semplice invocare SSH direttamente. Questo è il caso d'uso principale di libssh2. SwiftNIO SSH fornisce l'equivalente del livello di rete e crittografico di libssh2, permettendo agli utenti motivati di gestire sessioni SSH direttamente da servizi Swift.
Le versioni più recenti di SwiftNIO SSH supportano Swift 5.9 e successive. La versione minima di Swift supportata dalle release di SwiftNIO SSH è dettagliata di seguito:
SwiftNIO SSH supporta SSHv2 con le seguenti funzionalità:
SwiftNIO SSH fornisce un ChannelHandler di SwiftNIO, NIOSSHHandler. Questo handler implementa la maggior parte del protocollo SSH direttamente. Gli utenti non devono generare messaggi SSH direttamente: interagiscono invece con NIOSSHHandler tramite canali figli e delegati.
SSH è un protocollo multiplexato: ogni connessione SSH è suddivisa in più canali di comunicazione bidirezionale chiamati, appunto, canali. SwiftNIO SSH riflette questa struttura utilizzando un'astrazione di "canale figlio". Quando un peer crea un nuovo canale SSH, SwiftNIO SSH creerà un nuovo Channel NIO utilizzato per rappresentare tutto il traffico su quel canale SSH. All'interno di questo Channel figlio, tutti gli eventi sono strettamente ordinati tra loro; tuttavia, gli eventi di diversi Channel possono essere interallacciati liberamente dall'implementazione.
Una connessione SSH attiva si presenta quindi come segue:
┌ ─ NIO Channel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌────────────────────────────────┐ │
│ │
│ │ │ │
│ │
│ │ │ │
│ NIOSSHHandler │───────────────────────┐
│ │ │ │ │
│ │ │
│ │ │ │ │
│ │ │
│ └────────────────────────────────┘ │ │
│
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│
│
│
│
▼
┌── SSH Child Channel ─────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ├───┐
│ │ │ │ │ │ │
│ │ │ │ │ │ ├───┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ User Handler │ │ User Handler │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────┘ └────────────────────────────────┘ │ │ │
│ │ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
Un canale SSH viene invocato con un tipo di canale. NIOSSH ne supporta tre: session, directTCPIP e forwardedTCPIP. Il tipo di canale più comune è session: session viene utilizzato per rappresentare l'invocazione di un programma, sia esso un programma specifico o una shell. Gli altri due tipi di canale sono relativi al forwarding delle porte TCP e verranno discussi in seguito.
Un canale SSH opera su un singolo tipo di dato: SSHChannelData. Questa struttura incapsula il fatto che SSH supporta dati sia regolari che "estesi". I dati regolari del canale (SSHChannelData.DataType.channel) sono utilizzati per la stragrande maggioranza dei dati principali. Nei canali session il tipo .channel viene utilizzato per l'input standard e l'output standard: il tipo .stdErr viene utilizzato per l'errore standard (naturalmente). Nei canali di forwarding TCP, il tipo .channel è l'unico utilizzato e rappresenta i dati inoltrati.
Un canale session rappresenta l'invocazione di un comando. Il modo esatto in cui il canale opera viene comunicato tramite una serie di eventi utente in ingresso. I seguenti eventi sono importanti:
SSHChannelRequestEvent.PseudoTerminalRequest: Richiede l'allocazione di un pseudo-terminale.SSHChannelRequestEvent.EnvironmentRequest: Richiede una singola variabile d'ambiente per l'invocazione del comando. Viene sempre inviato prima del comando stesso.SSHChannelRequestEvent.ShellRequest: Richiede che il comando da invocare sia la shell dell'utente autenticato.SSHChannelRequestEvent.ExecRequest: Richiede l'invocazione di un comando specifico.SSHChannelRequestEvent.ExitStatus: Utilizzato per segnalare che il comando remoto è terminato e comunica il codice di uscita.SSHChannelRequestEvent.ExitSignal: Utilizzato per indicare che il comando remoto è stato terminato in risposta a un segnale e quale segnale è stato.SSHChannelRequestEvent.SignalRequest: Utilizzato per inviare un segnale al comando remoto.SSHChannelRequestEvent.LocalFlowControlRequest: Utilizzato per indicare se il client è in grado di eseguire il controllo di flusso Ctrl-Q/Ctrl-S autonomamente.SSHChannelRequestEvent.WindowChangeRequest: Utilizzato per comunicare una modifica delle dimensioni della finestra del terminale sul client al pseudo-terminale allocato.Questi eventi non vengono utilizzati nei messaggi di forwarding delle porte. Le implementazioni SSH che supportano canali di tipo .session devono essere pronte a gestire la maggior parte o tutti questi eventi in vari modi.
Ognuno di questi eventi ha anche un campo wantReply. Indica se la richiesta necessita di una risposta per indicare successo o fallimento. Se sì, vengono utilizzati i seguenti due eventi:
ChannelSuccessEvent, per comunicare il successo.ChannelFailureEvent, per comunicare il fallimento.Il protocollo di rete SSH utilizza pervasivamente la mezza chiusura nei canali figli. I canali NIO hanno tipicamente il supporto per la mezza chiusura disabilitato per impostazione predefinita, e SwiftNIO SSH rispetta questa impostazione predefinita anche nei suoi canali figli. Tuttavia, se si lascia questa impostazione al suo valore predefinito, i canali figli SSH si comporteranno in modo estremamente inaspettato. Per questo motivo, si raccomanda vivamente di abilitare il supporto per la mezza chiusura in tutti i canali figli:
channel.setOption(ChannelOptions.allowRemoteHalfClosure, true)
Questo utilizza quindi il normale supporto per la mezza chiusura di NIO. L'invio di EOF da parte del peer remoto verrà comunicato con un evento utente in ingresso, ChannelEvent.inputClosed. Per inviare EOF da soli, chiamare close(mode: .output).
L'autenticazione utente è una parte vitale di SSH. Per gestirla, SwiftNIO SSH utilizza due protocolli delegati: NIOSSHClientUserAuthenticationDelegate e NIOSSHServerUserAuthenticationDelegate. Client e server devono fornire implementazioni di questi protocolli delegati per gestire l'autenticazione utente.
Il protocollo client è semplice: SwiftNIO SSH invocherà il metodo nextAuthenticationType(availableMethods:nextChallengePromise:) sul delegato. availableMethods sarà un'istanza di NIOSSHAvailableUserAuthenticationMethods che comunica quali metodi di autenticazione il server ha suggerito come accettabili. Il delegato può quindi completare nextChallengePromise con una nuova richiesta di autenticazione o con nil per indicare che il client ha esaurito le possibilità.
Il protocollo server è più complesso. Il delegato deve fornire una proprietà supportedAuthenticationMethods che comunica quali metodi di autenticazione sono supportati dal delegato. Quindi, ogni volta che il client invia una richiesta di autenticazione utente, verrà invocato il metodo requestReceived(request:responsePromise:). Questo può essere invocato più volte in parallelo, poiché i client possono emettere richieste di autenticazione in parallelo. Il responsePromise deve essere soddisfatto con il risultato dell'autenticazione. Ci sono tre risultati: .success e .failure sono semplici, ma in linea di principio il server può richiedere più challenge utilizzando .partialSuccess(remainingMethods:).
Il forwarding diretto delle porte è il forwarding dal client al server. In questa modalità, tradizionalmente il client ascolta su una porta locale e inoltra le connessioni in entrata al server. Chiede al server di inoltrare queste connessioni come connessioni in uscita verso un host e una porta specifici.
Questi canali possono essere aperti direttamente dai client utilizzando il tipo di canale .directTCPIP.
Il forwarding remoto delle porte è una situazione meno comune in cui il client chiede al server di ascoltare su un indirizzo e una porta specifici e di inoltrare tutte le connessioni in entrata al client. Poiché il client deve richiedere questo comportamento, lo fa utilizzando richieste globali.
Le richieste globali vengono avviate utilizzando NIOSSHHandler.sendGlobalRequest e vengono ricevute e gestite tramite un GlobalRequestDelegate. Oggi sono supportate due richieste globali:
GlobalRequest.TCPForwardingRequest.listen(host:port:): una richiesta per far ascoltare il server su un determinato host e porta.GlobalRequest.TCPForwardingRequest.cancel(host:port:): una richiesta per annullare l'ascolto sul determinato host e porta.I server possono essere notificati e rispondere a queste richieste utilizzando un GlobalRequestDelegate. Il metodo da implementare qui è tcpForwardingRequest(_:handler:promise:). Questo metodo delegato verrà invocato ogni volta che viene ricevuta una richiesta globale. La risposta alla richiesta viene passata in promise.
I canali inoltrati vengono poi inviati dal server al client utilizzando il tipo di canale .forwardedTCPIP.
| SwiftNIO SSH | Versione minima di Swift |
|---|
0.0.0 ..< 0.3.0 | 5.1 |
0.3.0 ..< 0.4.0 | 5.2 |
0.4.0 ..< 0.5.0 | 5.4 |
0.5.0 ..< 0.6.2 | 5.5.2 |
0.6.2 ..< 0.9.0 | 5.6 |
0.9.0 ..< 0.9.2 | 5.8 |
0.9.2 ..< 0.10.0 | 5.9 |
0.10.0 ... 0.12.0 | 5.10 |
0.12.0 ..< 0.13.0 | 6.0 |
0.13.0 ..< | 6.1 |
SSHChannelRequestEvent.SubsystemRequest: Utilizzato per richiedere l'invocazione di un sottosistema specifico. Il significato di questo è specifico per i singoli casi d'uso.