iOS通过SSH远程管理macOS服务器文件

以前写过一个iOS客户端播放视频的App,我在macOS搭建了web服务器实现目录浏览功能,把下载的视频资源放到web服务器的目录,这样iOS客户端可以通过webview获取到macOS服务端的视频资源的链接了,从而实现iOS上播放电脑上视频资源的功能。
但是用久了之后,发现我看完的电影资源还在那里,每次看视频去找新的没看过的就很麻烦。这样就想能不能写个程序,当我看完这个电影之后,直接删掉这个资源,或者把这个资源移动到其他收藏目录。通过这个想法,找了一些实现方法,感觉还是使用ssh去做比较快和方便。

本文ssh的功能主要是通过开源工具blink实现,窗口实现的逻辑大部分在TermController里面

  • 创建终端显示器
    1
    2
    3
    4
    5
    6
    7
    8
    - (void)createPTY
    {
    pipe(_pinput);
    _termout = fterm_open(_terminal, 0);
    _termerr = fterm_open(_terminal, 0);
    _termin = fdopen(_pinput[0], "r");
    _termsz = malloc(sizeof(struct winsize));
    }

_terminal是一个UIView,他做了两件事,一个是创建WKWebView用来渲染我们的输入和输出数据,一个是监听键盘事件,把监听到的数据回调给TermController实现数据流的写入。

  • 监听键盘事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // If the key is a special key, we do not apply modifiers.
    if (text.length > 1) {
    // Check if we have a function key
    NSRange range = [text rangeOfString:@"FKEY"];
    if (range.location != NSNotFound) {
    NSString *value = [text substringFromIndex:(range.length)];
    [_delegate write:[CC FKEY:[value integerValue]]];
    } else {
    [_delegate write:[CC KEY:text MOD:0 RAW:_raw]];
    }
    } else {
    NSUInteger modifiers = [[_smartKeys view] modifiers];
    if (modifiers & KbdCtrlModifier) {
    [_delegate write:[CC CTRL:text]];
    } else if (modifiers & KbdAltModifier) {
    [_delegate write:[CC ESC:text]];
    } else {
    [_delegate write:[CC KEY:text MOD:0 RAW:_raw]];
    }
    }
  • 实现写入数据

1
2
3
4
5
6
- (void)write:(NSString *)input
{
// Trasform the string and write it, with the correct sequence
const char *str = [input UTF8String];
write(_pinput[1], str, [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
}
  • 创建session会话
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - (void)startSession
    {
    // Until we are able to duplicate the streams, we have to recreate them.
    TermStream *stream = [[TermStream alloc] init];
    stream.in = _termin;
    stream.out = _termout;
    stream.err = _termerr;
    stream.control = self;
    stream.sz = _termsz;
    _session = [[MCPSession alloc] initWithStream:stream];
    _session.delegate = self;
    [_session executeWithArgs:@""];
    }

有了MCPSession就完成了整个会话的过程,如图

  • WKWebView渲染功能,是通过js加载方法渲染页面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Write data to terminal control
    - (void)write:(NSString *)data
    {
    [_delegate receiveData:data];
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@[ data ] options:0 error:nil];
    NSString *jsString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    NSString *jsScript = [NSString stringWithFormat:@"write_to_term(%@[0])", jsString];
    dispatch_async(dispatch_get_main_queue(), ^{
    [_webView evaluateJavaScript:jsScript completionHandler:nil];
    });
    }

实现ssh输入输出

blink的ssh功能是在MCPSession里面实现了调用SSHSession,他们都是继承了Session类。SSHSession里面主要是对libssh2的framework提供的api实现了一套ssh的封装,比如登录中密码的判断、公私钥的验证等

libssh2 是一个使用 C 语言编写的以实现 SSH2 协议的代码库。

SSH 协议的全称是 Secure Shell,顾名思义就是为操作系统提供一个安全的 Shell 使得用户在和远程主机进行交互时的数据不易被第三方窃取。除了这个基本的功能之外,它还包含了很多其他的功能,比如 SFTP,端口转发等等。

  • 实现ssh登录判断
    1
    - (void)ssh_login:(NSArray *)ids to:(struct sockaddr *)addr port:(int)port user:(const char *)user timeout:(int)timeout error:(NSError **)error

登录成功之后实现SSHSession的输入输出
- (int)ssh_client_loop方法里面有个循环一直在处理输入输出数据流的解析工作

  • stream获取流数据,libssh2_channel_read读取字符数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Wait for stream->in or socket while not ready for reading
    do {
    if (!pfds[0].events || pfds[0].revents & (POLLIN)) {
    // Read from socket
    do {
    rc = libssh2_channel_read(_channel, inputbuf, BUFSIZ);
    if (rc > 0) {
    fwrite(inputbuf, rc, 1, _stream.out);
    pfds[0].events = 0;
    }
  • stream获取流数据,libssh2_channel_write写入字符数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Input from stream
    if (pfds[1].revents & POLLIN) {
    towrite = fread(streambuf, 1, BUFSIZ, _stream.in);
    rc = 0;
    do {
    rc = libssh2_channel_write(_channel, streambuf + rc, towrite);
    if (rc > 0) {
    towrite -= rc;
    }

实现文件列表和删除

在连上ssh之后,就可以通过命令实现列表和删除,命令太麻烦,那就写个界面实现操作。

  • 创建FileCommandDelegate代理和FileCommandViewController
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @protocol FileCommandDelegate <NSObject>
    - (void)excuteCommand:(NSString *)command;
    @end
    @interface FileCommandViewController : UIViewController
    @property (weak) id<FileCommandDelegate> delegate;
    @end

TermController实现代理,执行命令

1
2
3
4
#pragma mark FileCommandDelegate
- (void)excuteCommand:(NSString *)command {
[self write:command];
}

连上ssh,代理出去,弹出文件管理页面

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)sshConnected:(BOOL)isConnected {
if (isConnected == false) {
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
FileCommandViewController *vc = [[FileCommandViewController alloc] init];
vc.delegate = self;
UINavigationController *navi = [[UINavigationController alloc] initWithRootViewController:vc];
[self presentViewController:navi animated:YES completion:nil];
});
}

  • FileCommandViewController实现数据输入输出的通知

    1
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveData:) name:@"kCommandReceived" object:nil];
  • 过滤获取的数据,实现列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    - (void)receiveData:(NSNotification * _Nonnull)note {
    // NSLog(@"%@", note.object);
    NSString *checkString = note.object;
    //1.创建正则表达式,[0-9]:表示‘0’到‘9’的字符的集合
    NSString *pattern = @"\\[1m\\[36m.+\\[";
    //1.1将正则表达式设置为OC规则
    NSRegularExpression *regular = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    //2.利用规则测试字符串获取匹配结果
    NSArray *results = [regular matchesInString:checkString options:0 range:NSMakeRange(0, checkString.length)];
    if (results.count > 0) {
    // NSLog(@"%@", results);
    NSMutableArray *reguslars = [NSMutableArray array];
    [results enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSString *subStr = [checkString substringWithRange:obj.range];
    NSString *checkString = subStr;
    //1.创建正则表达式,[0-9]:表示‘0’到‘9’的字符的集合
    NSString *pattern = @"\\[1m\\[36m";
    //1.1将正则表达式设置为OC规则
    NSRegularExpression *regular = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    //2.利用规则测试字符串获取匹配结果
    NSArray *results = [regular matchesInString:checkString options:0 range:NSMakeRange(0, checkString.length)];
    [results enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSString *subStr = [checkString substringWithRange:NSMakeRange(obj.range.location+obj.range.length, checkString.length-obj.range.length-obj.range.location)];
    NSString *checkString = subStr;
    //1.创建正则表达式,[0-9]:表示‘0’到‘9’的字符的集合
    NSString *pattern = @"(?=\\[).+";
    //1.1将正则表达式设置为OC规则
    NSRegularExpression *regular = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    //2.利用规则测试字符串获取匹配结果
    NSArray *results = [regular matchesInString:checkString options:0 range:NSMakeRange(0, checkString.length)];
    if (results.count > 0) {
    NSTextCheckingResult *res = results[0];
    NSString *sst = [checkString substringWithRange:NSMakeRange(0, res.range.location-1)];
    NSLog(@"%@", sst);
    [reguslars addObject:sst];
    }
    }];
    }];
    _datas = reguslars;
    }
    //1.创建正则表达式,[0-9]:表示‘0’到‘9’的字符的集合
    NSString *pattern1 = @"\\s\\w+(\\.\\w+)";
    //1.1将正则表达式设置为OC规则
    NSRegularExpression *regular1 = [[NSRegularExpression alloc] initWithPattern:pattern1 options:NSRegularExpressionCaseInsensitive error:nil];
    //2.利用规则测试字符串获取匹配结果
    NSArray *results1 = [regular1 matchesInString:checkString options:0 range:NSMakeRange(0, checkString.length)];
    if (results1.count > 0) {
    NSMutableArray *reguslars = [NSMutableArray array];
    [results1 enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSString *subStr = [[checkString substringWithRange:obj.range] stringByReplacingOccurrencesOfString:@" " withString:@""];
    [reguslars addObject:subStr];
    }];
    [reguslars addObjectsFromArray:_datas];
    _datas = reguslars;
    }
    if (_datas.count > 0) {
    NSLog(@"%@", checkString);
    dispatch_async(dispatch_get_main_queue(), ^{
    [_tableView reloadData];
    });
    }
    }
  • 点击刷新获取列表数据

    1
    2
    3
    - (void)refreshList {
    [_delegate excuteCommand:@"ls\n"];
    }

  • 左滑删除文件
    1
    2
    3
    4
    - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    [_delegate excuteCommand:[[NSString alloc] initWithFormat:@"rm %@\n", _datas[indexPath.row]]];
    }

  • 点击进入目录

这里判断是不是文件,如果是文件就直接return,不跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
NSString *checkString = _datas[indexPath.row];
//1.创建正则表达式,[0-9]:表示‘0’到‘9’的字符的集合
NSString *pattern = @"\\.\\w+";
//1.1将正则表达式设置为OC规则
NSRegularExpression *regular = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
//2.利用规则测试字符串获取匹配结果
NSArray *results = [regular matchesInString:checkString options:0 range:NSMakeRange(0, checkString.length)];
if (results.count > 0) {
return;
}
[_delegate excuteCommand:[[NSString alloc] initWithFormat:@"cd %@\n", checkString]];
FileCommandViewController *vc = [[FileCommandViewController alloc] init];
vc.delegate = _delegate;
[self.navigationController pushViewController:vc animated:true];
}

代码

现在功能主要实现了列表和删除,出现一个问题,就是中文字符ssh返回的都是???,待研究解决。

https://github.com/jackyshan/iOSTerminalFromBlinkSSHFileManage