SFTP în C#/.NET folosind SSH.NET
September 30th, 2011
Una din sarcinile pe care le-am avut de îndeplinit recent a presupus, printre altele, și gestiunea unor fișiere aflate pe alte mașini (ex: listarea directoarelor, descărcarea fișierelor necesare, procesarea lor, încărcarea lor pe o altă masină etc.). Pentru ce aveam nevoie, o soluție bazată pe protocolul SFTP era deci cea mai indicată, iar singura soluție open-source pentru platforma .NET pe care am putut-o găsi a fost SSH.NET (SharpSSH nu funcționa corespunzător și nici nu mai este dezvoltată de ceva vreme, iar o soluție platită nu își justifica în nici un fel costurile în cazul de față).
SSH.NET este un proiect complet nou, foarte bine scris care, din câte observ, primește din ce în ce mai multă atenție (paradoxal însă, am dat destul de greu de el). Fiind însă destul de slab documentat (pe undeva e și normal să fie așa, focusul fiind momentan pe dezvoltare) m-am gândit că ar fi bine sa ilustrez într-o serie de două articole modalitatea de utilizare a acestei librării pentru a lucra cu SFTP, respectiv SSH în C#/.NET.
Conectarea
Întâi de toate, trebuie să aflați ce modalități de conectare suportă server-ul la care vă conectați. Odată ce aflați aceste detalii, veți configura o instanță a uneia dintre clasele:
- PrivateKeyConnectionInfo – pentru autentificarea folosind o pereche de chei;
- PasswordConnectionInfo – pentru autentificarea folosind un nume de utilizator și o parolă;
- KeyboardInteractiveConnectionInfo – pentru a folosi modalitatea de autentificare interactivă (care, în particular, poate funcționa asemănător modalității de autentificare cu parolă).
pentru a vă putea autentifica.
În exemplul de mai jos am folosit modalitatea de autentificare interactivă:
private void ConnectToServer()
{
_kbConnectionInfo = new KeyboardInteractiveConnectionInfo("127.0.0.1", 22, "root");
_kbConnectionInfo.AuthenticationBanner += new EventHandler(OnAuthenticationBanner);
_kbConnectionInfo.AuthenticationPrompt += new EventHandler(OnAuthenticationPrompt);
_sftpClient = new SftpClient(_kbConnectionInfo);
_sftpClient.Connect();
}
private void OnAuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
{
foreach (AuthenticationPrompt p in e.Prompts)
p.Response = "root";
}
private void OnAuthenticationBanner(object sender, AuthenticationBannerEventArgs e)
{
Console.WriteLine(e.BannerMessage);
}
Se observă că, pe lângă simpla furnizare a adresei server-ului, a portului si a numelui de utilizator, atașăm handlere pentru două evenimente:
- AuthenticationBanner – care este declanșat atunci când (și dacă) banner-ul de autentificare este trimis dinspre server. Acest eveniment este disponibil indiferent de ce modalitate de autentificare veți folosi și nu este neapărat necesar să înregistrați un handler pentru el;
- AuthenticationPrompt – este declanșat atunci când server-ul trimite prompt-urile de autentificare. Acest eveniment este specific modalității de autentificare interactivă și este obligatoriu să înregistrați un handler pentru el, în vederea finalizării cu succes a autentificării. În cazul de față, este vorba de un server care pur și simplu se așteptă să îi trimitem o parolă validă, așă că nu avem altceva de făcut decât să scriem parola în răspunsul prompt-ului conținut în colecția e.Prompts. Pentru scenarii mai avansate însă, puteți inspecta și proprietatea AuthenticationPrompt.Request pentru a lua deciziile potrivite.
Odată creat și configurat obiectul ce reprezintă modalitatea de autentificare, acesta va fi dat ca parametru la crearea unei noi instanțe de SftpClient, clasa care oferă metodele de lucru cu fișierele si directoarele server-ului prin intermediul SFTP.
Lucrul cu fișierele și directoarele
Cum scopul principal al unui client SFTP îl reprezintă manipularea fișierelor și directoarelor, să vedem ce ne oferă SSH.NET în acest sens.
Cea mai simplă operație posibilă este aflarea directorului curent:
Console.WriteLine(_sftpClient.WorkingDirectory);
Pentru a lista conținutul unui director vom folosi metoda SftpClient.ListDirectory, ce returnează o colecție de obiecte de tip SftpFile:
_entries = _sftpClient.ListDirectory("/path/or/dir");
foreach (SftpFile _entry in _entries)
Console.WriteLine(_entry.Name);
Schimbarea directorului curent se realizează foarte simplu, folosind metoda SftpClient.ChangeDirectory:
_sftpClient.ChangeDirectory("/path/to/dir");
Pentru a copia un fișier de pe server se folosește metoda SftpClient.DownloadFile, care ia ca parametri calea fișierului ce va fi descărcat de pe server și un System.IO.Stream în care va fi scris fișierul:
_fs = new FileStream("/to", FileMode.OpenOrCreate, FileAccess.Write);
_sftpClient.DownloadFile("/from", _fs);
Pentru a încărca un fișier pe server vom proceda asemănător, folsind metoda SftpClient.UploadFile, care ia ca parametri un System.IO.Stream din care va fi citit conținutul fișierului și calea de pe server unde va fi stocat:
_fs = new FileStream("/from", FileMode.Open, FileAccess.Read);
_sftpClient.UploadFile(_fs, "/to");
De remarcat că aceste două metode, UploadFile și DownloadFile nu limitează scenariile de utilizare la încărcarea dintr-un fișier, respectiv descărcarea într-un fișier, programatorul putând opta pentru folosirea și altor tipuri de stream-uri, cum ar fi un System.IO.MemoryStream.
Dar dacă dorim să ștergem un fișier sau un director? În acest caz vom folosi metodele SftpClient.DeleteFile, respectiv SftpClient.DeleteDirectory:
_sftpClient.DeleteFile("/path/to/delete");
O metodă la fel de utilă este și SftpClient.Exists care returnează true în cazul existenței căii către fișierul sau directorul dat ca parametru.
Tratarea erorilor
Cum lucrurile mai merg și prost uneori, aceste metode pot genera excepții, în funcție de cazurile limită întâlnite. Astfel:
- o excepție de tip SftpPermissionDeniedException este generată atunci când operația nu este permisă pentru utilizatorul curent;
- o excepție de tip SftpPathNotFoundException este generată atunci când calea asupra căreia se efectuează operația nu a fost găsită pe server;
- o excepție de tip SshException când orice altceva merge prost în comunicația dintre server și client.
Astfel, o operație efectuată pe server nu se reduce la un simplu apel al metodei corespunzătoare, fiind necesară și o tratare a erorilor. Spre exemplu, operația de ștergere a unui fișier ar putea arăta așa:
try
{
_sftpClient.DeleteFile("/at/path");
}
catch (SftpPathNotFoundException)
{
Console.WriteLine("Path not found");
}
catch (SftpPermissionDeniedException)
{
Console.WriteLine("Permission denied");
}
catch (Exception)
{
Console.WriteLine("File could not be deleted");
}
Operații asincrone
Toate operațiile descrise până acum au fost sincrone, fapt ce ne poate pune în dificultate dacă avem de-a face cu operații care necesită mult timp pentru a fi terminate (ex: transferul unui fișier mare, latența mare a rețelei etc.). Soluțiile la această problemă sunt fie desfășurarea operațiilor explicit într-un thread separat de thread-ul în care rulează interfața, fie utilizarea facilităților de operații asincrone oferite de clasa SftpClient. Pentru exemplificare am ales operația de încărcare a unui fișier:
_fs = new FileStream("/from", FileMode.Open, FileAccess.Read);
_sftpClient.BeginUploadFile(_fs, "/to", AsyncCopyCallback, _fs);
unde AsyncCopyCallback este definită astfel:
private void AsyncCopyCallback(IAsyncResult result)
{
FileStream _uploadedStream = null;
try
{
_sftpClient.EndUploadFile(result);
}
catch (SftpPathNotFoundException)
{
Console.WriteLine("Path not found");
}
catch (SftpPermissionDeniedException)
{
Console.WriteLine("Permission denied");
}
catch (Exception)
{
Console.WriteLine("Could not copy file");
}
finally
{
_uploadedStream = result.AsyncState as FileStream;
if (_uploadedStream != null)
_uploadedStream.Dispose();
}
}
Vă sunt dator cu câteva explicații:
- orice excepție apărută în timp ce fișierul este încărcat, va fi generată odată cu apelul metodei EndUploadFile (acest lucru este de fapt general valabil pentru orice metodă invocată asincron în .NET, nu numai în cazul particular al librăriei SSH.NET care reproduce acest comportament);
- toate operațiile primesc drept parametri suplimentari un callback ce va fi executat când se finalizează operația (putem seta acest parametru la valoarea null daca nu suntem interesați de a primi această notificare) și un obiect ce poate conține informații suplimentare și care se regăsește în IAsyncResult.AsyncState (și acesta poate fi, și de cele mai multe ori chiar este, setat la valoarea null). În cazul de față, cel din urmă parametru este folosit pentru a transmit stream-ul din care se citește, pentru a-l putea închide la finalul operației (evident, puteam să ținem o referință către stream într-o variabilă privată, însă poate nu ne dorim întotdeauna acest lucru).
La final, atașez un mic proiect care exemplifică utilizarea clasei SftpClient. Proiectul conține o aplicație ce rulează în consolă și oferă câteva funcționalități de bază pentru interacționarea cu un server SFTP, a cărui adresă o puteți configura în app.config. Numele de utilizator și parola folosite la autentificare pot fi deasemenea configurate în app.config.
.net, c#, SFTP, SSH, SSH.NET 299 afisari

