Commit 3c9ab553 authored by nextime's avatar nextime

Add configuration file support for wsssh and wsscp

- Added INI-formatted config file ~/.config/wsssh/wsssh.conf
- Default port and domain settings for both Python and C implementations
- Command line arguments take precedence over config file
- Enhanced hostname parsing with config-based defaults
- Updated documentation and version to 1.3.0
- Updated Debian package version to 1.3.0-1
parent e91872f7
......@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.3.0] - 2025-09-13
### Added
- **Configuration File Support**: Added INI-formatted config file `~/.config/wsssh/wsssh.conf`
- Default port and domain settings for wsssh and wsscp
- Command line arguments take precedence over config file
- Domain fallback to config when not specified in command line
- Port fallback to config when not specified via -p/-P options
- **Enhanced CLI Flexibility**: Improved hostname parsing with config-based defaults
- Support for `user@client` format using config domain
- Support for `user@client.domain` format overriding config domain
- Automatic port resolution: command line > config file > error
### Changed
- **Port Precedence**: Command line `-p`/`-P` options override config file port
- **Domain Precedence**: Command line domain specification overrides config domain
- **Error Handling**: Better error messages when port/domain cannot be determined
### Technical Details
- **Config File Format**: Simple INI format with `[default]` section
- **Cross-Implementation**: Consistent behavior between Python and C versions
- **Backward Compatibility**: All existing functionality preserved
## [1.2.0] - 2025-09-13
### Changed
......
......@@ -295,6 +295,29 @@ wsscp [SCP_OPTIONS...] SOURCE... DESTINATION
## Configuration
### Config File Configuration
WebSocket SSH supports an optional INI-formatted configuration file at `~/.config/wsssh/wsssh.conf`:
```ini
[default]
port=8080
domain=example.com
```
**Configuration Options:**
- `port`: Default WebSocket server port (used when not specified via `-p`/`-P` options)
- `domain`: Default domain suffix (used when hostname doesn't include domain)
**Precedence Rules:**
- **Port**: Command line `-p`/`-P` options override config file, config file overrides default (22)
- **Domain**: Command line domain (e.g., `user@client.domain`) overrides config domain, config domain overrides localhost
**File Location:**
- **Path**: `~/.config/wsssh/wsssh.conf`
- **Format**: Standard INI format with `[default]` section
- **Permissions**: Readable by the user running wsssh/wsscp
### SSL Certificate Configuration
The system uses SSL/TLS certificates for WebSocket encryption:
......
......@@ -160,24 +160,29 @@ This automatically:
## Hostname Format
The system uses intelligent hostname parsing:
The system uses intelligent hostname parsing with config file support:
```
<CLIENT_ID>.<WSSSHD_HOST>
```
Port is specified using `-p` (SSH) or `-P` (SCP) options.
**Supported Formats:**
- `user@client.domain` - Full specification with domain
- `user@client` - Uses domain from config file or defaults to localhost
Port is specified using `-p` (SSH) or `-P` (SCP) options, or from config file.
### Examples:
- `remote.example.com -p 9898` → Client: `remote`, Server: `example.com:9898`
- `server.datacenter.com -P 2222` → Client: `server`, Server: `datacenter.com:2222`
- `test.localhost -p 8080` → Client: `test`, Server: `localhost:8080`
- `myclient` (with config domain `example.com`) → Client: `myclient`, Server: `example.com:<config_port>`
## Port Detection Priority
1. **Hostname suffix**: `host:port` in the target hostname
2. **SSH/SCP port option**: `-p <port>` (SSH) or `-P <port>` (SCP)
1. **Command line option**: `-p <port>` (SSH) or `-P <port>` (SCP)
2. **Config file**: `port` setting in `~/.config/wsssh/wsssh.conf`
3. **Default**: `22` (standard SSH port)
## Detailed Usage
......@@ -230,12 +235,15 @@ When `--web-host` and `--web-port` are specified, a web management interface is
**Examples:**
```bash
# Basic SSH connection
# Basic SSH connection with explicit port and domain
./wsssh -p 9898 user@myclient.example.com
# SSH with custom port
./wsssh -p 2222 user@myclient.example.com
# SSH using config file defaults
./wsssh user@myclient
# SSH with options
./wsssh -p 9898 -i ~/.ssh/key user@myclient.example.com ls -la
```
......@@ -252,18 +260,51 @@ When `--web-host` and `--web-port` are specified, a web management interface is
**Examples:**
```bash
# Copy file to remote
# Copy file to remote with explicit port and domain
./wsscp -P 9898 localfile user@myclient.example.com:/remote/path/
# Copy file from remote
./wsscp -P 9898 user@myclient.example.com:/remote/file ./localfile
# Copy using config file defaults
./wsscp localfile user@myclient:/remote/path/
# Copy with custom port
./wsscp -P 2222 localfile user@myclient.example.com:/remote/path/
```
## Configuration
### Config File
WebSocket SSH supports an optional INI-formatted configuration file at `~/.config/wsssh/wsssh.conf`:
```ini
[default]
port=8080
domain=example.com
```
**Configuration Options:**
- `port`: Default WebSocket server port (used when not specified via `-p`/`-P` options)
- `domain`: Default domain suffix (used when hostname doesn't include domain)
**Precedence Rules:**
- **Port**: Command line `-p`/`-P` options override config file, config file overrides default (22)
- **Domain**: Command line domain (e.g., `user@client.domain`) overrides config domain, config domain overrides localhost
**Examples:**
```bash
# Uses config port 8080 and domain example.com
./wsssh user@myclient
# Uses command line port 2222, config domain example.com
./wsssh -p 2222 user@myclient
# Uses command line port 2222 and domain mydomain.com
./wsssh -p 2222 user@myclient.mydomain.com
```
### SSL Certificates
The system uses self-signed SSL certificates for WebSocket encryption. Certificates are automatically generated during the build process if they don't exist.
......
......@@ -28,6 +28,8 @@ import socket
import subprocess
import sys
import uuid
import configparser
import os
debug = False
......@@ -164,7 +166,7 @@ async def run_scp(server_ip, server_port, client_id, local_port, scp_args):
return 0
def parse_hostname(hostname):
def parse_hostname(hostname, config_domain=None):
"""Parse hostname to extract CLIENT_ID and WSSSHD_HOST"""
# Split by dots to get client_id and wssshd_host
parts = hostname.split('.')
......@@ -174,11 +176,27 @@ def parse_hostname(hostname):
else:
# No domain, assume whole hostname is client_id
client_id = hostname
wssshd_host = 'localhost' # Default fallback
wssshd_host = config_domain if config_domain else 'localhost' # Default fallback
return client_id, wssshd_host
def main():
# Read config file
config = configparser.ConfigParser()
config_file = os.path.expanduser("~/.config/wsssh/wsssh.conf")
config_port = None
config_domain = None
if os.path.exists(config_file):
config.read(config_file)
if 'default' in config:
if 'port' in config['default']:
try:
config_port = int(config['default']['port'])
except ValueError:
pass
if 'domain' in config['default']:
config_domain = config['default']['domain']
parser = argparse.ArgumentParser(description='WebSocket SCP (wsscp)', add_help=False)
parser.add_argument('--local-port', type=int, default=0, help='Local port for tunnel (0 = auto)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
......@@ -215,14 +233,17 @@ def main():
sys.exit(1)
if not scp_port:
print("Error: Could not determine wssshd port from -P option")
sys.exit(1)
if config_port:
scp_port = config_port
else:
print("Error: Could not determine wssshd port from -P option or config file")
sys.exit(1)
# Use the first host for parsing
host = hosts[0]
# Parse hostname to extract client_id and wssshd_host
client_id, wssshd_host = parse_hostname(host)
client_id, wssshd_host = parse_hostname(host, config_domain)
if debug:
print(f"[DEBUG] Hosts: {hosts}")
......
......@@ -28,6 +28,8 @@ import socket
import subprocess
import sys
import uuid
import configparser
import os
debug = False
......@@ -164,7 +166,7 @@ async def run_ssh(server_ip, server_port, client_id, local_port, ssh_args):
return 0
def parse_hostname(hostname):
def parse_hostname(hostname, config_domain=None):
"""Parse hostname to extract CLIENT_ID and WSSSHD_HOST"""
# Split by dots to get client_id and wssshd_host
parts = hostname.split('.')
......@@ -174,11 +176,27 @@ def parse_hostname(hostname):
else:
# No domain, assume whole hostname is client_id
client_id = hostname
wssshd_host = 'localhost' # Default fallback
wssshd_host = config_domain if config_domain else 'localhost' # Default fallback
return client_id, wssshd_host
def main():
# Read config file
config = configparser.ConfigParser()
config_file = os.path.expanduser("~/.config/wsssh/wsssh.conf")
config_port = None
config_domain = None
if os.path.exists(config_file):
config.read(config_file)
if 'default' in config:
if 'port' in config['default']:
try:
config_port = int(config['default']['port'])
except ValueError:
pass
if 'domain' in config['default']:
config_domain = config['default']['domain']
parser = argparse.ArgumentParser(description='WebSocket SSH (wsssh)', add_help=False)
parser.add_argument('--local-port', type=int, default=0, help='Local port for tunnel (0 = auto)')
parser.add_argument('--debug', action='store_true', help='Enable debug output')
......@@ -218,11 +236,14 @@ def main():
sys.exit(1)
if not ssh_port:
print("Error: Could not determine wssshd port from -p option")
sys.exit(1)
if config_port:
ssh_port = config_port
else:
print("Error: Could not determine wssshd port from -p option or config file")
sys.exit(1)
# Parse hostname to extract client_id and wssshd_host
client_id, wssshd_host = parse_hostname(host)
client_id, wssshd_host = parse_hostname(host, config_domain)
if debug:
print(f"[DEBUG] Host: {host}")
......
wsssh-tools (1.3.0-1) unstable; urgency=medium
* Version 1.3.0: Added configuration file support
* Added INI-formatted config file ~/.config/wsssh/wsssh.conf
* Default port and domain settings for wsssh and wsscp
* Command line arguments take precedence over config file
* Enhanced hostname parsing with config-based defaults
-- Stefy Lanza <stefy@nexlab.net> Fri, 13 Sep 2025 15:41:00 +0200
wsssh-tools (1.0.0-1) unstable; urgency=medium
* Initial release of WebSocket SSH Tools C implementation
......
......@@ -39,6 +39,27 @@
#define DEFAULT_PORT 22
#define INITIAL_FRAME_BUFFER_SIZE 8192
char *read_config_value(const char *key) {
char *home = getenv("HOME");
if (!home) return NULL;
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/.config/wsssh/wsssh.conf", home);
FILE *f = fopen(path, "r");
if (!f) return NULL;
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, key, strlen(key)) == 0 && line[strlen(key)] == '=') {
char *value = strdup(line + strlen(key) + 1);
// remove newline
value[strcspn(value, "\n")] = 0;
fclose(f);
return value;
}
}
fclose(f);
return NULL;
}
typedef struct {
char *local_port;
int debug;
......@@ -134,7 +155,7 @@ int parse_args(int argc, char *argv[], wsscp_config_t *config, int *remaining_ar
return 1;
}
int parse_hostname(const char *hostname, char **client_id, char **wssshd_host) {
int parse_hostname(const char *hostname, char **client_id, char **wssshd_host, const char *config_domain) {
char *colon_pos = strchr(hostname, ':');
if (!colon_pos) {
fprintf(stderr, "Error: Invalid hostname format. Expected user@host: or host:\n");
......@@ -157,20 +178,25 @@ int parse_hostname(const char *hostname, char **client_id, char **wssshd_host) {
char *dot_pos = strchr(actual_host, '.');
if (!dot_pos) {
fprintf(stderr, "Error: Invalid hostname format. Expected client.domain format\n");
free(host_part);
return 0;
// No domain, use config
if (!config_domain) {
fprintf(stderr, "Error: Invalid hostname format. Expected client.domain format or domain in config\n");
free(host_part);
return 0;
}
*client_id = strdup(actual_host);
*wssshd_host = strdup(config_domain);
} else {
*dot_pos = '\0';
*client_id = strdup(actual_host);
*wssshd_host = strdup(dot_pos + 1);
}
*dot_pos = '\0';
*client_id = strdup(actual_host);
*wssshd_host = strdup(dot_pos + 1);
free(host_part);
return 1;
}
int parse_scp_args(int argc, char *argv[], char **destination, int *scp_port, int debug) {
int parse_scp_args(int argc, char *argv[], char **destination, int *scp_port, int config_port, int debug) {
*destination = NULL;
*scp_port = DEFAULT_PORT;
......@@ -203,6 +229,11 @@ int parse_scp_args(int argc, char *argv[], char **destination, int *scp_port, in
return 0;
}
// If no port found in args, use config
if (*scp_port == DEFAULT_PORT && config_port != 0) {
*scp_port = config_port;
}
return 1;
}
......@@ -1023,6 +1054,12 @@ int setup_tunnel(const char *wssshd_host, int wssshd_port, const char *client_id
}
int main(int argc, char *argv[]) {
// Read config
char *config_port_str = read_config_value("port");
int config_port = config_port_str ? atoi(config_port_str) : 0;
free(config_port_str);
char *config_domain = read_config_value("domain");
wsscp_config_t config = {
.local_port = NULL,
.debug = 0
......@@ -1031,6 +1068,7 @@ int main(int argc, char *argv[]) {
// Easter egg: --support option (only when it's the only argument)
if (argc == 2 && strcmp(argv[1], "--support") == 0) {
print_trans_flag();
free(config_domain);
return 0;
}
......@@ -1038,6 +1076,7 @@ int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
print_usage(argv[0]);
free(config_domain);
return 0;
}
}
......@@ -1049,6 +1088,7 @@ int main(int argc, char *argv[]) {
char **remaining_argv;
if (!parse_args(argc, argv, &config, &remaining_argc, &remaining_argv)) {
pthread_mutex_destroy(&tunnel_mutex);
free(config_domain);
return 1;
}
......@@ -1063,8 +1103,9 @@ int main(int argc, char *argv[]) {
// Parse SCP arguments to extract destination and port
char *scp_destination = NULL;
int scp_port = DEFAULT_PORT;
if (!parse_scp_args(remaining_argc, remaining_argv, &scp_destination, &scp_port, config.debug)) {
if (!parse_scp_args(remaining_argc, remaining_argv, &scp_destination, &scp_port, config_port, config.debug)) {
pthread_mutex_destroy(&tunnel_mutex);
free(config_domain);
return 1;
}
......@@ -1073,8 +1114,9 @@ int main(int argc, char *argv[]) {
char *wssshd_host = NULL;
int wssshd_port = scp_port; // The -P port becomes the WebSocket server port
if (!parse_hostname(scp_destination, &client_id, &wssshd_host)) {
if (!parse_hostname(scp_destination, &client_id, &wssshd_host, config_domain)) {
pthread_mutex_destroy(&tunnel_mutex);
free(config_domain);
return 1;
}
......@@ -1581,6 +1623,7 @@ cleanup:
}
}
free(new_scp_args);
free(config_domain);
pthread_mutex_destroy(&tunnel_mutex);
return 0;
......
......@@ -37,6 +37,27 @@
#define BUFFER_SIZE 1048576
#define DEFAULT_PORT 22
char *read_config_value(const char *key) {
char *home = getenv("HOME");
if (!home) return NULL;
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/.config/wsssh/wsssh.conf", home);
FILE *f = fopen(path, "r");
if (!f) return NULL;
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, key, strlen(key)) == 0 && line[strlen(key)] == '=') {
char *value = strdup(line + strlen(key) + 1);
// remove newline
value[strcspn(value, "\n")] = 0;
fclose(f);
return value;
}
}
fclose(f);
return NULL;
}
typedef struct {
char *local_port;
int debug;
......@@ -168,7 +189,7 @@ int parse_args(int argc, char *argv[], wsssh_config_t *config, int *remaining_ar
return 1;
}
int parse_hostname(const char *hostname, char **client_id, char **wssshd_host) {
int parse_hostname(const char *hostname, char **client_id, char **wssshd_host, const char *config_domain) {
char *at_pos = strchr(hostname, '@');
if (!at_pos) {
fprintf(stderr, "Error: Invalid hostname format. Expected user@host\n");
......@@ -190,20 +211,25 @@ int parse_hostname(const char *hostname, char **client_id, char **wssshd_host) {
// Split host by dots to extract client_id
char *dot_pos = strchr(host_copy, '.');
if (!dot_pos) {
fprintf(stderr, "Error: Invalid hostname format. Expected client.domain format\n");
free(host_copy);
return 0;
// No domain, use config
if (!config_domain) {
fprintf(stderr, "Error: Invalid hostname format. Expected client.domain format or domain in config\n");
free(host_copy);
return 0;
}
*client_id = strdup(host_copy);
*wssshd_host = strdup(config_domain);
} else {
*dot_pos = '\0';
*client_id = strdup(host_copy);
*wssshd_host = strdup(dot_pos + 1);
}
*dot_pos = '\0';
*client_id = strdup(host_copy);
*wssshd_host = strdup(dot_pos + 1);
free(host_copy);
return 1;
}
int parse_ssh_args(int argc, char *argv[], char **host, int *ssh_port, int debug) {
int parse_ssh_args(int argc, char *argv[], char **host, int *ssh_port, int config_port, int debug) {
*host = NULL;
*ssh_port = DEFAULT_PORT;
......@@ -234,6 +260,11 @@ int parse_ssh_args(int argc, char *argv[], char **host, int *ssh_port, int debug
return 0;
}
// If no port found in args, use config
if (*ssh_port == DEFAULT_PORT && config_port != 0) {
*ssh_port = config_port;
}
return 1;
}
......@@ -905,6 +936,12 @@ int setup_tunnel(const char *wssshd_host, int wssshd_port, const char *client_id
}
int main(int argc, char *argv[]) {
// Read config
char *config_port_str = read_config_value("port");
int config_port = config_port_str ? atoi(config_port_str) : 0;
free(config_port_str);
char *config_domain = read_config_value("domain");
wsssh_config_t config = {
.local_port = NULL,
.debug = 0
......@@ -913,6 +950,7 @@ int main(int argc, char *argv[]) {
// Easter egg: --support option (only when it's the only argument)
if (argc == 2 && strcmp(argv[1], "--support") == 0) {
print_trans_flag();
free(config_domain);
return 0;
}
......@@ -920,6 +958,7 @@ int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
print_usage(argv[0]);
free(config_domain);
return 0;
}
}
......@@ -931,6 +970,7 @@ int main(int argc, char *argv[]) {
char **remaining_argv;
if (!parse_args(argc, argv, &config, &remaining_argc, &remaining_argv)) {
pthread_mutex_destroy(&tunnel_mutex);
free(config_domain);
return 1;
}
......@@ -945,8 +985,9 @@ int main(int argc, char *argv[]) {
// Parse SSH arguments to extract host and port
char *ssh_host = NULL;
int ssh_port = DEFAULT_PORT;
if (!parse_ssh_args(remaining_argc, remaining_argv, &ssh_host, &ssh_port, config.debug)) {
if (!parse_ssh_args(remaining_argc, remaining_argv, &ssh_host, &ssh_port, config_port, config.debug)) {
pthread_mutex_destroy(&tunnel_mutex);
free(config_domain);
return 1;
}
......@@ -955,8 +996,9 @@ int main(int argc, char *argv[]) {
char *wssshd_host = NULL;
int wssshd_port = ssh_port; // The -p port becomes the WebSocket server port
if (!parse_hostname(ssh_host, &client_id, &wssshd_host)) {
if (!parse_hostname(ssh_host, &client_id, &wssshd_host, config_domain)) {
pthread_mutex_destroy(&tunnel_mutex);
free(config_domain);
return 1;
}
......@@ -1341,6 +1383,7 @@ int main(int argc, char *argv[]) {
}
}
free(new_ssh_args);
free(config_domain);
pthread_mutex_destroy(&tunnel_mutex);
return 0;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment