Finding if an SSH connection succeeded - swift

I am trying to make my program connect to another computer using SSH. It would be generic (the user would provide the hostname, IP, and password), so keys can't be used. This is the code I have so far:
func terminalSSH(host:String, password:String, IP:String) {
let pipe = Pipe()
let args = ["-p", password, "ssh", "\(host)#\(IP)"]
let task = Process()
task.launchPath = "/usr/local/bin/sshpass"
task.arguments = args
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
print(output)
}
My issue is that I can't see if the connection was successful using sshpass. I want the user to be notified the moment the connection succeeded or failed. Also, it seems like sshpass is the only command I can use to SSH because Terminal forces the user to input the password. I know this code works to establish a connection because the system.log on my test target computer displays it. Thanks.

Firstly, you should not be using sshpass and a plain text password, but private keys.
To get the tasks exit code, you can use the following:
task.waitUntilExit()
let status = task.terminationStatus
Do know however, that waitUntilExit will block the return of this method until the session has ended.
Whether the session ends in 10 minutes, or immediately due to an error, you can evaluate the terminationStatus to see how the program exited.
If the return code is 0, everything exited nicely; if there is a non-zero value, you can take different actions based on the reason for failure.
task.waitUntilExit()
let status = task.terminationStatus
if status == 0 {
print("Graceful exit.")
} else {
print("Failed with code " + status)
}
Detecting and responding to a successful connection will be more difficult here, as the waitUntilExit call may block for as long as the ssh session is open.
You also have the option of using a loop, and checking task.isRunning.

Related

Shell command via NSTask's Process delayed until my Vapor app quits

I built a Vapor 4 app that is currently deployed on a local Ubuntu 18 server VM, running behind NGINX and serving users without any issues.
Now I would like one of my web server routes to react to specific POSTs by executing a Bash command via Process (this, in order to send messages to a dedicated Slack channel via slack-cli, a tool I already use for other purposes and that is already configured and working both on my development machine and on the Ubuntu server).
With the following code, everything's working as desired when I run my Vapor app on my local machine (i.e.: immediately after the POST to the route the expected message appears in the Slack channel):
// What follows is placed inside my dedicated app.post route, after checking the response is valid...
let slackCLIPath = "/home/linuxbrew/.linuxbrew/bin/" // This is the slack-cli path on the Linux VM; I swap it with "/opt/homebrew/bin/" when running the app on my local Mac
_ = runShellScript("\(slackCLIPath)slack chat send '\(myMessageComingFromThePOST)' '#myChannel'")
// ...
// runShellScript() called above is the dedicated function (coming from [this SO answer](https://stackoverflow.com/a/43014767/3765705) that executes the Shell process, and its code follows:
func runShellScript(_ cmd: String) -> String? {
let pipe = Pipe()
let process = Process()
process.launchPath = "/bin/sh"
process.arguments = ["-c", String(format:"%#", cmd)]
process.standardOutput = pipe
let fileHandle = pipe.fileHandleForReading
process.launch()
return String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
}
My issue is that, when I deploy my app on the Ubuntu server, both in debug and production, the execution of the Shell process does not happen like it did on my Mac: I have no errors logged by Vapor when I POST to that route, but no messages appear in Slack, even if I wait a while!
But here's the tricky part: as soon as I stop my Vapor app on the server, all messages are sent to Slack at once.
After a lot of testing (which obviously included confirming that from the server was possible to post without delays to Slack by using the exact same command passed to NSTask's Process class in my Vapor app), it appears like the Bash command is not executed until my Vapor app quits.
Clearly I'm missing something on how to make Process work "in realtime" with Vapor, and I'll be grateful for all the help I can get.
You need to wait until the task has finished. Looks like you're deadlocking yourself. This is how I run stuff on Linux:
// MARK: - Functions
#discardableResult
func shell(_ args: String..., returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> (Int32, Pipe) {
return try shell(args, returnStdOut: returnStdOut, stdIn: stdIn)
}
#discardableResult
func shell(_ args: [String], returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> (Int32, Pipe) {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/env")
task.arguments = args
let pipe = Pipe()
if returnStdOut {
task.standardOutput = pipe
}
if let stdIn = stdIn {
task.standardInput = stdIn
}
try task.run()
task.waitUntilExit()
return (task.terminationStatus, pipe)
}
extension Pipe {
func string() -> String? {
let data = self.fileHandleForReading.readDataToEndOfFile()
let result: String?
if let string = String(data: data, encoding: String.Encoding.utf8) {
result = string
} else {
result = nil
}
return result
}
}
Important lines being starting it with try task.run() and waiting with task.waitUntilExit()

Process.run Returns 'The file “<command>” doesn’t exist.'

I'm trying to write a small app to start/stop and display data from a command line "app" someone else wrote. The command executable is installed in '/usr/local/bin'. It outputs status text data to standardOutput while running. I can execute this "command" from the Terminal.app without issue. From swiftUI code I can successfully execute "built-in" commands like ls. However, when (in swiftUI code) I attempt to execute Process.run WITH the new command it throws the exception 'The file “” doesn’t exist.'
Anyone have any ideas? Thanks in advance!
Here's a code snip:
// NOTE: "installedCommand" is just a placeholder for the actual command.
let task = Process()
let connection = Pipe()
let exeUrl = URL(fileURLWithPath: "/usr/local/bin/installedCommand")
//let exeUrl = URL(fileURLWithPath: "/bin/ls") <--works fine
task.executableURL = exeUrl
task.standardOutput = connection
do
{
try task.run()
}
catch
{
print("Error: \(error.localizedDescription)")
return
}

Pass stdin to Process (e.g. to enabled sudo)

I am creating a Swift package that has an executable that will run commands via Process. Some of these commands will require user input, such as sudo.
How can I ensure that the commands output is shown to the user, and also allow them to interact with the commands, such as typing in their password for sudo?
I have a few ways of running the command, which all fail in different ways:
public func run(_ command: [String]) throws {
try Process.run(URL(fileURLWithPath: "/usr/bin/env"), arguments: command) { process in
print("Process terminated", process)
}
}
Fails without user input:
Password:
sudo: unable to read password: Input/output error
I've also tried:
public func run(_ command: [String]) throws {
let process = Process()
process.launchPath = "/usr/bin/env"
process.arguments = command
let standardError = Pipe()
process.standardError = standardError
print("Running \(command.joined(separator: " "))")
try process.run()
process.waitUntilExit()
if process.terminationStatus != 0 {
let errorData = standardError.fileHandleForReading.readDataToEndOfFile()
let error = String(data: errorData, encoding: .utf8)!
throw CommandError(message: error, exitCode: process.terminationStatus)
}
}
which will output Running sudo [command...] and stall until ctrl+C is pressed, at which point the process exits and Password: is output:
Running sudo [command...]
^CPassword:
Not setting process.standardError does the same, but outputs:
Running sudo [command...]
^CPassword:
sudo: unable to read password: Input/output error
I've come across similar questions asking about running commands with sudo via a GUI application, but I am trying to do this via an executable.
I'm assuming I need to pass stdin somehow, but the documentation for standardInput on Process states If this method isn’t used, the standard input is inherited from the process that created the receiver so I'm not sure why it's not working.
I just figured this out in kotlin so i hope this helps someone and translates to swift. So i wanted users to be able to enter a terminal command and i'd run the process on their behalf. If they entered a sudo command i'd need to prompt for a password and then continue with their command providing the password to sudo. The trick was piping it in with the -k and -S switched:
when {
input.startsWith("sudo ") -> {
val password = "12345" //just like my luggage!
val cmd = arrayOf("/bin/bash", "-c", "echo '$password' | ${input.replace("sudo", "sudo -k -S")}")
val proc = rt.exec(commands, emptyArray())
val stdInput = BufferedReader(InputStreamReader(proc.inputStream))
}
}

macOS - Use `Process` to telnet to an IP address and pass commands

I'm developing a macOS app where I need to connect using telnet to a device and send commands to it.
(BTW, I'm connecting to a Roku)
This is as far as I got
func telnet(to host: String, port: String) {
let process = Process()
let pipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/local/bin/telnet")
process.arguments = [host, port]
process.standardOutput = pipe
do {
try process.run()
} catch {
return
}
let resultData = pipe.fileHandleForReading.readDataToEndOfFile()
let result = String (data: resultData, encoding: .utf8) ?? ""
print(result)
}
telnet(to: "10.0.1.11", port: "8080")
The problem is that the Process is closed immediately. I need to leave the Process open and somehow send commands to the device and then close the connection.
So those are my questions:
How can I leave the Process running?
How can I send commands to the device through the telnet connection?
Your best bet will be to avoid using Process and make a network connection directly from your application. (This will also free you from your dependency on /usr/local/bin/telnet, which is not present in a clean install of macOS.)
The SwiftSocket pod looks like an excellent way of doing this. The "Client socket example" in the documentation appears to cover most of your questions.

Why does my Swift code work in playground but not in the real cocoa app?

I'm currently trying to automate things in a macOS status bar application.
Now I had tried to make the Kerberos Login in a Process (previous called NSTask). In my playground, the code creates successfully the token. But when I move the code to the real app, it failed. I get this error message: "kinit: resolving credentials cache: malloc: out of memory"
Here is my code:
import Cocoa
// user credentials
let username = "user#example.com"
let password = "password"
// previous called NSTask
let process = Process()
// set process parameters
process.launchPath = "/bin/sh"
process.arguments = ["-c", "/bin/echo \(password) | /usr/bin/kinit --password-file=STDIN \(username)"]
// create a pipe to lauch process
let pipe = Pipe()
process.standardOutput = pipe
// launch process
process.launch()
// get outcome
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
print(output!)
I think that it is a problem with the Credential cache. When I enter the command "klist -A", I get the following error: "klist: krb5_cc_cache_get_first: Failed to open kcm init".
Does anybody know what can I do to get this code running?