Skip to content

Commit

Permalink
Add support for executing shell commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Lejdborg committed Jun 30, 2012
1 parent 60310f7 commit ffd6c96
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 38 deletions.
16 changes: 16 additions & 0 deletions NMSSH.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
E4E96DA2158E10FD002E6E0A /* NMSSH.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4E96D84158E10FD002E6E0A /* NMSSH.framework */; };
E4E96DDA158FD65D002E6E0A /* YAML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4E96DD9158FD65D002E6E0A /* YAML.framework */; };
E4E96DDC158FD6B6002E6E0A /* config.yml in Resources */ = {isa = PBXBuildFile; fileRef = E4E96DDB158FD6B6002E6E0A /* config.yml */; };
E4F1E67C159F5923007B0B2F /* NMSSHChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E4F1E67B159F5923007B0B2F /* NMSSHChannelTests.m */; };
E4F1E680159F5B13007B0B2F /* NMSSHChannel.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F1E67E159F5B13007B0B2F /* NMSSHChannel.h */; };
E4F1E681159F5B13007B0B2F /* NMSSHChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = E4F1E67F159F5B13007B0B2F /* NMSSHChannel.m */; };
E4F1E682159F5B13007B0B2F /* NMSSHChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = E4F1E67F159F5B13007B0B2F /* NMSSHChannel.m */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -85,6 +89,10 @@
E4E96DA5158E10FD002E6E0A /* NMSSHTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "NMSSHTests-Info.plist"; sourceTree = "<group>"; };
E4E96DD9158FD65D002E6E0A /* YAML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = YAML.framework; sourceTree = "<group>"; };
E4E96DDB158FD6B6002E6E0A /* config.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = config.yml; sourceTree = "<group>"; };
E4F1E67A159F5923007B0B2F /* NMSSHChannelTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NMSSHChannelTests.h; path = NMSSHTests/NMSSHChannelTests.h; sourceTree = "<group>"; };
E4F1E67B159F5923007B0B2F /* NMSSHChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NMSSHChannelTests.m; path = NMSSHTests/NMSSHChannelTests.m; sourceTree = "<group>"; };
E4F1E67E159F5B13007B0B2F /* NMSSHChannel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NMSSHChannel.h; sourceTree = "<group>"; };
E4F1E67F159F5B13007B0B2F /* NMSSHChannel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NMSSHChannel.m; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -117,6 +125,8 @@
children = (
E4E96D8D158E10FD002E6E0A /* NMSSH */,
E4E96DA3158E10FD002E6E0A /* NMSSHTests */,
E4F1E67A159F5923007B0B2F /* NMSSHChannelTests.h */,
E4F1E67B159F5923007B0B2F /* NMSSHChannelTests.m */,
E42815BD1593D6E900CF680C /* NMSSHSessionTests.h */,
E42815BE1593D6E900CF680C /* NMSSHSessionTests.m */,
E4E96D86158E10FD002E6E0A /* Frameworks */,
Expand Down Expand Up @@ -159,6 +169,8 @@
E4E96DBD158EFBB4002E6E0A /* libssh2 */,
E4E96D8E158E10FD002E6E0A /* Supporting Files */,
E4E96D94158E10FD002E6E0A /* NMSSH.h */,
E4F1E67E159F5B13007B0B2F /* NMSSHChannel.h */,
E4F1E67F159F5B13007B0B2F /* NMSSHChannel.m */,
E42815C01593D95200CF680C /* NMSSHSession.h */,
E42815C11593D95200CF680C /* NMSSHSession.m */,
);
Expand Down Expand Up @@ -233,6 +245,7 @@
E428160C1596629A00CF680C /* libssh2_publickey.h in Headers */,
E428160D1596629A00CF680C /* libssh2_sftp.h in Headers */,
E428160E1596629A00CF680C /* libssh2.h in Headers */,
E4F1E680159F5B13007B0B2F /* NMSSHChannel.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -344,6 +357,7 @@
buildActionMask = 2147483647;
files = (
E42815C31593D95200CF680C /* NMSSHSession.m in Sources */,
E4F1E681159F5B13007B0B2F /* NMSSHChannel.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -354,6 +368,8 @@
E46A02E115919BE3007049AB /* ConfigHelper.m in Sources */,
E42815BF1593D6E900CF680C /* NMSSHSessionTests.m in Sources */,
E42815C41593D95200CF680C /* NMSSHSession.m in Sources */,
E4F1E67C159F5923007B0B2F /* NMSSHChannelTests.m in Sources */,
E4F1E682159F5B13007B0B2F /* NMSSHChannel.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
3 changes: 2 additions & 1 deletion NMSSH/NMSSH.h
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#import "NMSSHSession.h"
#import "NMSSHSession.h"
#import "NMSSHChannel.h"
48 changes: 40 additions & 8 deletions NMSSH/NMSSHChannel.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
//
// NMSSHChannel.h
// NMSSH
//
// Created by Christoffer Lejdborg on 2012-06-30.
// Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>

@class NMSSHSession;

enum {
NMSSHChannelExecutionError,
NMSSHChannelExecutionResponseError
};

/**
* NMSSHChannel provides functionality to work with SSH shells and SCP.
*/
@interface NMSSHChannel : NSObject

/** A valid NMSSHSession instance */
@property (nonatomic, readonly) NMSSHSession *session;

/** The last response from a shell command execution */
@property (nonatomic, readonly) NSString *lastResponse;

/**
* Create a new NMSSHChannel instance and open a channel on the provided
* session.
*
* aSession needs to be a valid, connected, NMSSHSession instance!
*
* @returns New NMSSHChannel instance
*/
- (id)initWithSession:(NMSSHSession *)aSession;

/**
* Close and cleanup the channel
*/
- (void)close;

/**
* Execute a shell command on the server.
*
* If an error occurs, it will return nil and populate the error object.
*
* @returns Shell command response
*/
- (NSString *)execute:(NSString *)command error:(NSError **)error;

@end
95 changes: 87 additions & 8 deletions NMSSH/NMSSHChannel.m
Original file line number Diff line number Diff line change
@@ -1,13 +1,92 @@
//
// NMSSHChannel.m
// NMSSH
//
// Created by Christoffer Lejdborg on 2012-06-30.
// Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import "NMSSHChannel.h"
#import "NMSSHSession.h"

#import "libssh2.h"

@interface NMSSHChannel () {
LIBSSH2_CHANNEL *channel;
}
@end

@implementation NMSSHChannel
@synthesize session, lastResponse;

// -----------------------------------------------------------------------------
// PUBLIC SETUP API
// -----------------------------------------------------------------------------

- (id)initWithSession:(NMSSHSession *)aSession {
if ((self = [super init])) {
session = aSession;

// Make sure we were provided a valid session
if (![session isKindOfClass:[NMSSHSession class]]) {
return nil;
}

// Open up the channel
if (!(channel = libssh2_channel_open_session([session rawSession]))) {
NSLog(@"NMSSH: Unable to open a session");
return nil;
}
}

return self;
}

- (void)close {
if (channel) {
libssh2_channel_close(channel);
libssh2_channel_free(channel);
channel = nil;
}
}

// -----------------------------------------------------------------------------
// PUBLIC SHELL EXECUTION API
// -----------------------------------------------------------------------------

- (NSString *)execute:(NSString *)command error:(NSError **)error {
lastResponse = nil;

// In case of error...
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:command
forKey:@"command"];

// Try executing command
int rc = libssh2_channel_exec(channel, [command UTF8String]);
if (rc) {
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelExecutionError
userInfo:userInfo];

NSLog(@"NMSSH: Error executing command");
return nil;
}

// Fetch response from output buffer
for (;;) {
int rc;
do {
char buffer[0x4000];
rc = libssh2_channel_read(channel, buffer, sizeof(buffer));

if (rc != LIBSSH2_ERROR_EAGAIN) {
lastResponse = [NSString stringWithCString:buffer
encoding:NSUTF8StringEncoding];
return lastResponse;
}
}
while (rc > 0);
}

// If we've got this far, it means fetching execution response failed
*error = [NSError errorWithDomain:@"NMSSH"
code:NMSSHChannelExecutionResponseError
userInfo:userInfo];

NSLog(@"NMSSH: Error fetching response from command");
return nil;
}

@end
4 changes: 4 additions & 0 deletions NMSSH/NMSSHSession.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#import <Foundation/Foundation.h>
#import "libssh2.h"

/**
* NMSSHSession provides functionality to setup a connection to a SSH server.
*/
@interface NMSSHSession : NSObject

/** Raw libssh2 session instance */
@property (readonly, getter=rawSession) LIBSSH2_SESSION *session;

/** Server hostname in the form "{hostname}:{port}" */
@property (readonly) NSString *host;

Expand Down
4 changes: 1 addition & 3 deletions NMSSH/NMSSHSession.m
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
#import "NMSSHSession.h"

#import "libssh2.h"
#import <netdb.h>
#import <sys/socket.h>
#import <arpa/inet.h>

@interface NMSSHSession () {
int sock;
LIBSSH2_SESSION *session;
LIBSSH2_AGENT *agent;
}
@end

@implementation NMSSHSession
@synthesize host, username, connected, authorized;
@synthesize session, host, username, connected, authorized;

// -----------------------------------------------------------------------------
// PUBLIC CONNECTION API
Expand Down
9 changes: 0 additions & 9 deletions NMSSHTests/NMSSHChannelTests.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
//
// NMSSHChannelTests.h
// NMSSH
//
// Created by Christoffer Lejdborg on 2012-06-30.
// Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <SenTestingKit/SenTestingKit.h>

@interface NMSSHChannelTests : SenTestCase

@end
66 changes: 58 additions & 8 deletions NMSSHTests/NMSSHChannelTests.m
Original file line number Diff line number Diff line change
@@ -1,13 +1,63 @@
//
// NMSSHChannelTests.m
// NMSSH
//
// Created by Christoffer Lejdborg on 2012-06-30.
// Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import "NMSSHChannelTests.h"
#import "ConfigHelper.h"

#import <NMSSH/NMSSH.h>

@interface NMSSHChannelTests () {
NSDictionary *settings;

NMSSHChannel *channel;
NMSSHSession *session;
}
@end

@implementation NMSSHChannelTests

// -----------------------------------------------------------------------------
// TEST SETUP
// -----------------------------------------------------------------------------

- (void)setUp {
settings = [ConfigHelper valueForKey:@"valid_password_protected_server"];

session = [NMSSHSession connectToHost:[settings objectForKey:@"host"]
withUsername:[settings objectForKey:@"user"]];
[session authenticateByPassword:[settings objectForKey:@"password"]];
assert([session isAuthorized]);
}

- (void)tearDown {
if (channel) {
[channel close];
channel = nil;
}

if (session) {
[session disconnect];
session = nil;
}
}

// -----------------------------------------------------------------------------
// SHELL EXECUTION TESTS
// -----------------------------------------------------------------------------

- (void)testCreatingChannelWorks {
STAssertNoThrow(channel = [[NMSSHChannel alloc] initWithSession:session],
@"Setting up channel does not throw exception");
}

- (void)testExecutingShellCommand {
channel = [[NMSSHChannel alloc] initWithSession:session];

NSError *error = nil;
STAssertNoThrow([channel execute:[settings objectForKey:@"execute_command"]
error:&error],
@"Execution should not throw an exception");

STAssertEqualObjects([channel lastResponse],
[settings objectForKey:@"execute_expected_response"],
@"Execution returns the expected response");
}

@end
4 changes: 4 additions & 0 deletions NMSSHTests/Settings/config.sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ valid_password_protected_server:
user: "user"
password: "password"

# Shell execution config
execute_command: "ls -1 /var/www/nmssh-tests/"
execute_expected_response: "invalid\nvalid\n"

# Directory config
writable_dir: "/var/www/nmssh-tests/valid/"
non_writable_dir: "/var/www/nmssh-tests/invalid/"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ Are you using NMSSH for something cool? [Let me know](http://twitter.com/Lejdbor

#### Executing shell commands

NSString *response = [[session channel] execute:@"echo foo"];
NSError *error = nil;
NSString *response = [[session channel] execute:@"echo foo" error:&error];
NSLog(@"Response: %@", response);

#### SCP file transfer
Expand Down

0 comments on commit ffd6c96

Please sign in to comment.