├── Sources └── NSRemoteShell │ ├── include │ └── include.h │ ├── Constructor.h │ ├── TSEventLoop.h │ ├── NSRemoteChannelSocketPair.h │ ├── GenericNetworking.h │ ├── NSRemoteFile.h │ ├── NSRemoteForward.h │ ├── NSLocalForward.h │ ├── Constructor.m │ ├── NSRemoteChannel.h │ ├── NSRemoteChannelSocketPair.m │ ├── TSEventLoop.m │ ├── NSRemoteFile.m │ ├── GenericHeaders.h │ ├── NSRemoteShell.h │ ├── NSRemoteForward.m │ ├── NSLocalForward.m │ ├── GenericNetworking.m │ ├── NSRemoteChannel.m │ └── NSRemoteShell.m ├── .gitmodules ├── Package.swift ├── Package.resolved ├── LICENSE ├── .gitignore └── README.md /Sources/NSRemoteShell/include/include.h: -------------------------------------------------------------------------------- 1 | #import "../NSRemoteShell.h" 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "External/CSSH"] 2 | path = External/CSSH 3 | url = https://github.com/Lakr233/CSSH 4 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/Constructor.h: -------------------------------------------------------------------------------- 1 | // 2 | // Constructor.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/5. 6 | // 7 | 8 | #ifndef Constructor_h 9 | #define Constructor_h 10 | 11 | /// return 1 if success 12 | int libssh2_init_check(void); 13 | 14 | #endif /* Constructor_h */ 15 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/TSEventLoop.h: -------------------------------------------------------------------------------- 1 | // 2 | // TSEventLoop.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/5. 6 | // 7 | 8 | #import 9 | 10 | #import "GenericHeaders.h" 11 | #import "NSRemoteShell.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface TSEventLoop : NSObject 16 | 17 | - (instancetype)initWithParent:(__weak NSRemoteShell*)parent; 18 | - (void)explicitRequestHandle; 19 | - (void)destroyLoop; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "NSRemoteShell", 7 | products: [ 8 | .library( 9 | name: "NSRemoteShell", 10 | targets: ["NSRemoteShell"] 11 | ), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/Lakr233/libssh2-spm", from: "1.0.0"), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "NSRemoteShell", 19 | dependencies: [ 20 | .product(name: "CSSH2", package: "libssh2-spm"), 21 | ] 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CSSH2", 6 | "repositoryURL": "https://github.com/Lakr233/libssh2-spm", 7 | "state": { 8 | "branch": null, 9 | "revision": "239cb276c2322335eaadc86b8119f74602215424", 10 | "version": "1.11.0" 11 | } 12 | }, 13 | { 14 | "package": "OpenSSL", 15 | "repositoryURL": "https://github.com/Lakr233/openssl-spm.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "327471fc025dea2adf7cd4e80257e76cf7e95851", 19 | "version": "3.2.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteChannelSocketPair.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteChannelSocketPair.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/9. 6 | // 7 | 8 | #import "GenericHeaders.h" 9 | #import "GenericNetworking.h" 10 | #import "NSRemoteShell.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface NSRemoteChannelSocketPair : NSObject 15 | 16 | @property (nonatomic, readwrite, assign) int socket; 17 | @property (nonatomic, readwrite, nullable, assign) LIBSSH2_CHANNEL *channel; 18 | @property (nonatomic, readwrite, assign) BOOL completed; 19 | 20 | - (instancetype)initWithSocket:(int)socket 21 | withChannel:(LIBSSH2_CHANNEL*)channel; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/GenericNetworking.h: -------------------------------------------------------------------------------- 1 | // 2 | // GenericNetworking.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/5. 6 | // 7 | 8 | #import 9 | 10 | #import "GenericHeaders.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface GenericNetworking : NSObject 15 | 16 | + (NSArray *)resolveIpAddressesFor:(NSString*)candidateHost; 17 | 18 | + (BOOL)isValidateWithPort:(NSNumber*)port; 19 | 20 | + (int)createSocketNonblockingListenerWithLocalPort:(NSNumber*)localPort; 21 | 22 | + (int)createSocketWithTargetHost:(NSString*)targetHost 23 | withTargetPort:(NSNumber*)targetPort 24 | requireNonblockingIO:(BOOL)useNonblocking 25 | ; 26 | 27 | + (void)destroyNativeSocket:(int)socketDescriptor; 28 | 29 | + (NSString*)getResolvedIpAddressWith:(int)socket; 30 | 31 | @end 32 | 33 | NS_ASSUME_NONNULL_END 34 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteFile.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteFile.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/14. 6 | // 7 | 8 | #ifndef NSRemoteFile_h 9 | #define NSRemoteFile_h 10 | 11 | #import "GenericHeaders.h" 12 | 13 | @interface NSRemoteFile : NSObject 14 | 15 | @property (nonatomic, readonly, nonnull, strong) NSString *name; 16 | @property (nonatomic, readonly, nullable, strong) NSNumber *size; 17 | @property (nonatomic, readonly, assign) BOOL isRegularFile; 18 | @property (nonatomic, readonly, assign) BOOL isDirectory; 19 | @property (nonatomic, readonly, assign) BOOL isLink; 20 | @property (nonatomic, readonly, nullable, strong) NSDate *modificationDate; 21 | @property (nonatomic, readonly, nullable, strong) NSDate *lastAccess; 22 | @property (nonatomic, readonly, assign) unsigned long ownerUID; 23 | @property (nonatomic, readonly, assign) unsigned long ownerGID; 24 | @property (nonatomic, readonly, nonnull, strong) NSString *permissionDescription; 25 | 26 | - (nonnull instancetype)initWithFilename:(nonnull NSString *)filename; 27 | - (void)populateAttributes:(LIBSSH2_SFTP_ATTRIBUTES)fileAttributes; 28 | 29 | @end 30 | 31 | #endif /* NSRemoteFile_h */ 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License - Lakr's Edition 2 | 3 | Copyright (c) 2022 Lakr Aream 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. 19 | 20 | The above copyright notice and this permission notice is NOT REQUIRED but 21 | encouraged in copies or substantial portions of the Software. 22 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteForward.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteForward.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/9. 6 | // 7 | 8 | #import "GenericHeaders.h" 9 | #import "GenericNetworking.h" 10 | #import "NSRemoteShell.h" 11 | #import "NSRemoteChannelSocketPair.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface NSRemoteForward : NSObject 16 | 17 | typedef BOOL (^NSRemoteChannelContinuationBlock)(void); 18 | 19 | - (instancetype)initWithRepresentedSession:(LIBSSH2_SESSION*)representedSession 20 | withRepresentedListener:(LIBSSH2_LISTENER*)representedListener 21 | withTargetHost:(NSString*)withTargetHost 22 | withTargetPort:(NSNumber*)withTargetPort 23 | withTimeout:(NSNumber*)withTimeout; 24 | 25 | - (void)onTermination:(dispatch_block_t)terminationHandler; 26 | - (void)setContinuationChain:(NSRemoteChannelContinuationBlock)continuation; 27 | 28 | - (void)unsafeCallNonblockingOperations; 29 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess; 30 | - (void)unsafeDisconnectAndPrepareForRelease; 31 | 32 | @end 33 | 34 | NS_ASSUME_NONNULL_END 35 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSLocalForward.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSLocalForward.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/9. 6 | // 7 | 8 | #import "GenericHeaders.h" 9 | #import "GenericNetworking.h" 10 | #import "NSRemoteShell.h" 11 | #import "NSRemoteChannelSocketPair.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface NSLocalForward : NSObject 16 | 17 | typedef BOOL (^NSRemoteChannelContinuationBlock)(void); 18 | 19 | - (instancetype)initWithRepresentedSession:(LIBSSH2_SESSION*)representedSession 20 | withRepresentedSocket:(int)socketDescriptor 21 | withTargetHost:(NSString*)withTargetHost 22 | withTargetPort:(NSNumber*)withTargetPort 23 | withLocalPort:(NSNumber*)withLocalPort 24 | withTimeout:(NSNumber*)withTimeout; 25 | 26 | - (void)onTermination:(dispatch_block_t)terminationHandler; 27 | - (void)setContinuationChain:(NSRemoteChannelContinuationBlock)continuation; 28 | 29 | - (void)unsafeCallNonblockingOperations; 30 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess; 31 | - (void)unsafeDisconnectAndPrepareForRelease; 32 | 33 | @end 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/Constructor.m: -------------------------------------------------------------------------------- 1 | // 2 | // Constructor.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/5. 6 | // 7 | 8 | #import "Constructor.h" 9 | #import "GenericHeaders.h" 10 | #import "TSEventLoop.h" 11 | 12 | #import 13 | 14 | int kLIBSSH2_CONSTRUCTOR_SUCCESS = 0; 15 | 16 | #if TARGET_OS_MAC 17 | 18 | /* 19 | 20 | https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/AppNap.html 21 | 22 | App Nap will make our Timer inside CFRunLoop not waking up on schedule 23 | which will then cause a port forward to die 24 | 25 | we are here to disable it on macOS 26 | 27 | */ 28 | NSObject *activity = NULL; 29 | 30 | #endif 31 | 32 | __attribute__((constructor)) void libssh2_constructor(void) { 33 | int ret = libssh2_init(0); // flag 1 == no crypto 34 | if (ret == 0) { 35 | kLIBSSH2_CONSTRUCTOR_SUCCESS = 1; 36 | NSLog(@"libssh2 init success"); 37 | } 38 | #if TARGET_OS_MAC 39 | activity = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityLatencyCritical reason:@"NSRemoteShell is latency critical"]; 40 | #endif 41 | } 42 | 43 | __attribute__((destructor)) void libssh2_destructor(void) { 44 | if (kLIBSSH2_CONSTRUCTOR_SUCCESS) { 45 | libssh2_exit(); 46 | } 47 | #if TARGET_OS_MAC 48 | if (activity) { 49 | [[NSProcessInfo processInfo] endActivity:activity]; 50 | } 51 | #endif 52 | } 53 | 54 | int libssh2_init_check() { 55 | return kLIBSSH2_CONSTRUCTOR_SUCCESS; 56 | } 57 | 58 | /* 59 | used for our CI machine, don't remove this if making contribute 60 | */ 61 | NSString *NSRemoteShellVersion = @"k.S-BrGcrAzymeD6jQ7FdFw6stCZW"; 62 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteChannel.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteChannel.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/6. 6 | // 7 | 8 | #import 9 | 10 | #import "GenericHeaders.h" 11 | #import "NSRemoteShell.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface NSRemoteChannel : NSObject 16 | 17 | typedef NSString* _Nonnull (^NSRemoteChannelRequestDataBlock)(void); 18 | typedef void (^NSRemoteChannelReceiveDataBlock)(NSString *); 19 | typedef BOOL (^NSRemoteChannelContinuationBlock)(void); 20 | typedef CGSize (^NSRemoteChannelTerminalSizeBlock)(void); 21 | 22 | @property (nonatomic, nullable, readonly, assign) LIBSSH2_SESSION *representedSession; 23 | @property (nonatomic, nullable, readonly, assign) LIBSSH2_CHANNEL *representedChannel; 24 | 25 | @property (nonatomic, readonly) BOOL channelCompleted; 26 | 27 | @property (nonatomic, readonly, assign) int exitStatus; 28 | 29 | - (instancetype)initWithRepresentedSession:(LIBSSH2_SESSION*)representedSession 30 | withRepresentedChanel:(LIBSSH2_CHANNEL*)representedChannel; 31 | 32 | - (void)onTermination:(dispatch_block_t)terminationHandler; 33 | 34 | - (void)setRequestDataChain:(NSRemoteChannelRequestDataBlock _Nonnull)requestData; 35 | - (void)setRecivedDataChain:(NSRemoteChannelReceiveDataBlock _Nonnull)receiveData; 36 | - (void)setContinuationChain:(NSRemoteChannelContinuationBlock _Nonnull)continuation; 37 | - (void)setTerminalSizeChain:(NSRemoteChannelTerminalSizeBlock _Nonnull)terminalSize; 38 | 39 | - (void)setChannelTimeoutWith:(double)timeoutValueFromNowInSecond; 40 | - (void)setChannelTimeoutWithScheduled:(NSDate*)timeoutDate; 41 | 42 | - (void)unsafeChannelTerminalSizeUpdate; 43 | 44 | - (void)unsafeCallNonblockingOperations; 45 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess; 46 | - (void)unsafeDisconnectAndPrepareForRelease; 47 | 48 | @end 49 | 50 | NS_ASSUME_NONNULL_END 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !default.mode1v3 2 | !default.mode2v3 3 | !default.pbxuser 4 | !default.perspectivev3 5 | !default.xcworkspace 6 | *.dSYM 7 | *.dSYM.zip 8 | *.hmap 9 | *.ipa 10 | *.lcov 11 | *.lock 12 | *.log 13 | *.mode1v3 14 | *.mode2v3 15 | *.moved-aside 16 | *.pbxuser 17 | *.perspectivev3 18 | *.pid 19 | *.pid.lock 20 | *.seed 21 | *.swp 22 | *.tgz 23 | *.tsbuildinfo 24 | *.xccheckout 25 | *.xcscmblueprint 26 | *.xcuserstate 27 | *~.nib 28 | .AppleDB 29 | .AppleDesktop 30 | .AppleDouble 31 | .DS_Store 32 | .DocumentRevisions-V100 33 | .LSOverride 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | ._* 39 | .apdisk 40 | .build 41 | .bundle 42 | .cache 43 | .cache/ 44 | .com.apple.timemachine.donotpresent 45 | .dynamodb/ 46 | .env 47 | .env.test 48 | .eslintcache 49 | .fseventsd 50 | .fusebox/ 51 | .grunt 52 | .idea 53 | .lock-wscript 54 | .next 55 | .node_repl_history 56 | .npm 57 | .nuxt 58 | .nyc_output 59 | .parcel-cache 60 | .pnp.* 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | .serverless/ 66 | .swiftpm 67 | .tern-port 68 | .vscode-test 69 | .vuepress/dist 70 | .yarn-integrity 71 | .yarn/build-state.yml 72 | .yarn/cache 73 | .yarn/unplugged 74 | /*.gcno 75 | Artifacts/ 76 | CI 77 | CI-Pods.tar 78 | Carthage/Build 79 | Carthage/Build/ 80 | DerivedData 81 | DerivedData/ 82 | Icon 83 | Network Trash Folder 84 | Pipeline/Dockers/Buildtime/ 85 | Podfile.lock 86 | Pods/ 87 | Temporary Items 88 | artifacts/ 89 | bower_components 90 | build/ 91 | build/Release 92 | coverage 93 | default.profraw 94 | dist 95 | dockerbuild 96 | dockermnt 97 | fastlane/Preview.html 98 | fastlane/report.xml 99 | fastlane/screenshots/**/*.png 100 | fastlane/test_output 101 | iOSInjectionProject/ 102 | jspm_packages/ 103 | lerna-debug.log* 104 | lib-cov 105 | logs 106 | node_modules/ 107 | npm-debug.log* 108 | pids 109 | profile 110 | project.xcworkspace 111 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 112 | temp/ 113 | temps/ 114 | web_modules/ 115 | xcuserdata 116 | xcuserdata/ 117 | yarn-debug.log* 118 | yarn-error.log* 119 | 120 | 121 | .vscode 122 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteChannelSocketPair.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteChannelSocketPair.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/9. 6 | // 7 | 8 | #import "NSRemoteChannelSocketPair.h" 9 | 10 | @implementation NSRemoteChannelSocketPair 11 | 12 | - (instancetype)initWithSocket:(int)socket 13 | withChannel:(LIBSSH2_CHANNEL*)channel 14 | { 15 | self = [super init]; 16 | if (self) { 17 | _socket = socket; 18 | _channel = channel; 19 | _completed = NO; 20 | } 21 | return self; 22 | } 23 | 24 | - (void)unsafeCallNonblockingOperations { 25 | if (self.completed) { return; } 26 | if (![self seatbeltCheckPassed]) { return; } 27 | [self unsafeProcessReadWrite]; 28 | } 29 | 30 | - (void)unsafeProcessReadWrite { 31 | do { 32 | long len = 0; 33 | char buf[BUFFER_SIZE]; 34 | memset(buf, 0, sizeof(buf)); 35 | len = recv(self.socket, buf, sizeof(buf), 0); 36 | if (len > 0) { 37 | long wr = 0; 38 | while(wr < len) { 39 | long i = libssh2_channel_write(self.channel, buf + wr, len - wr); 40 | if (LIBSSH2_ERROR_EAGAIN == i) { continue; } 41 | if (i <= 0) { 42 | NSLog(@"libssh2_channel_write returns failure %ld", len); 43 | self.completed = YES; 44 | return; 45 | } 46 | wr += i; 47 | } 48 | } 49 | } while (0); 50 | do { 51 | char buf[BUFFER_SIZE]; 52 | memset(buf, 0, sizeof(buf)); 53 | long len = 0; 54 | len = libssh2_channel_read(self.channel, buf, sizeof(buf)); 55 | if (len > 0) { 56 | long wr = 0; 57 | while(wr < len) { 58 | long i = send(self.socket, buf + wr, len - wr, 0); 59 | if (i <= 0) { self.completed = YES; return; } 60 | wr += i; 61 | } 62 | } else if (len != LIBSSH2_ERROR_EAGAIN) { 63 | NSLog(@"libssh2_channel_read returns failure %ld", len); 64 | self.completed = YES; 65 | return; 66 | } 67 | } while (0); 68 | // connection may send 0 tcp packet data but still keep alive 69 | // so only check eof 70 | } 71 | 72 | - (void)setCompleted:(BOOL)completed { 73 | if (_completed != completed) { 74 | _completed = completed; 75 | [self unsafeDisconnectAndPrepareForRelease]; 76 | } 77 | } 78 | 79 | - (BOOL)seatbeltCheckPassed { 80 | if (!self.channel) { self.completed = YES; return NO; } 81 | if (!self.socket) { self.completed = YES; return NO; } 82 | return YES; 83 | } 84 | 85 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess { 86 | do { 87 | if (self.completed) { break; } 88 | if (![self seatbeltCheckPassed]) { break; } 89 | if (libssh2_channel_eof(self.channel)) { break; } 90 | return YES; 91 | } while (0); 92 | return NO; 93 | } 94 | 95 | - (void)unsafeDisconnectAndPrepareForRelease { 96 | if (!self.completed) { self.completed = YES; } 97 | if (!self.channel) { return; } 98 | if (!self.socket) { return; } 99 | LIBSSH2_CHANNEL *channel = self.channel; 100 | [GenericNetworking destroyNativeSocket:self.socket]; 101 | self.channel = NULL; 102 | self.socket = 0; 103 | LIBSSH2_CHANNEL_SHUTDOWN(channel); 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/TSEventLoop.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteEvent.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/5. 6 | // 7 | 8 | // TS: Thread Safe 9 | 10 | #import "TSEventLoop.h" 11 | 12 | @interface TSEventLoop () 13 | 14 | @property (nonatomic, nonnull, strong) NSThread *associatedThread; 15 | @property (nonatomic, nonnull, strong) NSRunLoop *associatedRunLoop; 16 | @property (nonatomic, nonnull, strong) NSTimer *associatedTimer; 17 | @property (nonatomic, nonnull, strong) NSPort *associatedPort; 18 | @property (nonatomic, nullable, weak) NSRemoteShell *parent; 19 | 20 | @end 21 | 22 | @implementation TSEventLoop 23 | 24 | - (instancetype)initWithParent:(__weak NSRemoteShell*)parent { 25 | if (self = [super init]) { 26 | _parent = parent; 27 | _associatedThread = [[NSThread alloc] initWithTarget:self 28 | selector:@selector(associatedThreadHandler) 29 | object:NULL]; 30 | NSString *threadName = [[NSString alloc] initWithFormat:@"wiki.qaq.shell.%p", parent]; 31 | [_associatedThread setName:threadName]; 32 | NSLog(@"opening thread %@", threadName); 33 | [_associatedThread start]; 34 | } 35 | return self; 36 | } 37 | 38 | - (void)dealloc { 39 | NSLog(@"TSEventLoop object at %p deallocating", self); 40 | [self destroyLoop]; 41 | } 42 | 43 | - (void)explicitRequestHandle { 44 | [self.associatedPort sendBeforeDate:[[NSDate alloc] init] 45 | components:NULL 46 | from:NULL 47 | reserved:NO]; 48 | } 49 | 50 | - (void)associatedThreadHandler { 51 | self.associatedRunLoop = [NSRunLoop currentRunLoop]; 52 | 53 | self.associatedPort = [[NSPort alloc] init]; 54 | self.associatedPort.delegate = self; 55 | [self.associatedRunLoop addPort:self.associatedPort forMode:NSRunLoopCommonModes]; 56 | 57 | self.associatedTimer = [[NSTimer alloc] initWithFireDate: [[NSDate alloc] init] 58 | interval:0.1 59 | target:self selector:@selector(associatedLoopHandler) 60 | userInfo:NULL 61 | repeats:YES]; 62 | [self.associatedRunLoop addTimer:self.associatedTimer forMode:NSRunLoopCommonModes]; 63 | [self.associatedRunLoop run]; 64 | NSLog(@"thread %@ exiting", [[NSThread currentThread] name]); 65 | } 66 | 67 | - (void)handleMachMessage:(void *)msg { 68 | // we don't care about the message, if received any, call handler 69 | [self associatedLoopHandler]; 70 | } 71 | 72 | - (void)associatedLoopHandler { 73 | if (!self.parent) { 74 | [self destroyLoop]; 75 | return; 76 | } 77 | #if DEBUG 78 | NSString *name = [[NSThread currentThread] name]; 79 | NSString *want = [[NSString alloc] initWithFormat:@"wiki.qaq.shell.%p", self.parent]; 80 | if (![name isEqualToString:want]) { 81 | NSLog(@"\n\n"); 82 | NSLog(@"[E] shell name mismatch"); 83 | NSLog(@"expect: %@", want); 84 | NSLog(@" found: %@", name); 85 | NSLog(@"\n\n"); 86 | } 87 | #endif 88 | [self.parent handleRequestsIfNeeded]; 89 | usleep(20000); // 50 times each second 90 | } 91 | 92 | - (void)destroyLoop { 93 | [self.associatedTimer invalidate]; 94 | [self.associatedRunLoop removePort:self.associatedPort forMode:NSRunLoopCommonModes]; 95 | CFRunLoopRef runLoop = [self.associatedRunLoop getCFRunLoop]; 96 | if (runLoop) { CFRunLoopStop(runLoop); } 97 | } 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteFile.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteFile.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/14. 6 | // 7 | 8 | #import "NSRemoteFile.h" 9 | 10 | @interface NSRemoteFile () 11 | 12 | @property (nonatomic, readwrite, nonnull, strong) NSString *name; 13 | @property (nonatomic, readwrite, nullable, strong) NSNumber *size; 14 | @property (nonatomic, readwrite, assign) BOOL isRegularFile; 15 | @property (nonatomic, readwrite, assign) BOOL isDirectory; 16 | @property (nonatomic, readwrite, assign) BOOL isLink; 17 | @property (nonatomic, readwrite, nullable, strong) NSDate *modificationDate; 18 | @property (nonatomic, readwrite, nullable, strong) NSDate *lastAccess; 19 | @property (nonatomic, readwrite, assign) unsigned long ownerUID; 20 | @property (nonatomic, readwrite, assign) unsigned long ownerGID; 21 | @property (nonatomic, readwrite, nonnull, strong) NSString *permissionDescription; 22 | 23 | @end 24 | 25 | @implementation NSRemoteFile 26 | 27 | - (instancetype)initWithFilename:(NSString *)filename { 28 | self = [super init]; 29 | if (self) { 30 | _name = filename; 31 | _size = @(0); 32 | _isDirectory = NO; 33 | _isLink = NO; 34 | _modificationDate = [[NSDate alloc] init]; 35 | _lastAccess = [[NSDate alloc] init]; 36 | _ownerUID = 0; 37 | _ownerGID = 0; 38 | _permissionDescription = @""; 39 | } 40 | return self; 41 | } 42 | 43 | - (BOOL)isEqual:(NSRemoteFile*)object { 44 | if (self == object) { return YES; } 45 | do { 46 | if (![self.name isEqualToString:object.name]) { break; } 47 | if (self.size != object.size) { break; } 48 | if (self.isRegularFile != object.isRegularFile) { break; } 49 | if (self.isDirectory != object.isDirectory) { break; } 50 | if (self.isLink != object.isLink) { break; } 51 | if (![self.modificationDate isEqualToDate:object.modificationDate]) { break; } 52 | if (![self.lastAccess isEqualToDate:object.lastAccess]) { break; } 53 | if (self.ownerUID != object.ownerUID) { break; } 54 | if (self.ownerGID != object.ownerGID) { break; } 55 | if (![self.permissionDescription isEqualToString:object.permissionDescription]) { break; } 56 | return YES; 57 | } while (false); 58 | return NO; 59 | } 60 | 61 | - (void)populateAttributes:(LIBSSH2_SFTP_ATTRIBUTES)fileAttributes { 62 | self.modificationDate = [NSDate dateWithTimeIntervalSince1970:fileAttributes.mtime]; 63 | self.lastAccess = [NSDate dateWithTimeIntervalSinceNow:fileAttributes.atime]; 64 | self.size = @(fileAttributes.filesize); 65 | self.ownerUID = fileAttributes.uid; 66 | self.ownerGID = fileAttributes.gid; 67 | self.permissionDescription = [self permissionDescriptionForMode:fileAttributes.permissions]; 68 | self.isRegularFile = LIBSSH2_SFTP_S_ISREG(fileAttributes.permissions); 69 | self.isDirectory = LIBSSH2_SFTP_S_ISDIR(fileAttributes.permissions); 70 | self.isLink = LIBSSH2_SFTP_S_ISLNK(fileAttributes.permissions); 71 | } 72 | 73 | - (NSString *)permissionDescriptionForMode:(unsigned long)mode { 74 | static char *rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"}; 75 | char bits[11]; 76 | memset(bits, 0, sizeof(bits)); 77 | bits[0] = [self fileTypeLetterForMode:mode]; 78 | strcpy(&bits[1], rwx[(mode >> 6)& 7]); 79 | strcpy(&bits[4], rwx[(mode >> 3)& 7]); 80 | strcpy(&bits[7], rwx[(mode & 7)]); 81 | if (mode & S_ISUID) { bits[3] = (mode & 0100) ? 's' : 'S'; } 82 | if (mode & S_ISGID) { bits[6] = (mode & 0010) ? 's' : 'l'; } 83 | if (mode & S_ISVTX) { bits[9] = (mode & 0100) ? 't' : 'T'; } 84 | return [NSString stringWithCString:bits encoding:NSUTF8StringEncoding]; 85 | } 86 | 87 | - (char)fileTypeLetterForMode:(unsigned long)mode { 88 | char c; 89 | if (S_ISREG(mode)) { c = '-'; } 90 | else if (S_ISDIR(mode)) { c = 'd'; } 91 | else if (S_ISBLK(mode)) { c = 'b'; } 92 | else if (S_ISCHR(mode)) { c = 'c'; } 93 | else if (S_ISFIFO(mode)) { c = 'p'; } 94 | else if (S_ISLNK(mode)) { c = 'l'; } 95 | else if (S_ISSOCK(mode)) { c = 's'; } 96 | else { c = '?'; } // you have problem not me 97 | return c; 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/GenericHeaders.h: -------------------------------------------------------------------------------- 1 | // 2 | // GenericHeaders.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/4. 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | 12 | #import 13 | #import 14 | #import 15 | #import 16 | 17 | #import 18 | 19 | /* 20 | the buffer size define the size that should read from a socket at a time 21 | is required to be larger then socket opt size 22 | 23 | the default size of it is set by system and can be find by 24 | > sysctl -a | grep net.inet.tcp 25 | net.inet.tcp.sendspace: 131072 26 | net.inet.tcp.recvspace: 131072 27 | 28 | TODO: FIXME: 29 | this is a workaround for data being cut in half 30 | */ 31 | #define BUFFER_SIZE 131072 32 | 33 | /* 34 | the interval for sending keep alive packet, count in second 35 | [NSRemoteShell unsafeKeepAliveCheck] will skip 36 | if last success attempt was within the interval 37 | */ 38 | #define KEEPALIVE_INTERVAL 1 39 | 40 | /* 41 | represent how many failure sending keep alive packet shall we ignore 42 | before cutting down the connection on client (our) side 43 | */ 44 | #define KEEPALIVE_ERROR_TOLERANCE_MAX_RETRY 8 45 | 46 | /* 47 | represent max wait time of an operation with dispatch semaphore can wait 48 | counted in second, most used in requestXxxAndWait 49 | DONT USE IN RUNNING SESSION/CHANNEL 50 | */ 51 | #define DISPATCH_SEMAPHORE_MAX_WAIT 30 52 | #define MakeDispatchSemaphoreWaitWithTimeout(SEM) do { \ 53 | dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, DISPATCH_SEMAPHORE_MAX_WAIT * NSEC_PER_SEC); \ 54 | if (dispatch_semaphore_wait((SEM), timeout)) { \ 55 | NSLog(@"dispatch semaphore wait timeout for %d second, exiting blocked operation", DISPATCH_SEMAPHORE_MAX_WAIT); \ 56 | } \ 57 | } while (0); 58 | 59 | #define DISPATCH_SEMAPHORE_CHECK_SIGNLE(SEM) do { \ 60 | if ((SEM)) { dispatch_semaphore_signal((SEM)); } \ 61 | } while (0); 62 | 63 | /* 64 | common used libssh2 channel gracefully shutdown all in one 65 | */ 66 | #define LIBSSH2_CHANNEL_SHUTDOWN(CHANNEL) do { \ 67 | while (libssh2_channel_send_eof(CHANNEL) == LIBSSH2_ERROR_EAGAIN) {}; \ 68 | while (libssh2_channel_close(CHANNEL) == LIBSSH2_ERROR_EAGAIN) {}; \ 69 | while (libssh2_channel_wait_closed(CHANNEL) == LIBSSH2_ERROR_EAGAIN) {}; \ 70 | while (libssh2_channel_free(CHANNEL) == LIBSSH2_ERROR_EAGAIN) {}; \ 71 | } while (0); 72 | 73 | /* 74 | represent socket option at queue_maxsize, can be any size 75 | 76 | but libssh2 has this defined so might just use 16 to balance 77 | #define libssh2_channel_forward_listen(session, port) \ 78 | libssh2_channel_forward_listen_ex((session), NULL, (port), NULL, 16) 79 | */ 80 | #define SOCKET_QUEUE_MAXSIZE 16 81 | 82 | /* 83 | represent how much data shall we send per scp request 84 | */ 85 | #define SFTP_BUFFER_SIZE (BUFFER_SIZE) 86 | 87 | /* 88 | represent how deep we can go while using sftp delete 89 | used to prevent app from crash 90 | */ 91 | #define SFTP_RECURSIVE_DEPTH 32 // don't use our app to do heavy task! 92 | 93 | /* 94 | represent how much time we should sleep before continue next loop 95 | 96 | libssh2 with nonblocking mode shall go again immediately 97 | when returning LIBSSH2_ERROR_EAGAIN but this may cause extra high cpu usage 98 | if network condition is not looking good 99 | 100 | a better solution would be use select/poll/epoll to have kernel do it 101 | but that is a story for another day :p 102 | */ 103 | #define LIBSSH2_CONTINUE_EAGAIN_WAIT 800 104 | 105 | /* 106 | defines the event loop handler class for NSRemoteShell 107 | */ 108 | @protocol NSRemoteOperableObject 109 | 110 | // used in event loop call, it's time to handle operations inside this object 111 | // eg: NSRemoteChannel should read/write to socket, set changes and do anything else 112 | // is designed to be thread safe when calling 113 | - (void)unsafeCallNonblockingOperations; 114 | 115 | // used to evaluate if this object should be close and release 116 | // if a check failed, disconnect is immediately called 117 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess; 118 | 119 | // shutdown any associated resources and will soon be release 120 | - (void)unsafeDisconnectAndPrepareForRelease; 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteShell.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteShell.h 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/4. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "NSRemoteFile.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface NSRemoteShell : NSObject 16 | 17 | @property (nonatomic, readonly, getter=isConnected) BOOL connected; 18 | @property (nonatomic, readonly, getter=isConnectedFileTransfer) BOOL connectedFileTransfer; 19 | @property (nonatomic, readonly, getter=isAuthenticated) BOOL authenticated; 20 | 21 | @property (nonatomic, readonly, strong) NSString *remoteHost; 22 | @property (nonatomic, readonly, strong) NSNumber *remotePort; 23 | @property (nonatomic, readonly, strong) NSNumber *operationTimeout; 24 | 25 | @property (nonatomic, readonly, nullable, strong) NSString *resolvedRemoteIpAddress; 26 | @property (nonatomic, readonly, nullable, strong) NSString *remoteBanner; 27 | @property (nonatomic, readonly, nullable, strong) NSString *remoteFingerPrint; 28 | 29 | #pragma mark initializer 30 | 31 | - (instancetype)init; 32 | - (instancetype)setupConnectionHost:(NSString *)targetHost; 33 | - (instancetype)setupConnectionPort:(NSNumber *)targetPort; 34 | - (instancetype)setupConnectionTimeout:(NSNumber *)timeout; 35 | 36 | #pragma mark event loop 37 | 38 | - (void)handleRequestsIfNeeded; 39 | - (void)explicitRequestStatusPickup; 40 | 41 | #pragma mark connection 42 | 43 | - (void)requestConnectAndWait; 44 | - (void)requestDisconnectAndWait; 45 | 46 | #pragma mark authenticate 47 | 48 | - (void)authenticateWith:(NSString *)username 49 | andPassword:(NSString *)password; 50 | - (void)authenticateWith:(NSString *)username 51 | andPublicKey:(nullable NSString *)publicKey 52 | andPrivateKey:(NSString *)privateKey 53 | andPassword:(nullable NSString *)password; 54 | 55 | #pragma mark helper 56 | 57 | - (nullable NSString *)getLastError; 58 | - (nullable NSString*)getLastFileTransferError; 59 | 60 | #pragma mark execution 61 | 62 | - (int)beginExecuteWithCommand:(NSString*)withCommand 63 | withTimeout:(NSNumber*)withTimeoutSecond 64 | withOnCreate:(dispatch_block_t)withOnCreate 65 | withOutput:(nullable void (^)(NSString*))withOutput 66 | withContinuationHandler:(nullable BOOL (^)(void))withContinuationBlock; 67 | 68 | - (void)beginShellWithTerminalType:(nullable NSString*)withTerminalType 69 | withOnCreate:(dispatch_block_t)withOnCreate 70 | withTerminalSize:(nullable CGSize (^)(void))withRequestTerminalSize 71 | withWriteDataBuffer:(nullable NSString* (^)(void))withWriteDataBuffer 72 | withOutputDataBuffer:(void (^)(NSString * _Nonnull))withOutputDataBuffer 73 | withContinuationHandler:(BOOL (^)(void))withContinuationBlock; 74 | 75 | #pragma mark port map 76 | 77 | - (void)createPortForwardWithLocalPort:(NSNumber*)localPort 78 | withForwardTargetHost:(NSString*)targetHost 79 | withForwardTargetPort:(NSNumber*)targetPort 80 | withOnCreate:(dispatch_block_t)withOnCreate 81 | withContinuationHandler:(BOOL (^)(void))continuationBlock; 82 | 83 | - (void)createPortForwardWithRemotePort:(NSNumber*)remotePort 84 | withForwardTargetHost:(NSString*)targetHost 85 | withForwardTargetPort:(NSNumber*)targetPort 86 | withOnCreate:(dispatch_block_t)withOnCreate 87 | withContinuationHandler:(BOOL (^)(void))continuationBlock; 88 | 89 | #pragma mark sftp 90 | 91 | typedef void (^NSRemoteFileTransferProgressBlock)(NSString *filename, NSProgress *uploadProgress, long bytesPerSecond); 92 | typedef void (^NSRemoteFileDeleteProgressBlock)(NSString *currentFile); 93 | 94 | - (void)requestConnectFileTransferAndWait; 95 | - (void)requestDisconnectFileTransferAndWait; 96 | - (nullable NSArray*)requestFileListAt:(NSString*)atDirPath; 97 | - (nullable NSRemoteFile*)requestFileInfoAt:(NSString*)atPath; 98 | - (BOOL)requestRenameFileAndWait:(NSString*)atPath 99 | withNewPath:(NSString*)newPath; 100 | - (BOOL)requestUploadForFileAndWait:(NSString*)atPath 101 | toDirectory:(NSString*)toDirectory 102 | onProgress:(NSRemoteFileTransferProgressBlock _Nonnull)onProgress 103 | withContinuationHandler:(BOOL (^)(void))continuationBlock; 104 | - (BOOL)requestDeleteForFileAndWait:(NSString*)atPath 105 | withProgressBlock:(NSRemoteFileDeleteProgressBlock _Nonnull)onProgress 106 | withContinuationHandler:(BOOL (^)(void))continuationBlock; 107 | //- (void)requestDeleteUsingRMCommandForFileAndWait:(NSString*)atPath; // how to escape parameters safely? 108 | - (BOOL)requestCreateDirAndWait:(NSString*)atPath; 109 | - (BOOL)requestDownloadFromFileAndWait:(NSString*)atPath 110 | toLocalPath:(NSString*)toPath 111 | onProgress:(NSRemoteFileTransferProgressBlock _Nonnull)onProgress withContinuationHandler:(BOOL (^)(void))continuationBlock; 112 | 113 | #pragma mark destory 114 | 115 | /// This function is used to force shutdown everything, including the run loop and it's associated thread 116 | /// when ARC is not working, call this function 117 | - (void)destroyPermanently; 118 | 119 | @end 120 | 121 | NS_ASSUME_NONNULL_END 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSRemoteShell 2 | 3 | Remote shell using libssh2 with Objective-C. Thread safe implementation. Available as Swift Package. 4 | 5 | ## Usage 6 | 7 | In our design, all operation is blocked, and is recommended to call in background thread. 8 | 9 | ``` 10 | NSRemoteShell() 11 | .setupConnectionHost(host) 12 | .setupConnectionPort(NSNumber(value: port)) 13 | .requestConnectAndWait() 14 | .authenticate(with: username, andPassword: password) 15 | .executeRemote( 16 | command, 17 | withExecTimeout: .init(value: 0) 18 | ) { 19 | createOutput($0) 20 | } withContinuationHandler: { 21 | commandStatus != .terminating 22 | } 23 | ``` 24 | 25 | To connect, call setup function to set host and port. Class is designed with Swift function-like syntax chain. 26 | 27 | ``` 28 | - (instancetype)setupConnectionHost:(nonnull NSString *)targetHost; 29 | - (instancetype)setupConnectionPort:(nonnull NSNumber *)targetPort; 30 | - (instancetype)setupConnectionTimeout:(nonnull NSNumber *)timeout; 31 | 32 | - (instancetype)requestConnectAndWait; 33 | - (instancetype)requestDisconnectAndWait; 34 | ``` 35 | 36 | There is two authenticate method provided. Authenticate is required after connect. 37 | 38 | **Do not change username when authenticateing the same session.** 39 | 40 | ``` 41 | - (instancetype)authenticateWith:(nonnull NSString *)username 42 | andPassword:(nonnull NSString *)password; 43 | - (instancetype)authenticateWith:(NSString *)username 44 | andPublicKey:(nullable NSString *)publicKey 45 | andPrivateKey:(NSString *)privateKey 46 | andPassword:(nullable NSString *)password; 47 | ``` 48 | 49 | For various session property, see property list. 50 | 51 | ``` 52 | @property (nonatomic, readwrite, nullable, strong) NSString *resolvedRemoteIpAddress; 53 | @property (nonatomic, readwrite, nullable, strong) NSString *remoteBanner; 54 | @property (nonatomic, readwrite, nullable, strong) NSString *remoteFingerPrint; 55 | 56 | @property (nonatomic, readwrite, getter=isConnected) BOOL connected; 57 | @property (nonatomic, readwrite, getter=isAuthenticated) BOOL authenticated; 58 | ``` 59 | 60 | Request either command channel or shell channel with designated API, and do not access unexposed values. It may break the ARC or crash the app. 61 | 62 | ``` 63 | - (instancetype)executeRemote:(NSString*)command 64 | withExecTimeout:(NSNumber*)timeoutSecond 65 | withOutput:(nullable void (^)(NSString*))responseDataBlock 66 | withContinuationHandler:(nullable BOOL (^)(void))continuationBlock; 67 | 68 | - (instancetype)openShellWithTerminal:(nullable NSString*)terminalType 69 | withterminalSize:(nullable CGSize (^)(void))requestterminalSize 70 | withWriteData:(nullable NSString* (^)(void))requestWriteData 71 | withOutput:(void (^)(NSString * _Nonnull))responseDataBlock 72 | withContinuationHandler:(BOOL (^)(void))continuationBlock; 73 | ``` 74 | 75 | On execution, once your status is changed, to apply your status quickly, call explicitRequestStatusPickup(). Take an example, when shouldTerminate changes, call this function to terminate this channel immediately or wait for the event loop to pick up on a guaranteed schedule. 76 | 77 | ``` 78 | - (void)explicitRequestStatusPickup; 79 | ``` 80 | 81 | ## Thread Safe 82 | 83 | We implemented thread safe by using NSEventLoop to serialize single NSRemoteShell instance. Multiple NSRemoteShell object will be executed in parallel. Channel operations will be executed in serial for each NSRemoteShell. 84 | 85 | ``` 86 | @interface TSEventLoop : NSObject 87 | 88 | +(id)sharedLoop; 89 | 90 | - (void)explicitRequestHandle; 91 | - (void)delegatingRemoteWith:(NSRemoteShell*)object; 92 | 93 | @end 94 | ``` 95 | 96 | The event loop will guarantee status pickup is thread safe, called several times per second. To improve the performance and user experience, we use a dispatch source of your session's socket to trigger the event loop handler when you have at least one channel opened when data arrived. Check following code to see how it works. 97 | 98 | ``` 99 | - (void)unsafeDispatchSourceMakeDecision 100 | ``` 101 | 102 | All event loop will call a NSRemoteShell objects' handleRequestsIfNeeded method, we deal with control blocks first, and then iterate over all channel to see if data available. 103 | 104 | ``` 105 | for (dispatch_block_t invocation in self.requestInvokations) { 106 | if (invocation) { invocation(); } 107 | } 108 | [self.requestInvokations removeAllObjects]; 109 | for (NSRemoteChannel *channelObject in [self.associatedChannel copy]) { 110 | [channelObject insanityUncheckedEventLoop]; 111 | } 112 | ``` 113 | 114 | ARC will take place to disconnect if a shell object is no longer holds. You can close the session manually or let ARC handle it. 115 | 116 | ``` 117 | - (void)dealloc { 118 | NSLog(@"shell object at %p deallocating", self); 119 | [self unsafeDisconnect]; 120 | } 121 | ``` 122 | 123 | ## LICENSE 124 | 125 | NSRemoteShell is licensed under [MIT License - Lakr's Edition]. 126 | 127 | ``` 128 | Permissions 129 | - Commercial use 130 | - Modification 131 | - Distribution 132 | - Private use 133 | 134 | Limitations 135 | - NO Liability 136 | - NO Warranty 137 | 138 | Conditions 139 | - NO Conditions 140 | ``` 141 | 142 | --- 143 | 144 | Copyright © 2024 Lakr Aream. All Rights Reserved. 145 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteForward.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteForward.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/9. 6 | // 7 | 8 | #import "NSRemoteForward.h" 9 | 10 | @interface NSRemoteForward () 11 | 12 | @property (nonatomic, readwrite, strong) NSString *targetHost; 13 | @property (nonatomic, readwrite, strong) NSNumber *targetPort; 14 | @property (nonatomic, readwrite, strong) NSNumber *timeout; 15 | 16 | @property (nonatomic, nullable, readwrite, assign) LIBSSH2_SESSION *representedSession; 17 | @property (nonatomic, nullable, readwrite, assign) LIBSSH2_LISTENER *representedListener; 18 | @property (nonatomic, nonnull, readwrite, strong) NSMutableArray *forwardSocketPair; 19 | 20 | @property (nonatomic, nullable, strong) dispatch_block_t terminationBlock; 21 | @property (nonatomic, nullable, strong) NSRemoteChannelContinuationBlock continuationDecisionBlock; 22 | 23 | @property (nonatomic, readwrite) BOOL forwardCompleted; 24 | 25 | @end 26 | 27 | @implementation NSRemoteForward 28 | 29 | - (instancetype)initWithRepresentedSession:(LIBSSH2_SESSION *)representedSession 30 | withRepresentedListener:(LIBSSH2_LISTENER *)representedListener 31 | withTargetHost:(NSString*)withTargetHost 32 | withTargetPort:(NSNumber*)withTargetPort 33 | withTimeout:(NSNumber*)withTimeout 34 | { 35 | self = [super init]; 36 | if (self) { 37 | _timeout = withTimeout; 38 | _representedSession = representedSession; 39 | _representedListener = representedListener; 40 | _targetHost = withTargetHost; 41 | _targetPort = withTargetPort; 42 | _timeout = withTimeout; 43 | _forwardSocketPair = [[NSMutableArray alloc] init]; 44 | _forwardCompleted = NO; 45 | } 46 | return self; 47 | } 48 | 49 | - (void)setForwardCompleted:(BOOL)channelCompleted { 50 | if (_forwardCompleted != channelCompleted) { 51 | _forwardCompleted = channelCompleted; 52 | [self unsafeDisconnectAndPrepareForRelease]; 53 | } 54 | } 55 | 56 | - (void)onTermination:(dispatch_block_t)terminationHandler { 57 | self.terminationBlock = terminationHandler; 58 | } 59 | - (void)setContinuationChain:(NSRemoteChannelContinuationBlock)continuation { 60 | self.continuationDecisionBlock = continuation; 61 | } 62 | 63 | - (BOOL)seatbeltCheckPassed { 64 | if (!self.representedSession) { self.forwardCompleted = YES; return NO; } 65 | if (!self.representedListener) { self.forwardCompleted = YES; return NO; } 66 | return YES; 67 | } 68 | 69 | - (void)unsafeCallNonblockingOperations { 70 | if (self.forwardCompleted) { return; } 71 | if (![self seatbeltCheckPassed]) { return; } 72 | [self unsafeListenerAccept]; 73 | [self unsafeProcessAllSocket]; 74 | [self unsafeChannelShouldTerminate]; 75 | } 76 | 77 | - (void)unsafeListenerAccept { 78 | LIBSSH2_CHANNEL *channel = libssh2_channel_forward_accept(self.representedListener); 79 | if (!channel) { 80 | int rc = libssh2_session_last_errno(self.representedSession); 81 | if (rc != LIBSSH2_ERROR_EAGAIN) { self.forwardCompleted = YES; } 82 | return; 83 | } 84 | NSLog(@"channel forward accepted a channel"); 85 | int socket = [GenericNetworking createSocketWithTargetHost:self.targetHost 86 | withTargetPort:self.targetPort 87 | requireNonblockingIO:YES]; 88 | if (!socket) { 89 | NSLog(@"failed to create socket to target"); 90 | LIBSSH2_CHANNEL_SHUTDOWN(channel); 91 | return; 92 | } 93 | NSRemoteChannelSocketPair *pair = [[NSRemoteChannelSocketPair alloc] initWithSocket:socket 94 | withChannel:channel]; 95 | [self.forwardSocketPair addObject:pair]; 96 | } 97 | 98 | - (void)unsafeProcessAllSocket { 99 | NSMutableArray *newArray = [[NSMutableArray alloc] init]; 100 | for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) { 101 | if (![pair unsafeInsanityCheckAndReturnDidSuccess]) { 102 | [pair unsafeDisconnectAndPrepareForRelease]; 103 | continue; 104 | } 105 | [pair unsafeCallNonblockingOperations]; 106 | if (![pair unsafeInsanityCheckAndReturnDidSuccess]) { 107 | [pair unsafeDisconnectAndPrepareForRelease]; 108 | continue; 109 | } 110 | [newArray addObject:pair]; 111 | } 112 | self.forwardSocketPair = newArray; 113 | } 114 | 115 | - (BOOL)unsafeChannelShouldTerminate { 116 | do { 117 | if (self.continuationDecisionBlock && !self.continuationDecisionBlock()) { 118 | break; 119 | } 120 | return NO; 121 | } while (0); 122 | self.forwardCompleted = YES; 123 | return YES; 124 | } 125 | 126 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess { 127 | do { 128 | if (self.forwardCompleted) { break; } 129 | if (![self seatbeltCheckPassed]) { break; } 130 | return YES; 131 | } while (0); 132 | return NO; 133 | } 134 | 135 | - (void)unsafeDisconnectAndPrepareForRelease { 136 | if (!self.forwardCompleted) { self.forwardCompleted = YES; } 137 | if (!self.representedSession) { return; } 138 | if (!self.representedListener) { return; } 139 | self.representedSession = NULL; 140 | LIBSSH2_LISTENER *listener = self.representedListener; 141 | self.representedListener = NULL; 142 | while (libssh2_channel_forward_cancel(listener) == LIBSSH2_ERROR_EAGAIN) {}; 143 | for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) { 144 | [pair unsafeDisconnectAndPrepareForRelease]; 145 | } 146 | self.forwardSocketPair = [[NSMutableArray alloc] init]; 147 | if (self.terminationBlock) { self.terminationBlock(); } 148 | self.terminationBlock = NULL; 149 | } 150 | 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSLocalForward.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSLocalForward.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/3/9. 6 | // 7 | 8 | #import "NSLocalForward.h" 9 | 10 | @interface NSLocalForward () 11 | 12 | @property (nonatomic, readwrite, strong) NSString *targetHost; 13 | @property (nonatomic, readwrite, strong) NSNumber *targetPort; 14 | @property (nonatomic, readwrite, strong) NSNumber *localPort; 15 | @property (nonatomic, readwrite, strong) NSNumber *timeout; 16 | 17 | @property (nonatomic, nullable, readwrite, assign) LIBSSH2_SESSION *representedSession; 18 | @property (nonatomic, readwrite, assign) int representedSocket; 19 | @property (nonatomic, nonnull, readwrite, strong) NSMutableArray *forwardSocketPair; 20 | @property (nonatomic, nullable, strong) dispatch_block_t terminationBlock; 21 | @property (nonatomic, nullable, strong) NSRemoteChannelContinuationBlock continuationDecisionBlock; 22 | @property (nonatomic, readwrite) BOOL forwardCompleted; 23 | 24 | @end 25 | 26 | @implementation NSLocalForward 27 | 28 | - (instancetype)initWithRepresentedSession:(LIBSSH2_SESSION *)representedSession 29 | withRepresentedSocket:(int)socketDescriptor 30 | withTargetHost:(NSString*)withTargetHost 31 | withTargetPort:(NSNumber*)withTargetPort 32 | withLocalPort:(NSNumber*)withLocalPort 33 | withTimeout:(NSNumber *)withTimeout 34 | { 35 | self = [super init]; 36 | if (self) { 37 | _targetHost = withTargetHost; 38 | _targetPort = withTargetPort; 39 | _localPort = withLocalPort; 40 | _timeout = withTimeout; 41 | _representedSession = representedSession; 42 | _representedSocket = socketDescriptor; 43 | _forwardSocketPair = [[NSMutableArray alloc] init]; 44 | _forwardCompleted = NO; 45 | } 46 | return self; 47 | } 48 | 49 | - (void)onTermination:(dispatch_block_t)terminationHandler { 50 | self.terminationBlock = terminationHandler; 51 | } 52 | 53 | - (void)setContinuationChain:(NSRemoteChannelContinuationBlock _Nonnull)continuation { 54 | self.continuationDecisionBlock = continuation; 55 | } 56 | 57 | - (void)setForwardCompleted:(BOOL)channelCompleted { 58 | if (_forwardCompleted != channelCompleted) { 59 | _forwardCompleted = channelCompleted; 60 | [self unsafeDisconnectAndPrepareForRelease]; 61 | } 62 | } 63 | 64 | - (BOOL)seatbeltCheckPassed { 65 | if (!self.representedSession) { self.forwardCompleted = YES; return NO; } 66 | if (!self.representedSocket) { self.forwardCompleted = YES; return NO; } 67 | return YES; 68 | } 69 | 70 | - (void)unsafeCallNonblockingOperations { 71 | if (self.forwardCompleted) { return; } 72 | if (![self seatbeltCheckPassed]) { return; } 73 | [self unsafeChannelMainSocketAccept]; 74 | [self unsafeProcessAllSocket]; 75 | [self unsafeChannelShouldTerminate]; 76 | } 77 | 78 | - (void)unsafeProcessAllSocket { 79 | NSMutableArray *newArray = [[NSMutableArray alloc] init]; 80 | for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) { 81 | if (![pair unsafeInsanityCheckAndReturnDidSuccess]) { 82 | [pair unsafeDisconnectAndPrepareForRelease]; 83 | continue; 84 | } 85 | [pair unsafeCallNonblockingOperations]; 86 | if (![pair unsafeInsanityCheckAndReturnDidSuccess]) { 87 | [pair unsafeDisconnectAndPrepareForRelease]; 88 | continue; 89 | } 90 | [newArray addObject:pair]; 91 | } 92 | self.forwardSocketPair = newArray; 93 | } 94 | 95 | - (void)unsafeChannelMainSocketAccept { 96 | while (1) { 97 | struct sockaddr_in peeraddr; 98 | socklen_t peeraddrlen = sizeof(peeraddr); 99 | getpeername(STDIN_FILENO, (struct sockaddr*)&peeraddr, &peeraddrlen); 100 | int forwardsock = accept(self.representedSocket, (struct sockaddr *)&sin, &peeraddrlen); 101 | if (forwardsock <= 0) { return; } 102 | NSLog(@"accept is returning child socket %d", forwardsock); 103 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.timeout intValue]]; 104 | LIBSSH2_CHANNEL *channel = NULL; 105 | while (true) { 106 | if ([date timeIntervalSinceNow] < 0) { 107 | libssh2_session_set_last_error(self.representedSession, LIBSSH2_ERROR_TIMEOUT, NULL); 108 | break; 109 | } 110 | LIBSSH2_CHANNEL *channelBuilder = libssh2_channel_direct_tcpip_ex(self.representedSession, 111 | [self.targetHost UTF8String], 112 | [self.targetPort intValue], 113 | "127.0.0.1", 114 | [self.localPort intValue]); 115 | if (channelBuilder) { 116 | channel = channelBuilder; 117 | break; 118 | } 119 | long rc = libssh2_session_last_errno(self.representedSession); 120 | if (rc == LIBSSH2_ERROR_EAGAIN) { 121 | continue; 122 | } 123 | break; 124 | } 125 | if (!channel) { 126 | NSLog(@"accepted connection failed to open channel"); 127 | close(forwardsock); 128 | [self unsafeDisconnectAndPrepareForRelease]; 129 | return; 130 | } 131 | NSLog(@"created channel for forward socket %d %p", forwardsock, channel); 132 | NSRemoteChannelSocketPair *pair = [[NSRemoteChannelSocketPair alloc] initWithSocket:forwardsock 133 | withChannel:channel]; 134 | [self.forwardSocketPair addObject:pair]; 135 | } 136 | } 137 | 138 | - (BOOL)unsafeChannelShouldTerminate { 139 | do { 140 | if (self.continuationDecisionBlock && !self.continuationDecisionBlock()) { 141 | break; 142 | } 143 | return NO; 144 | } while (0); 145 | self.forwardCompleted = YES; 146 | return YES; 147 | } 148 | 149 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess { 150 | do { 151 | if (self.forwardCompleted) { break; } 152 | if (![self seatbeltCheckPassed]) { break; } 153 | return YES; 154 | } while (0); 155 | return NO; 156 | } 157 | 158 | - (void)unsafeDisconnectAndPrepareForRelease { 159 | if (!self.forwardCompleted) { self.forwardCompleted = YES; } 160 | if (!self.representedSession) { return; } 161 | if (!self.representedSocket) { return; } 162 | int socket = self.representedSocket; 163 | [GenericNetworking destroyNativeSocket:socket]; 164 | self.representedSession = NULL; 165 | self.representedSocket = 0; 166 | for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) { 167 | [pair unsafeDisconnectAndPrepareForRelease]; 168 | } 169 | self.forwardSocketPair = [[NSMutableArray alloc] init]; 170 | if (self.terminationBlock) { self.terminationBlock(); } 171 | self.terminationBlock = NULL; 172 | } 173 | 174 | @end 175 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/GenericNetworking.m: -------------------------------------------------------------------------------- 1 | // 2 | // GenericNetworking.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/5. 6 | // 7 | 8 | #import "GenericNetworking.h" 9 | 10 | @implementation GenericNetworking 11 | 12 | + (NSArray *)resolveIpAddressesFor:(NSString*)candidateHost 13 | { 14 | if (!candidateHost) return [[NSArray alloc] init]; 15 | NSArray *candidateHostData = [[NSArray alloc] init]; 16 | struct addrinfo hints; 17 | memset(&hints, 0, sizeof(hints)); 18 | hints.ai_family = PF_UNSPEC; // PF_INET if you want only IPv4 addresses 19 | hints.ai_protocol = IPPROTO_TCP; 20 | struct addrinfo *addrs, *addr; 21 | getaddrinfo([candidateHost UTF8String], NULL, &hints, &addrs); 22 | for (addr = addrs; addr; addr = addr->ai_next) { 23 | char host[NI_MAXHOST]; 24 | getnameinfo(addr->ai_addr, addr->ai_addrlen, host, sizeof(host), NULL, 0, NI_NUMERICHOST); 25 | if (strlen(host) <= 0) { continue; } 26 | NSString *hostStr = [[NSString alloc] initWithUTF8String:host]; 27 | NSLog(@"resolving host %@ loading result: %@", candidateHost, hostStr); 28 | NSData *build = [[NSData alloc] initWithBytes:addr->ai_addr length: addr->ai_addrlen]; 29 | candidateHostData = [candidateHostData arrayByAddingObject:build]; 30 | } 31 | freeaddrinfo(addrs); 32 | return candidateHostData; 33 | } 34 | 35 | + (BOOL)isValidateWithPort:(NSNumber*)port { 36 | int p = [port intValue]; 37 | // we are treating 0 as a valid port and technically it should work! 38 | if (p >= 0 && p <= 65535) { 39 | return YES; 40 | } 41 | return NO; 42 | } 43 | 44 | + (int)createSocketNonblockingListenerWithLocalPort:(NSNumber*)localPort 45 | { 46 | if (![GenericNetworking isValidateWithPort:localPort]) { 47 | NSLog(@"invalid port %@", [localPort stringValue]); 48 | return 0; 49 | } 50 | 51 | int port = [localPort intValue]; 52 | struct sockaddr_in server4; 53 | int socket_desc4 = socket(AF_INET, SOCK_STREAM, 0); 54 | if (socket_desc4 <= 0) { 55 | NSLog(@"failed to create socket for ipv4 at port %d", port); 56 | return 0; 57 | } 58 | server4.sin_family = AF_INET; 59 | server4.sin_addr.s_addr = inet_addr("127.0.0.1"); // for security? 60 | server4.sin_port = htons(port); 61 | if (setsockopt(socket_desc4, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)) == -1) { 62 | NSLog(@"failed to setsockopt for ipv4 at port %d", port); 63 | close(socket_desc4); 64 | return 0; 65 | } 66 | if (bind(socket_desc4, (struct sockaddr*)&server4, sizeof(server4)) < 0) { 67 | NSLog(@"failed to bind socket for ipv4 at port %d", port); 68 | close(socket_desc4); 69 | return 0; 70 | } else { 71 | NSLog(@"bound listener v4 for port %d", port); 72 | } 73 | if (fcntl(socket_desc4, F_SETFL, fcntl(socket_desc4, F_GETFL, 0) | O_NONBLOCK) == -1) { 74 | NSLog(@"failed to call fcntl for none-blocking ipv4 at port %d", port); 75 | close(socket_desc4); 76 | return 0; 77 | } 78 | if (listen(socket_desc4, SOCKET_QUEUE_MAXSIZE) == -1) { 79 | NSLog(@"failed to call fcntl for none-blocking ipv6 at port %d", port); 80 | close(socket_desc4); 81 | return 0; 82 | } 83 | 84 | NSLog(@"socket listener for port %d booted", port); 85 | return socket_desc4; 86 | } 87 | 88 | + (int)createSocketWithTargetHost:(NSString *)targetHost 89 | withTargetPort:(NSNumber *)targetPort 90 | requireNonblockingIO:(BOOL)useNonblocking 91 | { 92 | if (![self isValidateWithPort:targetPort]) { return 0; } 93 | int candidatePort = [targetPort intValue]; 94 | NSArray *addrData = [self resolveIpAddressesFor:targetHost]; 95 | if (!addrData) { return 0; } 96 | for (id candidateHostData in addrData) { 97 | if ([candidateHostData length] == sizeof(struct sockaddr_in)) { 98 | struct sockaddr_in address4; 99 | [candidateHostData getBytes:&address4 length:sizeof(address4)]; 100 | address4.sin_port = htons(candidatePort); 101 | char str[INET_ADDRSTRLEN]; 102 | inet_ntop(AF_INET, &(address4.sin_addr), str, INET_ADDRSTRLEN); 103 | int forwardsock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); 104 | if (forwardsock <= 0) { 105 | NSLog(@"socket failed to create, trying next"); 106 | continue; 107 | } 108 | int rv = connect(forwardsock, (struct sockaddr*)&address4, sizeof(struct sockaddr_in)); 109 | if (rv != 0) { 110 | NSLog(@"socket failed to connect, trying next address"); 111 | close(forwardsock); 112 | continue; 113 | } 114 | if (useNonblocking && fcntl(forwardsock, F_SETFL, fcntl(forwardsock, F_GETFL, 0) | O_NONBLOCK) == -1) { 115 | NSLog(@"failed to call fcntl for none-blocking for socket %d", forwardsock); 116 | close(forwardsock); 117 | continue; 118 | } 119 | NSLog(@"created socket %d to address %s", forwardsock, str); 120 | return forwardsock; 121 | } else if ([candidateHostData length] == sizeof(struct sockaddr_in6)) { 122 | struct sockaddr_in6 address6; 123 | [candidateHostData getBytes:&address6 length:sizeof(address6)]; 124 | address6.sin6_port = htons(candidatePort); 125 | char str[INET6_ADDRSTRLEN]; 126 | inet_ntop(AF_INET6, &(address6.sin6_addr), str, INET6_ADDRSTRLEN); 127 | int forwardsock = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP); 128 | if (forwardsock <= 0) { 129 | NSLog(@"socket failed to create, trying next"); 130 | continue; 131 | } 132 | int rv = connect(forwardsock, (struct sockaddr*)&address6, sizeof(struct sockaddr_in6)); 133 | if (rv != 0) { 134 | NSLog(@"socket failed to connect, trying next address"); 135 | close(forwardsock); 136 | continue; 137 | } 138 | if (useNonblocking && fcntl(forwardsock, F_SETFL, fcntl(forwardsock, F_GETFL, 0) | O_NONBLOCK) == -1) { 139 | NSLog(@"failed to call fcntl for none-blocking for socket %d", forwardsock); 140 | close(forwardsock); 141 | continue; 142 | } 143 | NSLog(@"created socket %d to address %s", forwardsock, str); 144 | return forwardsock; 145 | } else { 146 | NSLog(@"unrecognized address candidate size"); 147 | continue; 148 | } 149 | } 150 | return 0; 151 | } 152 | 153 | + (void)destroyNativeSocket:(int)socketDescriptor { 154 | if (socketDescriptor > 0) { 155 | NSLog(@"closing socket fd %d", socketDescriptor); 156 | close(socketDescriptor); 157 | } 158 | } 159 | 160 | + (NSString*)getResolvedIpAddressWith:(int)socket { 161 | if (!socket) { return @""; } 162 | struct sockaddr_in sin; 163 | socklen_t len = sizeof(sin); 164 | getpeername(socket, (struct sockaddr*)&sin, &len); 165 | char buf[255]; 166 | memset(buf, 0, sizeof(buf)); 167 | switch(sin.sin_family) { 168 | case AF_INET: { 169 | struct sockaddr_in *addr_in = (struct sockaddr_in *)&sin; 170 | inet_ntop(AF_INET, &(addr_in->sin_addr), buf, INET_ADDRSTRLEN); 171 | break; 172 | } 173 | case AF_INET6: { 174 | struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)&sin; 175 | inet_ntop(AF_INET6, &(addr_in6->sin6_addr), buf, INET6_ADDRSTRLEN); 176 | break; 177 | } 178 | default: 179 | break; 180 | } 181 | return [[NSString alloc] initWithUTF8String:buf]; 182 | } 183 | 184 | @end 185 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteChannel.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteChannel.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/6. 6 | // 7 | 8 | #import "NSRemoteChannel.h" 9 | 10 | @interface NSRemoteChannel () 11 | 12 | @property (nonatomic, nullable, readwrite, assign) LIBSSH2_SESSION *representedSession; 13 | @property (nonatomic, nullable, readwrite, assign) LIBSSH2_CHANNEL *representedChannel; 14 | 15 | @property (nonatomic, nullable, strong) NSRemoteChannelRequestDataBlock requestDataBlock; 16 | @property (nonatomic, nullable, strong) NSRemoteChannelReceiveDataBlock receiveDataBlock; 17 | @property (nonatomic, nullable, strong) NSRemoteChannelContinuationBlock continuationDecisionBlock; 18 | @property (nonatomic, nullable, strong) NSRemoteChannelTerminalSizeBlock requestTerminalSizeBlock; 19 | 20 | @property (nonatomic) CGSize currentTerminalSize; 21 | 22 | @property (nonatomic, nullable, strong) NSDate *scheduledTermination; 23 | @property (nonatomic, nullable, strong) dispatch_block_t terminationBlock; 24 | 25 | @property (nonatomic, readwrite) BOOL channelCompleted; 26 | @property (nonatomic, readwrite, assign) int exitStatus; 27 | 28 | @end 29 | 30 | @implementation NSRemoteChannel 31 | 32 | // MARK: - LIFE CYCLE 33 | 34 | - (instancetype)initWithRepresentedSession:(LIBSSH2_SESSION *)representedSession 35 | withRepresentedChanel:(LIBSSH2_CHANNEL *)representedChannel 36 | { 37 | self = [super init]; 38 | if (self) { 39 | _representedSession = representedSession; 40 | _representedChannel = representedChannel; 41 | _channelCompleted = NO; 42 | _currentTerminalSize = CGSizeMake(0, 0); 43 | _exitStatus = 0; 44 | } 45 | return self; 46 | } 47 | 48 | - (void)dealloc { 49 | NSLog(@"channel object at %p deallocating", self); 50 | [self unsafeDisconnectAndPrepareForRelease]; 51 | } 52 | 53 | // MARK: - SETUP 54 | 55 | - (void)onTermination:(dispatch_block_t)terminationHandler { 56 | self.terminationBlock = terminationHandler; 57 | } 58 | 59 | - (void)setRequestDataChain:(NSRemoteChannelRequestDataBlock _Nonnull)requestData { 60 | self.requestDataBlock = requestData; 61 | } 62 | 63 | - (void)setRecivedDataChain:(NSRemoteChannelReceiveDataBlock _Nonnull)receiveData { 64 | self.receiveDataBlock = receiveData; 65 | } 66 | 67 | - (void)setContinuationChain:(NSRemoteChannelContinuationBlock _Nonnull)continuation { 68 | self.continuationDecisionBlock = continuation; 69 | } 70 | 71 | - (void)setTerminalSizeChain:(NSRemoteChannelTerminalSizeBlock _Nonnull)terminalSize { 72 | self.requestTerminalSizeBlock = terminalSize; 73 | } 74 | 75 | - (void)setChannelTimeoutWith:(double)timeoutValueFromNowInSecond { 76 | if (timeoutValueFromNowInSecond <= 0) { 77 | #if DEBUG 78 | NSLog(@"setChannelTimeoutWith was called with negative value or zero, setChannelTimeoutWithScheduled skipped"); 79 | #endif 80 | } else { 81 | NSDate *schedule = [[NSDate alloc] initWithTimeIntervalSinceNow:timeoutValueFromNowInSecond]; 82 | [self setChannelTimeoutWithScheduled:schedule]; 83 | } 84 | } 85 | 86 | - (void)setChannelTimeoutWithScheduled:(NSDate*)timeoutDate { 87 | self.scheduledTermination = timeoutDate; 88 | } 89 | 90 | - (void)setChannelCompleted:(BOOL)channelCompleted { 91 | if (_channelCompleted != channelCompleted) { 92 | _channelCompleted = channelCompleted; 93 | [self unsafeDisconnectAndPrepareForRelease]; 94 | } 95 | } 96 | 97 | // MARK: - EXEC 98 | 99 | - (BOOL)seatbeltCheckPassed { 100 | if (!self.representedSession) { self.channelCompleted = YES; return NO; } 101 | if (!self.representedChannel) { self.channelCompleted = YES; return NO; } 102 | return YES; 103 | } 104 | 105 | - (void)unsafeChannelRead { 106 | char buffer[BUFFER_SIZE]; 107 | char errorBuffer[BUFFER_SIZE]; 108 | memset(buffer, 0, sizeof(buffer)); 109 | memset(errorBuffer, 0, sizeof(errorBuffer)); 110 | 111 | long rcout = libssh2_channel_read(self.representedChannel, buffer, (ssize_t)sizeof(buffer)); 112 | long rcerr = libssh2_channel_read_stderr(self.representedChannel, errorBuffer, (ssize_t)sizeof(errorBuffer)); 113 | 114 | if (rcout != LIBSSH2_ERROR_EAGAIN && rcout > 0) { 115 | NSString *read = [[NSString alloc] initWithUTF8String:buffer]; 116 | if (self.receiveDataBlock) { 117 | self.receiveDataBlock(read); 118 | } 119 | } 120 | if (rcerr != LIBSSH2_ERROR_EAGAIN && rcerr > 0) { 121 | NSString *read = [[NSString alloc] initWithUTF8String:errorBuffer]; 122 | if (self.receiveDataBlock) { 123 | self.receiveDataBlock(read); 124 | } 125 | } 126 | } 127 | 128 | - (void)unsafeChannelWrite { 129 | if (!self.requestDataBlock) { 130 | return; 131 | } 132 | NSString *requestedBuffer = self.requestDataBlock(); 133 | if (!requestedBuffer || [requestedBuffer length] < 1) { 134 | return; 135 | } 136 | NSData *data = [requestedBuffer dataUsingEncoding:NSUTF8StringEncoding]; 137 | if (!data || [data length] < 1) { 138 | NSLog(@"error occurred during message encode, ignoring empty data"); 139 | return; 140 | } 141 | while (true) { 142 | if ([self unsafeChannelShouldTerminate]) { 143 | break; 144 | } 145 | // Actual number of bytes written or negative on failure. 146 | long rc = libssh2_channel_write(self.representedChannel, [data bytes], [data length]); 147 | if (rc == LIBSSH2_ERROR_EAGAIN) { 148 | continue; 149 | } 150 | if (rc < 0) { 151 | NSLog(@"error occurred during message write, consider terminated channel"); 152 | break; 153 | } 154 | if (rc != [data length]) { 155 | NSLog(@"written data was smaller than giving, data might broke"); 156 | break; 157 | } 158 | // do not deal with error? 159 | break; 160 | } 161 | } 162 | 163 | - (BOOL)unsafeChannelShouldTerminate { 164 | do { 165 | if (self.scheduledTermination && [self.scheduledTermination timeIntervalSinceNow] < 0) { 166 | NSLog(@"channel terminating due to timeout schedule"); 167 | break; 168 | } 169 | if (self.continuationDecisionBlock && !self.continuationDecisionBlock()) { 170 | break; 171 | } 172 | long rc = libssh2_channel_eof(self.representedChannel); 173 | if (rc == 1) { 174 | break; 175 | } 176 | if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) { 177 | break; 178 | } 179 | return NO; 180 | } while (0); 181 | self.channelCompleted = YES; 182 | return YES; 183 | } 184 | 185 | - (void)unsafeChannelTerminalSizeUpdate { 186 | // may called from outside 187 | if (![self seatbeltCheckPassed]) { return; } 188 | if (!self.requestTerminalSizeBlock) { 189 | return; 190 | } 191 | CGSize targetSize = self.requestTerminalSizeBlock(); 192 | if (CGSizeEqualToSize(targetSize, self.currentTerminalSize)) { 193 | return; 194 | } 195 | self.currentTerminalSize = targetSize; 196 | while (true) { 197 | long rc = libssh2_channel_request_pty_size(self.representedChannel, 198 | targetSize.width, 199 | targetSize.height); 200 | if (rc == LIBSSH2_ERROR_EAGAIN) { 201 | continue; 202 | } 203 | // don't check error here? 204 | break; 205 | } 206 | } 207 | 208 | - (void)unsafeCallNonblockingOperations { 209 | if (self.channelCompleted) { return; } 210 | if (![self seatbeltCheckPassed]) { return; } 211 | [self unsafeChannelRead]; 212 | [self unsafeChannelTerminalSizeUpdate]; 213 | [self unsafeChannelWrite]; 214 | [self unsafeChannelShouldTerminate]; 215 | } 216 | 217 | - (BOOL)unsafeInsanityCheckAndReturnDidSuccess { 218 | do { 219 | if (self.channelCompleted) { break; } 220 | if (![self seatbeltCheckPassed]) { break; } 221 | return YES; 222 | } while (0); 223 | return NO; 224 | } 225 | 226 | - (void)unsafeDisconnectAndPrepareForRelease { 227 | if (!self.channelCompleted) { self.channelCompleted = YES; } 228 | if (!self.representedSession) { return; } 229 | if (!self.representedChannel) { return; } 230 | LIBSSH2_CHANNEL *channel = self.representedChannel; 231 | self.representedChannel = NULL; 232 | self.representedSession = NULL; 233 | while (libssh2_channel_send_eof(channel) == LIBSSH2_ERROR_EAGAIN) {}; 234 | while (libssh2_channel_close(channel) == LIBSSH2_ERROR_EAGAIN) {}; 235 | while (libssh2_channel_wait_closed(channel) == LIBSSH2_ERROR_EAGAIN) {}; 236 | int es = libssh2_channel_get_exit_status(channel); 237 | NSLog(@"channel get exit status returns: %d", es); 238 | self.exitStatus = es; 239 | while (libssh2_channel_free(channel) == LIBSSH2_ERROR_EAGAIN) {}; 240 | if (self.terminationBlock) { self.terminationBlock(); } 241 | self.terminationBlock = NULL; 242 | } 243 | 244 | @end 245 | -------------------------------------------------------------------------------- /Sources/NSRemoteShell/NSRemoteShell.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSRemoteShell.m 3 | // 4 | // 5 | // Created by Lakr Aream on 2022/2/4. 6 | // 7 | 8 | #import "NSRemoteShell.h" 9 | 10 | #import "TSEventLoop.h" 11 | 12 | #import "NSRemoteChannel.h" 13 | #import "NSLocalForward.h" 14 | #import "NSRemoteForward.h" 15 | 16 | #import "GenericHeaders.h" 17 | #import "GenericNetworking.h" 18 | 19 | #import "Constructor.h" 20 | 21 | @interface NSRemoteShell () 22 | 23 | @property (nonatomic, readwrite, assign) BOOL destroyed; 24 | 25 | @property (nonatomic, readonly, nonnull, strong) TSEventLoop *associatedLoop; 26 | 27 | @property (nonatomic, readwrite, nonnull, strong) NSString *remoteHost; 28 | @property (nonatomic, readwrite, nonnull, strong) NSNumber *remotePort; 29 | @property (nonatomic, readwrite, nonnull, strong) NSNumber *operationTimeout; 30 | 31 | @property (nonatomic, readwrite, nullable, strong) NSString *resolvedRemoteIpAddress; 32 | @property (nonatomic, readwrite, nullable, strong) NSString *remoteBanner; 33 | @property (nonatomic, readwrite, nullable, strong) NSString *remoteFingerPrint; 34 | @property (nonatomic, readwrite, nullable, strong) NSString *lastError; 35 | @property (nonatomic, readwrite, nullable, strong) NSString *lastFileTransferError; 36 | 37 | @property (nonatomic, readwrite, getter=isConnected) BOOL connected; 38 | @property (nonatomic, readwrite, getter=isConnectedFileTransfer) BOOL connectedFileTransfer; 39 | @property (nonatomic, readwrite, getter=isAuthenticated) BOOL authenticated; 40 | 41 | @property (nonatomic, readwrite, assign) int associatedSocket; 42 | @property (nonatomic, readwrite, nullable, assign) LIBSSH2_SESSION *associatedSession; 43 | @property (nonatomic, readwrite, nullable, assign) LIBSSH2_SFTP *associatedFileTransfer; 44 | @property (nonatomic, readwrite, nullable, strong) dispatch_source_t associatedSocketSource; 45 | 46 | @property (nonatomic, readwrite, nonnull, strong) NSMutableArray> *operableObjects; 47 | @property (nonatomic, readwrite, nonnull, strong) NSMutableArray *requestInvokations; 48 | @property (nonatomic, readwrite, nonnull, strong) NSLock *requestLoopLock; 49 | 50 | @property (nonatomic, readwrite, assign) unsigned keepAliveAttampt; 51 | @property (nonatomic, readwrite, nullable, strong) NSDate *keepAliveLastSuccessAttampt; 52 | 53 | @end 54 | 55 | @implementation NSRemoteShell 56 | 57 | #pragma mark init 58 | 59 | - (instancetype)init { 60 | self = [super init]; 61 | 62 | if (self) { 63 | _destroyed = NO; 64 | _associatedLoop = [[TSEventLoop alloc] initWithParent:self]; 65 | _remoteHost = @""; 66 | _remotePort = @(22); 67 | _operationTimeout = @(8); 68 | _connected = NO; 69 | _authenticated = NO; 70 | _resolvedRemoteIpAddress = NULL; 71 | _lastError = NULL; 72 | _associatedSocket = 0; 73 | _associatedSession = NULL; 74 | _associatedFileTransfer = NULL; 75 | _operableObjects = [[NSMutableArray alloc] init]; 76 | _requestInvokations = [[NSMutableArray alloc] init]; 77 | _requestLoopLock = [[NSLock alloc] init]; 78 | } 79 | 80 | return self; 81 | } 82 | 83 | - (void)dealloc { 84 | self.destroyed = YES; 85 | NSLog(@"shell object at %p deallocating", self); 86 | [self.requestLoopLock lock]; 87 | [self unsafeDisconnect]; 88 | [self.associatedLoop destroyLoop]; 89 | [self.requestLoopLock unlock]; 90 | } 91 | 92 | - (void)destroyPermanently { 93 | if (self.destroyed) { return; } 94 | self.destroyed = YES; 95 | NSLog(@"shell object at %p destroy permanently", self); 96 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 97 | [self.requestLoopLock lock]; 98 | [self.associatedLoop destroyLoop]; 99 | [self unsafeDisconnect]; 100 | // so there won't be any connect request semaphore f***ing us 101 | for (dispatch_block_t invocation in self.requestInvokations) { 102 | if (invocation) { invocation(); } 103 | } 104 | // call again to make sure no connect invocation 105 | [self unsafeDisconnect]; 106 | [self.requestInvokations removeAllObjects]; 107 | // and no more semaphore sitting there in sub channels 108 | for (id object in [self.operableObjects copy]) { 109 | [object unsafeDisconnectAndPrepareForRelease]; 110 | } 111 | [self.operableObjects removeAllObjects]; 112 | // should cancel any source 113 | [self unsafeDispatchSourceMakeDecision]; 114 | [self.requestLoopLock unlock]; 115 | }); 116 | } 117 | 118 | - (instancetype)setupConnectionHost:(NSString*)targetHost { 119 | @synchronized(self) { 120 | [self setRemoteHost:targetHost]; 121 | } 122 | return self; 123 | } 124 | 125 | - (instancetype)setupConnectionPort:(NSNumber*)targetPort { 126 | @synchronized(self) { 127 | [self setRemotePort:targetPort]; 128 | } 129 | return self; 130 | } 131 | 132 | - (instancetype)setupConnectionTimeout:(NSNumber*)timeout { 133 | if (timeout.doubleValue < 1) { 134 | NSLog(@"setting timeout value %@ below 1 is not supported", [timeout stringValue]); 135 | #if DEBUG 136 | NSLog(@"for debug purpose, call ivar setter on operationTimeout with a NSNumber"); 137 | #endif 138 | return self; 139 | } 140 | @synchronized(self) { 141 | [self setOperationTimeout:timeout]; 142 | } 143 | return self; 144 | } 145 | 146 | // MARK: - EVENT LOOP 147 | 148 | - (void)handleRequestsIfNeeded { 149 | if (![self.requestLoopLock tryLock]) { 150 | return; 151 | } 152 | @synchronized (self.requestInvokations) { 153 | for (dispatch_block_t invocation in self.requestInvokations) { 154 | if (invocation) { invocation(); } 155 | } 156 | [self.requestInvokations removeAllObjects]; 157 | [self unsafeKeepAliveCheck]; 158 | [self unsafeDispatchSourceMakeDecision]; 159 | NSMutableArray *newArray = [[NSMutableArray alloc] init]; 160 | for (id object in [self.operableObjects copy]) { 161 | #define NSRemoteOperableObjectCheck(OBJECT) \ 162 | do { \ 163 | if (!(OBJECT)) { continue; } \ 164 | if (![(OBJECT) unsafeInsanityCheckAndReturnDidSuccess]) { \ 165 | [(OBJECT) unsafeDisconnectAndPrepareForRelease]; \ 166 | continue; \ 167 | } \ 168 | } while (0); 169 | NSRemoteOperableObjectCheck(object); 170 | [object unsafeCallNonblockingOperations]; 171 | NSRemoteOperableObjectCheck(object); 172 | [newArray addObject:object]; 173 | } 174 | self.operableObjects = newArray; 175 | [self unsafeDispatchSourceMakeDecision]; 176 | } 177 | [self unsafeReadLastError]; 178 | [self.requestLoopLock unlock]; 179 | } 180 | 181 | - (void)explicitRequestStatusPickup { 182 | @synchronized (self.associatedLoop) { 183 | [self.associatedLoop explicitRequestHandle]; 184 | } 185 | } 186 | 187 | // MARK: - API 188 | 189 | #pragma control 190 | 191 | - (void)requestConnectAndWait { 192 | if (self.destroyed) return; 193 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 194 | __weak typeof(self) magic = self; 195 | @synchronized (self.requestInvokations) { 196 | id block = [^{ 197 | [magic unsafeConnect]; 198 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 199 | } copy]; 200 | [self.requestInvokations addObject:block]; 201 | } 202 | [self explicitRequestStatusPickup]; 203 | MakeDispatchSemaphoreWaitWithTimeout(sem) 204 | } 205 | 206 | - (void)requestDisconnectAndWait { 207 | if (self.destroyed) return; 208 | 209 | /* 210 | kill these values so we will clean the things up if in fly 211 | especially for file transfer 212 | the loop for read and loop for write will check the connectedFileTransfer 213 | each time before execution io rw 214 | so we won't wait too much time before f**k up 215 | */ 216 | self.connected = NO; 217 | self.connectedFileTransfer = NO; 218 | self.authenticated = NO; 219 | 220 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 221 | __weak typeof(self) magic = self; 222 | @synchronized (self.requestInvokations) { 223 | id block = [^{ 224 | [magic unsafeDisconnect]; 225 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 226 | } copy]; 227 | [self.requestInvokations addObject:block]; 228 | } 229 | [self.associatedLoop explicitRequestHandle]; 230 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 231 | } 232 | 233 | - (void)authenticateWith:(NSString*)username andPassword:(NSString*)password { 234 | if (self.destroyed) return; 235 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 236 | __weak typeof(self) magic = self; 237 | @synchronized (self.requestInvokations) { 238 | id block = [^{ 239 | [magic unsafeAuthenticateWith:username 240 | andPassword:password]; 241 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 242 | } copy]; 243 | [self.requestInvokations addObject:block]; 244 | } 245 | [self.associatedLoop explicitRequestHandle]; 246 | MakeDispatchSemaphoreWaitWithTimeout(sem) 247 | } 248 | 249 | - (void)authenticateWith:(NSString*)username andPublicKey:(NSString*)publicKey andPrivateKey:(NSString*)privateKey andPassword:(NSString*)password { 250 | if (self.destroyed) return; 251 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 252 | __weak typeof(self) magic = self; 253 | @synchronized (self.requestInvokations) { 254 | id block = [^{ 255 | [magic unsafeAuthenticateWith:username 256 | andPublicKey:publicKey 257 | andPrivateKey:privateKey 258 | andPassword:password]; 259 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 260 | } copy]; 261 | [self.requestInvokations addObject:block]; 262 | } 263 | [self.associatedLoop explicitRequestHandle]; 264 | MakeDispatchSemaphoreWaitWithTimeout(sem) 265 | } 266 | 267 | #pragma exec 268 | 269 | - (int)beginExecuteWithCommand:(NSString*)withCommand 270 | withTimeout:(NSNumber*)withTimeoutSecond 271 | withOnCreate:(dispatch_block_t)withOnCreate 272 | withOutput:(nullable void (^)(NSString*))withOutput 273 | withContinuationHandler:(nullable BOOL (^)(void))withContinuationBlock { 274 | if (self.destroyed) return 0; 275 | __block int exitCode = 0; 276 | NSLog(@"requesting execute: %@", withCommand); 277 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 278 | __weak typeof(self) magic = self; 279 | @synchronized (self.requestInvokations) { 280 | id block = [^{ 281 | [magic unsafeExecuteRemote:withCommand 282 | withExecTimeout:withTimeoutSecond 283 | withOnCreate:withOnCreate 284 | withOutput:withOutput 285 | withContinuationHandler:withContinuationBlock 286 | withSetExitCode:&exitCode 287 | withCompletionSemaphore:sem]; 288 | } copy]; 289 | [self.requestInvokations addObject:block]; 290 | } 291 | [self.associatedLoop explicitRequestHandle]; 292 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 293 | return exitCode; 294 | } 295 | 296 | - (void)beginShellWithTerminalType:(nullable NSString*)withTerminalType 297 | withOnCreate:(dispatch_block_t)withOnCreate 298 | withTerminalSize:(nullable CGSize (^)(void))withRequestTerminalSize 299 | withWriteDataBuffer:(nullable NSString* (^)(void))withWriteDataBuffer 300 | withOutputDataBuffer:(void (^)(NSString * _Nonnull))withOutputDataBuffer 301 | withContinuationHandler:(BOOL (^)(void))withContinuationBlock; 302 | { 303 | if (self.destroyed) return; 304 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 305 | __weak typeof(self) magic = self; 306 | @synchronized (self.requestInvokations) { 307 | id block = [^{ 308 | [magic unsafeOpenShellWithTerminal:withTerminalType 309 | withTerminalSize:withRequestTerminalSize 310 | withWriteData:withWriteDataBuffer 311 | withOutput:withOutputDataBuffer 312 | withOnCreate:withOnCreate 313 | withContinuationHandler:withContinuationBlock 314 | withCompletionSemaphore:sem]; 315 | } copy]; 316 | [self.requestInvokations addObject:block]; 317 | } 318 | [self.associatedLoop explicitRequestHandle]; 319 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 320 | } 321 | 322 | #pragma port 323 | 324 | - (void)createPortForwardWithLocalPort:(NSNumber*)localPort 325 | withForwardTargetHost:(NSString*)targetHost 326 | withForwardTargetPort:(NSNumber*)targetPort 327 | withOnCreate:(dispatch_block_t)withOnCreate 328 | withContinuationHandler:(BOOL (^)(void))continuationBlock 329 | { 330 | if (self.destroyed) return; 331 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 332 | __weak typeof(self) magic = self; 333 | @synchronized (self.requestInvokations) { 334 | id block = [^{ 335 | [magic unsafeCreatePortForwardWithLocalPort:localPort 336 | withForwardTargetHost:targetHost 337 | withForwardTargetPort:targetPort 338 | withOnCreate:withOnCreate 339 | withContinuationHandler:continuationBlock 340 | withCompletionSemaphore:sem]; 341 | } copy]; 342 | [self.requestInvokations addObject:block]; 343 | } 344 | [self.associatedLoop explicitRequestHandle]; 345 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 346 | } 347 | 348 | - (void)createPortForwardWithRemotePort:(NSNumber*)remotePort 349 | withForwardTargetHost:(NSString*)targetHost 350 | withForwardTargetPort:(NSNumber*)targetPort 351 | withOnCreate:(dispatch_block_t)withOnCreate 352 | withContinuationHandler:(BOOL (^)(void))continuationBlock 353 | { 354 | if (self.destroyed) return; 355 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 356 | __weak typeof(self) magic = self; 357 | @synchronized (self.requestInvokations) { 358 | id block = [^{ 359 | [magic unsafeCreatePortForwardWithRemotePort:remotePort 360 | withForwardTargetHost:targetHost 361 | withForwardTargetPort:targetPort 362 | withOnCreate:withOnCreate 363 | withContinuationHandler:continuationBlock 364 | withCompletionSemaphore:sem]; 365 | } copy]; 366 | [self.requestInvokations addObject:block]; 367 | } 368 | [self.associatedLoop explicitRequestHandle]; 369 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 370 | } 371 | 372 | #pragma sftp 373 | 374 | - (void)requestConnectFileTransferAndWait { 375 | if (self.destroyed) return; 376 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 377 | __weak typeof(self) magic = self; 378 | @synchronized (self.requestInvokations) { 379 | id block = [^{ 380 | [magic unsafeConnectFileTransferWithCompleteBlock:^{ 381 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 382 | }]; 383 | } copy]; 384 | [self.requestInvokations addObject:block]; 385 | } 386 | [self.associatedLoop explicitRequestHandle]; 387 | MakeDispatchSemaphoreWaitWithTimeout(sem); 388 | } 389 | - (void)requestDisconnectFileTransferAndWait { 390 | if (self.destroyed) return; 391 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 392 | @synchronized (self.requestInvokations) { 393 | id block = [^{ 394 | [self unsafeFileTransferCloseFor:self.associatedFileTransfer]; 395 | self.associatedFileTransfer = NULL; 396 | self.connectedFileTransfer = NO; 397 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 398 | } copy]; 399 | [self.requestInvokations addObject:block]; 400 | } 401 | [self.associatedLoop explicitRequestHandle]; 402 | MakeDispatchSemaphoreWaitWithTimeout(sem); 403 | } 404 | 405 | - (nullable NSArray*)requestFileListAt:(NSString*)atDirPath { 406 | if (self.destroyed) return NULL; 407 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 408 | __weak typeof(self) magic = self; 409 | __block NSArray* result = NULL; 410 | @synchronized (self.requestInvokations) { 411 | id block = [^{ 412 | result = [magic unsafeGetDirFileListAt:atDirPath]; 413 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 414 | } copy]; 415 | [self.requestInvokations addObject:block]; 416 | } 417 | [self.associatedLoop explicitRequestHandle]; 418 | MakeDispatchSemaphoreWaitWithTimeout(sem); 419 | return result; 420 | } 421 | - (nullable NSRemoteFile*)requestFileInfoAt:(NSString*)atPath { 422 | if (self.destroyed) return NULL; 423 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 424 | __weak typeof(self) magic = self; 425 | __block NSRemoteFile *result = NULL; 426 | @synchronized (self.requestInvokations) { 427 | id block = [^{ 428 | result = [magic unsafeGetFileInfo:atPath]; 429 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 430 | } copy]; 431 | [self.requestInvokations addObject:block]; 432 | } 433 | [self.associatedLoop explicitRequestHandle]; 434 | MakeDispatchSemaphoreWaitWithTimeout(sem); 435 | return result; 436 | } 437 | 438 | - (BOOL) requestRenameFileAndWait:(NSString *)atPath 439 | withNewPath:(NSString *)newPath 440 | { 441 | if (self.destroyed) return NO; 442 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 443 | __weak typeof(self) magic = self; 444 | __block BOOL success = NO; 445 | @synchronized (self.requestInvokations) { 446 | id block = [^{ 447 | success = [magic unsafeRenameFileAndWait:atPath 448 | withNewPath:newPath]; 449 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 450 | } copy]; 451 | [self.requestInvokations addObject:block]; 452 | } 453 | [self.associatedLoop explicitRequestHandle]; 454 | MakeDispatchSemaphoreWaitWithTimeout(sem); 455 | return success; 456 | } 457 | 458 | - (BOOL)requestUploadForFileAndWait:(NSString*)atPath 459 | toDirectory:(NSString*)toDirectory 460 | onProgress:(NSRemoteFileTransferProgressBlock _Nonnull)onProgress 461 | withContinuationHandler:(BOOL (^)(void))continuationBlock 462 | { 463 | if (self.destroyed) return NO; 464 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 465 | __weak typeof(self) magic = self; 466 | __block BOOL success = NO; 467 | @synchronized (self.requestInvokations) { 468 | id block = [^{ 469 | success = [magic unsafeUploadRecursiveForFile:atPath 470 | toDirectory:toDirectory 471 | onProgress:onProgress 472 | withContinuationHandler:continuationBlock 473 | depth:0]; 474 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 475 | } copy]; 476 | [self.requestInvokations addObject:block]; 477 | } 478 | [self.associatedLoop explicitRequestHandle]; 479 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 480 | return success; 481 | } 482 | 483 | - (BOOL)requestDeleteForFileAndWait:(NSString*)atPath 484 | withProgressBlock:(NSRemoteFileDeleteProgressBlock _Nonnull)onProgress 485 | withContinuationHandler:(BOOL (^)(void))continuationBlock 486 | { 487 | if (self.destroyed) return NO; 488 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 489 | __weak typeof(self) magic = self; 490 | __block BOOL success = NO; 491 | @synchronized (self.requestInvokations) { 492 | id block = [^{ 493 | success = [magic unsafeDeleteForFile:atPath 494 | withProgressBlock:onProgress 495 | withContinuationHandler:continuationBlock]; 496 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 497 | } copy]; 498 | [self.requestInvokations addObject:block]; 499 | } 500 | [self.associatedLoop explicitRequestHandle]; 501 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 502 | return success; 503 | } 504 | 505 | - (BOOL)requestCreateDirAndWait:(NSString*)atPath 506 | { 507 | if (self.destroyed) return NO; 508 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 509 | __weak typeof(self) magic = self; 510 | __block BOOL success = NO; 511 | @synchronized (self.requestInvokations) { 512 | id block = [^{ 513 | success = [magic unsafeCreateDirAndWait:atPath]; 514 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 515 | } copy]; 516 | [self.requestInvokations addObject:block]; 517 | } 518 | [self.associatedLoop explicitRequestHandle]; 519 | MakeDispatchSemaphoreWaitWithTimeout(sem); 520 | return success; 521 | } 522 | 523 | - (BOOL)requestDownloadFromFileAndWait:(NSString*)atPath 524 | toLocalPath:(NSString*)toPath 525 | onProgress:(NSRemoteFileTransferProgressBlock)onProgress 526 | withContinuationHandler:(BOOL (^)(void))continuationBlock 527 | { 528 | if (self.destroyed) return NO; 529 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); 530 | __weak typeof(self) magic = self; 531 | __block BOOL success = NO; 532 | @synchronized (self.requestInvokations) { 533 | id block = [^{ 534 | success = [magic unsafeDownloadRecursiveAtPath:atPath 535 | toLocalPath:toPath 536 | onProgress:onProgress 537 | withContinuationHandler:continuationBlock 538 | depth:0]; 539 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(sem); 540 | } copy]; 541 | [self.requestInvokations addObject:block]; 542 | } 543 | [self.associatedLoop explicitRequestHandle]; 544 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 545 | return success; 546 | } 547 | 548 | // MARK: - HELPER 549 | 550 | - (nullable NSString*)getLastError { 551 | NSString *result; 552 | @synchronized (self.lastError) { 553 | result = self.lastError; 554 | self.lastError = NULL; 555 | } 556 | if ([result isEqualToString:@""]) { 557 | return NULL; 558 | } 559 | return result; 560 | } 561 | 562 | - (nullable NSString*)getLastFileTransferError { 563 | NSString *result; 564 | @synchronized (self.lastFileTransferError) { 565 | result = self.lastFileTransferError; 566 | self.lastFileTransferError = NULL; 567 | } 568 | if ([result isEqualToString:@""]) { 569 | return NULL; 570 | } 571 | return result; 572 | } 573 | 574 | // MARK: - UNCHECKED CONCURRENCY 575 | 576 | #pragma control 577 | 578 | - (void)unsafeConnect { 579 | [self unsafeDisconnect]; 580 | 581 | int sock = [GenericNetworking createSocketWithTargetHost:self.remoteHost 582 | withTargetPort:self.remotePort 583 | requireNonblockingIO:NO]; 584 | if (!sock) { 585 | NSLog(@"failed to create socket to host"); 586 | return; 587 | } 588 | self.associatedSocket = sock; 589 | 590 | self.resolvedRemoteIpAddress = [GenericNetworking getResolvedIpAddressWith:sock]; 591 | 592 | LIBSSH2_SESSION *constructorSession = libssh2_session_init_ex(0, 0, 0, (__bridge void*)(self)); 593 | if (!constructorSession) { 594 | [self unsafeDisconnect]; 595 | return; 596 | } 597 | self.associatedSession = constructorSession; 598 | 599 | libssh2_session_set_timeout(constructorSession, [self.operationTimeout doubleValue] * 1000); 600 | 601 | libssh2_session_set_blocking(constructorSession, 0); 602 | BOOL sessionHandshakeComplete = NO; 603 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 604 | while (true) { 605 | if ([date timeIntervalSinceNow] < 0) { 606 | libssh2_session_set_last_error(self.associatedSession, LIBSSH2_ERROR_TIMEOUT, NULL); 607 | break; 608 | } 609 | long rc = libssh2_session_handshake(constructorSession, sock); 610 | if (rc == LIBSSH2_ERROR_EAGAIN) { 611 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 612 | continue; 613 | } 614 | sessionHandshakeComplete = (rc == 0); 615 | break; 616 | } 617 | if (!sessionHandshakeComplete) { 618 | [self unsafeDisconnect]; 619 | return; 620 | } 621 | 622 | do { 623 | const char *banner = libssh2_session_banner_get(constructorSession); 624 | if (banner) { 625 | NSString *generateBanner = [[NSString alloc] initWithUTF8String:banner]; 626 | self.remoteBanner = generateBanner; 627 | } 628 | } while (0); 629 | 630 | do { 631 | const char *hash = libssh2_hostkey_hash(constructorSession, LIBSSH2_HOSTKEY_HASH_SHA1); 632 | if (hash) { 633 | NSMutableString *fingerprint = [[NSMutableString alloc] 634 | initWithFormat:@"%02X", (unsigned char)hash[0]]; 635 | for (int i = 1; i < 20; i++) { 636 | [fingerprint appendFormat:@":%02X", (unsigned char)hash[i]]; 637 | } 638 | self.remoteFingerPrint = [fingerprint copy]; 639 | } 640 | } while (0); 641 | 642 | // because we are running non-blocking-mode 643 | // we are responsible for sending the keep alive packet 644 | // we set the interval value as smallest 645 | // so wont case other problem (not 1 but 2) 646 | libssh2_keepalive_config(constructorSession, 0, 2); 647 | 648 | self.connected = YES; 649 | NSLog(@"constructed libssh2 session to %@ with %@", self.remoteHost, self.resolvedRemoteIpAddress); 650 | } 651 | 652 | - (void)unsafeDisconnect { 653 | for (id object in [self.operableObjects copy]) { 654 | if (object) { [object unsafeDisconnectAndPrepareForRelease]; } 655 | } 656 | self.operableObjects = [[NSMutableArray alloc] init]; 657 | 658 | [self unsafeFileTransferCloseFor:self.associatedFileTransfer]; 659 | self.associatedFileTransfer = NULL; 660 | self.connectedFileTransfer = NO; 661 | 662 | [self unsafeSessionCloseFor:self.associatedSession]; 663 | self.associatedSession = NULL; 664 | self.connected = NO; 665 | self.authenticated = NO; 666 | 667 | if (self.associatedSocket) { 668 | [GenericNetworking destroyNativeSocket:self.associatedSocket]; 669 | } 670 | self.associatedSocket = 0; 671 | 672 | self.resolvedRemoteIpAddress = NULL; 673 | self.remoteBanner = NULL; 674 | self.remoteFingerPrint = NULL; 675 | 676 | self.keepAliveAttampt = 0; 677 | self.keepAliveLastSuccessAttampt = NULL; 678 | 679 | // any error occurred during connect will result disconnect 680 | [self unsafeReadLastError]; 681 | } 682 | 683 | - (void)unsafeDispatchSourceMakeDecision { 684 | if ([self.operableObjects count] > 0) { 685 | if (!self.associatedSocketSource) { 686 | dispatch_source_t socketDataSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, 687 | self.associatedSocket, 688 | 0, 689 | dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); 690 | if (!socketDataSource) { 691 | NSLog(@"failed to create dispatch source for socket"); 692 | [self unsafeDisconnect]; 693 | return; 694 | } 695 | dispatch_source_set_event_handler(socketDataSource, ^{ 696 | [self.associatedLoop explicitRequestHandle]; 697 | }); 698 | dispatch_resume(socketDataSource); 699 | self.associatedSocketSource = socketDataSource; 700 | } 701 | } else { 702 | if (self.associatedSocketSource) { 703 | dispatch_source_cancel(self.associatedSocketSource); 704 | self.associatedSocketSource = NULL; 705 | } 706 | } 707 | } 708 | 709 | - (void)unsafeKeepAliveCheck { 710 | // some ssh impl wont accept keep alive if not and may break connection 711 | if (!(self.isConnected && self.isAuthenticated)) { 712 | return; 713 | } 714 | 715 | // the session is valid, check if last success attempt is shorter than interval 716 | if (self.keepAliveLastSuccessAttampt) { 717 | NSDate *nextRun = [self.keepAliveLastSuccessAttampt dateByAddingTimeInterval:KEEPALIVE_INTERVAL]; 718 | if ([nextRun timeIntervalSinceNow] >= 0) { 719 | return; 720 | } 721 | } 722 | 723 | // now sending the keep alive packet 724 | self.keepAliveAttampt += 1; 725 | int nextInterval = 0; 726 | int retVal = libssh2_keepalive_send(self.associatedSession, &nextInterval); 727 | 728 | //Returns 0 on success, or LIBSSH2_ERROR_SOCKET_SEND on I/O errors. 729 | if (retVal == 0) { 730 | self.keepAliveLastSuccessAttampt = [[NSDate alloc] init]; 731 | self.keepAliveAttampt = 0; 732 | } else { 733 | // treat anything else as error and close if retry too much times 734 | if (self.keepAliveAttampt > KEEPALIVE_ERROR_TOLERANCE_MAX_RETRY) { 735 | NSLog(@"shell object at %p closing session due to broken pipe", self); 736 | [self unsafeDisconnect]; 737 | return; 738 | } 739 | return; 740 | } 741 | } 742 | 743 | - (void)unsafeReadLastError { 744 | if (!self.associatedSession) { return; } 745 | long long rv = libssh2_session_last_errno(self.associatedSession); 746 | if (rv == 0 || rv == LIBSSH2_ERROR_EAGAIN) { 747 | return; // reset when get 748 | } 749 | char *msg; 750 | int len; 751 | int rrv = libssh2_session_last_error(self.associatedSession, &msg, &len, 0); 752 | // clear error since we have it 753 | libssh2_session_set_last_error(self.associatedSession, 0, NULL); 754 | NSString *message = [[NSString alloc] initWithUTF8String:msg]; 755 | NSLog(@"shell object at %p setting last error %d %@", self, rrv, message); 756 | @synchronized (self.lastError) { 757 | self.lastError = message; 758 | } 759 | } 760 | 761 | - (void)unsafeFileTransferCloseFor:(LIBSSH2_SFTP*)sftp { 762 | if (!sftp) return; 763 | while (libssh2_sftp_shutdown(sftp) == LIBSSH2_ERROR_EAGAIN) {}; 764 | } 765 | 766 | - (void)unsafeSessionCloseFor:(LIBSSH2_SESSION*)session { 767 | if (!session) return; 768 | while (libssh2_session_disconnect(session, "closed by client") == LIBSSH2_ERROR_EAGAIN) {}; 769 | while (libssh2_session_free(session) == LIBSSH2_ERROR_EAGAIN) {}; 770 | } 771 | 772 | - (BOOL)unsafeValidateSession { 773 | do { 774 | if (!self.associatedSocket) { break; } 775 | if (!self.associatedSession) { break; } 776 | if (!self.connected) { break; } 777 | return YES; 778 | } while (0); 779 | [self unsafeDisconnect]; 780 | return NO; 781 | } 782 | 783 | - (BOOL)unsafeValidateSessionSFTP { 784 | do { 785 | if (!self.associatedSocket) { break; } 786 | if (!self.associatedSession) { break; } 787 | if (!self.connected) { break; } 788 | if (!self.isAuthenticated) { break; } 789 | if (!self.associatedFileTransfer) { break; } 790 | return YES; 791 | } while (0); 792 | [self unsafeDisconnect]; 793 | return NO; 794 | } 795 | 796 | #pragma auth 797 | 798 | - (void)unsafeAuthenticateWith:(NSString*)username 799 | andPassword:(NSString*)password { 800 | if (![self unsafeValidateSession]) { 801 | [self unsafeDisconnect]; 802 | return; 803 | } 804 | if (self.authenticated) { 805 | return; 806 | } 807 | LIBSSH2_SESSION *session = self.associatedSession; 808 | BOOL authenticated = NO; 809 | while (true) { 810 | long long rc = libssh2_userauth_password(session, [username UTF8String], [password UTF8String]); 811 | if (rc == LIBSSH2_ERROR_EAGAIN) { 812 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 813 | continue; 814 | } 815 | authenticated = (rc == 0); 816 | break; 817 | } 818 | [self unsafeReadLastError]; 819 | if (authenticated) { 820 | self.authenticated = YES; 821 | NSLog(@"authenticate success"); 822 | } 823 | } 824 | 825 | - (void)unsafeAuthenticateWith:(NSString*)username 826 | andPublicKey:(NSString*)publicKey 827 | andPrivateKey:(NSString*)privateKey 828 | andPassword:(NSString*)password { 829 | if (![self unsafeValidateSession]) { 830 | [self unsafeDisconnect]; 831 | return; 832 | } 833 | if (self.authenticated) { 834 | return; 835 | } 836 | LIBSSH2_SESSION *session = self.associatedSession; 837 | BOOL authenticated = NO; 838 | while (true) { 839 | const char *name = username ? [username UTF8String] : NULL; 840 | unsigned int nl = username ? (unsigned int)strlen(name) : 0; 841 | const char *pub = publicKey ? [publicKey UTF8String] : NULL; 842 | unsigned int pul = publicKey ? (unsigned int)strlen(pub): 0; 843 | const char *pri = privateKey ? [privateKey UTF8String] : NULL; 844 | unsigned int prl = privateKey ? (unsigned int)strlen(pri) : 0; 845 | const char *pwd = password ? [password UTF8String] : NULL; 846 | long long rc = libssh2_userauth_publickey_frommemory(session, 847 | name, (unsigned int)nl, 848 | pub, (unsigned int)pul, 849 | pri, (unsigned int)prl, 850 | pwd); 851 | if (rc == LIBSSH2_ERROR_EAGAIN) { 852 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 853 | continue; 854 | } 855 | authenticated = (rc == 0); 856 | break; 857 | } 858 | [self unsafeReadLastError]; 859 | if (authenticated) { 860 | self.authenticated = YES; 861 | NSLog(@"authenticate success"); 862 | } 863 | } 864 | 865 | #pragma exec 866 | 867 | - (void)unsafeExecuteRemote:(NSString*)command 868 | withExecTimeout:(NSNumber*)timeoutSecond 869 | withOnCreate:(dispatch_block_t)withOnCreate 870 | withOutput:(void (^)(NSString * _Nonnull))responseDataBlock 871 | withContinuationHandler:(BOOL (^)(void))continuationBlock 872 | withSetExitCode:(int*)exitCode 873 | withCompletionSemaphore:(dispatch_semaphore_t)completionSemaphore 874 | { 875 | if (exitCode) { *exitCode = 0; } 876 | 877 | if (![self unsafeValidateSession]) { 878 | [self unsafeDisconnect]; 879 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 880 | return; 881 | } 882 | if (!self.authenticated) { 883 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 884 | return; 885 | } 886 | LIBSSH2_SESSION *session = self.associatedSession; 887 | LIBSSH2_CHANNEL *channel = NULL; 888 | 889 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 890 | while (true) { 891 | if ([date timeIntervalSinceNow] < 0) { 892 | libssh2_session_set_last_error(self.associatedSession, LIBSSH2_ERROR_TIMEOUT, NULL); 893 | break; 894 | } 895 | LIBSSH2_CHANNEL *channelBuilder = libssh2_channel_open_session(session); 896 | if (channelBuilder) { 897 | libssh2_session_set_last_error(session, 0, NULL); 898 | channel = channelBuilder; 899 | break; 900 | } 901 | long rc = libssh2_session_last_errno(session); 902 | if (rc == LIBSSH2_ERROR_EAGAIN) { 903 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 904 | continue; 905 | } 906 | break; 907 | } 908 | [self unsafeReadLastError]; 909 | if (!channel) { 910 | NSLog(@"failed to allocate channel"); 911 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 912 | return; 913 | } 914 | NSRemoteChannel *channelObject = [[NSRemoteChannel alloc] initWithRepresentedSession:session 915 | withRepresentedChanel:channel]; 916 | 917 | BOOL channelStartupCompleted = NO; 918 | while (true) { 919 | long rc = libssh2_channel_exec(channel, [command UTF8String]); 920 | if (rc == LIBSSH2_ERROR_EAGAIN) { usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); continue; } 921 | channelStartupCompleted = (rc == 0); 922 | break; 923 | } 924 | if (!channelStartupCompleted) { 925 | [channelObject unsafeDisconnectAndPrepareForRelease]; 926 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 927 | return; 928 | } 929 | 930 | if ([timeoutSecond doubleValue] > 0) { 931 | [channelObject setChannelTimeoutWith:[timeoutSecond doubleValue]]; 932 | } 933 | 934 | if (responseDataBlock) { [channelObject setRecivedDataChain:responseDataBlock]; } 935 | if (continuationBlock) { [channelObject setContinuationChain:continuationBlock]; } 936 | 937 | if (completionSemaphore) { 938 | [channelObject onTermination:^{ 939 | *exitCode = channelObject.exitStatus; 940 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 941 | }]; 942 | } 943 | 944 | [self.operableObjects addObject:channelObject]; 945 | if (withOnCreate) { withOnCreate(); } 946 | } 947 | 948 | - (void)unsafeOpenShellWithTerminal:(nullable NSString*)terminalType 949 | withTerminalSize:(nullable CGSize (^)(void))requestTerminalSize 950 | withWriteData:(nullable NSString* (^)(void))requestWriteData 951 | withOutput:(void (^)(NSString * _Nonnull))responseDataBlock 952 | withOnCreate:(dispatch_block_t)withOnCreate 953 | withContinuationHandler:(BOOL (^)(void))continuationBlock 954 | withCompletionSemaphore:(dispatch_semaphore_t)completionSemaphore { 955 | if (![self unsafeValidateSession]) { 956 | [self unsafeDisconnect]; 957 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 958 | return; 959 | } 960 | if (!self.authenticated) { 961 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 962 | return; 963 | } 964 | LIBSSH2_SESSION *session = self.associatedSession; 965 | LIBSSH2_CHANNEL *channel = NULL; 966 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 967 | while (true) { 968 | if ([date timeIntervalSinceNow] < 0) { 969 | libssh2_session_set_last_error(self.associatedSession, LIBSSH2_ERROR_TIMEOUT, NULL); 970 | break; 971 | } 972 | LIBSSH2_CHANNEL *channelBuilder = libssh2_channel_open_session(session); 973 | if (channelBuilder) { 974 | libssh2_session_set_last_error(session, 0, NULL); 975 | channel = channelBuilder; 976 | break; 977 | } 978 | long rc = libssh2_session_last_errno(session); 979 | if (rc == LIBSSH2_ERROR_EAGAIN) { 980 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 981 | continue; 982 | } 983 | break; 984 | } 985 | [self unsafeReadLastError]; 986 | if (!channel) { 987 | NSLog(@"failed to allocate channel"); 988 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 989 | return; 990 | } 991 | NSRemoteChannel *channelObject = [[NSRemoteChannel alloc] initWithRepresentedSession:session 992 | withRepresentedChanel:channel]; 993 | if (requestTerminalSize) { [channelObject setTerminalSizeChain:requestTerminalSize]; } 994 | if (requestWriteData) { [channelObject setRequestDataChain:requestWriteData]; } 995 | if (responseDataBlock) { [channelObject setRecivedDataChain:responseDataBlock]; } 996 | if (continuationBlock) { [channelObject setContinuationChain:continuationBlock]; } 997 | 998 | do { 999 | NSString *requestPseudoTermial = @"xterm"; 1000 | if (terminalType) { requestPseudoTermial = terminalType; } 1001 | BOOL requestedPty = NO; 1002 | while (true) { 1003 | long rc = libssh2_channel_request_pty(channel, [requestPseudoTermial UTF8String]); 1004 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1005 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1006 | continue; 1007 | } 1008 | requestedPty = (rc == 0); 1009 | break; 1010 | } 1011 | if (!requestedPty) { 1012 | NSLog(@"failed to request pty"); 1013 | [channelObject unsafeDisconnectAndPrepareForRelease]; 1014 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1015 | return; 1016 | } 1017 | } while (0); 1018 | 1019 | [channelObject unsafeChannelTerminalSizeUpdate]; 1020 | 1021 | do { 1022 | BOOL channelStartupCompleted = NO; 1023 | while (true) { 1024 | long rc = libssh2_channel_shell(channel); 1025 | if (rc == LIBSSH2_ERROR_EAGAIN) { usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); continue; } 1026 | channelStartupCompleted = (rc == 0); 1027 | break; 1028 | } 1029 | if (!channelStartupCompleted) { 1030 | [channelObject unsafeDisconnectAndPrepareForRelease]; 1031 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1032 | return; 1033 | } 1034 | } while (0); 1035 | 1036 | if (completionSemaphore) { 1037 | [channelObject onTermination:^{ 1038 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1039 | }]; 1040 | } 1041 | 1042 | [self.operableObjects addObject:channelObject]; 1043 | if (withOnCreate) { withOnCreate(); } 1044 | } 1045 | 1046 | #pragma forward 1047 | 1048 | - (void)unsafeCreatePortForwardWithLocalPort:(NSNumber*)localPort 1049 | withForwardTargetHost:(NSString*)targetHost 1050 | withForwardTargetPort:(NSNumber*)targetPort 1051 | withOnCreate:(dispatch_block_t)withOnCreate 1052 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1053 | withCompletionSemaphore:(dispatch_semaphore_t)completionSemaphore 1054 | { 1055 | NSLog(@"requested port forward from localhost:%@ --tunnel--> %@:%@", [localPort stringValue], targetHost, [targetPort stringValue]); 1056 | BOOL invalid = NO || 1057 | ![GenericNetworking isValidateWithPort:localPort] || 1058 | ![GenericNetworking isValidateWithPort:targetPort] || 1059 | [targetHost isEqualToString:@""]; 1060 | if (invalid) { 1061 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1062 | NSLog(@"invalid parameter was found"); 1063 | return; 1064 | } 1065 | 1066 | if (![self unsafeValidateSession]) { 1067 | [self unsafeDisconnect]; 1068 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1069 | return; 1070 | } 1071 | if (!self.authenticated) { 1072 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1073 | return; 1074 | } 1075 | 1076 | int sock4 = [GenericNetworking createSocketNonblockingListenerWithLocalPort:localPort]; 1077 | if (sock4 <= 0) { 1078 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1079 | return; 1080 | } 1081 | 1082 | NSLog(@"processing channel startup for direct tcpip"); 1083 | 1084 | LIBSSH2_SESSION *session = self.associatedSession; 1085 | NSLocalForward *operator = [[NSLocalForward alloc] initWithRepresentedSession:session 1086 | withRepresentedSocket:sock4 1087 | withTargetHost:targetHost 1088 | withTargetPort:targetPort 1089 | withLocalPort:localPort 1090 | withTimeout:self.operationTimeout 1091 | ]; 1092 | 1093 | [operator setContinuationChain:continuationBlock]; 1094 | 1095 | if (completionSemaphore) { 1096 | [operator onTermination:^{ 1097 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1098 | }]; 1099 | } 1100 | 1101 | [self.operableObjects addObject:operator]; 1102 | if (withOnCreate) { withOnCreate(); } 1103 | } 1104 | 1105 | - (void)unsafeCreatePortForwardWithRemotePort:(NSNumber*)remotePort 1106 | withForwardTargetHost:(NSString*)targetHost 1107 | withForwardTargetPort:(NSNumber*)targetPort 1108 | withOnCreate:(dispatch_block_t)withOnCreate 1109 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1110 | withCompletionSemaphore:(dispatch_semaphore_t)completionSemaphore 1111 | { 1112 | NSLog(@"requested port forward from remote:%@ --tunnel--> %@:%@", [remotePort stringValue], targetHost, [targetPort stringValue]); 1113 | BOOL invalid = NO || 1114 | ![GenericNetworking isValidateWithPort:remotePort] || 1115 | ![GenericNetworking isValidateWithPort:targetPort] || 1116 | [targetHost isEqualToString:@""]; 1117 | if (invalid) { 1118 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1119 | NSLog(@"invalid parameter was found"); 1120 | return; 1121 | } 1122 | 1123 | if (![self unsafeValidateSession]) { 1124 | [self unsafeDisconnect]; 1125 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1126 | return; 1127 | } 1128 | if (!self.authenticated) { 1129 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1130 | return; 1131 | } 1132 | 1133 | LIBSSH2_SESSION *session = self.associatedSession; 1134 | LIBSSH2_LISTENER *listener = NULL; 1135 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1136 | while (true) { 1137 | if ([date timeIntervalSinceNow] < 0) { 1138 | libssh2_session_set_last_error(self.associatedSession, LIBSSH2_ERROR_TIMEOUT, NULL); 1139 | break; 1140 | } 1141 | LIBSSH2_LISTENER *builder = libssh2_channel_forward_listen_ex(session, 1142 | "127.0.0.1", // for security reason 1143 | [remotePort intValue], 1144 | NULL, 1145 | SOCKET_QUEUE_MAXSIZE); 1146 | if (builder) { 1147 | listener = builder; 1148 | break; 1149 | } 1150 | long rc = libssh2_session_last_errno(session); 1151 | // it's a bug 1152 | // looks like libssh2 reading with dirty memory data 1153 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1154 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1155 | continue; 1156 | } 1157 | break; 1158 | } 1159 | [self unsafeReadLastError]; 1160 | if (!listener) { 1161 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1162 | NSLog(@"libssh2_channel_forward_listen_ex was not able to receive listener"); 1163 | return; 1164 | } 1165 | 1166 | NSRemoteForward *operator = [[NSRemoteForward alloc] initWithRepresentedSession:session 1167 | withRepresentedListener:listener 1168 | withTargetHost:targetHost 1169 | withTargetPort:targetPort 1170 | withTimeout:self.operationTimeout]; 1171 | [operator setContinuationChain:continuationBlock]; 1172 | 1173 | if (completionSemaphore) { 1174 | [operator onTermination:^{ 1175 | DISPATCH_SEMAPHORE_CHECK_SIGNLE(completionSemaphore); 1176 | }]; 1177 | } 1178 | 1179 | [self.operableObjects addObject:operator]; 1180 | if (withOnCreate) { withOnCreate(); } 1181 | } 1182 | 1183 | #pragma sftp 1184 | 1185 | - (void)unsafeConnectFileTransferWithCompleteBlock:(dispatch_block_t)withComplete { 1186 | if (![self unsafeValidateSession]) { 1187 | [self unsafeDisconnect]; 1188 | if (withComplete) withComplete(); 1189 | return; 1190 | } 1191 | if (!self.authenticated) { 1192 | if (withComplete) withComplete(); 1193 | return; 1194 | } 1195 | LIBSSH2_SESSION *session = self.associatedSession; 1196 | LIBSSH2_SFTP *sftp = NULL; 1197 | 1198 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1199 | while (true) { 1200 | if ([date timeIntervalSinceNow] < 0) { 1201 | libssh2_session_set_last_error(self.associatedSession, LIBSSH2_ERROR_TIMEOUT, NULL); 1202 | break; 1203 | } 1204 | LIBSSH2_SFTP *sftpBuilder = libssh2_sftp_init(session); 1205 | if (sftpBuilder) { 1206 | libssh2_session_set_last_error(session, 0, NULL); 1207 | sftp = sftpBuilder; 1208 | break; 1209 | } 1210 | long rc = libssh2_session_last_errno(session); 1211 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1212 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1213 | continue; 1214 | } 1215 | break; 1216 | } 1217 | [self unsafeReadLastError]; 1218 | if (!sftp) { 1219 | if (withComplete) withComplete(); 1220 | [self unsafeFileTransferSetErrorForFile:@"null" pathIsRemote:YES failureReason:@"libssh2_sftp_init was not able to receive session"]; 1221 | return; 1222 | } 1223 | 1224 | self.associatedFileTransfer = sftp; 1225 | self.connectedFileTransfer = YES; 1226 | NSLog(@"libssh2_sftp_init success"); 1227 | if (withComplete) withComplete(); 1228 | } 1229 | 1230 | - (nullable LIBSSH2_SFTP_HANDLE*)unsafeFileTransferOpenDirHandlerWithSession:(LIBSSH2_SESSION*)session 1231 | withSFTP:(LIBSSH2_SFTP*)sftp 1232 | withPath:(NSString*)path 1233 | { 1234 | LIBSSH2_SFTP_HANDLE *handler = NULL; 1235 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1236 | while (true) { 1237 | if ([date timeIntervalSinceNow] < 0) { 1238 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1239 | break; 1240 | } 1241 | const char *cpath = [path UTF8String]; 1242 | LIBSSH2_SFTP_HANDLE *handlerBuilder = libssh2_sftp_open_ex(sftp, 1243 | cpath, 1244 | (unsigned int)strlen(cpath), 1245 | 0, 1246 | 0, 1247 | LIBSSH2_SFTP_OPENDIR); 1248 | if (handlerBuilder) { 1249 | libssh2_session_set_last_error(session, 0, NULL); 1250 | handler = handlerBuilder; 1251 | break; 1252 | } 1253 | long rc = libssh2_session_last_errno(session); 1254 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1255 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1256 | continue; 1257 | } 1258 | break; 1259 | } 1260 | [self unsafeReadLastError]; 1261 | if (!handler) { 1262 | [self unsafeFileTransferSetErrorForFile:path pathIsRemote:YES failureReason:@"remote permission denied"]; 1263 | } 1264 | return handler; 1265 | } 1266 | 1267 | - (nullable LIBSSH2_SFTP_HANDLE*)unsafeFileTransferOpenFileHandlerWithSession:(LIBSSH2_SESSION*)session 1268 | withSFTP:(LIBSSH2_SFTP*)sftp 1269 | withPath:(NSString*)path 1270 | withFlag:(unsigned long)flags 1271 | withMode:(long)mode 1272 | { 1273 | LIBSSH2_SFTP_HANDLE *handler = NULL; 1274 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1275 | while (true) { 1276 | if ([date timeIntervalSinceNow] < 0) { 1277 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1278 | break; 1279 | } 1280 | const char *cpath = [path UTF8String]; 1281 | /* 1282 | by using c path and strlen, utf8 char set with multi length will have the right space 1283 | 1284 | (lldb) po [path length]; 1285 | 25 1286 | 1287 | (lldb) po strlen(cpath) 1288 | 29 1289 | */ 1290 | LIBSSH2_SFTP_HANDLE *handlerBuilder = libssh2_sftp_open_ex(sftp, 1291 | cpath, 1292 | (unsigned int)strlen(cpath), 1293 | flags, 1294 | mode, 1295 | LIBSSH2_SFTP_OPENFILE); 1296 | if (handlerBuilder) { 1297 | libssh2_session_set_last_error(session, 0, NULL); 1298 | handler = handlerBuilder; 1299 | break; 1300 | } 1301 | long rc = libssh2_session_last_errno(session); 1302 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1303 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1304 | continue; 1305 | } 1306 | break; 1307 | } 1308 | [self unsafeReadLastError]; 1309 | if (!handler) { 1310 | [self unsafeFileTransferSetErrorForFile:path pathIsRemote:YES failureReason:@"remote permission denied"]; 1311 | } 1312 | return handler; 1313 | } 1314 | 1315 | - (nullable NSArray*)unsafeGetDirFileListAt:(NSString*)withDirPath { 1316 | if (![self unsafeValidateSessionSFTP]) { 1317 | [self unsafeFileTransferSetErrorForFile:withDirPath pathIsRemote:YES failureReason:@"connection broken"]; 1318 | return NULL; 1319 | } 1320 | LIBSSH2_SESSION *session = self.associatedSession; 1321 | LIBSSH2_SFTP *sftp = self.associatedFileTransfer; 1322 | LIBSSH2_SFTP_HANDLE *handle = [self unsafeFileTransferOpenDirHandlerWithSession:session 1323 | withSFTP:sftp 1324 | withPath:withDirPath]; 1325 | if (!handle) { 1326 | [self unsafeFileTransferSetErrorForFile:withDirPath pathIsRemote:YES failureReason:@"remote permission denied"]; 1327 | return NULL; 1328 | } 1329 | 1330 | NSArray *ignoredFiles = @[@".", @".."]; 1331 | NSMutableArray *contents = [NSMutableArray array]; 1332 | 1333 | long long rc = 0; 1334 | do { 1335 | char buffer[512]; 1336 | memset(buffer, 0, sizeof(buffer)); 1337 | LIBSSH2_SFTP_ATTRIBUTES fileAttributes = { 0 }; 1338 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1339 | while (true) { 1340 | if ([date timeIntervalSinceNow] < 0) { 1341 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1342 | break; 1343 | } 1344 | rc = libssh2_sftp_readdir(handle, buffer, sizeof(buffer), &fileAttributes); 1345 | if (rc >= 0) { break; } // read success 1346 | if (rc == LIBSSH2_ERROR_EAGAIN) { usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); continue; } // go around 1347 | break; 1348 | } 1349 | if (rc > 0) { 1350 | NSString *fileName = [[NSString alloc] initWithBytes:buffer length:rc encoding:NSUTF8StringEncoding]; 1351 | if (![ignoredFiles containsObject:fileName]) { 1352 | // Append a "/" at the end of all directories 1353 | NSRemoteFile *file = [[NSRemoteFile alloc] initWithFilename:fileName]; 1354 | [file populateAttributes:fileAttributes]; 1355 | [contents addObject:file]; 1356 | } 1357 | } 1358 | } while (rc > 0); 1359 | 1360 | while (libssh2_sftp_closedir(handle) == LIBSSH2_ERROR_EAGAIN) {}; 1361 | if (rc < 0) { 1362 | [self unsafeFileTransferSetErrorForFile:withDirPath pathIsRemote:YES failureReason:@"remote permission denied"]; 1363 | return NULL; 1364 | } 1365 | [contents sortUsingDescriptors: @[ 1366 | [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES] 1367 | ]]; 1368 | return contents; 1369 | } 1370 | 1371 | - (nullable NSRemoteFile*)unsafeGetFileInfo:(NSString*)atPath 1372 | { 1373 | if (![self unsafeValidateSessionSFTP]) { 1374 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"connection broken"]; 1375 | return NULL; 1376 | } 1377 | LIBSSH2_SESSION *session = self.associatedSession; 1378 | LIBSSH2_SFTP *sftp = self.associatedFileTransfer; 1379 | LIBSSH2_SFTP_HANDLE *handle = [self unsafeFileTransferOpenFileHandlerWithSession:session 1380 | withSFTP:sftp 1381 | withPath:atPath 1382 | withFlag:LIBSSH2_FXF_READ 1383 | withMode:0]; 1384 | if (!handle) { 1385 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"remote permission denied"]; 1386 | return NULL; 1387 | } 1388 | 1389 | LIBSSH2_SFTP_ATTRIBUTES fileAttributes = { 0 }; 1390 | BOOL statSuccess = NO; 1391 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1392 | while (true) { 1393 | if ([date timeIntervalSinceNow] < 0) { 1394 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1395 | break; 1396 | } 1397 | ssize_t rc = libssh2_sftp_fstat(handle, &fileAttributes); 1398 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1399 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1400 | continue; 1401 | } 1402 | if (rc == 0) { 1403 | statSuccess = YES; 1404 | break; 1405 | } 1406 | break; 1407 | } 1408 | 1409 | while (libssh2_sftp_closedir(handle) == LIBSSH2_ERROR_EAGAIN) {}; 1410 | 1411 | if (!statSuccess) { 1412 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"remote permission denied"]; 1413 | return NULL; 1414 | } 1415 | NSRemoteFile *file = [[NSRemoteFile alloc] initWithFilename:atPath.lastPathComponent]; 1416 | [file populateAttributes:fileAttributes]; 1417 | 1418 | return file; 1419 | } 1420 | 1421 | - (BOOL)unsafeUploadForFile:(NSString*)atPath 1422 | toDirectory:(NSString*)toDirectory 1423 | onProgress:(NSRemoteFileTransferProgressBlock _Nonnull)onProgress 1424 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1425 | { 1426 | NSString *localPath = [atPath stringByExpandingTildeInPath]; 1427 | NSURL *localFile = [[NSURL alloc] initFileURLWithPath:localPath]; 1428 | NSURL *remoteFile = [[NSURL alloc] initFileURLWithPath:toDirectory]; 1429 | remoteFile = [remoteFile URLByAppendingPathComponent:localFile.lastPathComponent]; 1430 | 1431 | // NSLog(@"uploading %@ to %@", localFile.path, remoteFile.path); 1432 | 1433 | BOOL isDir = NO; 1434 | BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:localFile.path isDirectory:&isDir]; 1435 | if (!exists) { 1436 | [self unsafeFileTransferSetErrorForFile:localFile.path pathIsRemote:NO failureReason:@"file not found"]; 1437 | return NO; 1438 | } 1439 | 1440 | if (![self unsafeValidateSession]) { return NO; } 1441 | LIBSSH2_SESSION *session = self.associatedSession; 1442 | 1443 | if (isDir) { 1444 | [self unsafeFileTransferSetErrorForFile:localFile.path 1445 | pathIsRemote:NO 1446 | failureReason:@"this function does not support upload directory"]; 1447 | return NO; 1448 | } 1449 | // NSLog(@"requesting upload file at %@ to %@", localFile.path, remoteFile.path); 1450 | FILE *f = fopen([localFile.path UTF8String], "rb"); 1451 | if (!f) { 1452 | [self unsafeFileTransferSetErrorForFile:localFile.path pathIsRemote:NO failureReason:@"failed to read"]; 1453 | return NO; 1454 | } 1455 | struct stat fi; 1456 | memset(&fi, 0, sizeof(fi)); 1457 | stat([localFile.path UTF8String], &fi); 1458 | LIBSSH2_CHANNEL *channel = NULL; 1459 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1460 | while (true) { 1461 | if ([date timeIntervalSinceNow] < 0) { 1462 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1463 | break; 1464 | } 1465 | LIBSSH2_CHANNEL *channelBuilder = libssh2_scp_send64(session, 1466 | [remoteFile.path UTF8String], 1467 | fi.st_mode & 0644, 1468 | (unsigned long)fi.st_size, 1469 | 0, 1470 | 0); 1471 | if (channelBuilder) { 1472 | libssh2_session_set_last_error(session, 0, NULL); 1473 | channel = channelBuilder; 1474 | break; 1475 | } 1476 | long rc = libssh2_session_last_errno(session); 1477 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1478 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1479 | continue; 1480 | } 1481 | break; 1482 | } 1483 | [self unsafeReadLastError]; 1484 | if (!channel) { 1485 | [self unsafeFileTransferSetErrorForFile:localFile.path pathIsRemote:NO failureReason:@"remote permission denied"]; 1486 | return NO; 1487 | } 1488 | 1489 | NSDate *begin = [[NSDate alloc] init]; 1490 | NSDate *previousProgressSent = [[NSDate alloc] initWithTimeIntervalSince1970:0]; 1491 | 1492 | // char *buff = (char*)malloc(SFTP_BUFFER_SIZE); 1493 | char buff[SFTP_BUFFER_SIZE]; 1494 | size_t read_size; 1495 | char *ptr; 1496 | NSUInteger sent_size = 0; 1497 | NSUInteger total_size = fi.st_size; 1498 | while (sent_size < total_size) { 1499 | if (continuationBlock && !continuationBlock()) { break; } 1500 | if (!self.isConnectedFileTransfer) { break; } 1501 | // memset(buff, 0, SFTP_BUFFER_SIZE); 1502 | memset(buff, 0, sizeof(buff)); 1503 | read_size = fread(buff, 1, sizeof(buff), f); 1504 | if (read_size <= 0) { 1505 | break; // done or error 1506 | } 1507 | ptr = buff; 1508 | long long rc = LIBSSH2_ERROR_EAGAIN; 1509 | // we are not really count the timeout here tho 1510 | while (rc == LIBSSH2_ERROR_EAGAIN || read_size) { 1511 | rc = libssh2_channel_write(channel, ptr, read_size); 1512 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1513 | continue; 1514 | } else if (rc > 0) { 1515 | // rc indicates how many bytes were written this time 1516 | sent_size += rc; 1517 | ptr += rc; 1518 | read_size -= rc; 1519 | } else { 1520 | // has error 1521 | break; 1522 | } 1523 | }; 1524 | if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) { 1525 | break; 1526 | } 1527 | if (onProgress && [previousProgressSent timeIntervalSinceNow] < -0.2) { 1528 | previousProgressSent = [[NSDate alloc] init]; 1529 | NSTimeInterval interval = [[[NSDate alloc] init] timeIntervalSinceDate:begin]; 1530 | double speed = 0; 1531 | if (interval != 0) { 1532 | speed = sent_size / interval; 1533 | } 1534 | NSProgress *progress = [[NSProgress alloc] init]; 1535 | [progress setTotalUnitCount:total_size]; 1536 | [progress setCompletedUnitCount:sent_size]; 1537 | dispatch_async(dispatch_get_main_queue(), ^{ 1538 | onProgress(atPath, progress, speed); 1539 | }); 1540 | } 1541 | }; 1542 | // free(buff); 1543 | [self unsafeReadLastError]; 1544 | 1545 | if (sent_size < total_size) { 1546 | [self unsafeFileTransferSetErrorForFile:localFile.path 1547 | pathIsRemote:NO 1548 | failureReason:@"transport sent did not write enough data"]; 1549 | } 1550 | 1551 | fclose(f); 1552 | LIBSSH2_CHANNEL_SHUTDOWN(channel); 1553 | // NSLog(@"upload %@ to %@ done", localFile.path, remoteFile.path); 1554 | if (onProgress) { 1555 | NSProgress *progress = [[NSProgress alloc] init]; 1556 | [progress setTotalUnitCount:total_size]; 1557 | [progress setCompletedUnitCount:total_size]; 1558 | NSTimeInterval interval = [[[NSDate alloc] init] timeIntervalSinceDate:begin]; 1559 | double speed = 0; 1560 | if (interval != 0) { 1561 | speed = sent_size / interval; 1562 | } 1563 | dispatch_async(dispatch_get_main_queue(), ^{ 1564 | onProgress(atPath, progress, speed); 1565 | }); 1566 | } 1567 | return sent_size == total_size; 1568 | } 1569 | 1570 | - (BOOL)unsafeUploadRecursiveForFile:(NSString*)atPath 1571 | toDirectory:(NSString*)toDirectory 1572 | onProgress:(NSRemoteFileTransferProgressBlock _Nonnull)onProgress 1573 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1574 | depth:(int)depth 1575 | { 1576 | @autoreleasepool { 1577 | if (depth > SFTP_RECURSIVE_DEPTH) { 1578 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:NO failureReason:@"too many items inside dir"]; 1579 | return NO; 1580 | } 1581 | if (continuationBlock && !continuationBlock()) { 1582 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:NO failureReason:@"user cancel"]; 1583 | return NO; 1584 | } 1585 | BOOL isDir = NO; 1586 | BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:atPath isDirectory:&isDir]; 1587 | if (!exists) { 1588 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:NO failureReason:@"file not found"]; 1589 | return NO; 1590 | } 1591 | 1592 | if (isDir) { 1593 | NSError *error = NULL; 1594 | NSArray *content = [NSFileManager.defaultManager contentsOfDirectoryAtPath:atPath 1595 | error:&error]; 1596 | if (error) { 1597 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:NO failureReason:@"permission denied"]; 1598 | return NO; 1599 | } 1600 | NSURL *localBase = [[NSURL alloc] initFileURLWithPath:atPath]; 1601 | NSURL *remoteBase = [[[NSURL alloc] initFileURLWithPath:toDirectory] 1602 | URLByAppendingPathComponent:localBase.lastPathComponent]; 1603 | // now create dir on remote 1604 | if (![self unsafeCreateDirAndWait:remoteBase.path]) { 1605 | [self unsafeFileTransferSetErrorForFile:remoteBase.path pathIsRemote:YES failureReason:@"remote permission denied"]; 1606 | return NO; 1607 | } 1608 | for (NSString *file in content) { 1609 | NSURL *atPath = [localBase URLByAppendingPathComponent:file]; 1610 | NSURL *toDir = remoteBase; 1611 | BOOL ret = [self unsafeUploadRecursiveForFile:atPath.path 1612 | toDirectory:toDir.path 1613 | onProgress:onProgress 1614 | withContinuationHandler:continuationBlock 1615 | depth:depth + 1]; 1616 | if (!ret) { return NO; } 1617 | } 1618 | return YES; 1619 | } else { 1620 | return [self unsafeUploadForFile:atPath 1621 | toDirectory:toDirectory 1622 | onProgress:onProgress 1623 | withContinuationHandler:continuationBlock]; 1624 | } 1625 | } 1626 | } 1627 | 1628 | - (BOOL)unsafeDeleteForFile:(NSString*)atPath 1629 | withProgressBlock:(NSRemoteFileDeleteProgressBlock _Nonnull)onProgress 1630 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1631 | { 1632 | if (![atPath hasPrefix:@"/"] || atPath.length < 1) { 1633 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"invalid parameters"]; 1634 | return NO; 1635 | } 1636 | 1637 | #if DEBUG 1638 | assert(atPath.length > 1); 1639 | #endif 1640 | 1641 | if (![self unsafeValidateSessionSFTP]) { return NO; } 1642 | LIBSSH2_SESSION *session = self.associatedSession; 1643 | LIBSSH2_SFTP *sftp = self.associatedFileTransfer; 1644 | // NSLog(@"removing file %@", atPath); 1645 | 1646 | return [self unsafeDeleteRecursivelyForPathAndReturnContinue:atPath 1647 | withSession:session 1648 | withFileTransferHandler:sftp 1649 | depth:0 1650 | withProgressBlock:onProgress 1651 | withContinuationHandler:continuationBlock]; 1652 | } 1653 | 1654 | - (BOOL)unsafeCreateDirAndWait:(NSString*)atPath 1655 | { 1656 | if (![atPath hasPrefix:@"/"] || atPath.length < 1) { 1657 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"invalid parameter"]; 1658 | return NO; 1659 | } 1660 | 1661 | #if DEBUG 1662 | assert(atPath.length > 1); 1663 | #endif 1664 | 1665 | if (![self unsafeValidateSessionSFTP]) { 1666 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"connection broken"]; 1667 | return NO; 1668 | } 1669 | LIBSSH2_SESSION *session = self.associatedSession; 1670 | LIBSSH2_SFTP *sftp = self.associatedFileTransfer; 1671 | // NSLog(@"creating dir %@", atPath); 1672 | 1673 | // before we create, ask to check if already exists 1674 | NSRemoteFile *file = [self unsafeGetFileInfo:atPath]; 1675 | if (file) { 1676 | if (file.isDirectory) { return YES; } // already exists 1677 | // otherwise error later on 1678 | } 1679 | 1680 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1681 | long long rc = 0; 1682 | while (true) { 1683 | if ([date timeIntervalSinceNow] < 0) { 1684 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1685 | break; 1686 | } 1687 | int mode = 0 1688 | | LIBSSH2_SFTP_S_IRWXU 1689 | | LIBSSH2_SFTP_S_IRGRP 1690 | | LIBSSH2_SFTP_S_IXGRP 1691 | | LIBSSH2_SFTP_S_IROTH 1692 | | LIBSSH2_SFTP_S_IXOTH; 1693 | const char *cpath = [atPath UTF8String]; 1694 | rc = libssh2_sftp_mkdir_ex(sftp, cpath, (unsigned int)strlen(cpath), mode); 1695 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1696 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1697 | continue; 1698 | } 1699 | break; 1700 | } 1701 | [self unsafeReadLastError]; 1702 | if (rc != 0) { 1703 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"remote permission denied"]; 1704 | return NO; 1705 | } 1706 | return YES; 1707 | } 1708 | 1709 | - (BOOL)unsafeDeleteRecursivelyForPathAndReturnContinue:(NSString*)atPath 1710 | withSession:(LIBSSH2_SESSION*)session 1711 | withFileTransferHandler:(LIBSSH2_SFTP*)sftp 1712 | depth:(int)depth 1713 | withProgressBlock:(NSRemoteFileDeleteProgressBlock _Nonnull)onProgress 1714 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1715 | { 1716 | if (depth > SFTP_RECURSIVE_DEPTH) { 1717 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"too many items inside dir"]; 1718 | return NO; 1719 | } 1720 | if (continuationBlock && !continuationBlock()) { 1721 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"user cancel"]; 1722 | return NO; 1723 | } 1724 | 1725 | // get file info at path 1726 | NSRemoteFile *file = [self unsafeGetFileInfo:atPath]; 1727 | // note that we are unable to get handler for fstat with a dead link 1728 | // call unlink is still possible, but we are unable to check for isDir 1729 | if (file && file.isDirectory) { 1730 | NSURL *curr = [[NSURL alloc] initFileURLWithPath:atPath]; 1731 | NSArray *array = [self unsafeGetDirFileListAt:curr.path]; 1732 | for (NSRemoteFile *file in array) { 1733 | NSURL *res = [curr URLByAppendingPathComponent:file.name]; 1734 | BOOL ret = [self unsafeDeleteRecursivelyForPathAndReturnContinue:res.path 1735 | withSession:session 1736 | withFileTransferHandler:sftp 1737 | depth:depth + 1 1738 | withProgressBlock:onProgress 1739 | withContinuationHandler:continuationBlock]; 1740 | if (!ret) { return NO; } 1741 | } 1742 | // NSLog(@"calling rmdir at %@", atPath); 1743 | if (onProgress) { 1744 | dispatch_async(dispatch_get_main_queue(), ^{ 1745 | onProgress(atPath); 1746 | }); 1747 | } 1748 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1749 | long long rc = 0; 1750 | while (true) { 1751 | if ([date timeIntervalSinceNow] < 0) { 1752 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1753 | break; 1754 | } 1755 | const char *cpath = [atPath UTF8String]; 1756 | rc = libssh2_sftp_rmdir_ex(sftp, cpath, (unsigned int)strlen(cpath)); 1757 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1758 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1759 | continue; 1760 | } 1761 | break; 1762 | } 1763 | [self unsafeReadLastError]; 1764 | if (rc != 0) { 1765 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"remote permission denied"]; 1766 | return NO; 1767 | } 1768 | return YES; 1769 | } else { 1770 | // NSLog(@"calling unlink at %@", atPath); 1771 | if (onProgress) { 1772 | dispatch_async(dispatch_get_main_queue(), ^{ 1773 | onProgress(atPath); 1774 | }); 1775 | } 1776 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1777 | long long rc = 0; 1778 | while (true) { 1779 | if ([date timeIntervalSinceNow] < 0) { 1780 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1781 | break; 1782 | } 1783 | const char *cpath = [atPath UTF8String]; 1784 | rc = libssh2_sftp_unlink_ex(sftp, cpath, (unsigned int)strlen(cpath)); 1785 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1786 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1787 | continue; 1788 | } 1789 | break; 1790 | } 1791 | [self unsafeReadLastError]; 1792 | if (rc != 0) { 1793 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"remote permission denied"]; 1794 | return NO; 1795 | } 1796 | return YES; 1797 | } 1798 | } 1799 | 1800 | - (BOOL)unsafeDownloadFromFileAndWait:(NSString*)atFullPath // full path 1801 | toLocalPath:(NSString*)toFullPath // full path 1802 | onProgress:(NSRemoteFileTransferProgressBlock)onProgress 1803 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1804 | { 1805 | // we are not in charge to fix any stuff related to path 1806 | // we are called from ourselves 1807 | NSString *atPath = atFullPath; 1808 | NSURL *localFile = [[NSURL alloc] initFileURLWithPath:toFullPath]; 1809 | // NSString *toPath = toFullPath; 1810 | NSURL *remoteFile = [[NSURL alloc] initFileURLWithPath:atFullPath]; 1811 | 1812 | // NSLog(@"%@ to %@", remoteFile.path, localFile.path); 1813 | 1814 | if (![self unsafeValidateSessionSFTP]) { 1815 | [self unsafeFileTransferSetErrorForFile:atFullPath pathIsRemote:YES failureReason:@"broken connection"]; 1816 | return NO; 1817 | } 1818 | LIBSSH2_SESSION *session = self.associatedSession; 1819 | LIBSSH2_SFTP *sftp = self.associatedFileTransfer; 1820 | LIBSSH2_SFTP_HANDLE *handle = [self unsafeFileTransferOpenFileHandlerWithSession:session 1821 | withSFTP:sftp 1822 | withPath:atPath 1823 | withFlag:LIBSSH2_FXF_READ 1824 | withMode:0]; 1825 | if (!handle) { 1826 | [self unsafeFileTransferSetErrorForFile:atFullPath pathIsRemote:YES failureReason:@"broken connection"]; 1827 | return NO; 1828 | } 1829 | // NSLog(@"downloading file %@ to %@", atPath, toPath); 1830 | 1831 | struct stat fi; 1832 | memset(&fi, 0, sizeof(fi)); 1833 | LIBSSH2_CHANNEL *channel = NULL; 1834 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 1835 | while (true) { 1836 | if ([date timeIntervalSinceNow] < 0) { 1837 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 1838 | break; 1839 | } 1840 | LIBSSH2_CHANNEL *channelBuilder = libssh2_scp_recv(session, [atPath UTF8String], &fi); 1841 | if (channelBuilder) { 1842 | libssh2_session_set_last_error(session, 0, NULL); 1843 | channel = channelBuilder; 1844 | break; 1845 | } 1846 | long long rc = libssh2_session_last_errno(session); 1847 | if (rc == LIBSSH2_ERROR_EAGAIN) { 1848 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 1849 | continue; 1850 | } 1851 | break; 1852 | } 1853 | [self unsafeReadLastError]; 1854 | if (!channel) { 1855 | libssh2_sftp_close_handle(handle); 1856 | [self unsafeFileTransferSetErrorForFile:atFullPath pathIsRemote:YES failureReason:@"broken connection"]; 1857 | return NO; 1858 | } 1859 | 1860 | int f = open([localFile.path UTF8String], O_WRONLY | O_CREAT, 0644); 1861 | if (!f) { 1862 | LIBSSH2_CHANNEL_SHUTDOWN(channel); 1863 | libssh2_sftp_close_handle(handle); 1864 | [self unsafeFileTransferSetErrorForFile:atFullPath pathIsRemote:YES failureReason:@"broken connection"]; 1865 | return NO; 1866 | } 1867 | 1868 | NSDate *begin = [[NSDate alloc] init]; 1869 | NSDate *lastProgress = [[NSDate alloc] initWithTimeIntervalSince1970:0]; 1870 | 1871 | // do note that the file could be empty 1872 | // fi.st_size may be zero, the entire loop is going to be skipped 1873 | long long recv_size = 0; 1874 | // char *buff = (char*)malloc(SFTP_BUFFER_SIZE); 1875 | char buff[SFTP_BUFFER_SIZE]; 1876 | while (recv_size < fi.st_size) { 1877 | if (continuationBlock && !continuationBlock()) { break; } 1878 | if (!self.isConnectedFileTransfer) { break; } 1879 | long long recv_decision = sizeof(buff); 1880 | // memset(buff, 0, SFTP_BUFFER_SIZE); 1881 | memset(buff, 0, sizeof(buff)); 1882 | if ((fi.st_size - recv_size) < recv_decision) { 1883 | // do not write over! 1884 | // libssh2_channel_read may have dirty data 1885 | recv_decision = (size_t)(fi.st_size - recv_size); 1886 | } 1887 | long long recv_write_size = libssh2_channel_read(channel, buff, recv_decision); 1888 | if (recv_write_size < 0) { 1889 | if (recv_write_size == LIBSSH2_ERROR_EAGAIN) { 1890 | // do not change anything 1891 | continue; 1892 | } 1893 | // error occurred 1894 | break; 1895 | } 1896 | if (recv_write_size > 0) { 1897 | long long f_write_size = write(f, buff, recv_write_size); 1898 | if (f_write_size < 0) { 1899 | NSLog(@"failed to write returns %lld", f_write_size); 1900 | break; 1901 | } 1902 | if (f_write_size < recv_write_size) { 1903 | NSLog(@"write call failed to write all buffer, required %lld returns %lld", 1904 | f_write_size, 1905 | recv_write_size); 1906 | break; 1907 | } 1908 | recv_size += f_write_size; 1909 | } 1910 | if (onProgress) { 1911 | NSDate *now = [[NSDate alloc] init]; 1912 | if ([now timeIntervalSinceDate:lastProgress] > 0.1) { 1913 | lastProgress = now; 1914 | NSTimeInterval interval = [now timeIntervalSinceDate:begin]; 1915 | long long speed = 0; 1916 | if (interval) { 1917 | speed = recv_size / interval; 1918 | } 1919 | NSProgress *progress = [[NSProgress alloc] init]; 1920 | [progress setTotalUnitCount:fi.st_size]; 1921 | [progress setCompletedUnitCount:recv_size]; 1922 | dispatch_async(dispatch_get_main_queue(), ^{ 1923 | onProgress(remoteFile.path, progress, speed); 1924 | }); 1925 | } 1926 | } 1927 | } 1928 | // free(buff); 1929 | [self unsafeReadLastError]; 1930 | if (onProgress) { 1931 | NSProgress *progress = [[NSProgress alloc] init]; 1932 | [progress setTotalUnitCount:fi.st_size]; 1933 | [progress setCompletedUnitCount:recv_size]; 1934 | NSTimeInterval interval = [[[NSDate alloc] init] timeIntervalSinceDate:begin]; 1935 | long long speed = 0; 1936 | if (interval) { 1937 | speed = recv_size / interval; 1938 | } 1939 | dispatch_async(dispatch_get_main_queue(), ^{ 1940 | onProgress(remoteFile.path, progress, speed); 1941 | }); 1942 | } 1943 | if (recv_size != fi.st_size) { 1944 | [self unsafeFileTransferSetErrorForFile:atFullPath 1945 | pathIsRemote:YES 1946 | failureReason:@"transport receive did not write full data"]; 1947 | } 1948 | [self unsafeReadLastError]; 1949 | 1950 | close(f); 1951 | LIBSSH2_CHANNEL_SHUTDOWN(channel); 1952 | libssh2_sftp_close_handle(handle); 1953 | return recv_size == fi.st_size; 1954 | } 1955 | 1956 | - (BOOL)unsafeDownloadRecursiveAtPath:(NSString*)atPath 1957 | toLocalPath:(NSString*)toPath // target path, full path or inherit name from target 1958 | onProgress:(NSRemoteFileTransferProgressBlock)onProgress 1959 | withContinuationHandler:(BOOL (^)(void))continuationBlock 1960 | depth:(int)depth 1961 | { 1962 | @autoreleasepool { 1963 | if (depth > SFTP_RECURSIVE_DEPTH) { 1964 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"too many items inside dir"]; 1965 | return NO; 1966 | } 1967 | if (continuationBlock && !continuationBlock()) { 1968 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"user canceled"]; 1969 | return NO; 1970 | } 1971 | 1972 | atPath = [atPath stringByExpandingTildeInPath]; 1973 | 1974 | NSURL *remoteFile = [[NSURL alloc] initFileURLWithPath:atPath]; 1975 | NSURL *localFile = [[NSURL alloc] initFileURLWithPath:toPath]; 1976 | 1977 | // inherit filename from remote file path 1978 | if ([toPath hasSuffix:@"/"]) { 1979 | localFile = [localFile URLByAppendingPathComponent:atPath.lastPathComponent]; 1980 | } 1981 | 1982 | // now make sure the dir exists 1983 | do { 1984 | NSURL *dir = [localFile URLByDeletingLastPathComponent]; 1985 | [NSFileManager.defaultManager createDirectoryAtURL:dir 1986 | withIntermediateDirectories:YES 1987 | attributes:NULL 1988 | error:NULL]; 1989 | BOOL isDir; 1990 | BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:dir.path 1991 | isDirectory:&isDir]; 1992 | if (!exists || !isDir) { 1993 | [self unsafeFileTransferSetErrorForFile:dir.path pathIsRemote:NO failureReason:@"permission denied"]; 1994 | return NO; 1995 | } 1996 | } while (false); 1997 | 1998 | // get list from remote 1999 | NSRemoteFile *file = [self unsafeGetFileInfo:atPath]; 2000 | if (file && file.isDirectory) { 2001 | NSArray *array = [self unsafeGetDirFileListAt:atPath]; 2002 | if (!array) { 2003 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"failed to retrieve information"]; 2004 | return NO; 2005 | } 2006 | // now because it is a directory, we create one on our local path 2007 | NSURL *localBase = localFile; // don't remove this, it will make code cleaner 2008 | // TODO: Copy All File Attribute 2009 | // NSLog(@"creating dir %@ to %@", remoteFile.path, localBase.path); 2010 | [NSFileManager.defaultManager createDirectoryAtURL:localBase 2011 | withIntermediateDirectories:YES 2012 | attributes:NULL 2013 | error:NULL]; 2014 | BOOL isDir = NO; 2015 | BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:localBase.path isDirectory:&isDir]; 2016 | if (!exists || !isDir) { 2017 | [self unsafeFileTransferSetErrorForFile:localBase.path pathIsRemote:NO failureReason:@"permission denied"]; 2018 | return NO; 2019 | } 2020 | NSURL *base = [[NSURL alloc] initFileURLWithPath:atPath]; 2021 | for (NSRemoteFile *file in array) { 2022 | NSURL *targetPath = [base URLByAppendingPathComponent:file.name]; 2023 | NSURL *localTargetPath = [localBase URLByAppendingPathComponent:file.name]; 2024 | int ret = [self unsafeDownloadRecursiveAtPath:targetPath.path 2025 | toLocalPath:localTargetPath.path 2026 | onProgress:onProgress 2027 | withContinuationHandler:continuationBlock 2028 | depth:depth + 1]; 2029 | if (!ret) { return NO; } 2030 | } 2031 | return YES; 2032 | } else { 2033 | // treat anything else as regular file or we will handle error later on 2034 | // now because it is a "regular" file, overwrite requires to remove it 2035 | BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:localFile.path]; 2036 | if (exists) { 2037 | NSError *error = NULL; 2038 | [NSFileManager.defaultManager removeItemAtURL:localFile error:&error]; 2039 | if (error) { 2040 | [self unsafeFileTransferSetErrorForFile:localFile.path pathIsRemote:NO failureReason:@"permission denied"]; 2041 | return NO; 2042 | } 2043 | } 2044 | return [self unsafeDownloadFromFileAndWait:remoteFile.path 2045 | toLocalPath:localFile.path 2046 | onProgress:onProgress 2047 | withContinuationHandler:continuationBlock]; 2048 | } 2049 | } 2050 | } 2051 | 2052 | - (BOOL)unsafeRenameFileAndWait:(NSString *)atPath 2053 | withNewPath:(NSString *)newPath 2054 | { 2055 | if (![atPath hasPrefix:@"/"] || atPath.length < 1 || ![newPath hasPrefix:@"/"] || newPath.length < 1) { 2056 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"invalid parameter"]; 2057 | return NO; 2058 | } 2059 | 2060 | #if DEBUG 2061 | assert(atPath.length > 1); 2062 | assert(newPath.length > 1); 2063 | #endif 2064 | 2065 | if (![self unsafeValidateSessionSFTP]) { 2066 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"connection broken"]; 2067 | return NO; 2068 | } 2069 | LIBSSH2_SESSION *session = self.associatedSession; 2070 | LIBSSH2_SFTP *sftp = self.associatedFileTransfer; 2071 | // NSLog(@"rename %@ to %@", atPath, newPath); 2072 | 2073 | NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:[self.operationTimeout intValue]]; 2074 | long long rc = 0; 2075 | while (true) { 2076 | if ([date timeIntervalSinceNow] < 0) { 2077 | libssh2_session_set_last_error(session, LIBSSH2_ERROR_TIMEOUT, NULL); 2078 | break; 2079 | } 2080 | int mode = 0 2081 | | LIBSSH2_SFTP_RENAME_OVERWRITE 2082 | | LIBSSH2_SFTP_RENAME_ATOMIC 2083 | | LIBSSH2_SFTP_RENAME_NATIVE; 2084 | const char *acp = [atPath UTF8String]; 2085 | const char *ncp = [newPath UTF8String]; 2086 | rc = libssh2_sftp_rename_ex(sftp, 2087 | acp, 2088 | (unsigned int)strlen(acp), 2089 | newPath.UTF8String, 2090 | (unsigned int)strlen(ncp), 2091 | mode); 2092 | if (rc == LIBSSH2_ERROR_EAGAIN) { 2093 | usleep(LIBSSH2_CONTINUE_EAGAIN_WAIT); 2094 | continue; 2095 | } 2096 | break; 2097 | } 2098 | [self unsafeReadLastError]; 2099 | if (rc != 0) { 2100 | [self unsafeFileTransferSetErrorForFile:atPath pathIsRemote:YES failureReason:@"remote permission denied"]; 2101 | return NO; 2102 | } 2103 | return YES; 2104 | } 2105 | 2106 | - (void)unsafeFileTransferSetErrorForFile:(NSString*)filePath 2107 | pathIsRemote:(BOOL)pathIsRemote 2108 | failureReason:(NSString*)failureReason 2109 | { 2110 | NSString *description = [[NSString alloc] initWithFormat:@"%@ raising error %@ with file at path %@", 2111 | pathIsRemote ? @"remote" : @"local", 2112 | failureReason, 2113 | filePath]; 2114 | NSLog(@"%@", description); 2115 | @synchronized (self.lastFileTransferError) { 2116 | self.lastFileTransferError = description; 2117 | } 2118 | } 2119 | 2120 | @end 2121 | --------------------------------------------------------------------------------