First working version
parent
c70bcae6af
commit
26694240c7
@ -0,0 +1,15 @@
|
||||
CC=gcc
|
||||
CFLAGS=-I.
|
||||
DEPS = utils.h
|
||||
OBJ = utils.o main.o
|
||||
|
||||
%.o: %.c $(DEPS)
|
||||
$(CC) -c -o $@ $< $(CFLAGS)
|
||||
|
||||
parse-ssh-cl: $(OBJ)
|
||||
$(CC) -o $@ $^ $(CFLAGS)
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
clean:
|
||||
rm -f *.o *~ core *~
|
@ -0,0 +1,82 @@
|
||||
# SSH Command Line Parser
|
||||
|
||||
## What.
|
||||
|
||||
This program is called exactly like the `ssh` binary from OpenSSH, except
|
||||
instead of opening a remote shell it will print the username (if specified),
|
||||
host, and port (if specified) given on the command line.
|
||||
|
||||
```bash
|
||||
./parse-ssh-cl -J bastion -X estelle@myhost:1234
|
||||
User: estelle
|
||||
Host: myhost
|
||||
Port: 1234
|
||||
Command:
|
||||
|
||||
./parse-ssh-cl -4 -q -L 8080:localhost:8080 app@prod
|
||||
User: app
|
||||
Host: prod
|
||||
Port:
|
||||
Command:
|
||||
|
||||
./parse-ssh-cl ssh://dev
|
||||
User:
|
||||
Host: dev
|
||||
Port:
|
||||
Command:
|
||||
|
||||
./parse-ssh-cl host ls -l
|
||||
User:
|
||||
Host: host
|
||||
Port:
|
||||
Command: ls -l
|
||||
```
|
||||
|
||||
## Why?
|
||||
|
||||
Because when I use tmux I want to be able to open a shell on a remote host and
|
||||
have the window title show the host I'm connected to. Something that can take an
|
||||
SSH command line string and output the host is an important piece of that
|
||||
puzzle.
|
||||
|
||||
## How?
|
||||
|
||||
* I looked at the source of OpenSSH (`ssh.c`) and saw two functions
|
||||
`parse_ssh_uri` and `parse_user_host_port` that seemed like they did what I
|
||||
wanted so I just copied them over and tried to compile them standalone.
|
||||
|
||||
* GCC yelled at me about all these missing function definitions so I copied
|
||||
them from around the code base (mostly `misc.c`).
|
||||
|
||||
* If the function wasn't there I Googled around to find out what random header I
|
||||
needed.
|
||||
|
||||
* I unearthed some heated drama surrounding `strlcpy` so I decided to copy the
|
||||
implementation from OpenBSD inline rather than link against libbsd.
|
||||
|
||||
* I found [greymd/ssh_opt_parse](https://github.com/greymd/ssh_opt_parse) which
|
||||
is honestly better than this project in every conceivable way.
|
||||
|
||||
* I copied the `getopt` string so that I could ignore all of the options, ran my
|
||||
two functions that now work, and boom, command line parsing just how upstream
|
||||
does it.
|
||||
|
||||
## Do.
|
||||
|
||||
Are you sure you really want to use this thing? Just run `make` in the source
|
||||
directory and then do whatever with the `parse-ssh-cl` binary it spits out.
|
||||
|
||||
As far as dependencies go just about any distribution's development tools should
|
||||
be more than enough to build and use. It pretty just uses libc.
|
||||
|
||||
## Never Asked Questions.
|
||||
|
||||
Do you support `-l` and `-p` parsing? Not yet. That's a great idea me.
|
||||
|
||||
## License.
|
||||
|
||||
Pretty much all of this code is straight up copy/pasted from
|
||||
[OpenSSH](https://github.com/openssh/openssh-portable) and the one function from
|
||||
[OpenBSD](https://github.com/openbsd/src). After reading the entire LICENSE file
|
||||
from OpenSSH I have concluded that I have no idea if and how I'm supposed to
|
||||
give attribution.
|
@ -0,0 +1,57 @@
|
||||
#include "utils.h"
|
||||
|
||||
int
|
||||
main(int argc, char *argv[]) {
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "-1246ab:c:e:fgi:kl:m:no:p:qstvx"
|
||||
"AB:CD:E:F:GI:J:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) {
|
||||
// Stop processing options after the first positional argument and set
|
||||
// optind back to the location of that argument.
|
||||
if (opt == 1) {
|
||||
optind -= 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (optind >= argc) {
|
||||
fprintf(stderr, "No destination found.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *destination = argv[optind];
|
||||
char *user;
|
||||
char *host;
|
||||
int port = -1;
|
||||
|
||||
if (parse_ssh_uri(destination, &user, &host, &port) == 0
|
||||
|| parse_user_host_port(destination, &user, &host, &port) == 0) {
|
||||
|
||||
if (user != NULL) {
|
||||
printf("User: %s\n", user);
|
||||
} else {
|
||||
printf("User:\n");
|
||||
}
|
||||
|
||||
printf("Host: %s\n", host);
|
||||
|
||||
if (port != -1) {
|
||||
printf("Port: %d\n", port);
|
||||
} else {
|
||||
printf("Port:\n");
|
||||
}
|
||||
|
||||
free(user);
|
||||
free(host);
|
||||
|
||||
printf("Command:");
|
||||
for (int i = ++optind; i < argc; i++) {
|
||||
printf(" %s", argv[i]);
|
||||
}
|
||||
printf("\n");
|
||||
} else {
|
||||
fprintf(stderr, "Could not parse destination host.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,454 @@
|
||||
#include "utils.h"
|
||||
|
||||
long long
|
||||
strtonum(const char *numstr, long long minval, long long maxval,
|
||||
const char **errstrp)
|
||||
{
|
||||
long long ll = 0;
|
||||
char *ep;
|
||||
int error = 0;
|
||||
struct errval {
|
||||
const char *errstr;
|
||||
int err;
|
||||
} ev[4] = {
|
||||
{ NULL, 0 },
|
||||
{ "invalid", EINVAL },
|
||||
{ "too small", ERANGE },
|
||||
{ "too large", ERANGE },
|
||||
};
|
||||
|
||||
ev[0].err = errno;
|
||||
errno = 0;
|
||||
if (minval > maxval)
|
||||
error = INVALID;
|
||||
else {
|
||||
ll = strtoll(numstr, &ep, 10);
|
||||
if (numstr == ep || *ep != '\0')
|
||||
error = INVALID;
|
||||
else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval)
|
||||
error = TOOSMALL;
|
||||
else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval)
|
||||
error = TOOLARGE;
|
||||
}
|
||||
if (errstrp != NULL)
|
||||
*errstrp = ev[error].errstr;
|
||||
errno = ev[error].err;
|
||||
if (error)
|
||||
ll = 0;
|
||||
|
||||
return (ll);
|
||||
}
|
||||
|
||||
void *
|
||||
xmalloc(size_t size)
|
||||
{
|
||||
void *ptr;
|
||||
|
||||
if (size == 0)
|
||||
fprintf(stderr, "xmalloc: zero size");
|
||||
ptr = malloc(size);
|
||||
if (ptr == NULL)
|
||||
fprintf(stderr, "xmalloc: out of memory (allocating %zu bytes)", size);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts a two-byte hex string to decimal.
|
||||
* Returns the decimal value or -1 for invalid input.
|
||||
*/
|
||||
static int
|
||||
hexchar(const char *s)
|
||||
{
|
||||
unsigned char result[2];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (s[i] >= '0' && s[i] <= '9')
|
||||
result[i] = (unsigned char)(s[i] - '0');
|
||||
else if (s[i] >= 'a' && s[i] <= 'f')
|
||||
result[i] = (unsigned char)(s[i] - 'a') + 10;
|
||||
else if (s[i] >= 'A' && s[i] <= 'F')
|
||||
result[i] = (unsigned char)(s[i] - 'A') + 10;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
return (result[0] << 4) | result[1];
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert ASCII string to TCP/IP port number.
|
||||
* Port must be >=0 and <=65535.
|
||||
* Return -1 if invalid.
|
||||
*/
|
||||
int
|
||||
a2port(const char *s)
|
||||
{
|
||||
struct servent *se;
|
||||
long long port;
|
||||
const char *errstr;
|
||||
|
||||
port = strtonum(s, 0, 65535, &errstr);
|
||||
if (errstr == NULL)
|
||||
return (int)port;
|
||||
if ((se = getservbyname(s, "tcp")) != NULL)
|
||||
return ntohs(se->s_port);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode an url-encoded string.
|
||||
* Returns a newly allocated string on success or NULL on failure.
|
||||
*/
|
||||
static char *
|
||||
urldecode(const char *src)
|
||||
{
|
||||
char *ret, *dst;
|
||||
int ch;
|
||||
|
||||
ret = xmalloc(strlen(src) + 1);
|
||||
for (dst = ret; *src != '\0'; src++) {
|
||||
switch (*src) {
|
||||
case '+':
|
||||
*dst++ = ' ';
|
||||
break;
|
||||
case '%':
|
||||
if (!isxdigit((unsigned char)src[1]) ||
|
||||
!isxdigit((unsigned char)src[2]) ||
|
||||
(ch = hexchar(src + 1)) == -1) {
|
||||
free(ret);
|
||||
return NULL;
|
||||
}
|
||||
*dst++ = ch;
|
||||
src += 2;
|
||||
break;
|
||||
default:
|
||||
*dst++ = *src;
|
||||
break;
|
||||
}
|
||||
}
|
||||
*dst = '\0';
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Search for next delimiter between hostnames/addresses and ports.
|
||||
* Argument may be modified (for termination).
|
||||
* Returns *cp if parsing succeeds.
|
||||
* *cp is set to the start of the next field, if one was found.
|
||||
* The delimiter char, if present, is stored in delim.
|
||||
* If this is the last field, *cp is set to NULL.
|
||||
*/
|
||||
char *
|
||||
hpdelim2(char **cp, char *delim)
|
||||
{
|
||||
char *s, *old;
|
||||
|
||||
if (cp == NULL || *cp == NULL)
|
||||
return NULL;
|
||||
|
||||
old = s = *cp;
|
||||
if (*s == '[') {
|
||||
if ((s = strchr(s, ']')) == NULL)
|
||||
return NULL;
|
||||
else
|
||||
s++;
|
||||
} else if ((s = strpbrk(s, ":/")) == NULL)
|
||||
s = *cp + strlen(*cp); /* skip to end (see first case below) */
|
||||
|
||||
switch (*s) {
|
||||
case '\0':
|
||||
*cp = NULL; /* no more fields*/
|
||||
break;
|
||||
|
||||
case ':':
|
||||
case '/':
|
||||
if (delim != NULL)
|
||||
*delim = *s;
|
||||
*s = '\0'; /* terminate */
|
||||
*cp = s + 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return old;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check and optionally lowercase a domain name, also removes trailing '.'
|
||||
* Returns 1 on success and 0 on failure, storing an error message in errstr.
|
||||
*/
|
||||
int
|
||||
valid_domain(char *name, int makelower, const char **errstr)
|
||||
{
|
||||
size_t i, l = strlen(name);
|
||||
u_char c, last = '\0';
|
||||
static char errbuf[256];
|
||||
|
||||
if (l == 0) {
|
||||
strlcpy(errbuf, "empty domain name", sizeof(errbuf));
|
||||
goto bad;
|
||||
}
|
||||
if (!isalpha((u_char)name[0]) && !isdigit((u_char)name[0])) {
|
||||
snprintf(errbuf, sizeof(errbuf), "domain name \"%.100s\" "
|
||||
"starts with invalid character", name);
|
||||
goto bad;
|
||||
}
|
||||
for (i = 0; i < l; i++) {
|
||||
c = tolower((u_char)name[i]);
|
||||
if (makelower)
|
||||
name[i] = (char)c;
|
||||
if (last == '.' && c == '.') {
|
||||
snprintf(errbuf, sizeof(errbuf), "domain name "
|
||||
"\"%.100s\" contains consecutive separators", name);
|
||||
goto bad;
|
||||
}
|
||||
if (c != '.' && c != '-' && !isalnum(c) &&
|
||||
c != '_') /* technically invalid, but common */ {
|
||||
snprintf(errbuf, sizeof(errbuf), "domain name "
|
||||
"\"%.100s\" contains invalid characters", name);
|
||||
goto bad;
|
||||
}
|
||||
last = c;
|
||||
}
|
||||
if (name[l - 1] == '.')
|
||||
name[l - 1] = '\0';
|
||||
if (errstr != NULL)
|
||||
*errstr = NULL;
|
||||
return 1;
|
||||
bad:
|
||||
if (errstr != NULL)
|
||||
*errstr = errbuf;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
char *
|
||||
cleanhostname(char *host)
|
||||
{
|
||||
if (*host == '[' && host[strlen(host) - 1] == ']') {
|
||||
host[strlen(host) - 1] = '\0';
|
||||
return (host + 1);
|
||||
} else
|
||||
return host;
|
||||
}
|
||||
|
||||
char *
|
||||
xstrdup(const char *str)
|
||||
{
|
||||
size_t len;
|
||||
char *cp;
|
||||
|
||||
len = strlen(str) + 1;
|
||||
cp = xmalloc(len);
|
||||
strlcpy(cp, str, len);
|
||||
return cp;
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse an (scp|ssh|sftp)://[user@]host[:port][/path] URI.
|
||||
* See https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04
|
||||
* Either user or path may be url-encoded (but not host or port).
|
||||
* Caller must free returned user, host and path.
|
||||
* Any of the pointer return arguments may be NULL (useful for syntax checking)
|
||||
* but the scheme must always be specified.
|
||||
* If user was not specified then *userp will be set to NULL.
|
||||
* If port was not specified then *portp will be -1.
|
||||
* If path was not specified then *pathp will be set to NULL.
|
||||
* Returns 0 on success, 1 if non-uri/wrong scheme, -1 on error/invalid uri.
|
||||
*/
|
||||
int
|
||||
parse_uri(const char *scheme, const char *uri, char **userp, char **hostp,
|
||||
int *portp, char **pathp)
|
||||
{
|
||||
char *uridup, *cp, *tmp, ch;
|
||||
char *user = NULL, *host = NULL, *path = NULL;
|
||||
int port = -1, ret = -1;
|
||||
size_t len;
|
||||
|
||||
len = strlen(scheme);
|
||||
if (strncmp(uri, scheme, len) != 0 || strncmp(uri + len, "://", 3) != 0)
|
||||
return 1;
|
||||
uri += len + 3;
|
||||
|
||||
if (userp != NULL)
|
||||
*userp = NULL;
|
||||
if (hostp != NULL)
|
||||
*hostp = NULL;
|
||||
if (portp != NULL)
|
||||
*portp = -1;
|
||||
if (pathp != NULL)
|
||||
*pathp = NULL;
|
||||
|
||||
uridup = tmp = xstrdup(uri);
|
||||
|
||||
/* Extract optional ssh-info (username + connection params) */
|
||||
if ((cp = strchr(tmp, '@')) != NULL) {
|
||||
char *delim;
|
||||
|
||||
*cp = '\0';
|
||||
/* Extract username and connection params */
|
||||
if ((delim = strchr(tmp, ';')) != NULL) {
|
||||
/* Just ignore connection params for now */
|
||||
*delim = '\0';
|
||||
}
|
||||
if (*tmp == '\0') {
|
||||
/* Empty username */
|
||||
goto out;
|
||||
}
|
||||
if ((user = urldecode(tmp)) == NULL)
|
||||
goto out;
|
||||
tmp = cp + 1;
|
||||
}
|
||||
|
||||
/* Extract mandatory hostname */
|
||||
if ((cp = hpdelim2(&tmp, &ch)) == NULL || *cp == '\0')
|
||||
goto out;
|
||||
host = xstrdup(cleanhostname(cp));
|
||||
if (!valid_domain(host, 0, NULL))
|
||||
goto out;
|
||||
|
||||
if (tmp != NULL && *tmp != '\0') {
|
||||
if (ch == ':') {
|
||||
/* Convert and verify port. */
|
||||
if ((cp = strchr(tmp, '/')) != NULL)
|
||||
*cp = '\0';
|
||||
if ((port = a2port(tmp)) <= 0)
|
||||
goto out;
|
||||
tmp = cp ? cp + 1 : NULL;
|
||||
}
|
||||
if (tmp != NULL && *tmp != '\0') {
|
||||
/* Extract optional path */
|
||||
if ((path = urldecode(tmp)) == NULL)
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Success */
|
||||
if (userp != NULL) {
|
||||
*userp = user;
|
||||
user = NULL;
|
||||
}
|
||||
if (hostp != NULL) {
|
||||
*hostp = host;
|
||||
host = NULL;
|
||||
}
|
||||
if (portp != NULL)
|
||||
*portp = port;
|
||||
if (pathp != NULL) {
|
||||
*pathp = path;
|
||||
path = NULL;
|
||||
}
|
||||
ret = 0;
|
||||
out:
|
||||
free(uridup);
|
||||
free(user);
|
||||
free(host);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
parse_ssh_uri(const char *uri, char **userp, char **hostp, int *portp)
|
||||
{
|
||||
char *path;
|
||||
int r;
|
||||
|
||||
r = parse_uri("ssh", uri, userp, hostp, portp, &path);
|
||||
if (r == 0 && path != NULL)
|
||||
r = -1; /* path not allowed */
|
||||
return r;
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a [user@]host[:port] string.
|
||||
* Caller must free returned user and host.
|
||||
* Any of the pointer return arguments may be NULL (useful for syntax checking).
|
||||
* If user was not specified then *userp will be set to NULL.
|
||||
* If port was not specified then *portp will be -1.
|
||||
* Returns 0 on success, -1 on failure.
|
||||
*/
|
||||
int
|
||||
parse_user_host_port(const char *s, char **userp, char **hostp, int *portp)
|
||||
{
|
||||
char *sdup, *cp, *tmp;
|
||||
char *user = NULL, *host = NULL;
|
||||
int port = -1, ret = -1;
|
||||
|
||||
if (userp != NULL)
|
||||
*userp = NULL;
|
||||
if (hostp != NULL)
|
||||
*hostp = NULL;
|
||||
if (portp != NULL)
|
||||
*portp = -1;
|
||||
|
||||
if ((sdup = tmp = strdup(s)) == NULL)
|
||||
return -1;
|
||||
/* Extract optional username */
|
||||
if ((cp = strrchr(tmp, '@')) != NULL) {
|
||||
*cp = '\0';
|
||||
if (*tmp == '\0')
|
||||
goto out;
|
||||
if ((user = strdup(tmp)) == NULL)
|
||||
goto out;
|
||||
tmp = cp + 1;
|
||||
}
|
||||
/* Extract mandatory hostname */
|
||||
if ((cp = hpdelim2(&tmp, NULL)) == NULL || *cp == '\0')
|
||||
goto out;
|
||||
host = xstrdup(cleanhostname(cp));
|
||||
/* Convert and verify optional port */
|
||||
if (tmp != NULL && *tmp != '\0') {
|
||||
if ((port = a2port(tmp)) <= 0)
|
||||
goto out;
|
||||
}
|
||||
/* Success */
|
||||
if (userp != NULL) {
|
||||
*userp = user;
|
||||
user = NULL;
|
||||
}
|
||||
if (hostp != NULL) {
|
||||
*hostp = host;
|
||||
host = NULL;
|
||||
}
|
||||
if (portp != NULL)
|
||||
*portp = port;
|
||||
ret = 0;
|
||||
out:
|
||||
free(sdup);
|
||||
free(user);
|
||||
free(host);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Copy string src to buffer dst of size dsize. At most dsize-1
|
||||
* chars will be copied. Always NUL terminates (unless dsize == 0).
|
||||
* Returns strlen(src); if retval >= dsize, truncation occurred.
|
||||
*/
|
||||
size_t
|
||||
strlcpy(char * __restrict dst, const char * __restrict src, size_t dsize)
|
||||
{
|
||||
const char *osrc = src;
|
||||
size_t nleft = dsize;
|
||||
|
||||
/* Copy as many bytes as will fit. */
|
||||
if (nleft != 0) {
|
||||
while (--nleft != 0) {
|
||||
if ((*dst++ = *src++) == '\0')
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Not enough room in dst, add NUL and traverse rest of src. */
|
||||
if (nleft == 0) {
|
||||
if (dsize != 0)
|
||||
*dst = '\0'; /* NUL-terminate dst */
|
||||
while (*src++)
|
||||
;
|
||||
}
|
||||
|
||||
return(src - osrc - 1); /* count does not include NUL */
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <limits.h>
|
||||
#include <netdb.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
|
||||
#define INVALID 1
|
||||
#define TOOSMALL 2
|
||||
#define TOOLARGE 3
|
||||
|
||||
long long
|
||||
strtonum(const char *numstr, long long minval, long long maxval,
|
||||
const char **errstrp);
|
||||
|
||||
void *
|
||||
xmalloc(size_t size);
|
||||
|
||||
static int
|
||||
hexchar(const char *s);
|
||||
|
||||
int
|
||||
a2port(const char *s);
|
||||
|
||||
static char *
|
||||
urldecode(const char *src);
|
||||
|
||||
char *
|
||||
hpdelim2(char **cp, char *delim);
|
||||
|
||||
int
|
||||
valid_domain(char *name, int makelower, const char **errstr);
|
||||
|
||||
char *
|
||||
cleanhostname(char *host);
|
||||
|
||||
char *
|
||||
xstrdup(const char *str);
|
||||
|
||||
int
|
||||
parse_uri(const char *scheme, const char *uri, char **userp, char **hostp,
|
||||
int *portp, char **pathp);
|
||||
|
||||
int
|
||||
parse_ssh_uri(const char *uri, char **userp, char **hostp, int *portp);
|
||||
|
||||
int
|
||||
parse_user_host_port(const char *s, char **userp, char **hostp, int *portp);
|
||||
|
||||
size_t
|
||||
strlcpy(char * __restrict dst, const char * __restrict src, size_t dsize);
|
Loading…
Reference in New Issue