SFTP客户端代码示例

参考链接:SFTP客户端代码示例

操作系统:Windows7/8,VS2013

环境:libssh2 1.4.3、zlib-1.2.8、openssl-1.0.1g

原文:

“从http://www.libssh2.org/下载libssh2-1.4.3.tar.gz文件,解压后打开libssh2.dsw文件升级项目到VisualStudio 2013,里面有两个项目,只要编译libssh2项目就可以了。编译前需要添加zlib和openssl的头文件和库文件链接位置,如果编译libssh2提示找不到msvcrt.lib,为链接库添加下面的路径

C:\Program Files (x86)\Microsoft VisualStudio 12.0\VC\lib

提示找不到ws2_32.lib或odbc32.lib,添加下面的链接路径

C:\Program Files (x86)\MicrosoftSDKs\Windows\v7.1A\Lib

编译通过后文件输出到\libssh2-1.4.3\win32\Release_lib路径下”

更新:

http://www.libssh2.org/网址访问不通,故换个方式下载了libssh2-1.4.3.tar.gz文件,下载地址:百度网盘(密码:2hwo

libssh2.dsw文件位置:\libssh2-1.4.3\libssh2-1.4.3\win32

zlib的头文件是zlib.h,库文件是:zlib.lib

openssl的头文件是:opensslconf.h,库文件是:libeay32.lib,ssleay32.lib

zlib的头文件(zlib.h)位置:\libssh2-1.4.3\zte\zlib-1.2.5,库文件(zlib.lib)位置:\libssh2-1.4.3\zte\openssl-1.0.0b staticlib\Debug

openssl头文件(opensslconf.h)位置:\libssh2-1.4.3\zte\openssl-1.0.1l\include\openssl,库文件(libeay32.lib,ssleay32.lib)位置:\libssh2-1.4.3\zte\openssl-1.0.0b staticlib\Debug

 分两个步骤:

1.先编译输出libssh2.lib

(1)配置好zlib和openssl的头文件和库文件链接位置后,编译libssh2项目即可,这时可以把tests项目卸载,反正用不到嘛;

编译成功后,输出libssh2.lib到\libssh2-1.4.3\win32\Release_lib路径下,(ps:我这里生成的是libssh2d.lib,我把文件名改为libssh2.lib后可以正常使用)

2.执行客户端,进行下载图片;

新建个项目,把下文三个文件拷贝到项目中,配置libssh2.h,libssh2.lib链接位置后,即可执行成功;

注意:路径及文件名不能含有中文,否则会报错;

下面是SFTP客户端示例代码:

三个文件的下载地址:百度网盘(密码:1qhf

main.cpp

 1 #include "SFTP_Libssh2.h"
 2 #include <iostream>
 3  
 4 int main(int argc, char* argv[])
 5 {
 6     //下面的代码只要在进程初始化的时候执行
 7     kagula::network::SFTP_Init();
 8  
 9     //测试SFTP链接
10     kagula::network::SFTP_Libssh2* client = kagula::network::SFTP_Libssh2::Inst();
11     std::string ip = "192.168.19.130";
12     uint16_t port = 22;
13     std::string usr = "kagula";
14     std::string pwd = "123456";
15     if (false == client->IsAbilityConn(ip, port, usr, pwd))
16     {
17         std::cout << client->strLastError << std::endl;
18         return -1;
19     }
20  
21     //测试文件上传,d:\\temp\\a.html
22     if (0 != client->upload(ip, 22, usr, pwd, "d:\\temp\\a.html", "/home/kagula/a.html"))
23     {
24         std::cout << "Error:" << client->strLastError << std::endl;
25     } else
26     {
27         std::cout << client->strLastError << std::endl;
28     }
29     
30  
31     //测试文件下载
32     if (0 != client->download(ip, 22, usr, pwd, "/home/kagula/a.html","d:\\temp\\b.html" ))
33     {
34         std::cout << "Error:" << client->strLastError << std::endl;
35     }
36     else
37     {
38         std::cout << client->strLastError << std::endl;
39     }
40  
41     //进程准备结束,释放资源的时候,运行下面的代码
42     kagula::network::SFTP_Exit();
43     return 0;
44 }

 

SFTP_Libssh2.h

 1 #pragma once
 2  
 3 #include <string>
 4 #include <atomic>
 5  
 6 /*
 7 功能:SFTP协议的文件传输功能
 8 最后更新日期:2014-5-17
 9 简介:借助Libssh2库很容易实现sftp,ssh2客户端,这里给出
10       如何实现Sftp客户端的代码
11 测试环境:Windows 8.1 64bit、Visual Studio 2013 Professional SP1
12        OpenSSL 1.0.1g、zlib-1.2.8、libssh2  1.4.3
13        Win32控制台项目
14 注意:动态链接需要把“libssh2.dll”文件复制到当前项目路径下
15 说明:原来的代码支持多线程,从应用程序抽出来的时候简化了,
16      你可以修改代码使它同时支持上传或下载多个文件。
17 建议:[1]第三方库直接下载源代码自己编译免得库由于编译器版本的
18      不同或设置的不同链接的时候一大堆麻烦。
19      [2]读懂代码根据项目需求作相应修改
20 补充阅读资料:
21 《使用libssh2库实现支持密码参数的ssh2客户端》
22 http://blog.chinaunix.net/uid-24382173-id-229823.html
23 */
24 namespace kagula {
25     namespace network {
26         int SFTP_Init();
27         void SFTP_Exit();
28  
29         class SFTP_BKCall
30         {
31         public:
32             /* progress返回值范围[0.0,1.0] */
33             virtual void OnProgress(float progress) = 0;
34         };
35  
36         class SFTP_Libssh2
37         {
38         public:
39             static SFTP_Libssh2* Inst()
40             {
41                 static SFTP_Libssh2 inst;
42  
43                 return &inst;
44             }
45  
46             /*
47             入口参数使用说明
48             ip:      就填一个IP地址就好了,例如“127.0.0.1”。
49             port: 端口,SFTP服务器默认端口为22。
50             username:
51             password:
52             sftppath: 远程路径“/”开头 ,例如“/a.jpg”
53             localpath: 本地路径,例如“d:\\temp\\test.jpg”
54             strLastError: 错误信息
55             出口参数
56             返回不等于零,代表失败!
57             */
58             int upload(std::string ip, unsigned short port, std::string username,
59                 std::string password, std::string localpath, std::string remotepath);
60             int download(std::string ip, unsigned short port, std::string username,
61                 std::string password, std::string sftppath, std::string localpath);
62  
63             //测试SFTP服务器是否可以链接
64             bool IsAbilityConn(std::string ip, unsigned short port, std::string username,
65                 std::string password);
66  
67             //设置回掉函数
68             void SetBKCall(SFTP_BKCall *bkCall)    { m_bkCall = bkCall; }
69  
70             //存放最近的错误信息
71             std::string   strLastError;
72  
73             //用于停止当前上传或下载线程
74             void stop() { m_isBreak.store(true); }
75         private:
76             SFTP_Libssh2() :m_bkCall(NULL) { m_isBreak.store(false); };//防止直接初始化
77             SFTP_Libssh2(const SFTP_Libssh2&);                 //防止拷贝复制
78             SFTP_Libssh2& operator=(const SFTP_Libssh2&);      //防止分配(运算符函数的调用)
79  
80             SFTP_BKCall  *m_bkCall;            
81             std::atomic_bool m_isBreak; //带读写保护的bool值
82         };
83     }
84 }

SFTP_Libssh2.cpp

  1 //SFTP_Libssh2.cpp文件清单
  2 #include "SFTP_Libssh2.h"
  3  
  4 #include <libssh2.h>
  5 #include <libssh2_sftp.h>
  6  
  7 #ifdef HAVE_WINSOCK2_H
  8 #include <winsock2.h>
  9 #endif
 10 #ifdef HAVE_SYS_SOCKET_H
 11 #include <sys/socket.h>
 12 #endif
 13 #ifdef HAVE_NETINET_IN_H
 14 #include <netinet/in.h>
 15 #endif
 16 #ifdef HAVE_UNISTD_H
 17 #include <unistd.h>
 18 #endif
 19 #ifdef HAVE_ARPA_INET_H
 20 #include <arpa/inet.h>
 21 #endif
 22 #ifdef HAVE_SYS_TIME_H
 23 #include <sys/time.h>
 24 #endif
 25  
 26 #include <sys/types.h>
 27 #include <fcntl.h>
 28 #include <errno.h>
 29 #include <stdio.h>
 30 #include <ctype.h>
 31  
 32 #include <sstream>
 33 #include <iomanip>
 34  
 35 #pragma comment(lib, "ws2_32.lib")
 36  
 37 #pragma comment(lib, "libeay32.lib")  
 38 #pragma comment(lib, "libssh2.lib")  
 39  
 40 namespace kagula {
 41     namespace network
 42     {
 43         //初始化进程的时候调用
 44         //如果非0表示初始化失败!
 45         int SFTP_Init()
 46         {
 47             WSADATA wsadata;
 48             int rc = WSAStartup(MAKEWORD(2, 0), &wsadata);
 49             if (rc != 0) {
 50                 return rc;
 51             }
 52  
 53             rc = libssh2_init(0);
 54  
 55             return rc;
 56         }
 57  
 58         //进程结束的时候调用
 59         void SFTP_Exit()
 60         {
 61             libssh2_exit();
 62  
 63             WSACleanup();
 64         }
 65  
 66         bool SFTP_Libssh2::IsAbilityConn(std::string ip, unsigned short port, std::string username,
 67             std::string password)
 68         {
 69             unsigned long hostaddr;
 70             struct sockaddr_in sin;
 71             const char *fingerprint;
 72             LIBSSH2_SESSION *session;
 73             int rc;
 74             bool bR = false;
 75             FILE *local;
 76             LIBSSH2_SFTP *sftp_session;
 77             LIBSSH2_SFTP_HANDLE *sftp_handle;
 78  
 79             hostaddr = inet_addr(ip.c_str());//hostaddr = htonl(0x7F000001);
 80  
 81  
 82             //新建连接
 83             int sock = socket(AF_INET, SOCK_STREAM, 0);
 84  
 85             sin.sin_family = AF_INET;
 86             sin.sin_port = htons(port);
 87             sin.sin_addr.s_addr = hostaddr;
 88             if (connect(sock, (struct sockaddr*)(&sin),
 89                 sizeof(struct sockaddr_in)) != 0) {
 90                 std::ostringstream ostr;
 91                 ostr << "[" << __FILE__ << "][" << __LINE__ << "]failed to connect" << ip << "!" << std::endl;
 92                 strLastError = ostr.str();
 93  
 94                 return bR;
 95             }
 96  
 97             //新建对话实例
 98             session = libssh2_session_init();
 99             if (!session)
100             {
101                 closesocket(sock);
102                 return bR;
103             }
104  
105             //设置调用阻塞
106             libssh2_session_set_blocking(session, 1);
107  
108             //进行握手
109             rc = libssh2_session_handshake(session, sock);
110             if (rc) {
111                 std::ostringstream ostr;
112                 ostr << "[" << __FILE__ << "][" << __LINE__ << "]Failure establishing SSH session: " << rc << std::endl;
113                 strLastError = ostr.str();
114  
115                 libssh2_session_free(session); closesocket(sock);
116                 return bR;
117             }
118  
119             //检查主机指纹
120             std::ostringstream ostr;
121             fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
122             ostr << "Fingerprint: ";
123             for (int i = 0; i < 20; i++) {
124                 unsigned char c = fingerprint[i];
125                 int nT = c;
126                 ostr << std::hex << std::setw(2) << std::setfill('0') << nT;
127             }
128             strLastError = ostr.str();
129  
130             //通过密码验证登陆用户身份
131             if (libssh2_userauth_password(session, username.c_str(), password.c_str())) {
132                 std::ostringstream ostr;
133                 ostr << "[" << __FILE__ << "][" << __LINE__ << "]Authentication by password failed." << std::endl;
134                 strLastError = ostr.str();
135                 goto shutdown;
136             }
137  
138             sftp_session = libssh2_sftp_init(session);
139  
140             if (!sftp_session) {
141                 std::ostringstream ostr;
142                 ostr << "[" << __FILE__ << "][" << __LINE__ << "]Unable to init SFTP session " << std::endl;
143                 strLastError = ostr.str();
144  
145                 goto shutdown;
146             }
147  
148             bR = true;
149  
150  
151             libssh2_sftp_shutdown(sftp_session);
152  
153         shutdown:
154             libssh2_session_disconnect(session,
155                 "Normal Shutdown, Thank you for playing");
156             libssh2_session_free(session);
157             closesocket(sock);
158             return bR;
159         }
160  
161         /*
162         源码参考地址
163         http://www.libssh2.org/examples/sftp_write.html
164         */
165         int SFTP_Libssh2::upload(std::string ip, unsigned short port, std::string username, std::string password,
166             std::string localpath, std::string remotepath)
167         {
168             if (ip.length()<1 || username.length()<1 || password.length()<1 || localpath.length()<1 || remotepath.length()<1)
169             {
170                 return -1;
171             }
172  
173             int nR = 0;
174             unsigned long hostaddr;
175             struct sockaddr_in sin;
176             const char *fingerprint;
177             LIBSSH2_SESSION *session;
178             int rc = -1;
179             FILE *local = NULL;
180             LIBSSH2_SFTP *sftp_session;
181             LIBSSH2_SFTP_HANDLE *sftp_handle;
182  
183             hostaddr = inet_addr(ip.c_str());//hostaddr = htonl(0x7F000001);
184  
185             if (fopen_s(&local, localpath.c_str(), "rb") != 0) {
186                 std::ostringstream ostr;
187                 ostr << "[" << __FILE__ << "][" << __LINE__ << "]Can't open local file " << localpath << std::endl;
188                 strLastError = ostr.str();
189  
190                 return -2;
191             }
192  
193             //取待上传文件整个size.
194             fseek(local, 0, SEEK_END);
195             size_t filesize = ftell(local);//local file大小,在readFromDisk中被引用
196             fseek(local, 0, SEEK_SET);//文件指针重置到文件头
197  
198             //新建连接
199             int sock = socket(AF_INET, SOCK_STREAM, 0);
200  
201             sin.sin_family = AF_INET;
202             sin.sin_port = htons(port);
203             sin.sin_addr.s_addr = hostaddr;
204             if (connect(sock, (struct sockaddr*)(&sin),
205                 sizeof(struct sockaddr_in)) != 0) {
206                 std::ostringstream ostr;
207                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] failed to connect " << ip << std::endl;
208                 strLastError = ostr.str();
209  
210                 fclose(local);
211                 return -3;
212             }
213  
214  
215             //创建会话实例
216             session = libssh2_session_init();
217             if (!session)
218             {
219                 fclose(local); closesocket(sock);
220                 return -4;
221             }
222  
223             //阻塞方式调用libssh2
224             libssh2_session_set_blocking(session, 1);
225  
226             //进行握手
227             rc = libssh2_session_handshake(session, sock);
228             if (rc) {
229                 std::ostringstream ostr;
230                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] Failure establishing SSH session:" << rc << std::endl;
231                 strLastError = ostr.str();
232  
233                 fclose(local); libssh2_session_free(session); closesocket(sock);
234                 return -5;
235             }
236  
237             //获取主机指纹
238             std::ostringstream ostr;
239             fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
240             ostr << "Fingerprint: ";
241             for (int i = 0; i < 20; i++) {
242                 unsigned char c = fingerprint[i];
243                 int nT = c;//这样转是为了防止符号位扩展
244                 ostr << std::hex << std::setw(2) << std::setfill('0') << nT;
245             }
246             strLastError = ostr.str();
247  
248             //通过密码验证
249             if (libssh2_userauth_password(session, username.c_str(), password.c_str())) {
250                 std::ostringstream ostr;
251                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] Authentication by password failed ["
252                     << username << "][" << password << "]" << rc << std::endl;
253                 strLastError = ostr.str();
254  
255                 goto shutdown;
256             }
257  
258             sftp_session = libssh2_sftp_init(session);
259  
260             if (!sftp_session) {
261                 std::ostringstream ostr;
262                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] Unable to init SFTP session" << std::endl;
263                 strLastError = ostr.str();
264  
265                 goto shutdown;
266             }
267  
268             //向SFTP服务器发出新建文件请求
269             sftp_handle =
270                 libssh2_sftp_open(sftp_session, remotepath.c_str(),
271                 LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC,
272                 LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR |
273                 LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IROTH);
274  
275             if (!sftp_handle) {
276                 std::ostringstream ostr;
277                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] Unable to open file with SFTP.  ip=" 
278                     << ip <<"] remotepath=[" << remotepath << "]" << std::endl;
279                 strLastError = ostr.str();
280  
281                 nR = -1;
282  
283                 goto shutdown;
284             }
285  
286  
287             char mem[1024 * 16];
288             size_t nread;
289             char *ptr;
290             size_t count = 0;
291  
292             do {
293                 nread = fread(mem, 1, sizeof(mem), local);
294                 if (nread <= 0) {
295                     //到达文件尾部
296                     break;
297                 }
298                 ptr = mem;
299                 do {
300                     // 向服务器写数据,直到数据写完毕
301                     rc = libssh2_sftp_write(sftp_handle, ptr, nread);
302                     if (rc < 0)
303                         break;
304                     ptr += rc; count += nread;
305                     nread -= rc;
306  
307                     //如果设置了回调,进行回调
308                     if (m_bkCall)
309                     {
310                         float p = count / (float)filesize;
311                         m_bkCall->OnProgress(p);
312                     }
313                     //callback.end
314                 } while (nread);
315  
316                 if ( m_isBreak.load() == true )
317                 {
318                     std::ostringstream ostr;
319                     ostr << "[" << __FILE__ << "][" << __LINE__ << "] 上传文件任务被用户break!" << std::endl;
320                     strLastError = ostr.str();
321  
322                     nR = -6;
323                     break;
324                 }
325             } while (rc > 0);
326  
327             libssh2_sftp_close(sftp_handle);
328             libssh2_sftp_shutdown(sftp_session);
329  
330         shutdown:
331             libssh2_session_disconnect(session,
332                 "Normal Shutdown, Thank you for playing");
333             libssh2_session_free(session);
334  
335             closesocket(sock);
336  
337             fclose(local);
338  
339             return nR;//返回“0”表示成功
340         }
341  
342         /*
343         源码参考地址
344         http://www.oschina.net/code/snippet_12_10717
345         */
346         int SFTP_Libssh2::download(std::string ip, unsigned short port, std::string username, std::string password,
347             std::string sftppath, std::string localpath)
348         {
349             unsigned long hostaddr;
350             int sock, i, auth_pw = 0;
351             struct sockaddr_in sin;
352             const char *fingerprint;
353             char *userauthlist;
354             LIBSSH2_SESSION *session;
355             int rc;
356             LIBSSH2_SFTP *sftp_session;
357             LIBSSH2_SFTP_HANDLE *sftp_handle;
358  
359             hostaddr = inet_addr(ip.c_str()); //hostaddr = htonl(0x7F000001);
360  
361             /*
362             * The application code is responsible for creating the socket
363             * and establishing the connection
364             */
365             sock = socket(AF_INET, SOCK_STREAM, 0);
366  
367             sin.sin_family = AF_INET;
368             sin.sin_port = htons(port);
369             sin.sin_addr.s_addr = hostaddr;
370             if (connect(sock, (struct sockaddr*)(&sin),
371                 sizeof(struct sockaddr_in)) != 0) {
372                 std::ostringstream ostr;
373                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] 连接失败!" << std::endl;
374                 strLastError = ostr.str();
375                 return -1;
376             }
377  
378             /* Create a session instance
379             */
380             session = libssh2_session_init();
381  
382             if (!session)
383                 return -1;
384  
385             /* Since we have set non-blocking, tell libssh2 we are blocking */
386             libssh2_session_set_blocking(session, 1);
387  
388  
389             /* ... start it up. This will trade welcome banners, exchange keys,
390             * and setup crypto, compression, and MAC layers
391             */
392             rc = libssh2_session_handshake(session, sock);
393  
394             if (rc) {
395                 std::ostringstream ostr;
396                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] 建立SSH会话失败" << rc << std::endl;
397                 strLastError = ostr.str();
398  
399                 return -1;
400             }
401  
402             /* At this point we havn't yet authenticated.  The first thing to do
403             * is check the hostkey's fingerprint against our known hosts Your app
404             * may have it hard coded, may go to a file, may present it to the
405             * user, that's your call
406             */
407             fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
408  
409             std::ostringstream ostr;
410             fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
411             ostr << "Fingerprint: ";
412             for (int i = 0; i < 20; i++) {
413                 unsigned char c = fingerprint[i];
414                 int nT = c;
415                 ostr << std::hex << std::setw(2) << std::setfill('0') << nT;
416             }
417             strLastError = ostr.str();
418  
419             /* check what authentication methods are available */
420             userauthlist = libssh2_userauth_list(session, username.c_str(), username.length());
421             if (strstr(userauthlist, "password") == NULL)
422             {
423                 std::ostringstream ostr;
424                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] 服务器不支持输入password方式验证!" << std::endl;
425                 strLastError = ostr.str();
426                 goto shutdown;
427             }
428  
429             /* We could authenticate via password */
430             if (libssh2_userauth_password(session, username.c_str(), password.c_str())) {
431  
432                 std::ostringstream ostr;
433                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] 密码错误!" << std::endl;
434                 strLastError = ostr.str();
435                 goto shutdown;
436             }
437  
438             sftp_session = libssh2_sftp_init(session);
439             if (!sftp_session) {
440                 std::ostringstream ostr;
441                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] 初始化FTL对话失败!" << std::endl;
442                 strLastError = ostr.str();
443                 goto shutdown;
444             }
445  
446             /* Request a file via SFTP */
447             sftp_handle =
448                 libssh2_sftp_open(sftp_session, sftppath.c_str(), LIBSSH2_FXF_READ, 0);
449  
450  
451             if (!sftp_handle) {
452                 std::ostringstream ostr;
453                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] 打开文件失败! " << libssh2_sftp_last_error(sftp_session) << std::endl;
454                 strLastError = ostr.str();
455  
456                 goto shutdown;
457             }
458  
459             FILE *stream;
460             if (fopen_s(&stream, localpath.c_str(), "wb") == 0)
461             {
462                 do {
463                     char mem[1024];
464  
465                     /* loop until we fail */
466                     rc = libssh2_sftp_read(sftp_handle, mem, sizeof(mem));
467  
468                     if (rc > 0) {
469                         //从内存到磁盘
470                         fwrite(mem, 1, rc, stream);
471                     }
472                     else {
473                         break;
474                     }
475                 } while (1);
476  
477                 fclose(stream);
478             }
479             else {
480                 std::ostringstream ostr;
481                 ostr << "[" << __FILE__ << "][" << __LINE__ << "] 新建本地文件失败 " << localpath << std::endl;
482                 strLastError = ostr.str();
483             }
484  
485             libssh2_sftp_close(sftp_handle);
486  
487             libssh2_sftp_shutdown(sftp_session);
488  
489         shutdown:
490  
491             libssh2_session_disconnect(session, "Normal Shutdown, Thank you for playing");
492             libssh2_session_free(session);
493  
494             closesocket(sock);//INVALID_SOCKET
495  
496             return 0;
497         }
498     }
499 }

 根据上述环境编译成功的libssh2.lib,csdn下载地址:https://download.csdn.net/download/zkfopen/10606098

posted on 2018-08-15 16:59  zkfopen  阅读(3200)  评论(0编辑  收藏  举报

导航