commit
beb573d6ef
@ -0,0 +1,28 @@
|
||||
BIN = sbotc
|
||||
LDLIBS = -lsodium
|
||||
|
||||
PREFIX = /usr/local
|
||||
BINDIR = $(PREFIX)/bin
|
||||
MANDIR = $(PREFIX)/share/man
|
||||
|
||||
all: $(BIN)
|
||||
|
||||
$(BIN): $(BIN).c base64.c jsmn.c
|
||||
|
||||
install: all
|
||||
@mkdir -vp $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1
|
||||
@cp -vf $(BIN) $(DESTDIR)$(BINDIR)
|
||||
@cp -vf $(BIN).1 $(DESTDIR)$(MANDIR)/man1
|
||||
|
||||
link: all
|
||||
@mkdir -vp $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1
|
||||
@ln -rsvf $(BIN) $(DESTDIR)$(BINDIR)
|
||||
@ln -rsvf $(BIN).1 $(DESTDIR)$(MANDIR)/man1
|
||||
|
||||
uninstall:
|
||||
@rm -vf \
|
||||
$(DESTDIR)$(BINDIR)/$(BIN) \
|
||||
$(DESTDIR)$(MANDIR)/man1/$(BIN).1
|
||||
|
||||
clean:
|
||||
@rm -vf $(BIN)
|
@ -0,0 +1,24 @@
|
||||
# sbotc
|
||||
|
||||
A command-line SSB client in C. Use like the `sbot` command.
|
||||
|
||||
## Install
|
||||
|
||||
Install the dependency, *sodium*. On Debian: `sudo apt-get install libsodium-dev`
|
||||
|
||||
Compile and install the program:
|
||||
|
||||
```sh
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
sbotc [-s <host>] [-p <port>] [-k <key>] [-t <type>] <method> [<argument>...]
|
||||
```
|
||||
|
||||
Arguments must be explicitly JSON-encoded.
|
||||
|
||||
For more information, see the manual page `sbotc(1)`.
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
|
||||
This code is public domain software.
|
||||
|
||||
*/
|
||||
|
||||
#include "base64.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
|
||||
// single base64 character conversion
|
||||
//
|
||||
static int POS(char c)
|
||||
{
|
||||
if (c>='A' && c<='Z') return c - 'A';
|
||||
if (c>='a' && c<='z') return c - 'a' + 26;
|
||||
if (c>='0' && c<='9') return c - '0' + 52;
|
||||
if (c == '+') return 62;
|
||||
if (c == '/') return 63;
|
||||
if (c == '=') return -1;
|
||||
return -2;
|
||||
}
|
||||
|
||||
// base64 decoding
|
||||
//
|
||||
// s: base64 string
|
||||
// str_len size of the base64 string
|
||||
// data: output buffer for decoded data
|
||||
// data_len expected size of decoded data
|
||||
// return: 0 on success, -1 on failure
|
||||
//
|
||||
int base64_decode(const char* s, size_t str_len, void *data, size_t data_len)
|
||||
{
|
||||
const char *p, *str_end;
|
||||
unsigned char *q, *end;
|
||||
int n[4] = { 0, 0, 0, 0 };
|
||||
|
||||
if (str_len % 4) { errno = EBADMSG; return -1; }
|
||||
q = (unsigned char*) data;
|
||||
end = q + data_len;
|
||||
str_end = s + str_len;
|
||||
|
||||
for (p = s; p < str_end; ) {
|
||||
n[0] = POS(*p++);
|
||||
n[1] = POS(*p++);
|
||||
n[2] = POS(*p++);
|
||||
n[3] = POS(*p++);
|
||||
|
||||
if (n[0] == -2 || n[1] == -2 || n[2] == -2 || n[3] == -2)
|
||||
{ errno = EBADMSG; return -1; }
|
||||
|
||||
if (n[0] == -1 || n[1] == -1)
|
||||
{ errno = EBADMSG; return -1; }
|
||||
|
||||
if (n[2] == -1 && n[3] != -1)
|
||||
{ errno = EBADMSG; return -1; }
|
||||
|
||||
if (q >= end) { errno = EMSGSIZE; return -1; }
|
||||
q[0] = (n[0] << 2) + (n[1] >> 4);
|
||||
if (n[2] != -1) {
|
||||
if (q+1 >= end) { errno = EMSGSIZE; return -1; }
|
||||
q[1] = ((n[1] & 15) << 4) + (n[2] >> 2);
|
||||
}
|
||||
if (n[3] != -1) {
|
||||
if (q+2 >= end) { errno = EMSGSIZE; return -1; }
|
||||
q[2] = ((n[2] & 3) << 6) + n[3];
|
||||
}
|
||||
q += 3;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
int base64_decode(const char *s, size_t str_len, void *data, size_t data_len);
|
@ -0,0 +1,278 @@
|
||||
/**
|
||||
* jsmn
|
||||
* Copyright (c) 2010 Serge A. Zaitsev
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "jsmn.h"
|
||||
|
||||
/**
|
||||
* Allocates a fresh unused token from the token pull.
|
||||
*/
|
||||
static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser,
|
||||
jsmntok_t *tokens, size_t num_tokens) {
|
||||
jsmntok_t *tok;
|
||||
if (parser->toknext >= num_tokens) {
|
||||
return NULL;
|
||||
}
|
||||
tok = &tokens[parser->toknext++];
|
||||
tok->start = tok->end = -1;
|
||||
tok->size = 0;
|
||||
#ifdef JSMN_PARENT_LINKS
|
||||
tok->parent = -1;
|
||||
#endif
|
||||
return tok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills token type and boundaries.
|
||||
*/
|
||||
static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type,
|
||||
int start, int end) {
|
||||
token->type = type;
|
||||
token->start = start;
|
||||
token->end = end;
|
||||
token->size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills next available token with JSON primitive.
|
||||
*/
|
||||
static jsmnerr_t jsmn_parse_primitive(jsmn_parser *parser, const char *js,
|
||||
jsmntok_t *tokens, size_t num_tokens) {
|
||||
jsmntok_t *token;
|
||||
int start;
|
||||
|
||||
start = parser->pos;
|
||||
|
||||
for (; js[parser->pos] != '\0'; parser->pos++) {
|
||||
switch (js[parser->pos]) {
|
||||
#ifndef JSMN_STRICT
|
||||
/* In strict mode primitive must be followed by "," or "}" or "]" */
|
||||
case ':':
|
||||
#endif
|
||||
case '\t' : case '\r' : case '\n' : case ' ' :
|
||||
case ',' : case ']' : case '}' :
|
||||
goto found;
|
||||
}
|
||||
if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
|
||||
parser->pos = start;
|
||||
return JSMN_ERROR_INVAL;
|
||||
}
|
||||
}
|
||||
#ifdef JSMN_STRICT
|
||||
/* In strict mode primitive must be followed by a comma/object/array */
|
||||
parser->pos = start;
|
||||
return JSMN_ERROR_PART;
|
||||
#endif
|
||||
|
||||
found:
|
||||
token = jsmn_alloc_token(parser, tokens, num_tokens);
|
||||
if (token == NULL) {
|
||||
parser->pos = start;
|
||||
return JSMN_ERROR_NOMEM;
|
||||
}
|
||||
jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
|
||||
#ifdef JSMN_PARENT_LINKS
|
||||
token->parent = parser->toksuper;
|
||||
#endif
|
||||
parser->pos--;
|
||||
return JSMN_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filsl next token with JSON string.
|
||||
*/
|
||||
static jsmnerr_t jsmn_parse_string(jsmn_parser *parser, const char *js,
|
||||
jsmntok_t *tokens, size_t num_tokens) {
|
||||
jsmntok_t *token;
|
||||
|
||||
int start = parser->pos;
|
||||
|
||||
parser->pos++;
|
||||
|
||||
/* Skip starting quote */
|
||||
for (; js[parser->pos] != '\0'; parser->pos++) {
|
||||
char c = js[parser->pos];
|
||||
|
||||
/* Quote: end of string */
|
||||
if (c == '\"') {
|
||||
token = jsmn_alloc_token(parser, tokens, num_tokens);
|
||||
if (token == NULL) {
|
||||
parser->pos = start;
|
||||
return JSMN_ERROR_NOMEM;
|
||||
}
|
||||
jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos);
|
||||
#ifdef JSMN_PARENT_LINKS
|
||||
token->parent = parser->toksuper;
|
||||
#endif
|
||||
return JSMN_SUCCESS;
|
||||
}
|
||||
|
||||
/* Backslash: Quoted symbol expected */
|
||||
if (c == '\\') {
|
||||
parser->pos++;
|
||||
switch (js[parser->pos]) {
|
||||
/* Allowed escaped symbols */
|
||||
case '\"': case '/' : case '\\' : case 'b' :
|
||||
case 'f' : case 'r' : case 'n' : case 't' :
|
||||
break;
|
||||
/* Allows escaped symbol \uXXXX */
|
||||
case 'u':
|
||||
/* TODO */
|
||||
break;
|
||||
/* Unexpected symbol */
|
||||
default:
|
||||
parser->pos = start;
|
||||
return JSMN_ERROR_INVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
parser->pos = start;
|
||||
return JSMN_ERROR_PART;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON string and fill tokens.
|
||||
*/
|
||||
jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, jsmntok_t *tokens,
|
||||
unsigned int num_tokens) {
|
||||
jsmnerr_t r;
|
||||
int i;
|
||||
jsmntok_t *token;
|
||||
|
||||
for (; js[parser->pos] != '\0'; parser->pos++) {
|
||||
char c;
|
||||
jsmntype_t type;
|
||||
|
||||
c = js[parser->pos];
|
||||
switch (c) {
|
||||
case '{': case '[':
|
||||
token = jsmn_alloc_token(parser, tokens, num_tokens);
|
||||
if (token == NULL)
|
||||
return JSMN_ERROR_NOMEM;
|
||||
if (parser->toksuper != -1) {
|
||||
tokens[parser->toksuper].size++;
|
||||
#ifdef JSMN_PARENT_LINKS
|
||||
token->parent = parser->toksuper;
|
||||
#endif
|
||||
}
|
||||
token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
|
||||
token->start = parser->pos;
|
||||
parser->toksuper = parser->toknext - 1;
|
||||
break;
|
||||
case '}': case ']':
|
||||
type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
|
||||
#ifdef JSMN_PARENT_LINKS
|
||||
if (parser->toknext < 1) {
|
||||
return JSMN_ERROR_INVAL;
|
||||
}
|
||||
token = &tokens[parser->toknext - 1];
|
||||
for (;;) {
|
||||
if (token->start != -1 && token->end == -1) {
|
||||
if (token->type != type) {
|
||||
return JSMN_ERROR_INVAL;
|
||||
}
|
||||
token->end = parser->pos + 1;
|
||||
parser->toksuper = token->parent;
|
||||
break;
|
||||
}
|
||||
if (token->parent == -1) {
|
||||
break;
|
||||
}
|
||||
token = &tokens[token->parent];
|
||||
}
|
||||
#else
|
||||
for (i = parser->toknext - 1; i >= 0; i--) {
|
||||
token = &tokens[i];
|
||||
if (token->start != -1 && token->end == -1) {
|
||||
if (token->type != type) {
|
||||
return JSMN_ERROR_INVAL;
|
||||
}
|
||||
parser->toksuper = -1;
|
||||
token->end = parser->pos + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Error if unmatched closing bracket */
|
||||
if (i == -1) return JSMN_ERROR_INVAL;
|
||||
for (; i >= 0; i--) {
|
||||
token = &tokens[i];
|
||||
if (token->start != -1 && token->end == -1) {
|
||||
parser->toksuper = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case '\"':
|
||||
r = jsmn_parse_string(parser, js, tokens, num_tokens);
|
||||
if (r < 0) return r;
|
||||
if (parser->toksuper != -1)
|
||||
tokens[parser->toksuper].size++;
|
||||
break;
|
||||
case '\t' : case '\r' : case '\n' : case ':' : case ',': case ' ':
|
||||
break;
|
||||
#ifdef JSMN_STRICT
|
||||
/* In strict mode primitives are: numbers and booleans */
|
||||
case '-': case '0': case '1' : case '2': case '3' : case '4':
|
||||
case '5': case '6': case '7' : case '8': case '9':
|
||||
case 't': case 'f': case 'n' :
|
||||
#else
|
||||
/* In non-strict mode every unquoted value is a primitive */
|
||||
default:
|
||||
#endif
|
||||
r = jsmn_parse_primitive(parser, js, tokens, num_tokens);
|
||||
if (r < 0) return r;
|
||||
if (parser->toksuper != -1)
|
||||
tokens[parser->toksuper].size++;
|
||||
break;
|
||||
|
||||
#ifdef JSMN_STRICT
|
||||
/* Unexpected char in strict mode */
|
||||
default:
|
||||
return JSMN_ERROR_INVAL;
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (i = parser->toknext - 1; i >= 0; i--) {
|
||||
/* Unmatched opened object or array */
|
||||
if (tokens[i].start != -1 && tokens[i].end == -1) {
|
||||
return JSMN_ERROR_PART;
|
||||
}
|
||||
}
|
||||
|
||||
return JSMN_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new parser based over a given buffer with an array of tokens
|
||||
* available.
|
||||
*/
|
||||
void jsmn_init(jsmn_parser *parser) {
|
||||
parser->pos = 0;
|
||||
parser->toknext = 0;
|
||||
parser->toksuper = -1;
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
#ifndef __JSMN_H_
|
||||
#define __JSMN_H_
|
||||
|
||||
/**
|
||||
* JSON type identifier. Basic types are:
|
||||
* o Object
|
||||
* o Array
|
||||
* o String
|
||||
* o Other primitive: number, boolean (true/false) or null
|
||||
*/
|
||||
typedef enum {
|
||||
JSMN_PRIMITIVE = 0,
|
||||
JSMN_OBJECT = 1,
|
||||
JSMN_ARRAY = 2,
|
||||
JSMN_STRING = 3
|
||||
} jsmntype_t;
|
||||
|
||||
typedef enum {
|
||||
/* Not enough tokens were provided */
|
||||
JSMN_ERROR_NOMEM = -1,
|
||||
/* Invalid character inside JSON string */
|
||||
JSMN_ERROR_INVAL = -2,
|
||||
/* The string is not a full JSON packet, more bytes expected */
|
||||
JSMN_ERROR_PART = -3,
|
||||
/* Everything was fine */
|
||||
JSMN_SUCCESS = 0
|
||||
} jsmnerr_t;
|
||||
|
||||
/**
|
||||
* JSON token description.
|
||||
* @param type type (object, array, string etc.)
|
||||
* @param start start position in JSON data string
|
||||
* @param end end position in JSON data string
|
||||
*/
|
||||
typedef struct {
|
||||
jsmntype_t type;
|
||||
int start;
|
||||
int end;
|
||||
int size;
|
||||
#ifdef JSMN_PARENT_LINKS
|
||||
int parent;
|
||||
#endif
|
||||
} jsmntok_t;
|
||||
|
||||
/**
|
||||
* JSON parser. Contains an array of token blocks available. Also stores
|
||||
* the string being parsed now and current position in that string
|
||||
*/
|
||||
typedef struct {
|
||||
unsigned int pos; /* offset in the JSON string */
|
||||
int toknext; /* next token to allocate */
|
||||
int toksuper; /* superior token node, e.g parent object or array */
|
||||
} jsmn_parser;
|
||||
|
||||
/**
|
||||
* Create JSON parser over an array of tokens
|
||||
*/
|
||||
void jsmn_init(jsmn_parser *parser);
|
||||
|
||||
/**
|
||||
* Run JSON parser. It parses a JSON data string into and array of tokens, each describing
|
||||
* a single JSON object.
|
||||
*/
|
||||
jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js,
|
||||
jsmntok_t *tokens, unsigned int num_tokens);
|
||||
|
||||
#endif /* __JSMN_H_ */
|
@ -0,0 +1,91 @@
|
||||
.Dd 2017-06-03
|
||||
.Dt SBOTC 1
|
||||
.Os SSBC
|
||||
.ds REPO ssb://%133ulDgs/oC1DXjoK04vDFy6DgVBB/Zok15YJmuhD5Q=.sha256
|
||||
.Sh NAME
|
||||
.Nm sbotc
|
||||
.Nd Call a scuttlebot/secret-stack RPC method
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl s Ar host
|
||||
.Op Fl p Ar port
|
||||
.Op Fl k Ar key
|
||||
.Op Fl t Ar type
|
||||
.Ar method
|
||||
.Op Ar argument ...
|
||||
.Sh DESCRIPTION
|
||||
Connect to a scuttlebot/secret-stack server, and call a method on it, with
|
||||
standard I/O.
|
||||
.Sh OPTIONS
|
||||
.Bl -tag
|
||||
.It Fl s Ar host
|
||||
The hostname to connect to. Default is localhost.
|
||||
.It Fl p Ar port
|
||||
The port to connect to. Default is 8008.
|
||||
.It Fl k Ar key
|
||||
The key to connect to. Default is your public key.
|
||||
.It Fl t Ar type
|
||||
The type of method:
|
||||
.Dq async ,
|
||||
.Dq source ,
|
||||
.Dq sink ,
|
||||
or
|
||||
.Dq duplex .
|
||||
Default is to look up the method in
|
||||
.Pa ~/.ssb/manifest.json .
|
||||
.It Ar method
|
||||
Method name.
|
||||
.It Op Ar argument ...
|
||||
Arguments to pass to the method call. Each argument must be JSON-encoded.
|
||||
.El
|
||||
.Sh ENVIRONMENT
|
||||
.Bl -tag
|
||||
.It Ev ssb_appname
|
||||
Name of the app. Default is
|
||||
.Dq ssb .
|
||||
Used to construct the app's directory if
|
||||
.Ev ssb_path
|
||||
is not present.
|
||||
.It Ev ssb_path
|
||||
Path to the app's directory. Default is to use
|
||||
.Ev ssb_appname to construct the path as
|
||||
.Dq ~/.<ssb_appname>
|
||||
.El
|
||||
.Sh FILES
|
||||
.Bl -tag -width -indent
|
||||
.It Pa ~/.ssb/secret
|
||||
Your private key, used for authenticating to the server with the
|
||||
secret-handshake protocol.
|
||||
.It Pa ~/.ssb/manifest.json
|
||||
A map of method names to method types.
|
||||
.It Pa ~/.ssb/config
|
||||
JSON file containing host and port to use if the
|
||||
.Ar -s
|
||||
or
|
||||
.Ar -p
|
||||
options are not given.
|
||||
.El
|
||||
.Pp
|
||||
The base path
|
||||
.Dq ~/.ssb/
|
||||
of these file names may be changed by setting
|
||||
.Ev ssb_appname
|
||||
or
|
||||
.Ev ssb_path .
|
||||
.Sh EXIT STATUS
|
||||
.Bl -tag -width Ds
|
||||
.It 0
|
||||
The command completed successfully.
|
||||
.It 1
|
||||
An error occurred.
|
||||
.It 2
|
||||
The command completed with an error.
|
||||
.El
|
||||
.Sh AUTHORS
|
||||
.Nm
|
||||
was written by
|
||||
.An cel Aq @f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519 .
|
||||
.Sh BUGS
|
||||
.Pp
|
||||
Please report any bugs by making a post on SSB mentioning the repo,
|
||||
.Lk \*[REPO]
|
@ -0,0 +1,856 @@
|
||||
/*
|
||||
* sbotc.c
|
||||
* Copyright (c) 2017 Secure Scuttlebutt Consortium
|
||||
*
|
||||
* Usage of the works is permitted provided that this instrument is
|
||||
* retained with the works, so that any entity that uses the works is
|
||||
* notified of this instrument.
|
||||
*
|
||||
* DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
|
||||
*/
|
||||
|
||||
#include <ctype.h>
|
||||
#include <err.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include "base64.h"
|
||||
#include "jsmn.h"
|
||||
|
||||
#define BOXS_MAXLEN 4096
|
||||
|
||||
#define write_buf(fd, buf) \
|
||||
write_all(fd, buf, sizeof(buf))
|
||||
|
||||
struct boxs_header {
|
||||
uint16_t len;
|
||||
uint8_t mac[16];
|
||||
};
|
||||
|
||||
struct boxs {
|
||||
int s;
|
||||
unsigned char encrypt_key[32];
|
||||
unsigned char decrypt_key[32];
|
||||
unsigned char nonce1[24];
|
||||
unsigned char nonce2[24];
|
||||
unsigned char rx_nonce[24];
|
||||
unsigned char rx_buf[BOXS_MAXLEN];
|
||||
size_t rx_buf_pos;
|
||||
size_t rx_buf_len;
|
||||
};
|
||||
|
||||
enum pkt_type {
|
||||
pkt_type_buffer = 0,
|
||||
pkt_type_string = 1,
|
||||
pkt_type_json = 2,
|
||||
};
|
||||
|
||||
enum pkt_flags {
|
||||
pkt_flags_buffer = 0,
|
||||
pkt_flags_string = 1,
|
||||
pkt_flags_json = 2,
|
||||
pkt_flags_end = 4,
|
||||
pkt_flags_stream = 8,
|
||||
};
|
||||
|
||||
struct pkt_header {
|
||||
uint32_t len;
|
||||
int32_t req;
|
||||
} __attribute__((packed));
|
||||
|
||||
enum muxrpc_type {
|
||||
muxrpc_type_async,
|
||||
muxrpc_type_source,
|
||||
muxrpc_type_sink,
|
||||
muxrpc_type_duplex,
|
||||
};
|
||||
|
||||
enum stream_state {
|
||||
stream_state_open,
|
||||
stream_state_ended_ok,
|
||||
stream_state_ended_error,
|
||||
};
|
||||
|
||||
static unsigned char zeros[24] = {0};
|
||||
|
||||
static const unsigned char ssb_cap[] = {
|
||||
0xd4, 0xa1, 0xcb, 0x88, 0xa6, 0x6f, 0x02, 0xf8,
|
||||
0xdb, 0x63, 0x5c, 0xe2, 0x64, 0x41, 0xcc, 0x5d,
|
||||
0xac, 0x1b, 0x08, 0x42, 0x0c, 0xea, 0xac, 0x23,
|
||||
0x08, 0x39, 0xb7, 0x55, 0x84, 0x5a, 0x9f, 0xfb
|
||||
};
|
||||
|
||||
static void usage() {
|
||||
fputs("usage: sbotc [-s <host>] [-p <port>] [-k <key>] [-t <type>] "
|
||||
"<method> [<argument>...]\n", stderr);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static int auth_keypair(unsigned char *pk, unsigned char *sk, unsigned char *seed) {
|
||||
unsigned char pk_ed[32], sk_ed[32];
|
||||
int rc = crypto_sign_seed_keypair(pk_ed, sk_ed, seed);
|
||||
rc |= crypto_sign_ed25519_pk_to_curve25519(pk, pk_ed);
|
||||
rc |= crypto_sign_ed25519_sk_to_curve25519(sk, sk_ed);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int tcp_connect(const char *host, const char *port) {
|
||||
struct addrinfo hints;
|
||||
struct addrinfo *result, *rp;
|
||||
int s;
|
||||
int fd;
|
||||
int err;
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_protocol = IPPROTO_TCP;
|
||||
|
||||
s = getaddrinfo(host, port, &hints, &result);
|
||||
if (s < 0) errx(1, "unable to resolve host: %s", gai_strerror(s));
|
||||
|
||||
for (rp = result; rp; rp = rp->ai_next) {
|
||||
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (fd < 0) continue;
|
||||
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break;
|
||||
err = errno;
|
||||
close(fd);
|
||||
errno = err;
|
||||
}
|
||||
if (rp == NULL) fd = -1;
|
||||
|
||||
freeaddrinfo(result);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int read_all(int fd, void *buf, size_t count) {
|
||||
ssize_t nbytes;
|
||||
while (count > 0) {
|
||||
nbytes = read(fd, buf, count);
|
||||
if (nbytes == 0) { errno = EPIPE; return -1; }
|
||||
if (nbytes < 0 && errno == EINTR) continue;
|
||||
if (nbytes < 0) return -1;
|
||||
buf += nbytes;
|
||||
count -= nbytes;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_all(int fd, const void *buf, size_t count) {
|
||||
ssize_t nbytes;
|
||||
while (count > 0) {
|
||||
nbytes = write(fd, buf, count);
|
||||
if (nbytes < 0 && errno == EINTR) continue;
|
||||
if (nbytes < 0) return -1;
|
||||
buf += nbytes;
|
||||
count -= nbytes;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void shs_connect(int s, const unsigned char pubkey[32], const unsigned char seckey[64], const unsigned char appkey[32], const unsigned char server_pubkey[32], struct boxs *bs) {
|
||||
int rc;
|
||||
unsigned char local_app_mac[32], remote_app_mac[32];
|
||||
|
||||
unsigned char kx_pk[32], kx_sk[32];
|
||||
unsigned char seed[32];
|
||||
randombytes_buf(seed, sizeof(seed));
|
||||
rc = auth_keypair(kx_pk, kx_sk, seed);
|
||||
if (rc < 0) errx(1, "failed to generate auth keypair");
|
||||
|
||||
rc = crypto_auth(local_app_mac, kx_pk, 32, appkey);
|
||||
if (rc < 0) err(1, "failed to generate app mac");
|
||||
|
||||
// send challenge
|
||||
unsigned char buf[64];
|
||||
memcpy(buf, local_app_mac, 32);
|
||||
memcpy(buf+32, kx_pk, 32);
|
||||
rc = write_all(s, buf, sizeof(buf));
|
||||
if (rc < 0) err(1, "failed to send challenge");
|
||||
|
||||
// recv challenge
|
||||
unsigned char remote_kx_pk[32];
|
||||
rc = read_all(s, buf, sizeof(buf));
|
||||
if (rc < 0) err(1, "challenge not accepted");
|
||||
memcpy(remote_app_mac, buf, 32);
|
||||
memcpy(remote_kx_pk, buf+32, 32);
|
||||
rc = crypto_auth_verify(buf, remote_kx_pk, 32, appkey);
|
||||
if (rc < 0) errx(1, "wrong protocol (version?)");
|
||||
|
||||
// send auth
|
||||
|
||||
unsigned char secret[32];
|
||||
rc = crypto_scalarmult(secret, kx_sk, remote_kx_pk);
|
||||
if (rc < 0) errx(1, "failed to derive shared secret");
|
||||
|
||||
unsigned char remote_pk_curve[32];
|
||||
rc = crypto_sign_ed25519_pk_to_curve25519(remote_pk_curve, server_pubkey);
|
||||
if (rc < 0) errx(1, "failed to curvify remote public key");
|
||||
|
||||
unsigned char a_bob[32];
|
||||
rc = crypto_scalarmult(a_bob, kx_sk, remote_pk_curve);
|
||||
if (rc < 0) errx(1, "failed to derive a_bob");
|
||||
|
||||
unsigned char secret2a[96];
|
||||
memcpy(secret2a, appkey, 32);
|
||||
memcpy(secret2a+32, secret, 32);
|
||||
memcpy(secret2a+64, a_bob, 32);
|
||||
|
||||
unsigned char secret2[32];
|
||||
rc = crypto_hash_sha256(secret2, secret2a, sizeof(secret2a));
|
||||
if (rc < 0) errx(1, "failed to hash secret2");
|
||||
|
||||
unsigned char shash[32];
|
||||
rc = crypto_hash_sha256(shash, secret, sizeof(secret));
|
||||
if (rc < 0) errx(1, "failed to hash secret");
|
||||
|
||||
unsigned char signed1[96];
|
||||
memcpy(signed1, appkey, 32);
|
||||
memcpy(signed1+32, server_pubkey, 32);
|
||||
memcpy(signed1+64, shash, 32);
|
||||
|
||||
unsigned char sig[64];
|
||||
rc = crypto_sign_detached(sig, NULL, signed1, sizeof(signed1), seckey);
|
||||
if (rc < 0) errx(1, "failed to sign inner hello");
|
||||
|
||||
unsigned char hello[96];
|
||||
memcpy(hello, sig, 64);
|
||||
memcpy(hello+64, pubkey, 32);
|
||||
|
||||
unsigned char boxed_auth[112];
|
||||
rc = crypto_secretbox_easy(boxed_auth, hello, sizeof(hello), zeros, secret2);
|
||||
if (rc < 0) errx(1, "failed to box hello");
|
||||
|
||||
rc = write_all(s, boxed_auth, sizeof(boxed_auth));
|
||||
if (rc < 0) errx(1, "failed to send auth");
|
||||
|
||||
// verify accept
|
||||
|
||||
unsigned char boxed_okay[80];
|
||||
rc = read_all(s, boxed_okay, sizeof(boxed_okay));
|
||||
if (rc < 0) err(1, "hello not accepted");
|
||||
|
||||
unsigned char local_sk_curve[32];
|
||||
rc = crypto_sign_ed25519_sk_to_curve25519(local_sk_curve, seckey);
|
||||
if (rc < 0) errx(1, "failed to curvify local secret key");
|
||||
|
||||
unsigned char b_alice[32];
|
||||
rc = crypto_scalarmult(b_alice, local_sk_curve, remote_kx_pk);
|
||||
if (rc < 0) errx(1, "failed to derive b_alice");
|
||||
|
||||
unsigned char secret3a[128];
|
||||
memcpy(secret3a, appkey, 32);
|
||||
memcpy(secret3a+32, secret, 32);
|
||||
memcpy(secret3a+64, a_bob, 32);
|
||||
memcpy(secret3a+96, b_alice, 32);
|
||||
|
||||
unsigned char secret3[32];
|
||||
rc = crypto_hash_sha256(secret3, secret3a, sizeof(secret3a));
|
||||
if (rc < 0) errx(1, "failed to hash secret3");
|
||||
|
||||
rc = crypto_secretbox_open_easy(sig, boxed_okay, sizeof(boxed_okay), zeros, secret3);
|
||||
if (rc < 0) errx(1, "failed to unbox the okay");
|
||||
|
||||
unsigned char signed2[160];
|
||||
memcpy(signed2, appkey, 32);
|
||||
memcpy(signed2+32, hello, 96);
|
||||
memcpy(signed2+128, shash, 32);
|
||||
|
||||
rc = crypto_sign_verify_detached(sig, signed2, sizeof(signed2), server_pubkey);
|
||||
if (rc < 0) errx(1, "server not authenticated");
|
||||
|
||||
rc = crypto_hash_sha256(secret, secret3, 32);
|
||||
if (rc < 0) errx(1, "failed to hash secret3");
|
||||
|
||||
unsigned char enc_key_hashed[64];
|
||||
memcpy(enc_key_hashed, secret, 32);
|
||||
memcpy(enc_key_hashed+32, server_pubkey, 32);
|
||||
rc = crypto_hash_sha256(bs->encrypt_key, enc_key_hashed, 64);
|
||||
if (rc < 0) errx(1, "failed to hash the encrypt key");
|
||||
|
||||
unsigned char dec_key_hashed[64];
|
||||
memcpy(dec_key_hashed, secret, 32);
|
||||
memcpy(dec_key_hashed+32, pubkey, 32);
|
||||
rc = crypto_hash_sha256(bs->decrypt_key, dec_key_hashed, 64);
|
||||
if (rc < 0) errx(1, "failed to hash the decrypt key");
|
||||
|
||||
memcpy(bs->nonce1, remote_app_mac, 24);
|
||||
memcpy(bs->nonce2, remote_app_mac, 24);
|
||||
memcpy(bs->rx_nonce, local_app_mac, 24);
|
||||
|
||||
bs->rx_buf_pos = 0;
|
||||
bs->rx_buf_len = 0;
|
||||
bs->s = s;
|
||||
}
|
||||
|
||||
static int pubkey_decode(const char *key_str, unsigned char key[32]) {
|
||||
if (!key_str) { errno = EPROTO; return -1; }
|
||||
if (!*key_str) { errno = EPROTO; return -1; }
|
||||
if (*key_str == '@') key_str++;
|
||||
size_t len = strlen(key_str);
|
||||
if (len == 52 && strcmp(key_str+44, ".ed25519") == 0) {}
|
||||
else if (len != 44) { errno = EMSGSIZE; return -1; }
|
||||
return base64_decode(key_str, 44, key, 32);
|
||||
}
|
||||
|
||||
static jsmntok_t *json_lookup(const char *buf, jsmntok_t *tok, const char *prop, size_t prop_len) {
|
||||
jsmntok_t *end = tok + tok->size + 1;
|
||||
if (tok->type != JSMN_OBJECT) { errno = EPROTO; return NULL; }
|
||||
tok++;
|
||||
while (tok < end) {
|
||||
if (tok + 1 >= end) { errno = EPROTO; return NULL; }
|
||||
if (tok->type == JSMN_STRING
|
||||
&& tok->end - tok->start == (int)prop_len
|
||||
&& memcmp(buf + tok->start, prop, prop_len) == 0)
|
||||
return tok + 1;
|
||||
tok += tok->size + 1;
|
||||
end += tok->size;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ssize_t json_get_value(const char *buf, const char *path, const char **value) {
|
||||
static const int num_tokens = 1024;
|
||||
jsmntok_t tokens[num_tokens], *tok = tokens;
|
||||
jsmn_parser parser;
|
||||
|
||||
jsmn_init(&parser);
|
||||
switch (jsmn_parse(&parser, buf, tokens, num_tokens)) {
|
||||
case JSMN_ERROR_NOMEM: errno = ENOMEM; return -1;
|
||||
case JSMN_ERROR_INVAL: errno = EINVAL; return -1;
|
||||
case JSMN_ERROR_PART: errno = EMSGSIZE; return -1;
|
||||
case JSMN_SUCCESS: break;
|
||||
default: errno = EPROTO; return -1;
|
||||
}
|
||||
|
||||
while (*path) {
|
||||
const char *end = strchr(path, '.');
|
||||
size_t part_len = end ? end - path : strlen(path);
|
||||
tok = json_lookup(buf, tok, path, part_len);
|
||||
if (!tok) { errno = ENOMSG; return -1; }
|
||||
path += part_len;
|
||||
if (*path == '.') path++;
|
||||
}
|
||||
|
||||
*value = buf + tok->start;
|
||||
return tok->end - tok->start;
|
||||
}
|
||||
|
||||
static void get_app_dir(char *dir, size_t len) {
|
||||
const char *path, *home, *appname;
|
||||
int rc;
|
||||
path = getenv("ssb_path");
|
||||
if (path) {
|
||||
if (strlen(path) > len) errx(1, "ssb_path too long");
|
||||
strncpy(dir, path, len);
|
||||
return;
|
||||
}
|
||||
home = getenv("HOME");
|
||||
if (!home) home = ".";
|
||||
appname = getenv("ssb_appname");
|
||||
if (!appname) appname = "ssb";
|
||||
rc = snprintf(dir, len, "%s/.%s", home, appname);
|
||||
if (rc < 0) err(1, "failed to get app dir");
|
||||
if (rc >= len) errx(1, "path to app dir too long");
|
||||
}
|
||||
|
||||
static ssize_t read_file(char *buf, size_t len, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
int rc;
|
||||
struct stat st;
|
||||
int fd;
|
||||
|
||||
va_start(ap, fmt);
|
||||
rc = vsnprintf(buf, len, fmt, ap);
|
||||
va_end(ap);
|
||||
if (rc < 0) return -1;
|
||||
if (rc >= len) { errno = ENAMETOOLONG; return -1; }
|
||||
|
||||
rc = stat(buf, &st);
|
||||
if (rc < 0) return -1;
|
||||
if (st.st_size > len-1) { errno = EMSGSIZE; return -1; }
|
||||
|
||||
fd = open(buf, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
rc = read_all(fd, buf, st.st_size);
|
||||
if (rc < 0) return -1;
|
||||
buf[st.st_size] = '\0';
|
||||
|
||||
close(fd);
|
||||
return st.st_size;
|
||||
}
|
||||
|
||||
|
||||
static void read_private_key(const char *dir, unsigned char pk[64]) {
|
||||
ssize_t len;
|
||||
char buf[8192];
|
||||
const char *pk_b64;
|
||||
int rc;
|
||||
ssize_t key_len;
|
||||
char *line;
|
||||
|
||||
len = read_file(buf, sizeof(buf), "%s/secret", dir);
|
||||
if (len < 0) err(1, "failed to read secret file");
|
||||
|
||||
// strip comments
|
||||
for (line = buf; *line; ) {
|
||||
if (*line == '#') while (*line && *line != '\n') *line++ = ' ';
|
||||
else while (*line && *line++ != '\n');
|
||||
}
|
||||
|
||||
key_len = json_get_value(buf, "private", &pk_b64);
|
||||
if (key_len < 0) err(1, "unable to read private key");
|
||||
|
||||
if (key_len > 8 && memcmp(pk_b64 + key_len - 8, ".ed25519", 8) == 0)
|
||||
key_len -= 8;
|
||||
rc = base64_decode(pk_b64, key_len, pk, 64);
|
||||
if (rc < 0) err(1, "unable to decode private key");
|
||||
}
|
||||
|
||||
static void increment_nonce(uint8_t nonce[24]) {
|
||||
int i;
|
||||
for (i = 23; i >= 0 && nonce[i] == 0xff; i--) nonce[i] = 0;
|
||||
if (i >= 0) nonce[i]++;
|
||||
}
|
||||
|
||||
static void bs_write_packet(struct boxs *bs, const unsigned char *buf, uint16_t len) {
|
||||
size_t boxed_len = len + 34;
|
||||
unsigned char boxed[boxed_len];
|
||||
increment_nonce(bs->nonce2);
|
||||
int rc = crypto_secretbox_easy(boxed + 18, buf, len, bs->nonce2, bs->encrypt_key);
|
||||
if (rc < 0) errx(1, "failed to box packet data");
|
||||
struct boxs_header header;
|
||||
header.len = htons(len);
|
||||
memcpy(header.mac, boxed + 18, 16);
|
||||
rc = crypto_secretbox_easy(boxed, (unsigned char *)&header, 18, bs->nonce1, bs->encrypt_key);
|
||||
if (rc < 0) errx(1, "failed to box packet header");
|
||||
increment_nonce(bs->nonce1);
|
||||
increment_nonce(bs->nonce1);
|
||||
increment_nonce(bs->nonce2);
|
||||
rc = write_all(bs->s, boxed, boxed_len);
|
||||
if (rc < 0) err(1, "failed to write boxed packet");
|
||||
}
|
||||
|
||||
static int bs_read_packet(struct boxs *bs, void *buf, size_t *lenp) {
|
||||
unsigned char boxed_header[34];
|
||||
struct boxs_header header;
|
||||
int rc = read_all(bs->s, boxed_header, 34);
|
||||
if (rc < 0 && errno == EPIPE) errx(1, "unexpected end of parent stream");
|
||||
if (rc < 0) err(1, "failed to read boxed packet header");
|
||||
rc = crypto_secretbox_open_easy((unsigned char *)&header, boxed_header, 34, bs->rx_nonce, bs->decrypt_key);
|
||||
if (rc < 0) errx(1, "failed to unbox packet header");
|
||||
increment_nonce(bs->rx_nonce);
|
||||
if (header.len == 0 && !memcmp(header.mac, zeros, 16)) { errno = EPIPE; return -1; }
|
||||
size_t len = ntohs(header.len);
|
||||
if (len > BOXS_MAXLEN) errx(1, "received boxed packet too large");
|
||||
unsigned char boxed_data[len + 16];
|
||||
rc = read_all(bs->s, boxed_data + 16, len);
|
||||
if (rc < 0) err(1, "failed to read boxed packet data");
|
||||
memcpy(boxed_data, header.mac, 16);
|
||||
rc = crypto_secretbox_open_easy(buf, boxed_data, len+16, bs->rx_nonce, bs->decrypt_key);
|
||||
if (rc < 0) errx(1, "failed to unbox packet data");
|
||||
increment_nonce(bs->rx_nonce);
|
||||
*lenp = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bs_read(struct boxs *bs, char *buf, size_t len) {
|
||||
size_t remaining;
|
||||
while (len > 0) {
|
||||
remaining = bs->rx_buf_len > len ? len : bs->rx_buf_len;
|
||||
if (buf) memcpy(buf, bs->rx_buf + bs->rx_buf_pos, remaining);
|
||||
bs->rx_buf_len -= remaining;
|
||||
bs->rx_buf_pos += remaining;
|
||||
len -= remaining;
|
||||
buf += remaining;
|
||||
if (len == 0) return 0;
|
||||
if (bs_read_packet(bs, bs->rx_buf, &bs->rx_buf_len) < 0) return -1;
|
||||
bs->rx_buf_pos = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bs_read_out(struct boxs *bs, int fd, size_t len) {
|
||||
size_t chunk;
|
||||
char buf[4096];
|
||||
int rc;
|
||||
while (len > 0) {
|
||||
chunk = len > sizeof(buf) ? sizeof(buf) : len;
|
||||
rc = bs_read(bs, buf, chunk);
|
||||
if (rc < 0) return -1;
|
||||
rc = write_all(fd, buf, chunk);
|
||||
if (rc < 0) return -1;
|
||||
len -= chunk;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bs_read_error(struct boxs *bs, int errfd, enum pkt_flags flags, size_t len) {
|
||||
// suppress printing "true" indicating end without error
|
||||
if (flags & pkt_flags_json && len == 4) {
|
||||
char buf[4];
|
||||
if (bs_read(bs, buf, 4) < 0) return -1;
|
||||
if (strncmp(buf, "true", 0) == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (write_all(errfd, buf, 4) < 0) return -1;
|
||||
} else {
|
||||
if (bs_read_out(bs, errfd, len) < 0) return -1;
|
||||
}
|
||||
if (flags & (pkt_flags_json | pkt_flags_string)) {
|
||||
if (write_buf(errfd, "\n") < 0) return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void bs_write(struct boxs *bs, const unsigned char *buf, size_t len) {
|
||||
while (len > 0) {
|
||||
size_t l = len > BOXS_MAXLEN ? BOXS_MAXLEN : len;
|
||||
bs_write_packet(bs, buf, l);
|
||||
len -= l;
|
||||
buf += l;
|
||||
}
|
||||
}
|
||||
|
||||
static void ps_write(struct boxs *bs, const char *data, size_t len, enum pkt_type type, int req_id, bool stream, bool end) {
|
||||
size_t out_len = 9 + len;
|
||||
unsigned char out_buf[out_len];
|
||||
struct pkt_header header = {htonl(len), htonl(req_id)};
|
||||
out_buf[0] = (stream << 3) | (end << 2) | (type & 3);
|
||||
memcpy(out_buf+1, &header, 8);
|
||||
memcpy(out_buf+9, data, len);
|
||||
bs_write(bs, out_buf, out_len);
|
||||
}
|
||||
|
||||
static int ps_read_header(struct boxs *bs, size_t *len, int *req_id, enum pkt_flags *flags) {
|
||||
char buf[9];
|
||||
struct pkt_header header;
|
||||
if (bs_read(bs, buf, sizeof(buf)) < 0) return -1;
|
||||
memcpy(&header, buf+1, 8);
|
||||
if (len) *len = ntohl(header.len);
|
||||
if (req_id) *req_id = ntohl(header.req);
|
||||
if (flags) *flags = buf[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void muxrpc_call(struct boxs *bs, const char *method, const char *argument, enum muxrpc_type type, const char *typestr, int req_id) {
|
||||
char req[8096];
|
||||
ssize_t reqlen;
|
||||
bool is_request = type == muxrpc_type_async;
|
||||
|
||||
if (is_request) {
|
||||
reqlen = snprintf(req, sizeof(req),
|
||||
"{\"name\":%s,\"args\":%s}",
|
||||
method, argument);
|
||||
} else {
|
||||
reqlen = snprintf(req, sizeof(req),
|
||||
"{\"name\":%s,\"args\":%s,\"type\":\"%s\"}",
|
||||
method, argument, typestr);
|
||||
}
|
||||
if (reqlen < 0) err(1, "failed to construct request");
|
||||
if (reqlen >= sizeof(req)) errx(1, "request too large");
|
||||
|
||||
ps_write(bs, req, reqlen, pkt_type_json, req_id, !is_request, false);
|
||||
}
|
||||
|
||||
static void ps_reject(struct boxs *bs, size_t len, int32_t req, enum pkt_flags flags) {
|
||||
// ignore the packet. if this is a request, the substream on the other end
|
||||
// will just have to wait until the rpc connection closes.
|
||||
write_buf(STDERR_FILENO, "ignoring packet: ");
|
||||
int rc = bs_read_out(bs, STDERR_FILENO, len);
|
||||
if (rc < 0) err(1, "bs_read_out");
|
||||
write_buf(STDERR_FILENO, "\n");
|
||||
}
|
||||
|
||||
static enum stream_state muxrpc_read_source_1(struct boxs *bs, int outfd, int req_id) {
|
||||
enum pkt_flags flags;
|
||||
size_t len;
|
||||
int32_t req;
|
||||
int rc = ps_read_header(bs, &len, &req, &flags);
|
||||
if (rc < 0) err(1, "ps_read_header");
|
||||
if (req == 0 && len == 0) {
|
||||
warnx("unexpected end of parent stream");
|
||||
return stream_state_ended_error;
|
||||
}
|
||||
if (req != -req_id) {
|
||||
ps_reject(bs, len, req, flags);
|
||||
return stream_state_open;
|
||||
}
|
||||
if (flags & pkt_flags_end) {
|
||||
rc = bs_read_error(bs, STDERR_FILENO, flags, len);
|
||||
if (rc < 0) err(1, "bs_read_error");
|
||||
if (rc == 1) return stream_state_ended_error;
|
||||
return stream_state_ended_ok;
|
||||
}
|
||||
rc = bs_read_out(bs, outfd, len);
|
||||
if (rc < 0) err(1, "bs_read_out");
|
||||
if (flags & (pkt_flags_json | pkt_flags_string)) {
|
||||
rc = write_buf(outfd, "\n");
|
||||
if (rc < 0) err(1, "write_buf");
|
||||
}
|
||||
return stream_state_open;
|
||||
}
|
||||
|
||||
static int muxrpc_read_source(struct boxs *bs, int outfd, int req_id) {
|
||||
enum stream_state state;
|
||||
while ((state = muxrpc_read_source_1(bs, outfd, req_id)) == stream_state_open);
|
||||
return state == stream_state_ended_ok ? 0 :
|
||||
state == stream_state_ended_error ? 2 : 1;
|
||||
}
|
||||
|
||||
static int muxrpc_read_async(struct boxs *bs, int outfd, int req_id) {
|
||||
enum pkt_flags flags;
|
||||
size_t len;
|
||||
int32_t req;
|
||||
int rc;
|
||||
|
||||
while (1) {
|
||||
rc = ps_read_header(bs, &len, &req, &flags);
|
||||
if (rc < 0) err(1, "ps_read_header");
|
||||
if (req == -req_id) break;
|
||||
if (req == 0 && len == 0) errx(1, "unexpected end of parent stream");
|
||||
ps_reject(bs, len, req, flags);
|
||||
}
|
||||
if (flags & pkt_flags_end) {
|
||||
rc = bs_read_error(bs, STDERR_FILENO, flags, len);
|
||||
if (rc < 0) err(1, "bs_read_error");
|
||||
if (rc == 1) return 2;
|
||||
return 1;
|
||||
}
|
||||
rc = bs_read_out(bs, outfd, len);
|
||||
if (rc < 0) err(1, "bs_read_out");
|
||||
if (flags & (pkt_flags_json | pkt_flags_string)) {
|
||||
rc = write_buf(outfd, "\n");
|
||||
if (rc < 0) err(1, "write_buf");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum stream_state muxrpc_write_sink_1(struct boxs *bs, int infd, int req_id) {
|
||||
char buf[4096];
|
||||
ssize_t sz = read(infd, buf, sizeof(buf));
|
||||
if (sz < 0) err(1, "read");
|
||||
if (sz == 0) {
|
||||
ps_write(bs, "true", 4, pkt_type_json, req_id, true, true);
|
||||
return stream_state_ended_ok;
|
||||
}
|
||||
ps_write(bs, buf, sz, pkt_type_buffer, req_id, true, false);
|
||||
return stream_state_open;
|
||||
}
|
||||
|
||||
static int muxrpc_write_sink(struct boxs *bs, int infd, int req_id) {
|
||||
int rc;
|
||||
fd_set rd;
|
||||
int sfd = bs->s;
|
||||
int maxfd = infd > sfd ? infd : sfd;
|
||||
enum stream_state in = stream_state_open;
|
||||
enum stream_state out = stream_state_open;
|
||||
|
||||
while (out == stream_state_open) {
|
||||
FD_ZERO(&rd);
|
||||
if (in == stream_state_open) FD_SET(infd, &rd);
|
||||
if (out == stream_state_open) FD_SET(sfd, &rd);
|
||||
rc = select(maxfd + 1, &rd, 0, 0, NULL);
|
||||
if (rc < 0) err(1, "select");
|
||||
if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1(bs, infd, req_id);
|
||||
if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, -1, req_id);
|
||||
}
|
||||
|
||||
return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 :
|
||||
in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1;
|
||||
}
|
||||
|
||||
static int muxrpc_duplex(struct boxs *bs, int infd, int outfd, int req_id) {
|
||||
int rc;
|
||||
fd_set rd;
|
||||
int sfd = bs->s;
|
||||
int maxfd = infd > sfd ? infd : sfd;
|
||||
enum stream_state in = stream_state_open;
|
||||
enum stream_state out = stream_state_open;
|
||||
|
||||
while (out == stream_state_open
|
||||
|| (in == stream_state_open && out != stream_state_ended_error)) {
|
||||
FD_ZERO(&rd);
|
||||
if (in == stream_state_open) FD_SET(infd, &rd);
|
||||
if (out == stream_state_open) FD_SET(sfd, &rd);
|
||||
rc = select(maxfd + 1, &rd, 0, 0, NULL);
|
||||
if (rc < 0) err(1, "select");
|
||||
if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1(bs, infd, req_id);
|
||||
if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, outfd, req_id);
|
||||
}
|
||||
|
||||
return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 :
|
||||
in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1;
|
||||
}
|
||||
|
||||
static int method_to_json(char *out, size_t outlen, const char *str) {
|
||||
// blobs.get => ["blobs", "get"]
|
||||
size_t i = 0;
|
||||
char c;
|
||||
if (i+2 > outlen) return -1;
|
||||
out[i++] = '[';
|
||||
out[i++] = '"';
|
||||
while ((c = *str++)) {
|
||||
if (c == '.') {
|
||||
if (i+3 > outlen) return -1;
|
||||
out[i++] = '"';
|
||||
out[i++] = ',';
|
||||
out[i++] = '"';
|
||||
} else if (c == '"') {
|
||||
if (i+2 > outlen) return -1;
|
||||
out[i++] = '\\';
|
||||
out[i++] = '"';
|
||||
} else {
|
||||
if (i+1 > outlen) return -1;
|
||||
out[i++] = c;
|
||||
}
|
||||
}
|
||||
if (i+3 > outlen) return -1;
|
||||
out[i++] = '"';
|
||||
out[i++] = ']';
|
||||
out[i++] = '\0';
|
||||
return i;
|
||||
}
|
||||
|
||||
static int args_to_json_length(int argc, char *argv[]) {
|
||||
int i = 0;
|
||||
int len = 3; // "[]\0"
|
||||
for (i = 0; i < argc; i++)
|
||||
len += strlen(argv[i])+1;
|
||||
return len;
|
||||
}
|
||||
|
||||
static int args_to_json(char *out, size_t outlen, unsigned int argc, char *argv[]) {
|
||||
size_t i = 0;
|
||||
size_t j;
|
||||
if (i+1 > outlen) return -1;
|
||||
out[i++] = '[';
|
||||
for (j = 0; j < argc; j++) {
|
||||
size_t len = strlen(argv[j]);
|
||||
if (j > 0) out[i++] = ',';
|
||||
if (i+len > outlen) return -1;
|
||||
strncpy(out+i, argv[j], len);
|
||||
i += len;
|
||||
}
|
||||
if (i+2 > outlen) return -1;
|
||||
out[i++] = ']';
|
||||
out[i++] = '\0';
|
||||
return i;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int i, s, rc;
|
||||
const char *key = NULL;
|
||||
const char *host = NULL;
|
||||
const char *port = "8008";
|
||||
const char *typestr = NULL, *methodstr;
|
||||
size_t argument_len;
|
||||
unsigned char private_key[64];
|
||||
unsigned char public_key[32];
|
||||
unsigned char remote_key[32];
|
||||
enum muxrpc_type type;
|
||||
char method[256];
|
||||
char app_dir[_POSIX_PATH_MAX];
|
||||
ssize_t len;
|
||||
|
||||
get_app_dir(app_dir, sizeof(app_dir));
|
||||
|
||||
char config_buf[8192];
|
||||
len = read_file(config_buf, sizeof(config_buf), "%s/config", app_dir);
|
||||
if (len > 0) {
|
||||
ssize_t host_len = json_get_value(config_buf, "host", &host);
|
||||
ssize_t port_len = json_get_value(config_buf, "port", &port);
|
||||
if (host_len >= 0) ((char *)host)[host_len] = '\0';
|
||||
if (port_len >= 0) ((char *)port)[port_len] = '\0';
|
||||
} else if (len < 0 && errno != ENOENT) {
|
||||
err(1, "failed to read config");
|
||||
}
|
||||
|
||||
for (i = 1; (i + 1 < argc) && (argv[i][0] == '-'); i++) {
|
||||
switch (argv[i][1]) {
|
||||
case 's': host = argv[++i]; break;
|
||||
case 'k': key = argv[++i]; break;
|
||||
case 'p': port = argv[++i]; break;
|
||||
case 't': typestr = argv[++i]; break;
|
||||
default: usage();
|
||||
}
|
||||
}
|
||||
if (i < argc) methodstr = argv[i++]; else usage();
|
||||
|
||||
argument_len = args_to_json_length(argc-i, argv+i);
|
||||
char argument[argument_len];
|
||||
rc = args_to_json(argument, sizeof(argument), argc-i, argv+i);
|
||||
if (rc < 0) errx(0, "unable to collect arguments");
|
||||
|
||||
char manifest_buf[8192];
|
||||
if (!typestr) {
|
||||
len = read_file(manifest_buf, sizeof(manifest_buf),
|
||||
"%s/manifest.json", app_dir);
|
||||
if (len < 0) err(1, "failed to read manifest file");
|
||||
|
||||
ssize_t type_len = json_get_value(manifest_buf, methodstr, &typestr);
|
||||
if (!typestr && errno == ENOMSG) errx(1,
|
||||
"unable to find method '%s' in manifest", methodstr);
|
||||
if (!typestr) err(1, "unable to read manifest");
|
||||
((char *)typestr)[type_len] = '\0';
|
||||
}
|
||||
if (strcmp(typestr, "sync") == 0) type = muxrpc_type_async;
|
||||
else if (strcmp(typestr, "async") == 0) type = muxrpc_type_async;
|
||||
else if (strcmp(typestr, "sink") == 0) type = muxrpc_type_sink;
|
||||
else if (strcmp(typestr, "source") == 0) type = muxrpc_type_source;
|
||||
else if (strcmp(typestr, "duplex") == 0) type = muxrpc_type_duplex;
|
||||
else errx(1, "type must be one of <async|sink|source|duplex>");
|
||||
|
||||
rc = method_to_json(method, sizeof(method), methodstr);
|
||||
if (rc < 0) errx(0, "unable to convert method name");
|
||||
|
||||
read_private_key(app_dir, private_key);
|
||||
|
||||
memcpy(public_key, private_key+32, 32);
|
||||
if (key) {
|
||||
rc = pubkey_decode(key, remote_key);
|
||||
if (rc < 0) err(1, "unable to decode remote key '%s'", key);
|
||||
} else {
|
||||
memcpy(remote_key, public_key, 32);
|
||||
}
|
||||
|
||||
s = tcp_connect(host, port);
|
||||
if (s < 0) err(1, "tcp_connect");
|
||||
|
||||
struct boxs bs;
|
||||
shs_connect(s, public_key, private_key, ssb_cap, remote_key, &bs);
|
||||
|
||||
muxrpc_call(&bs, method, argument, type, typestr, 1);
|
||||
|
||||
switch (type) {
|
||||
case muxrpc_type_async:
|
||||
rc = muxrpc_read_async(&bs, STDOUT_FILENO, 1);
|
||||
break;
|
||||
case muxrpc_type_source:
|
||||
rc = muxrpc_read_source(&bs, STDOUT_FILENO, 1);
|
||||
break;
|
||||
case muxrpc_type_sink:
|
||||
rc = muxrpc_write_sink(&bs, STDIN_FILENO, 1);
|
||||
break;
|
||||
case muxrpc_type_duplex:
|
||||
rc = muxrpc_duplex(&bs, STDIN_FILENO, STDOUT_FILENO, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
close(s);
|
||||
return rc;
|
||||
}
|
Loading…
Reference in new issue