First working version

master
Estelle Poulin 5 years ago
parent c70bcae6af
commit 26694240c7

2
.gitignore vendored

@ -1,4 +1,4 @@
parse-ssh
parse-ssh-cl
##
# From github/gitignore C.gitignore

@ -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…
Cancel
Save