ConnectionKit and SFTP: How to authenticate CK2FileManager - objective-c

For my Mac OS X Cocoa app, I am trying to
connect to a SFTP server that only accepts username/password credentials
get the contents of a remote directory
upload files
and find it surprisingly complicated.
After trying ConnectionKit (nearly no documentation), NMSSH (crashed once too often with simultaneous uploads), rsync (not supported by the server), sftp (needs key authentication if scripted, doesn't work with username/password), I am now back to ConnectionKit: https://github.com/karelia/ConnectionKit
However, I am struggling with the authentication challenge, as I don’t know what to do with my credential in the delegate method.
I downloaded and compiled ConnectionKit (apparently version 2).
I am trying to use CK2FileManager as the Readme indicates (is this the right approach at all? Or should I use the libssh2_sftp-Cocoa-wrapper instead?… however I had troubles with libssh2 blocking methods in NMSSH before)
I am successfully setting up my connection URL and
my delegates' -didReceiveAuthenticationChallenge is called
But this is where I struggle: I know how to create a NSURLCredential, however, I can’t figure out what to do with it =>
- (void)fileManager:(CK2FileManager *)manager
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSURLCredential *credentials = [NSURLCredential
credentialWithUser:self.username
password:[self getPassword]
persistence:NSURLCredentialPersistenceForSession];
// what to do now?
// [manager useCredential:] doesn’t exist, nor is there a manager.connection?
// ...
}
I already read the header, I searched the archives of this list, but all answers seem to be outdated.
I also searched Google, Bing and StackOverflow and found one promising example from 2011 using CKFTPConnection, which doesn’t seem to be included in the current framework anymore.
Thanks so much for any pointer to the right direction.
tl;dr
I don't know how to respond to ConnectionKit's CK2FileManager authenticationChallenge:
see the comment in the code example

For CK2:
- (void)listDirectoryAtPath:(NSString *)path
{
// path is here #"download"
NSURL *ftpServer = [NSURL URLWithString:#"sftp://companyname.topLevelDomain"];
NSURL *directory = [CK2FileManager URLWithPath:path isDirectory:YES hostURL:ftpServer];
CK2FileManager *fileManager = [[CK2FileManager alloc] init];
fileManager.delegate = self;
[fileManager contentsOfDirectoryAtURL:directory
includingPropertiesForKeys:nil
options:NSDirectoryEnumerationSkipsHiddenFiles
completionHandler:^(NSArray *contents, NSError *error) {
if (!error) {
NSLog(#"%#", contents);
} else {
NSLog(#"ERROR: %#", error.localizedDescription);
}
}];
}
and than you have to implement the following protocol
- (void)fileManager:(CK2FileManager *)manager operation:(CK2FileOperation *)operation
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(CK2AuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodDefault]) {
completionHandler(CK2AuthChallengePerformDefaultHandling, nil);
return;
}
NSString * username = #"<username>";
NSString * pathToPrivateSSHKey = #"</Users/myNameOnLocalMaschine/.ssh/id_rsa>"
NSURLCredential *cred = [NSURLCredential ck2_credentialWithUser:username
publicKeyURL:nil
privateKeyURL:[NSURL fileURLWithPath:pathToPrivateSSHKey]
password:nil
persistence:NSURLCredentialPersistenceForSession];
completionHandler(CK2AuthChallengeUseCredential, cred);
}
That's it.
Call -listDirectoryAtPath: and then you will get in the Completion Handler Block in contents array all the files located on the given path :)

Okay, that was easy and I could have found out that on my own; just for the reference: [[challenge sender] useCredential:credentials forAuthenticationChallenge:challenge];
Sorry to reward myself for my own question, but maybe this code snippet helps filling the missing docs, this is how I connect to my SFTP server with ConnectionKit:
- (void)connectWithCompletionBlock:(void (^)(void))completionBlock {
if(!self.cFileManager) {
self.cFileManager = [[CK2FileManager alloc] init];
self.cFileManager.delegate = self;
}
NSURL *sftpServer = [NSURL URLWithString:[#"sftp://" stringByAppendingString:self.server]];
self.remoteFolder = [CK2FileManager URLWithPath:self.remotePath relativeToURL:sftpServer];
// try to get the contents of the current directory
[self.cFileManager contentsOfDirectoryAtURL:self.remoteFolder
includingPropertiesForKeys:nil
options:NSDirectoryEnumerationSkipsHiddenFiles
completionHandler:^(NSArray *contents, NSError *error)
{
NSLog(#"remote folder contents: \n%#", contents);
// invoke completion block
completionBlock();
}];
}
- (void)fileManager:(CK2FileManager *)manager
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSURLCredential *credentials = [NSURLCredential
credentialWithUser:self.username
password:[self getPassword]
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credentials forAuthenticationChallenge:challenge]
}

Related

How can I check if my OS X application can connect to a specific server-- assuming there is internet connection?

I currently am using AFNetworking to determine if my application has network reachability.
NSNumber *s = notification.userInfo[AFNetworkingReachabilityNotificationStatusItem];
AFNetworkReachabilityStatus status = [s integerValue];
if (status == AFNetworkReachabilityStatusReachableViaWWAN || status == AFNetworkReachabilityStatusReachableViaWiFi) {
But, now I also need to know if my application can reach a specific server. More specifically, the server I am connecting to may be down and I need a way to determine if this is the case, from the client side, so I can notify my users appropriately.
It's a very tough google because all searches I do just point me to "How to determine network reachability". Has anybody dealt with this before, and have a solution in mind?
EDIT: #AvT recommended a promising looking solution, so I tried it like this:
self.testTSCReachabilityManager = [AFNetworkReachabilityManager managerForDomain:#"www.asdasfjsldfkjslefjslkjslfs.com"];
__weak MyObject *weakSelf = self;
[self.testReachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
if (weakSelf.testReachabilityManager.reachable) {
NSLog(#"YES");
}
else
{
NSLog(#"NO");
}
}];
But unfortunately, it is logging out "YES" for me, even after I have confirmed it is most definitely not reachable.
Instantiate AFNetworkReachabilityManager with class method
+ (instancetype)managerForDomain:(NSString *)domain;
and pass string with the required domain. AFNetworkReachabilityManager will check reachability of this domain.
If serverURL is an url of your server you should use it the following way:
[AFNetworkReachabilityManager managerForDomain:serverURL.host]
Update
Following code works as expected:
static AFNetworkReachabilityManager *testTSCReachabilityManager;
testTSCReachabilityManager = [AFNetworkReachabilityManager managerForDomain:#"www.asdasfjsldfkjslefjslkjslfs.com"];
[testTSCReachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
if (testTSCReachabilityManager.reachable) {
NSLog(#"YES");
}
else
{
NSLog(#"NO");
}
}];
[testTSCReachabilityManager startMonitoring];
Update: I actually ended up going w/ a different implementation than what Avt recommended, and did what matt recommended in the comments instead
I created an NSURLRequest and make a request to my server, then used the delegate callbacks to determine if the server was reachable. Works like a charm
-(void)checkConnectionToServers
{
NSMutableURLRequest* request = [NSMutableURLRequest new];
request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"www.myserver.com"] cachePolicy:0 timeoutInterval:(NSTimeInterval)5.0];
[request setHTTPMethod:#"GET"];
[NSURLConnection connectionWithRequest:request delegate:self];
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(#"SUCCESS");
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(#"FAIL");
}

Stuck Uploading (Google Drive Objective-C API on OS X)

I am trying to upload simple .txt file as a test. It's only 4kb. Here is my code:
-(IBAction)uploadFile:(id)sender{
if (![self.credentials canAuthorize]) {
GTMOAuth2WindowController *windowController;
windowController = [[[GTMOAuth2WindowController alloc] initWithScope:scope clientID:kClientID clientSecret:kClientSecret keychainItemName:kKeychainItemName resourceBundle:nil] autorelease];
[windowController signInSheetModalForWindow:self.window
delegate:self
finishedSelector:#selector(windowController:finishedWithAuth:error:)];
}else{
NSLog(#"Credentials already authorized.");
}
GTLDriveFile *driveFile = [GTLDriveFile object];
driveFile.title = #"myfile";
driveFile.descriptionProperty = #"Uploaded by Google Drive API test.";
driveFile.mimeType = #"text/plain";
NSData *data = [[NSFileManager defaultManager] contentsAtPath:#"/Users/Blake/Downloads/glm/util/autoexp.txt"];
GTLUploadParameters *params = [GTLUploadParameters uploadParametersWithData:data MIMEType:driveFile.mimeType];
GTLQueryDrive *query = [GTLQueryDrive queryForFilesInsertWithObject:driveFile uploadParameters:params];
NSLog(#"Starting Upload to Google Drive");
[self.progressBar setHidden:FALSE];
[self.progressBar startAnimation:sender];
[self.driveService executeQuery:query completionHandler:^(GTLServiceTicket *st, GTLDriveFile *df, NSError *error){
[self.progressBar setHidden:TRUE];
if (error == nil) {
NSLog(#"File upload succeeded");
}else{
NSLog(#"Uh-oh: File upload failed");
}
}];
}
The GTMOAuth2Authentication is authorized and the completion handler block never executes.
I looked at Activity Monitor and it says my application has sent 54 packets in the beginning. So I guess something is being sent.
How can I tell what's going wrong here, if the block never finishes to give me an error?
Could someone please help me understand what I am doing wrong?
I've recently got problem with sticking on GTMOAuth2WindowController. It was unable to present self window. I spent a lot of time in debugger and found that I forget to add WebKit.framework. It was not documented explicitly and there were no any messages in runtime (minus in google dev karma)))). So, maybe your problem also in that? Try to check your frameworks set and compare it with examples shipped with GTL.

AFNetworking POST Request Sending Blank Parameters to Server

I am trying to send a POST request to a server using AFNetworking, and everything seems to be working, i.e. the application is successfully pinging the server. However, the parameter values that it is sending are blank when it reaches the server even though after stepping through my code below using the debugger, the values appear to be being passed successfully. Any help on this would be greatly appreciated.
APIClient.m
#import "APIClient.h"
#import "AFJSONRequestOperation.h"
// Removed URL for privacy purposes.
static NSString * const kAPIBaseURLString = #"string goes here";
#implementation APIClient
+ (APIClient *)sharedClient {
static APIClient *_sharedClient;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[APIClient alloc] initWithBaseURL:[NSURL URLWithString:kAPIBaseURLString]];
});
return _sharedClient;
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (self) {
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
// Accept HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
[self setDefaultHeader:#"Accept" value:#"application/json"];
}
return self;
}
#end
Login Method in LoginBrain.m
- (void)loginUsingEmail:(NSString *)email andPassword:(NSString *)password withBlock:(void (^)(NSDictionary *loginResults))block {
self.email = email;
self.password = password;
// Removed path for privacy purposes
[[APIClient sharedClient] postPath:#"insert path here" parameters:[NSDictionary dictionaryWithObjectsAndKeys:email, #"uname", password, #"pw", nil] success:^(AFHTTPRequestOperation *operation, id responseJSON) {
if (block) {
block(responseJSON);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
if (block) {
block(nil);
}
}];
// Store user data in app?
}
Login Called Method in LoginViewController.m
- (IBAction)loginPressed {
[self.loginProgressIndicator startAnimating];
NSString *email = self.emailTextField.text;
NSString *password = self.passwordTextField.text;
[self.brain loginUsingEmail:email andPassword:password withBlock:^(NSDictionary *loginResults) {
[self.loginProgressIndicator stopAnimating];
[self.delegate uloopLoginViewController:self didLoginUserWithEmail:email andPassword:password];
}];
}
UPDATE
I tried changing the parameterEncoding as recommended here, but it did not fix the problem.
SECOND UPDATE
Here is the PHP code from the server side that is accessing the POST data. This was written by a co-worker of mine, as I don't do anything on the server side and am very unfamiliar with how it works.
header('Content-type: application/json');
$username = $_POST['uname'];
$pw = $_POST['pw'];
The server code is pretty straight forward. He has some sort of log script that checks to see what the variable values are, and he says that the client is hitting the server, but the variable values are blank.
THIRD UPDATE
This is a dump of the HTTP request by generating a print_r of the $_REQUEST variable:
Array ( [sid] => FwAqvZrfckw )
And here is a dump of the $_POST variable. As you can see, it's completely blank:
Array ( )
FOURTH UPDATE
I used Wireshark to capture the packet before it's being sent to the server, and everything appears to be in order:
Accept: application/json
Content-Type: application/x-www-form-urlencoded; charset=utf-8
And the POST parameters were all there as well. We also created a test file on the server side and just did a test POST to make sure that the code there is working, and it is.
Thank you.
With the same problem, using AFFormURLParameterEncoding was what I needed.
So just to simplify all the thread, you have to use :
[[APIClient sharedClient] setParameterEncoding:AFFormURLParameterEncoding];
I don't see anything in particular that would cause a problem here but I'll start off by giving you the steps I used to solve a similar problem.
To start, checkout the tool, Charles, which is a Debugging Web Proxy that will intercept the response from the server and should give you a more clear idea of what's going wrong. There's a 30 day free trial and it really helped me pick out the little bugs. To use it, press the sequence button and filter the results via your server url. From there you can see the request and response sent and received from the server. If the following doesn't fix your problem, post the request and response that Charles spits out.
Fix wise, try adding [[APIClient sharedClient] setParameterEncoding:AFJSONParameterEncoding] right before you send the POST request. It looks like yall are using JSON as the server-side format.
So in loginUsingEmail:
self.email = email;
self.password = password;
[[APIClient sharedClient] setParameterEncoding:AFJSONParameterEncoding];
[[APIClient sharedClient] postPath:#"insert path here" parameters:[NSDictionary dictionaryWithObjectsAndKeys:email, #"uname", password, #"pw", nil] success:^(AFHTTPRequestOperation *operation, id responseJSON) {
if (block) {
block(responseJSON);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
if (block) {
block(nil);
}
}];
// Store user data in app?
}

SoundCloud API and 401 Error for registered application

I have a problem with SoundCloud API in my application.
I have created an app, what get link to the track (e.g. something like this http://api.soundcloud.com/tracks/48760960) and get stream_url, what later can be played using SCAudioStream. There is no auth for users, I use only client id and client secret of my app. App is registered.
I create soundCloudManager this way:
- (id)init
{
if(self = [super init])
{
conf = [SCSoundCloudAPIConfiguration configurationForProductionWithClientID: kSCClientID
clientSecret: kSCClientSecret
redirectURL: kSCRedirectURL];
conf.accessTokenURL = kSCAccessTokenURL;
api = [[SCSoundCloudAPI alloc] initWithDelegate: self
authenticationDelegate: self
apiConfiguration: conf];
//[api checkAuthentication];
}
return self;
}
If uncomment [api checkAuthentication], then logs will says next:
"auth ready with URL:https://soundcloud.com/connect?client_id=&redirect_uri=&response_type=code"
Then I call this method to get data from track:
- (void)runSearchingStreamWithTrackID: (NSString *)trackID
{
[api performMethod: #"GET"
onResource: #"tracks"
withParameters: [NSDictionary dictionaryWithObject:trackID forKey:#"ids"]
context: #"trackid"
userInfo: nil];
}
And then this delegate's method calls:
- (void)soundCloudAPI:(SCSoundCloudAPI *)soundCloudAPI
didFailWithError:(NSError *)error
context:(id)context
userInfo:(id)userInfo
Text of error:
"HTTP Error: 401".
It means unauthorized. But why? Should I be authorized as soundCloud user to getting info about tracks?
I'm using this for SoundCloudAPI:
https://github.com/soundcloud/cocoa-api-wrapper
Please, help! :(
Today I fixed it. I just add this method to init :
[api authenticateWithUsername:kLogin password:kPwd];
After it this method was called:
- (void)soundCloudAPIDidAuthenticate
So, this test account was authorized.
Then I call this method:
- (void)runSearchingStreamWithTrackID: (NSString *)trackID
{
[api performMethod: #"GET"
onResource: #"tracks"
withParameters: [NSDictionary dictionaryWithObject:trackID forKey:#"ids"]
context: #"trackid"
userInfo: nil];
}
And no one of these methods will be called:
- (void)soundCloudAPI:(SCSoundCloudAPI *)soundCloudAPI
didFailWithError:(NSError *)error
context:(id)context
userInfo:(id)userInfo;
- (void)soundCloudAPI:(SCSoundCloudAPI *)soundCloudAPI
didFinishWithData:(NSData *)data
context:(id)context
userInfo:(id)userInfo;
- (void)soundCloudAPIDidAuthenticate;
- (void)soundCloudAPIDidResetAuthentication;
- (void)soundCloudAPIDidFailToGetAccessTokenWithError:(NSError *)error;
- (void)soundCloudAPIPreparedAuthorizationURL:(NSURL *)authorizationURL;
But there is log:
-[NXOAuth2PostBodyStream open] Stream has been reopened after close
And this method was called:
[NXOAuth2Client oauthConnection:connection didFailWithError:error];
error: HTTP 401
What do I wrong?
I understood the problem. Just download the SC API from the official site and install the latest SC API. Then do something like this:
- (void)searchStreamURLByTrackID: (NSString *)trackID
{
NSString *string = [NSString stringWithFormat:#"https://api.soundcloud.com/tracks/%#.json?client_id=%#", trackID, kSCClientID];
[SCRequest performMethod: SCRequestMethodGET
onResource: [NSURL URLWithString:string]
usingParameters: nil
withAccount: nil
sendingProgressHandler: nil
responseHandler:^(NSURLResponse *response, NSData *data, NSError *error){
NSString *resultString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"FOUND:\n%#", resultString);
}
];
}
After that obtain the stream url from the JSON in the resultString.
Finally, you have to write this code:
NSString *streamURL = [(NSDictionary *)jsonData objectForKey:#"stream_url"];
NSString *realURL = [streamURL stringByAppendingFormat:#".json?client_id=%#", kSCClientID];
AVPlayer *player = [[AVPlayer alloc] initWithURL:[NSURL URLWithString:realURL]];
Enjoy!
Did you check https://github.com/soundcloud/CocoaSoundCloudAPI ?
This is the more current version of the API wrapper (if you don't need iOS 3.0 support).
It also comes with a tutorial video: http://vimeo.com/28715664
If you want to stay with this library you should try to figure out where the log message ("auth ready with ...") appears. Did you implement any of the AuthorizationDelegate methods?
See https://github.com/soundcloud/cocoa-api-wrapper/blob/master/Usage.md#the-authentication-delegate-the-easy-way

Download file (using NSURLDownload) after Login (using POST Request)

I am trying to download a file using NDURL Download. For that, I have to log in to a site.
I do this using a NSMutableURLRequest that I send using sendSynchronousRequest of NSURLConnection
The data that I receive from that message call is indeed the html page confirming my successful login.
To download the file I use the following code:
NSURLRequest *theRequest = [NSURLRequest requestWithURL:[NSURL URLWithString: #"http://www.domain.com/getfile.php?file=1"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create the connection with the request and start loading the data.
NSURLDownload *theDownload = [[NSURLDownload alloc] initWithRequest:theRequest
delegate:self];
if (theDownload) {
// Set the destination file.
NSLog(#"Starting Download...");
NSLog(#"%#", [theDownload description]);
[theDownload setDestination:destinationFilename allowOverwrite:YES];
pathToZipFile = destinationFilename;
} else {
NSLog(#"Download failed...");
return nil;
}
But the data I receive is the HTML page telling me I have to be logged in to download the file.
Any idea on this one?
Does the NSURLDownload have an different session than the NSURLConnection?
Thanks in advance!
Okey, so you have logged in and then you trying to download a file. But how the server knows you are the same user that has logged in before?
There are different ways how it can know it. Some cookie, some request parameter, some HTTP header. But you have to add something to the request, that says "I am the user that has logged in a minute ago".
I feel you have to implement delegates for NSURLDownload, like this :
- (void)downloadDidBegin:(NSURLDownload *)download{
}
- (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response{
_totalLength = response.expectedContentLength;
}
- (void)download:(NSURLDownload *)download willResumeWithResponse:(NSURLResponse *)response fromByte:(long long)startingByte{
}
- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(NSUInteger)length{
_recievedLength = _recievedLength + length;
}
- (void)downloadDidFinish:(NSURLDownload *)download{
//Completed//
}
- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error{
//Error
}