Files
ShhShell/Carthage/Checkouts/NMSSH/NMSSH/NMSSHChannel.m

686 lines
22 KiB
Objective-C

#import "NMSSHChannel.h"
#import "NMSSH+Protected.h"
@interface NMSSHChannel ()
@property (nonatomic, strong) NMSSHSession *session;
@property (nonatomic, assign) LIBSSH2_CHANNEL *channel;
@property (nonatomic, readwrite) NMSSHChannelType type;
@property (nonatomic, assign) const char *ptyTerminalName;
@property (nonatomic, strong) NSString *lastResponse;
#if OS_OBJECT_USE_OBJC
@property (nonatomic, strong) dispatch_source_t source;
#else
@property (nonatomic, assign) dispatch_source_t source;
#endif
@end
@implementation NMSSHChannel
// -----------------------------------------------------------------------------
#pragma mark - INITIALIZER
// -----------------------------------------------------------------------------
- (instancetype)initWithSession:(NMSSHSession *)session {
if ((self = [super init])) {
[self setSession:session];
[self setBufferSize:kNMSSHBufferSize];
[self setRequestPty:NO];
[self setPtyTerminalType:NMSSHChannelPtyTerminalVanilla];
[self setType:NMSSHChannelTypeClosed];
// Make sure we were provided a valid session
if (![self.session isKindOfClass:[NMSSHSession class]]) {
@throw @"You have to provide a valid NMSSHSession!";
}
}
return self;
}
- (BOOL)openChannel:(NSError *__autoreleasing *)error {
if (self.channel != NULL) {
NMSSHLogWarn(@"The channel will be closed before continue");
if (self.type == NMSSHChannelTypeShell) {
[self closeShell];
}
else {
[self closeChannel];
}
}
// Set blocking mode
libssh2_session_set_blocking(self.session.rawSession, 1);
// Open up the channel
LIBSSH2_CHANNEL *channel = libssh2_channel_open_session(self.session.rawSession);
if (channel == NULL){
NMSSHLogError(@"Unable to open a session");
if (error) {
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelAllocationError
userInfo:@{ NSLocalizedDescriptionKey : @"Channel allocation error" }];
}
return NO;
}
[self setChannel:channel];
// Try to set environment variables
if (self.environmentVariables) {
for (NSString *key in self.environmentVariables) {
if ([key isKindOfClass:[NSString class]] && [[self.environmentVariables objectForKey:key] isKindOfClass:[NSString class]]) {
libssh2_channel_setenv(self.channel, [key UTF8String], [[self.environmentVariables objectForKey:key] UTF8String]);
}
}
}
int rc = 0;
// If requested, try to allocate a pty
if (self.requestPty) {
rc = libssh2_channel_request_pty(self.channel, self.ptyTerminalName);
if (rc != 0) {
if (error) {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error requesting %s pty: %@", self.ptyTerminalName, [[self.session lastError] localizedDescription]] };
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelRequestPtyError
userInfo:userInfo];
}
NMSSHLogError(@"Error requesting pseudo terminal");
[self closeChannel];
return NO;
}
}
return YES;
}
- (void)closeChannel {
// Set blocking mode
if (self.session.rawSession) {
libssh2_session_set_blocking(self.session.rawSession, 1);
}
if (self.channel) {
int rc;
rc = libssh2_channel_close(self.channel);
if (rc == 0) {
libssh2_channel_wait_closed(self.channel);
}
libssh2_channel_free(self.channel);
[self setType:NMSSHChannelTypeClosed];
[self setChannel:NULL];
}
}
- (BOOL)sendEOF {
int rc;
// Send EOF to host
rc = libssh2_channel_send_eof(self.channel);
NMSSHLogVerbose(@"Sent EOF to host (return code = %i)", rc);
return rc == 0;
}
- (void)waitEOF {
if (libssh2_channel_eof(self.channel) == 0) {
// Wait for host acknowledge
int rc = libssh2_channel_wait_eof(self.channel);
NMSSHLogVerbose(@"Received host acknowledge for EOF (return code = %i)", rc);
}
}
// -----------------------------------------------------------------------------
#pragma mark - SHELL COMMAND EXECUTION
// -----------------------------------------------------------------------------
- (const char *)ptyTerminalName {
switch (self.ptyTerminalType) {
case NMSSHChannelPtyTerminalVanilla:
return "vanilla";
case NMSSHChannelPtyTerminalVT100:
return "vt100";
case NMSSHChannelPtyTerminalVT102:
return "vt102";
case NMSSHChannelPtyTerminalVT220:
return "vt220";
case NMSSHChannelPtyTerminalAnsi:
return "ansi";
case NMSSHChannelPtyTerminalXterm:
return "xterm";
}
// catch invalid values
return "vanilla";
}
- (NSString *)execute:(NSString *)command error:(NSError *__autoreleasing *)error {
return [self execute:command error:error timeout:@0];
}
- (NSString *)execute:(NSString *)command error:(NSError *__autoreleasing *)error timeout:(NSNumber *)timeout {
NMSSHLogInfo(@"Exec command %@", command);
// In case of error...
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:command forKey:@"command"];
if (![self openChannel:error]) {
return nil;
}
[self setLastResponse:nil];
int rc = 0;
[self setType:NMSSHChannelTypeExec];
// Try executing command
rc = libssh2_channel_exec(self.channel, [command UTF8String]);
if (rc != 0) {
if (error) {
[userInfo setObject:[[self.session lastError] localizedDescription] forKey:NSLocalizedDescriptionKey];
[userInfo setObject:[NSString stringWithFormat:@"%i", rc] forKey:NSLocalizedFailureReasonErrorKey];
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelExecutionError
userInfo:userInfo];
}
NMSSHLogError(@"Error executing command");
[self closeChannel];
return nil;
}
// Set non-blocking mode
libssh2_session_set_blocking(self.session.rawSession, 0);
// Set the timeout for blocking session
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent() + [timeout doubleValue];
// Fetch response from output buffer
NSMutableString *response = [[NSMutableString alloc] init];
for (;;) {
ssize_t rc;
char buffer[self.bufferSize];
char errorBuffer[self.bufferSize];
do {
rc = libssh2_channel_read(self.channel, buffer, (ssize_t)sizeof(buffer));
if (rc > 0) {
[response appendFormat:@"%@", [[NSString alloc] initWithBytes:buffer length:rc encoding:NSUTF8StringEncoding]];
}
// Store all errors that might occur
if (libssh2_channel_get_exit_status(self.channel)) {
if (error) {
ssize_t erc = libssh2_channel_read_stderr(self.channel, errorBuffer, (ssize_t)sizeof(errorBuffer));
NSString *desc = [[NSString alloc] initWithBytes:errorBuffer length:erc encoding:NSUTF8StringEncoding];
if (!desc) {
desc = @"An unspecified error occurred";
}
[userInfo setObject:desc forKey:NSLocalizedDescriptionKey];
[userInfo setObject:[NSString stringWithFormat:@"%zi", erc] forKey:NSLocalizedFailureReasonErrorKey];
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelExecutionError
userInfo:userInfo];
}
}
if (libssh2_channel_eof(self.channel) == 1 || rc == 0) {
while ((rc = libssh2_channel_read(self.channel, buffer, (ssize_t)sizeof(buffer))) > 0) {
[response appendFormat:@"%@", [[NSString alloc] initWithBytes:buffer length:rc encoding:NSUTF8StringEncoding] ];
}
[self setLastResponse:[response copy]];
[self closeChannel];
return self.lastResponse;
}
// Check if the connection timed out
if ([timeout longValue] > 0 && time < CFAbsoluteTimeGetCurrent()) {
if (error) {
NSString *desc = @"Connection timed out";
[userInfo setObject:desc forKey:NSLocalizedDescriptionKey];
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelExecutionTimeout
userInfo:userInfo];
}
while ((rc = libssh2_channel_read(self.channel, buffer, (ssize_t)sizeof(buffer))) > 0) {
[response appendFormat:@"%@", [[NSString alloc] initWithBytes:buffer length:rc encoding:NSUTF8StringEncoding] ];
}
[self setLastResponse:[response copy]];
[self closeChannel];
return self.lastResponse;
}
} while (rc > 0);
if (rc != LIBSSH2_ERROR_EAGAIN) {
break;
}
waitsocket(CFSocketGetNative([self.session socket]), self.session.rawSession);
}
// If we've got this far, it means fetching execution response failed
if (error) {
[userInfo setObject:[[self.session lastError] localizedDescription] forKey:NSLocalizedDescriptionKey];
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelExecutionResponseError
userInfo:userInfo];
}
NMSSHLogError(@"Error fetching response from command");
[self closeChannel];
return nil;
}
// -----------------------------------------------------------------------------
#pragma mark - REMOTE SHELL SESSION
// -----------------------------------------------------------------------------
- (BOOL)startShell:(NSError *__autoreleasing *)error {
NMSSHLogInfo(@"Starting shell");
if (![self openChannel:error]) {
return NO;
}
// Set non-blocking mode
libssh2_session_set_blocking(self.session.rawSession, 0);
// Fetch response from output buffer
#if !(OS_OBJECT_USE_OBJC)
if (self.source) {
dispatch_release(self.source);
}
#endif
[self setLastResponse:nil];
[self setSource:dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, CFSocketGetNative([self.session socket]),
0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0))];
dispatch_source_set_event_handler(self.source, ^{
NMSSHLogVerbose(@"Data available on the socket!");
ssize_t rc, erc=0;
char buffer[self.bufferSize];
while (self.channel != NULL) {
rc = libssh2_channel_read(self.channel, buffer, (ssize_t)sizeof(buffer));
erc = libssh2_channel_read_stderr(self.channel, buffer, (ssize_t)sizeof(buffer));
if (!(rc >=0 || erc >= 0)) {
NMSSHLogVerbose(@"Return code of response %ld, error %ld", (long)rc, (long)erc);
if (rc == LIBSSH2_ERROR_SOCKET_RECV || erc == LIBSSH2_ERROR_SOCKET_RECV) {
NMSSHLogVerbose(@"Error received, closing channel...");
[self closeShell];
}
return;
}
else if (rc > 0) {
NSData *data = [[NSData alloc] initWithBytes:buffer length:rc];
NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[self setLastResponse:[response copy]];
if (response && self.delegate && [self.delegate respondsToSelector:@selector(channel:didReadData:)]) {
[self.delegate channel:self didReadData:self.lastResponse];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(channel:didReadRawData:)]) {
[self.delegate channel:self didReadRawData:data];
}
}
else if (erc > 0) {
NSData *data = [[NSData alloc] initWithBytes:buffer length:erc];
NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (response && self.delegate && [self.delegate respondsToSelector:@selector(channel:didReadError:)]) {
[self.delegate channel:self didReadError:response];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(channel:didReadRawError:)]) {
[self.delegate channel:self didReadRawError:data];
}
}
else if (libssh2_channel_eof(self.channel) == 1) {
NMSSHLogVerbose(@"Host EOF received, closing channel...");
[self closeShell];
return;
}
}
});
dispatch_source_set_cancel_handler(self.source, ^{
NMSSHLogVerbose(@"Shell source cancelled");
if (self.delegate && [self.delegate respondsToSelector:@selector(channelShellDidClose:)]) {
[self.delegate channelShellDidClose:self];
}
});
dispatch_resume(self.source);
int rc = 0;
// Try opening the shell
while ((rc = libssh2_channel_shell(self.channel)) == LIBSSH2_ERROR_EAGAIN) {
waitsocket(CFSocketGetNative([self.session socket]), [self.session rawSession]);
}
if (rc != 0) {
NMSSHLogError(@"Shell request error");
if (error) {
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelRequestShellError
userInfo:@{ NSLocalizedDescriptionKey : [[self.session lastError] localizedDescription] }];
}
[self closeShell];
return NO;
}
NMSSHLogVerbose(@"Shell allocated");
[self setType:NMSSHChannelTypeShell];
return YES;
}
- (void)closeShell {
if (self.source) {
dispatch_source_cancel(self.source);
#if !(OS_OBJECT_USE_OBJC)
dispatch_release(self.source);
#endif
[self setSource: nil];
}
if (self.type == NMSSHChannelTypeShell) {
// Set blocking mode
libssh2_session_set_blocking(self.session.rawSession, 1);
[self sendEOF];
}
[self closeChannel];
}
- (BOOL)write:(NSString *)command error:(NSError *__autoreleasing *)error {
return [self write:command error:error timeout:@0];
}
- (BOOL)write:(NSString *)command error:(NSError *__autoreleasing *)error timeout:(NSNumber *)timeout {
return [self writeData:[command dataUsingEncoding:NSUTF8StringEncoding] error:error timeout:timeout];
}
- (BOOL)writeData:(NSData *)data error:(NSError *__autoreleasing *)error {
return [self writeData:data error:error timeout:@0];
}
- (BOOL)writeData:(NSData *)data error:(NSError *__autoreleasing *)error timeout:(NSNumber *)timeout {
if (self.type != NMSSHChannelTypeShell) {
NMSSHLogError(@"Shell required");
return NO;
}
ssize_t rc;
// Set the timeout
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent() + [timeout doubleValue];
// Try writing on shell
while ((rc = libssh2_channel_write(self.channel, [data bytes], [data length])) == LIBSSH2_ERROR_EAGAIN) {
// Check if the connection timed out
if ([timeout longValue] > 0 && time < CFAbsoluteTimeGetCurrent()) {
if (error) {
NSString *description = @"Connection timed out";
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelExecutionTimeout
userInfo:@{ NSLocalizedDescriptionKey : description }];
}
return NO;
}
waitsocket(CFSocketGetNative([self.session socket]), self.session.rawSession);
}
if (rc < 0) {
NMSSHLogError(@"Error writing on the shell");
if (error) {
NSString *command = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelWriteError
userInfo:@{ NSLocalizedDescriptionKey : [[self.session lastError] localizedDescription],
@"command" : command }];
}
}
return YES;
}
- (BOOL)requestSizeWidth:(NSUInteger)width height:(NSUInteger)height {
int rc = libssh2_channel_request_pty_size(self.channel, (int)width, (int)height);
if (rc) {
NMSSHLogError(@"Request size failed with error %i", rc);
}
return rc == 0;
}
// -----------------------------------------------------------------------------
#pragma mark - SCP FILE TRANSFER
// -----------------------------------------------------------------------------
- (BOOL)uploadFile:(NSString *)localPath to:(NSString *)remotePath {
return [self uploadFile:localPath to:remotePath progress:NULL];
}
- (BOOL)uploadFile:(NSString *)localPath to:(NSString *)remotePath progress:(BOOL (^)(NSUInteger))progress {
if (self.channel != NULL) {
NMSSHLogWarn(@"The channel will be closed before continue");
if (self.type == NMSSHChannelTypeShell) {
[self closeShell];
}
else {
[self closeChannel];
}
}
localPath = [localPath stringByExpandingTildeInPath];
// Inherit file name if to: contains a directory
if ([remotePath hasSuffix:@"/"]) {
remotePath = [remotePath stringByAppendingString:
[[localPath componentsSeparatedByString:@"/"] lastObject]];
}
// Read local file
FILE *local = fopen([localPath UTF8String], "rb");
if (!local) {
NMSSHLogError(@"Can't read local file");
return NO;
}
// Set blocking mode
libssh2_session_set_blocking(self.session.rawSession, 1);
// Try to send a file via SCP.
struct stat fileinfo;
stat([localPath UTF8String], &fileinfo);
LIBSSH2_CHANNEL *channel = libssh2_scp_send64(self.session.rawSession, [remotePath UTF8String], fileinfo.st_mode & 0644,
(unsigned long)fileinfo.st_size, 0, 0);;
if (channel == NULL) {
NMSSHLogError(@"Unable to open SCP session");
fclose(local);
return NO;
}
[self setChannel:channel];
[self setType:NMSSHChannelTypeSCP];
// Wait for file transfer to finish
char mem[self.bufferSize];
size_t nread;
char *ptr;
long rc;
NSUInteger total = 0;
BOOL abort = NO;
while (!abort && (nread = fread(mem, 1, sizeof(mem), local)) > 0) {
ptr = mem;
do {
// Write the same data over and over, until error or completion
rc = libssh2_channel_write(self.channel, ptr, nread);
if (rc < 0) {
NMSSHLogError(@"Failed writing file");
[self closeChannel];
return NO;
}
else {
// rc indicates how many bytes were written this time
total += rc;
if (progress && !progress(total)) {
abort = YES;
break;
}
ptr += rc;
nread -= rc;
}
} while (nread);
};
fclose(local);
if ([self sendEOF]) {
[self waitEOF];
}
[self closeChannel];
return !abort;
}
- (BOOL)downloadFile:(NSString *)remotePath to:(NSString *)localPath {
return [self downloadFile:remotePath to:localPath progress:NULL];
}
- (BOOL)downloadFile:(NSString *)remotePath to:(NSString *)localPath progress:(BOOL (^)(NSUInteger, NSUInteger))progress {
if (self.channel != NULL) {
NMSSHLogWarn(@"The channel will be closed before continue");
if (self.type == NMSSHChannelTypeShell) {
[self closeShell];
}
else {
[self closeChannel];
}
}
localPath = [localPath stringByExpandingTildeInPath];
// Inherit file name if to: contains a directory
if ([localPath hasSuffix:@"/"]) {
localPath = [localPath stringByAppendingString:[[remotePath componentsSeparatedByString:@"/"] lastObject]];
}
// Set blocking mode
libssh2_session_set_blocking(self.session.rawSession, 1);
// Request a file via SCP
struct stat fileinfo;
LIBSSH2_CHANNEL *channel = libssh2_scp_recv(self.session.rawSession, [remotePath UTF8String], &fileinfo);
if (channel == NULL) {
NMSSHLogError(@"Unable to open SCP session");
return NO;
}
[self setChannel:channel];
[self setType:NMSSHChannelTypeSCP];
if ([[NSFileManager defaultManager] fileExistsAtPath:localPath]) {
NMSSHLogInfo(@"A file already exists at %@, it will be overwritten", localPath);
[[NSFileManager defaultManager] removeItemAtPath:localPath error:nil];
}
// Open local file in order to write to it
int localFile = open([localPath UTF8String], O_WRONLY|O_CREAT, 0644);
// Save data to local file
off_t got = 0;
while (got < fileinfo.st_size) {
char mem[self.bufferSize];
size_t amount = sizeof(mem);
if ((fileinfo.st_size - got) < amount) {
amount = (size_t)(fileinfo.st_size - got);
}
ssize_t rc = libssh2_channel_read(self.channel, mem, amount);
if (rc > 0) {
size_t n = write(localFile, mem, rc);
if (n < rc) {
NMSSHLogError(@"Failed to write to local file");
close(localFile);
[self closeChannel];
return NO;
}
got += rc;
if (progress && !progress((NSUInteger)got, (NSUInteger)fileinfo.st_size)) {
close(localFile);
[self closeChannel];
return NO;
}
}
else if (rc < 0) {
NMSSHLogError(@"Failed to read SCP data");
close(localFile);
[self closeChannel];
return NO;
}
memset(mem, 0x0, sizeof(mem));
}
close(localFile);
[self closeChannel];
return YES;
}
@end