336 lines
11 KiB
Objective-C
336 lines
11 KiB
Objective-C
//
|
|
// WQSSHServer.m
|
|
// FilzaSlop
|
|
//
|
|
// In-process SSH server built on libssh (weakly linked; the public build
|
|
// does not ship libssh). Authentication is intentionally simple:
|
|
// mobile/alpine and root/alpine. Each session runs the same in-process shell
|
|
// engine used by Filza's terminal, starting in the worse_query hub.
|
|
//
|
|
|
|
#import "WQSSHServer.h"
|
|
#import "WQTerminal.h"
|
|
#import "WQFilzaIntegration.h"
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import <UIKit/UIKit.h>
|
|
#import <dlfcn.h>
|
|
#import <errno.h>
|
|
#import <fcntl.h>
|
|
#import <pthread.h>
|
|
#import <string.h>
|
|
#import <sys/socket.h>
|
|
#import <arpa/inet.h>
|
|
#import <ifaddrs.h>
|
|
#import <netinet/in.h>
|
|
#import <unistd.h>
|
|
|
|
#include <libssh/libssh.h>
|
|
#include <libssh/server.h>
|
|
|
|
static ssh_bind gSSHBind;
|
|
static ssh_key gSSHHostKey;
|
|
static pthread_t gSSHListenerThread;
|
|
static BOOL gSSHRunning;
|
|
static NSInteger gSSHPort = 2222;
|
|
static NSString *gSSHLastError;
|
|
static NSObject *gSSHLock;
|
|
|
|
static void WQSSHLog(NSString *message)
|
|
{
|
|
NSLog(@"[WQSSH] %@", message);
|
|
}
|
|
|
|
static BOOL WQSSHLibraryAvailable(void)
|
|
{
|
|
return dlsym(RTLD_DEFAULT, "ssh_bind_new") != NULL &&
|
|
dlsym(RTLD_DEFAULT, "ssh_handle_key_exchange") != NULL;
|
|
}
|
|
|
|
#pragma mark - Session shell
|
|
|
|
static const char *WQSSHAuthorizedPassword(const char *user)
|
|
{
|
|
if (!user) return NULL;
|
|
if (strcmp(user, "mobile") == 0 || strcmp(user, "root") == 0)
|
|
return "alpine";
|
|
return NULL;
|
|
}
|
|
|
|
static void *WQSSHClientThread(void *argument)
|
|
{
|
|
ssh_session session = (ssh_session)argument;
|
|
@autoreleasepool {
|
|
if (!session) return NULL;
|
|
if (ssh_handle_key_exchange(session) != SSH_OK) {
|
|
WQSSHLog([NSString stringWithFormat:@"key exchange failed: %s",
|
|
ssh_get_error(session)]);
|
|
ssh_disconnect(session);
|
|
ssh_free(session);
|
|
return NULL;
|
|
}
|
|
|
|
ssh_channel channel = NULL;
|
|
char authenticatedUser[64] = {0};
|
|
BOOL authenticated = NO;
|
|
|
|
while (gSSHRunning) {
|
|
ssh_message message = ssh_message_get(session);
|
|
if (!message) break;
|
|
int type = ssh_message_type(message);
|
|
int subtype = ssh_message_subtype(message);
|
|
|
|
if (type == SSH_REQUEST_AUTH && !authenticated) {
|
|
if (subtype == SSH_AUTH_METHOD_PASSWORD) {
|
|
const char *user = ssh_message_auth_user(message);
|
|
const char *password = ssh_message_auth_password(message);
|
|
const char *expected = WQSSHAuthorizedPassword(user);
|
|
if (expected && password && strcmp(password, expected) == 0) {
|
|
ssh_message_auth_reply_success(message, 0);
|
|
authenticated = YES;
|
|
strncpy(authenticatedUser, user,
|
|
sizeof(authenticatedUser) - 1);
|
|
WQSSHLog([NSString stringWithFormat:@"login user=%s", user]);
|
|
ssh_message_free(message);
|
|
continue;
|
|
}
|
|
}
|
|
ssh_message_auth_set_methods(message, SSH_AUTH_METHOD_PASSWORD);
|
|
ssh_message_reply_default(message);
|
|
} else if (authenticated && type == SSH_REQUEST_CHANNEL_OPEN &&
|
|
subtype == SSH_CHANNEL_SESSION) {
|
|
channel = ssh_message_channel_request_open_reply_accept(message);
|
|
} else if (authenticated && type == SSH_REQUEST_CHANNEL && channel) {
|
|
if (subtype == SSH_CHANNEL_REQUEST_PTY) {
|
|
ssh_message_channel_request_reply_success(message);
|
|
} else if (subtype == SSH_CHANNEL_REQUEST_SHELL) {
|
|
ssh_message_channel_request_reply_success(message);
|
|
ssh_message_free(message);
|
|
break;
|
|
} else {
|
|
ssh_message_reply_default(message);
|
|
}
|
|
} else {
|
|
ssh_message_reply_default(message);
|
|
}
|
|
ssh_message_free(message);
|
|
}
|
|
|
|
if (!authenticated || !channel) {
|
|
ssh_disconnect(session);
|
|
ssh_free(session);
|
|
return NULL;
|
|
}
|
|
|
|
setenv("USER", authenticatedUser, 1);
|
|
setenv("LOGNAME", authenticatedUser, 1);
|
|
setenv("HOME", NSHomeDirectory().UTF8String, 1);
|
|
setenv("SHELL", "fish", 1);
|
|
|
|
int pid = -1;
|
|
int master = WQTerminalSpawnShell(&pid, WQVirtualRootFolder());
|
|
if (master < 0) {
|
|
const char *failure = "Unable to start the in-process shell.\r\n";
|
|
ssh_channel_write(channel, failure, (uint32_t)strlen(failure));
|
|
ssh_channel_send_eof(channel);
|
|
ssh_channel_close(channel);
|
|
ssh_channel_free(channel);
|
|
ssh_disconnect(session);
|
|
ssh_free(session);
|
|
return NULL;
|
|
}
|
|
fcntl(master, F_SETFL, O_NONBLOCK);
|
|
|
|
char buffer[4096];
|
|
while (gSSHRunning && ssh_channel_is_open(channel)) {
|
|
int incoming = ssh_channel_read_nonblocking(channel, buffer,
|
|
sizeof(buffer), 0);
|
|
if (incoming > 0) {
|
|
ssize_t written = 0;
|
|
while (written < incoming) {
|
|
ssize_t step = write(master, buffer + written,
|
|
(size_t)(incoming - written));
|
|
if (step <= 0) break;
|
|
written += step;
|
|
}
|
|
} else if (incoming == SSH_ERROR) {
|
|
// Client may have half-closed stdin after piping a command.
|
|
// Keep pumping shell output until the shell exits.
|
|
}
|
|
|
|
ssize_t outgoing = read(master, buffer, sizeof(buffer));
|
|
if (outgoing > 0) {
|
|
int offset = 0;
|
|
while (offset < outgoing) {
|
|
int sent = ssh_channel_write(channel, buffer + offset,
|
|
(uint32_t)(outgoing - offset));
|
|
if (sent <= 0) break;
|
|
offset += sent;
|
|
}
|
|
} else if (outgoing < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
|
break;
|
|
}
|
|
usleep(5000);
|
|
}
|
|
|
|
close(master);
|
|
ssh_channel_send_eof(channel);
|
|
ssh_channel_close(channel);
|
|
ssh_channel_free(channel);
|
|
ssh_disconnect(session);
|
|
ssh_free(session);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
#pragma mark - Listener
|
|
|
|
static void *WQSSHListener(void *argument)
|
|
{
|
|
@autoreleasepool {
|
|
while (gSSHRunning) {
|
|
ssh_session session = ssh_new();
|
|
if (!session) break;
|
|
if (ssh_bind_accept(gSSHBind, session) != SSH_OK) {
|
|
ssh_free(session);
|
|
if (!gSSHRunning) break;
|
|
usleep(20000);
|
|
continue;
|
|
}
|
|
pthread_t thread;
|
|
if (pthread_create(&thread, NULL, WQSSHClientThread, session) == 0) {
|
|
pthread_detach(thread);
|
|
} else {
|
|
ssh_disconnect(session);
|
|
ssh_free(session);
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static BOOL WQSSHTryPort(NSInteger port, NSString **error)
|
|
{
|
|
gSSHBind = ssh_bind_new();
|
|
if (!gSSHBind) {
|
|
if (error) *error = @"ssh_bind_new failed";
|
|
return NO;
|
|
}
|
|
unsigned int bindPort = (unsigned int)port;
|
|
ssh_bind_options_set(gSSHBind, SSH_BIND_OPTIONS_BINDADDR, "0.0.0.0");
|
|
ssh_bind_options_set(gSSHBind, SSH_BIND_OPTIONS_BINDPORT, &bindPort);
|
|
if (gSSHHostKey)
|
|
ssh_bind_options_set(gSSHBind, SSH_BIND_OPTIONS_IMPORT_KEY, gSSHHostKey);
|
|
if (ssh_bind_listen(gSSHBind) != SSH_OK) {
|
|
if (error) *error = [NSString stringWithFormat:@"listen failed: %s",
|
|
ssh_get_error(gSSHBind)];
|
|
ssh_bind_free(gSSHBind);
|
|
gSSHBind = NULL;
|
|
return NO;
|
|
}
|
|
gSSHPort = port;
|
|
return YES;
|
|
}
|
|
|
|
BOOL WQSSHServerStart(NSString **error)
|
|
{
|
|
@synchronized (gSSHLock ?: (gSSHLock = [NSObject new])) {
|
|
if (gSSHRunning) return YES;
|
|
if (!WQSSHLibraryAvailable()) {
|
|
if (error) *error = @"libssh is not bundled in this build";
|
|
return NO;
|
|
}
|
|
ssh_init();
|
|
if (!gSSHHostKey) {
|
|
if (ssh_pki_generate(SSH_KEYTYPE_RSA, 2048, &gSSHHostKey) != SSH_OK) {
|
|
if (error) *error = @"failed to generate SSH host key";
|
|
ssh_finalize();
|
|
return NO;
|
|
}
|
|
}
|
|
gSSHRunning = YES;
|
|
BOOL bound = NO;
|
|
NSString *lastError = nil;
|
|
NSInteger requested = WQSSHServerPort();
|
|
for (NSInteger port = requested; port < requested + 8; port++) {
|
|
if (WQSSHTryPort(port, &lastError)) { bound = YES; break; }
|
|
}
|
|
if (!bound) {
|
|
gSSHRunning = NO;
|
|
ssh_finalize();
|
|
if (error) *error = lastError ?: @"unable to bind SSH port";
|
|
WQSSHLog([NSString stringWithFormat:@"start failed: %@", lastError]);
|
|
return NO;
|
|
}
|
|
if (pthread_create(&gSSHListenerThread, NULL, WQSSHListener, NULL) != 0) {
|
|
gSSHRunning = NO;
|
|
ssh_bind_free(gSSHBind);
|
|
gSSHBind = NULL;
|
|
if (error) *error = @"failed to create SSH listener thread";
|
|
return NO;
|
|
}
|
|
pthread_detach(gSSHListenerThread);
|
|
gSSHLastError = nil;
|
|
WQSSHLog([NSString stringWithFormat:@"started port=%ld url=%@",
|
|
(long)gSSHPort, WQSSHServerURL()]);
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
void WQSSHServerStop(void)
|
|
{
|
|
@synchronized (gSSHLock ?: (gSSHLock = [NSObject new])) {
|
|
if (!gSSHRunning && !gSSHBind) return;
|
|
gSSHRunning = NO;
|
|
if (gSSHBind) {
|
|
ssh_bind_free(gSSHBind);
|
|
gSSHBind = NULL;
|
|
}
|
|
if (gSSHHostKey) {
|
|
ssh_key_free(gSSHHostKey);
|
|
gSSHHostKey = NULL;
|
|
}
|
|
ssh_finalize();
|
|
WQSSHLog(@"stopped");
|
|
}
|
|
}
|
|
|
|
BOOL WQSSHServerIsRunning(void)
|
|
{
|
|
return gSSHRunning;
|
|
}
|
|
|
|
NSInteger WQSSHServerPort(void)
|
|
{
|
|
id value = [NSUserDefaults.standardUserDefaults objectForKey:@"WQSSHPort"];
|
|
NSInteger port = [value respondsToSelector:@selector(integerValue)]
|
|
? [value integerValue] : 2222;
|
|
if (port < 1024 || port > 65535) port = 2222;
|
|
return port;
|
|
}
|
|
|
|
NSString *WQSSHServerURL(void)
|
|
{
|
|
struct ifaddrs *interfaces = NULL;
|
|
NSString *ip = nil;
|
|
if (getifaddrs(&interfaces) == 0) {
|
|
for (struct ifaddrs *cursor = interfaces; cursor; cursor = cursor->ifa_next) {
|
|
if (!cursor->ifa_addr || cursor->ifa_addr->sa_family != AF_INET) continue;
|
|
NSString *name = [NSString stringWithUTF8String:cursor->ifa_name ?: ""];
|
|
if ([name isEqualToString:@"lo0"]) continue;
|
|
char address[INET_ADDRSTRLEN] = {0};
|
|
struct sockaddr_in *sin = (struct sockaddr_in *)cursor->ifa_addr;
|
|
if (!inet_ntop(AF_INET, &sin->sin_addr, address, sizeof(address))) continue;
|
|
NSString *candidate = [NSString stringWithUTF8String:address];
|
|
if ([name isEqualToString:@"en0"] || [name isEqualToString:@"en1"]) {
|
|
ip = candidate;
|
|
break;
|
|
}
|
|
if (!ip) ip = candidate;
|
|
}
|
|
freeifaddrs(interfaces);
|
|
}
|
|
return [NSString stringWithFormat:@"ssh -p %ld mobile@%@",
|
|
(long)gSSHPort, ip ?: @"127.0.0.1"];
|
|
}
|