Add `gemini_envinit', add proxy support, simplfy header handling
Add `gemini_envinit' which initializes some parts of the structure from the environment. Add proxy support in the form of a `proxy' field in the structure that can hold a host string. Simplfy header handling in `gemini_read', removing the `GEMINI_STRICT' flag. Add the `-h' option to gemini-cat(1) to set the proxy host. Remove the `-e' option from gemini-cat(1). Add environment variable support to gemini-cat(1), including `GEMINI_TOFU_WRITE', which will set the corresponding flag.
This commit is contained in:
parent
e0d7ff540e
commit
5dcddc7077
46
gemini-cat.1
46
gemini-cat.1
|
@ -1,4 +1,4 @@
|
|||
.Dd Jun 10, 2020
|
||||
.Dd Jun 20, 2020
|
||||
.Dt GEMINI-CAT 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
@ -6,8 +6,9 @@
|
|||
.Nd concatnate responses from a Gemini URLs
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl RSeisw
|
||||
.Op Fl RSisw
|
||||
.Op Fl c Ar cert-file
|
||||
.Op Fl h Ar proxy-host
|
||||
.Op Fl k Ar key-file
|
||||
.Op Fl p Ar port
|
||||
.Op Fl r Ar max-redirects
|
||||
|
@ -26,8 +27,10 @@ Strip out non-space control-characters before writing, the default for
|
|||
TTY outputs.
|
||||
.It Fl c Ar cert-file
|
||||
The certificate file to use for TLS.
|
||||
.It Fl e
|
||||
Enforce a strict interpretation of the Gemini specification.
|
||||
.It Fl h Ar proxy-host
|
||||
The host to connect through.
|
||||
.Ar proxy-host
|
||||
can also specify a port number.
|
||||
.It Fl i
|
||||
Interactive mode (allow prompts).
|
||||
.It Fl k Ar key-file
|
||||
|
@ -47,6 +50,30 @@ The file to read host certificate hashes from.
|
|||
Write any new hashes to the
|
||||
.Ar tofu-file .
|
||||
.El
|
||||
.Sh ENVIRONMENT
|
||||
.Bl -tag -width Ds
|
||||
.It Ev GEMINI_KEYFILE
|
||||
Sets the TLS key file before
|
||||
.Fl k .
|
||||
.It Ev GEMINI_CERTFILE
|
||||
Sets the TLS certificate file before
|
||||
.Fl c .
|
||||
.It Ev GEMINI_TOFUFILE
|
||||
Sets the TOFU tile before
|
||||
.Fl t .
|
||||
.It Ev GEMINI_PROXY
|
||||
Sets the proxy host before
|
||||
.Fl h .
|
||||
.It Ev GEMINI_MAX_REDIRECTS
|
||||
Sets the maximum number of redirects before
|
||||
.Fl r .
|
||||
.It Ev GEMINI_PORT
|
||||
Sets the default port number before
|
||||
.Fl p .
|
||||
.It Ev GEMINI_TOFU_WRITE
|
||||
If set to a non-empty string, it is equivalent to
|
||||
.Fl w .
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Ex -std
|
||||
.Sh EXAMPLES
|
||||
|
@ -54,6 +81,15 @@ Fetch the
|
|||
.Lk tilde.black
|
||||
root index:
|
||||
.Dl gemini-cat gemini://tilde.black/ | more
|
||||
Fetch
|
||||
.Lk tilde.black
|
||||
from its onion address:
|
||||
.Bd -literal -offset indent -compact
|
||||
torsocks gemini-cat -h \\
|
||||
black6kfjetfuzaeozz7fs53whh7xtd4e27telrf5fg5kgdt5ah5plad.onion \\
|
||||
//tilde.black/
|
||||
.Ed
|
||||
.Sh SEE ALSO
|
||||
.Xr tls_config_set_cert_file 3
|
||||
.Xr libgeminiclient 3 ,
|
||||
.Xr tls_config_set_cert_file 3 ,
|
||||
.Xr tls_config_set_key_file 3
|
||||
|
|
18
gemini-cat.c
18
gemini-cat.c
|
@ -32,19 +32,23 @@ main(int argc, char *argv[])
|
|||
char b[BUFFER_SIZE];
|
||||
struct gemini gemini = GEMINI_INITIALIZER;
|
||||
const char *query = NULL;
|
||||
char *p;
|
||||
ssize_t r;
|
||||
size_t i;
|
||||
int prompt = 0;
|
||||
int rawout;
|
||||
int c;
|
||||
|
||||
gemini_envinit(&gemini);
|
||||
if ((p = getenv("GEMINI_TOFU_WRITE")) != NULL && *p != '\0')
|
||||
gemini.flags |= GEMINI_TOFU_WRITE;
|
||||
rawout = !isatty(STDOUT_FILENO);
|
||||
#ifdef __OpenBSD__
|
||||
if (pledge("stdio cpath rpath wpath flock inet dns tty", NULL)
|
||||
!= 0)
|
||||
err(EX_NOPERM, "Failed pledge(2)");
|
||||
#endif
|
||||
while ((c = getopt(argc, argv, "RSc:eik:p:r:st:w")) != -1)
|
||||
while ((c = getopt(argc, argv, "RSc:h:ik:p:r:st:w")) != -1)
|
||||
switch (c) {
|
||||
case 'R':
|
||||
rawout = 1;
|
||||
|
@ -55,8 +59,8 @@ main(int argc, char *argv[])
|
|||
case 'c':
|
||||
gemini.certfile = optarg;
|
||||
break;
|
||||
case 'e':
|
||||
gemini.flags |= GEMINI_STRICT;
|
||||
case 'h':
|
||||
gemini.proxy = optarg;
|
||||
break;
|
||||
case 'i':
|
||||
prompt = 1;
|
||||
|
@ -157,10 +161,10 @@ err:
|
|||
gemini_fini(&gemini);
|
||||
return (EX_SOFTWARE);
|
||||
usage:
|
||||
(void)fprintf(stderr, "usage: gemini-cat [-RSeisw] "
|
||||
"[-c cert-file] [-k key-file] [-p port]\n"
|
||||
" [-r max-redirects] [-t tofu-file] "
|
||||
"URL...\n");
|
||||
(void)fprintf(stderr, "usage: gemini-cat [-RSisw] "
|
||||
"[-c cert-file] [-h proxy-host] [-k key-file]\n"
|
||||
" [-p port] [-r max-redirects] "
|
||||
"[-t tofu-file] URL...\n");
|
||||
return (EX_USAGE);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.Dd May 1, 2020
|
||||
.Dd Jun 20, 2020
|
||||
.Dt LIBGEMINICLIENT 3
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
@ -11,6 +11,8 @@
|
|||
.Fn gemini_create void
|
||||
.Ft void
|
||||
.Fn gemini_init "struct gemini *ctx"
|
||||
.Ft void
|
||||
.Fn gemini_envinit "struct gemini *ctx"
|
||||
.Ft int
|
||||
.Fn gemini_connect "struct gemini *ctx" "const char *url"
|
||||
.Ft int
|
||||
|
@ -37,6 +39,39 @@ Initializes the provided
|
|||
.Vt gemini
|
||||
structure.
|
||||
.Pp
|
||||
.Fn gemini_envinit
|
||||
Initializes the provided
|
||||
.Vt gemini
|
||||
structure, using the following environment variables,
|
||||
if they are provided:
|
||||
.Pp
|
||||
.Bl -tag -width Ds -compact
|
||||
.It Ev GEMINI_KEYFILE
|
||||
Initializes the
|
||||
.Fa keyfile
|
||||
field.
|
||||
.It Ev GEMINI_CERTFILE
|
||||
Initializes the
|
||||
.Fa certfile
|
||||
field.
|
||||
.It Ev GEMINI_TOFUFILE
|
||||
Initializes the
|
||||
.Fa tofufile
|
||||
field.
|
||||
.It Ev GEMINI_PROXY
|
||||
Initializes the
|
||||
.Fa proxy
|
||||
field.
|
||||
.It Ev GEMINI_MAX_REDIRECTS
|
||||
Initializes the
|
||||
.Fa maxredirects
|
||||
field.
|
||||
.It Ev GEMINI_PORT
|
||||
Initializes the
|
||||
.Fa port
|
||||
field.
|
||||
.El
|
||||
.Pp
|
||||
.Fn gemini_connect
|
||||
opens a connection to the provided
|
||||
.Fa url ,
|
||||
|
@ -123,16 +158,17 @@ The useful members of the
|
|||
structure are as follows:
|
||||
.Bd -literal -offset indent -compact
|
||||
struct gemini {
|
||||
char meta[GEMINI_META_MAX + 1];
|
||||
const char *certfile; /* Certificate file */
|
||||
const char *keyfile; /* Key file */
|
||||
const char *tofufile; /* Known-hosts file */
|
||||
size_t metalen; /* Length of `meta' */
|
||||
int flags; /* Configuration flags */
|
||||
int maxredirects; /* Default 5 */
|
||||
int socketfd; /* The socket to use */
|
||||
short status; /* Response STATUS */
|
||||
short port; /* Default port */
|
||||
const char *certfile; /* Certificate file */
|
||||
const char *keyfile; /* Key file */
|
||||
const char *tofufile; /* Known-hosts file */
|
||||
const char *proxy; /* Proxy host */
|
||||
char *meta; /* Response META */
|
||||
size_t metalen; /* Length of `meta' */
|
||||
int flags; /* Configuration flags */
|
||||
int maxredirects; /* Default 5 */
|
||||
int socketfd; /* The socket to use */
|
||||
short status; /* Response STATUS */
|
||||
short port; /* Default port */
|
||||
};
|
||||
.Ed
|
||||
.Pp
|
||||
|
@ -145,8 +181,6 @@ Write any new certificate information to
|
|||
.Fa tofufile
|
||||
on
|
||||
.Dn gemini_close .
|
||||
.It Dv GEMINI_STRICT
|
||||
Enforce a strict interpretation of the Gemini protocol.
|
||||
.El
|
||||
.Pp
|
||||
All knows
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
@ -21,8 +23,6 @@ enum {
|
|||
GEMINI_STATE_INIT = 0,
|
||||
GEMINI_STATE_REQUEST,
|
||||
GEMINI_STATE_RESPONSE,
|
||||
GEMINI_STATE_RESPONSE_WHITESPACE,
|
||||
GEMINI_STATE_RESPONSE_META,
|
||||
GEMINI_STATE_READ,
|
||||
GEMINI_STATE_DONE,
|
||||
GEMINI_STATE_ERROR
|
||||
|
@ -32,6 +32,7 @@ enum {
|
|||
#define GEMINI_DEFAULT_PORT 1965
|
||||
#endif
|
||||
|
||||
#define ISASCIIBLANK(c) ((c) == '\t' || (c) == ' ')
|
||||
#define ISASCIILOWER(c) ((c) >= 'a' && (c) <= 'z')
|
||||
#define ISASCIIUPPER(c) ((c) >= 'A' && (c) <= 'Z')
|
||||
#define ISASCIIDIGIT(c) ((c) >= '0' && (c) <= '9')
|
||||
|
@ -65,8 +66,13 @@ gemini_init(struct gemini *g)
|
|||
g->certmem = NULL;
|
||||
g->keyfile = NULL;
|
||||
g->certfile = NULL;
|
||||
g->metalen = 0;
|
||||
g->proxy = NULL;
|
||||
g->meta = NULL;
|
||||
g->extra = NULL;
|
||||
g->reqlen = 0;
|
||||
g->reslen = 0;
|
||||
g->metalen = 0;
|
||||
g->extralen = 0;
|
||||
g->keylen = 0;
|
||||
g->certlen = 0;
|
||||
g->index = 0;
|
||||
|
@ -81,6 +87,44 @@ gemini_init(struct gemini *g)
|
|||
g->port = 0;
|
||||
}
|
||||
|
||||
void
|
||||
gemini_envinit(struct gemini *g)
|
||||
{
|
||||
const char *s;
|
||||
char *p;
|
||||
unsigned long ul;
|
||||
|
||||
gemini_init(g);
|
||||
g->keyfile = getenv("GEMINI_KEYFILE");
|
||||
g->certfile = getenv("GEMINI_CERTFILE");
|
||||
g->certfile = getenv("GEMINI_TOFUFILE");
|
||||
g->proxy = getenv("GEMINI_PROXY");
|
||||
if ((s = getenv("GEMINI_MAX_REDIRECTS")) != NULL) {
|
||||
errno = 0;
|
||||
ul = strtoul(s, &p, 0);
|
||||
if (errno == 0 && (p == s || *p != '\0'))
|
||||
errno = EINVAL;
|
||||
if (errno == 0 && ul > INT_MAX)
|
||||
errno = ERANGE;
|
||||
if (errno == 0)
|
||||
g->maxredirects = ul;
|
||||
else
|
||||
warn("GEMINI_MAX_REDIRECTS");
|
||||
}
|
||||
if ((s = getenv("GEMINI_PORT")) != NULL) {
|
||||
errno = 0;
|
||||
ul = strtoul(s, &p, 0);
|
||||
if (errno == 0 && (p == s || *p != '\0'))
|
||||
errno = EINVAL;
|
||||
if (errno == 0 && ul > SHRT_MAX)
|
||||
errno = ERANGE;
|
||||
if (errno == 0)
|
||||
g->port = ul;
|
||||
else
|
||||
warn("GEMINI_PORT");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
gemini_reset(struct gemini *g)
|
||||
{
|
||||
|
@ -95,8 +139,12 @@ gemini_reset(struct gemini *g)
|
|||
}
|
||||
g->tls = NULL;
|
||||
g->tls_config = NULL;
|
||||
g->metalen = 0;
|
||||
g->meta = NULL;
|
||||
g->extra = NULL;
|
||||
g->reqlen = 0;
|
||||
g->reslen = 0;
|
||||
g->metalen = 0;
|
||||
g->extralen = 0;
|
||||
g->index = 0;
|
||||
g->state = GEMINI_STATE_INIT;
|
||||
g->status = 0;
|
||||
|
@ -177,6 +225,12 @@ gemini_connect(struct gemini *g, const char *url)
|
|||
errno = EINVAL;
|
||||
return (-1);
|
||||
}
|
||||
/* Proxy */
|
||||
if (g->proxy != NULL && strlcpy(g->request, g->proxy,
|
||||
sizeof(g->request)) >= sizeof(g->request)) {
|
||||
errno = EINVAL;
|
||||
return (-1);
|
||||
}
|
||||
/* Initialize TLS */
|
||||
if ((g->tls_config = tls_config_new()) == NULL)
|
||||
return (-1);
|
||||
|
@ -276,7 +330,6 @@ gemini_read(struct gemini *g, void *b, size_t bs)
|
|||
size_t j;
|
||||
int c;
|
||||
|
||||
cb = b;
|
||||
if (g->state == GEMINI_STATE_ERROR)
|
||||
return (-1);
|
||||
if (g->state == GEMINI_STATE_DONE)
|
||||
|
@ -289,113 +342,87 @@ gemini_read(struct gemini *g, void *b, size_t bs)
|
|||
goto errt;
|
||||
g->index += r;
|
||||
}
|
||||
g->status = 0;
|
||||
g->reslen = 0;
|
||||
g->index = 0;
|
||||
g->status = 0;
|
||||
g->state = GEMINI_STATE_RESPONSE;
|
||||
}
|
||||
for (;;) {
|
||||
i = i != r ? i : 0;
|
||||
if (i == 0 && (r = tls_read(g->tls, b, bs)) <= 0)
|
||||
goto errt;
|
||||
switch (g->state) {
|
||||
case GEMINI_STATE_RESPONSE:
|
||||
/* This is (probably) over-engineered. */
|
||||
for (; i < r && g->index < 2; i++, g->index++) {
|
||||
if (!ISASCIIDIGIT(cb[i]))
|
||||
goto errinval;
|
||||
g->status =
|
||||
g->status * 10 + (cb[i] - '0');
|
||||
}
|
||||
if (g->index < 2)
|
||||
break;
|
||||
g->state = GEMINI_STATE_RESPONSE_WHITESPACE;
|
||||
break;
|
||||
case GEMINI_STATE_RESPONSE_WHITESPACE:
|
||||
/* The unspecified maximum is kind-of absurd. */
|
||||
for (; i < r && (cb[i] == ' ' || cb[i] == '\t');
|
||||
if (g->state == GEMINI_STATE_RESPONSE) {
|
||||
do {
|
||||
if (g->reslen >= sizeof(g->response))
|
||||
goto errinval;
|
||||
if ((r = tls_read(g->tls,
|
||||
g->response + g->reslen,
|
||||
sizeof(g->response) - g->reslen)) <= 0)
|
||||
goto errt;
|
||||
for (i = g->reslen, g->reslen += r;
|
||||
i < g->reslen && g->response[i] != '\n';
|
||||
i++)
|
||||
/* do nothing */;
|
||||
if (i == r)
|
||||
break;
|
||||
g->metalen = 0;
|
||||
g->state = GEMINI_STATE_RESPONSE_META;
|
||||
} while (i == g->reslen);
|
||||
g->extra = g->response + i + 1;
|
||||
g->extralen = g->reslen - i - 1;
|
||||
g->reslen = i - (g->response[i - 1] == '\r');
|
||||
g->response[g->reslen] = '\0';
|
||||
if (g->reslen < 2)
|
||||
goto errinval;
|
||||
if (!ISASCIIDIGIT(g->response[0]) ||
|
||||
!ISASCIIDIGIT(g->response[1]))
|
||||
goto errinval;
|
||||
if (g->reslen > 2 && !ISASCIIBLANK(g->response[2]))
|
||||
goto errinval;
|
||||
g->meta = g->response + (g->reslen > 2 ? 3 : 2);
|
||||
g->metalen = g->reslen > 2 ? g->reslen - 3 : 0;
|
||||
g->status = (g->response[0] - '0') * 10 +
|
||||
(g->response[1] - '0');
|
||||
if (g->response[0] != '3')
|
||||
g->redirects = 0;
|
||||
if (g->response[0] != '2' && g->extralen > 0)
|
||||
goto errinval;
|
||||
switch (g->response[0]) {
|
||||
case '0':
|
||||
goto errinval;
|
||||
case '1':
|
||||
goto done;
|
||||
case '2':
|
||||
g->index = 0;
|
||||
g->state = GEMINI_STATE_READ;
|
||||
break;
|
||||
case GEMINI_STATE_RESPONSE_META:
|
||||
/*
|
||||
* If the white-space had a reasonable maximum
|
||||
* then this would be easier.
|
||||
*/
|
||||
for (j = i; j < r && cb[j] != '\n'; j++)
|
||||
/* do nothing */;
|
||||
if (g->metalen + j - i > sizeof(g->meta))
|
||||
goto errinval;
|
||||
(void)memcpy(g->meta + g->metalen, cb + i,
|
||||
j - i);
|
||||
g->metalen += j - i;
|
||||
if (cb[j] != '\n') {
|
||||
i = j;
|
||||
break;
|
||||
}
|
||||
if (g->metalen > 0 &&
|
||||
g->meta[g->metalen - 1] == '\r') {
|
||||
g->metalen--;
|
||||
} else if (g->flags & GEMINI_STRICT) {
|
||||
if (g->metalen == GEMINI_META_MAX)
|
||||
goto errinval;
|
||||
g->meta[g->metalen++] = '\n';
|
||||
i = j + 1;
|
||||
break;
|
||||
}
|
||||
if (g->metalen > GEMINI_META_MAX)
|
||||
goto errinval;
|
||||
g->meta[g->metalen] = '\0';
|
||||
i = j + 1;
|
||||
if (g->status / 10 != 3)
|
||||
g->redirects = 0;
|
||||
switch (g->status / 10) {
|
||||
case 0:
|
||||
goto errinval;
|
||||
case 1:
|
||||
case '3':
|
||||
if (g->maxredirects == 0)
|
||||
goto done;
|
||||
case 2:
|
||||
g->state = GEMINI_STATE_READ;
|
||||
break;
|
||||
case 3:
|
||||
if (g->maxredirects == 0)
|
||||
goto done;
|
||||
if (g->redirects >= g->maxredirects)
|
||||
goto errredir;
|
||||
g->redirects++;
|
||||
if (gemini_connect_query(g, g->meta,
|
||||
NULL) != 0)
|
||||
goto err;
|
||||
return (gemini_read(g, b, bs));
|
||||
case 4:
|
||||
goto errg;
|
||||
case 5:
|
||||
goto errg;
|
||||
case 6:
|
||||
goto done;
|
||||
case 7:
|
||||
goto errinval;
|
||||
case 8:
|
||||
goto errinval;
|
||||
case 9:
|
||||
goto errinval;
|
||||
default:
|
||||
abort(); /* unreachable */
|
||||
}
|
||||
break;
|
||||
case GEMINI_STATE_READ:
|
||||
if (i > 0) {
|
||||
(void)memmove(cb, cb + i, r - i);
|
||||
r -= i;
|
||||
}
|
||||
return (r);
|
||||
if (g->redirects >= g->maxredirects)
|
||||
goto errredir;
|
||||
g->redirects++;
|
||||
if (gemini_connect_query(g, g->meta, NULL) != 0)
|
||||
goto err;
|
||||
return (gemini_read(g, b, bs));
|
||||
case '4':
|
||||
goto errg;
|
||||
case '5':
|
||||
goto errg;
|
||||
case '6':
|
||||
goto done;
|
||||
case '7':
|
||||
goto errinval;
|
||||
case '8':
|
||||
goto errinval;
|
||||
case '9':
|
||||
goto errinval;
|
||||
default:
|
||||
abort(); /* unreachable */;
|
||||
abort(); /* unreachable */
|
||||
}
|
||||
}
|
||||
if (g->state != GEMINI_STATE_READ)
|
||||
abort(); /* unreachable */
|
||||
if (g->index < g->extralen) {
|
||||
i = g->extralen - g->index;
|
||||
i = i < bs ? i : bs;
|
||||
(void)memcpy(b, g->extra + g->index, i);
|
||||
g->index += i;
|
||||
return (i);
|
||||
}
|
||||
return (tls_read(g->tls, b, bs));
|
||||
eof:
|
||||
if (g->state != GEMINI_STATE_READ) {
|
||||
warnx("Unexpected EOF");
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
#define GEMINI_REDIRECT_MAX 5
|
||||
|
||||
#define GEMINI_TOFU_WRITE 0x01
|
||||
#define GEMINI_STRICT 0x02
|
||||
|
||||
struct gemini_tofu {
|
||||
struct gemini_tofu *next;
|
||||
|
@ -24,10 +23,10 @@ struct gemini_tofu {
|
|||
|
||||
#define GEMINI_INITIALIZER \
|
||||
{ { 0 }, { 0 }, NULL, NULL, NULL, NULL, NULL, NULL, \
|
||||
NULL, NULL, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, \
|
||||
GEMINI_REDIRECT_MAX, 0, 0 }
|
||||
NULL, NULL, NULL, NULL, NULL, 0, 0, 0, 0, 0, 0, 0, \
|
||||
-1, -1, 0, 0, 0, 0, GEMINI_REDIRECT_MAX, 0, 0 }
|
||||
struct gemini {
|
||||
char meta[GEMINI_META_MAX + 1];
|
||||
char response[3 + GEMINI_META_MAX + 2];
|
||||
char request[GEMINI_URL_MAX + 2];
|
||||
struct tls *tls;
|
||||
struct tls_config *tls_config;
|
||||
|
@ -37,8 +36,13 @@ struct gemini {
|
|||
const char *keyfile;
|
||||
const char *certfile;
|
||||
const char *tofufile;
|
||||
size_t metalen;
|
||||
const char *proxy;
|
||||
const char *meta;
|
||||
const char *extra;
|
||||
size_t reqlen;
|
||||
size_t reslen;
|
||||
size_t metalen;
|
||||
size_t extralen;
|
||||
size_t keylen;
|
||||
size_t certlen;
|
||||
size_t index;
|
||||
|
@ -55,6 +59,7 @@ struct gemini {
|
|||
|
||||
struct gemini *gemini_create(void);
|
||||
void gemini_init(struct gemini *);
|
||||
void gemini_envinit(struct gemini *);
|
||||
void gemini_reset(struct gemini *);
|
||||
int gemini_connect_query(struct gemini *, const char *,
|
||||
const char *);
|
||||
|
|
Loading…
Reference in New Issue