add initial gopher over TLS support - gopherproxy-c - Gopher HTTP proxy in C (C… | |
git clone git://git.codemadness.org/gopherproxy-c | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
commit b52a2076670c215f88202f0062cbe101b4954055 | |
parent f8d0a722a5cb43ef0d208b11dd377ea3b02a8695 | |
Author: Hiltjo Posthuma <[email protected]> | |
Date: Sun, 3 Aug 2025 21:59:39 +0200 | |
add initial gopher over TLS support | |
Diffstat: | |
M Makefile | 15 ++++++++++++--- | |
M README | 18 ++++++++++++++++++ | |
M gopherproxy.c | 411 ++++++++++++++++++++++++++---… | |
3 files changed, 375 insertions(+), 69 deletions(-) | |
--- | |
diff --git a/Makefile b/Makefile | |
@@ -7,12 +7,21 @@ PREFIX = /usr/local | |
BINDIR = ${PREFIX}/bin | |
MANDIR = ${PREFIX}/man/man1 | |
+# no TLS: fallback gophers:// to gopher:// | |
+#GOPHER_CFLAGS = ${CFLAGS} | |
+#GOPHER_LDFLAGS = ${LDFLAGS} | |
+#GOPHER_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE | |
+ | |
+# build static without TLS: useful in www chroot. | |
+#GOPHER_LDFLAGS = ${LDFLAGS} -static | |
+ | |
+# TLS | |
GOPHER_CFLAGS = ${CFLAGS} | |
-GOPHER_LDFLAGS = ${LDFLAGS} | |
-GOPHER_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE | |
+GOPHER_LDFLAGS = -ltls ${LDFLAGS} | |
+GOPHER_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE -DUSE_TLS | |
# build static: useful in www chroot. | |
-GOPHER_LDFLAGS = ${LDFLAGS} -static | |
+GOPHER_LDFLAGS = -ltls -lssl -lcrypto ${LDFLAGS} -static | |
SRC = gopherproxy.c | |
OBJ = ${SRC:.c=.o} | |
diff --git a/README b/README | |
@@ -8,6 +8,7 @@ Build dependencies | |
- libc + some BSD extensions (dprintf). | |
- POSIX system. | |
- make (optional). | |
+- LibreSSL libtls for gophers:// support (optional). | |
Features | |
@@ -16,6 +17,7 @@ Features | |
- Works in older browsers such as links, lynx, w3m, dillo, etc. | |
- No Javascript or CSS required. | |
- Gopher+ is not supported. | |
+- Support for Gopher over TLS encryption (gophers://). | |
Cons | |
@@ -24,6 +26,22 @@ Cons | |
- Not all gopher types are supported. | |
+Gopher over TLS | |
+--------------- | |
+ | |
+For a description of the protocol see near the section "TLS support": | |
+gopher://bitreich.org/1/scm/gopher-protocol/file/gopher-extension.md.gph | |
+ | |
+For a server implementation see: | |
+- geomyidae: gopher://bitreich.org/1/scm/geomyidae | |
+ | |
+For client implementations that support it see: | |
+ | |
+- cURL: https://curl.se/ | |
+- sacc: gopher://bitreich.org/1/scm/sacc | |
+- hurl: gopher://codemadness.org/1/git/hurl | |
+ | |
+ | |
CGI configuration examples | |
-------------------------- | |
diff --git a/gopherproxy.c b/gopherproxy.c | |
@@ -5,17 +5,33 @@ | |
#include <ctype.h> | |
#include <errno.h> | |
#include <netdb.h> | |
+#include <signal.h> | |
#include <stdarg.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <unistd.h> | |
+#include <tls.h> | |
+ | |
+#ifndef TLS_CA_CERT_FILE | |
+#define TLS_CA_CERT_FILE "/etc/ssl/cert.pem" | |
+#endif | |
+ | |
+#ifdef USE_TLS | |
+static int usetls = 0; | |
+/* TLS context */ | |
+static struct tls *t; | |
+/* TLS config */ | |
+static struct tls_config *tls_config; | |
+#endif | |
+ | |
#define MAX_RESPONSETIMEOUT 10 /* timeout in seconds */ | |
#define MAX_RESPONSESIZ 4000000 /* max download size in bytes */ | |
#ifndef __OpenBSD__ | |
-#define pledge(a,b) 0 | |
+#define pledge(p1,p2) 0 | |
+#define unveil(p1,p2) 0 | |
#endif | |
/* URI */ | |
@@ -37,7 +53,59 @@ struct visited { | |
char port[8]; | |
}; | |
+/* parsed URI */ | |
+static struct uri u; | |
+/* socket fd */ | |
+static int sock = -1; | |
+ | |
int headerset = 0, isdir = 0; | |
+ssize_t (*readbuf)(char *, size_t); | |
+ssize_t (*writebuf)(const char *, size_t); | |
+ | |
+void | |
+sighandler(int signo) | |
+{ | |
+ if (signo == SIGALRM) | |
+ _exit(2); | |
+} | |
+ | |
+/* print to stderr, print error message of errno and exit(). | |
+ * Unlike BSD err() it does not prefix __progname */ | |
+void | |
+err(int exitstatus, const char *fmt, ...) | |
+{ | |
+ va_list ap; | |
+ int saved_errno; | |
+ | |
+ saved_errno = errno; | |
+ | |
+ if (fmt) { | |
+ va_start(ap, fmt); | |
+ vfprintf(stderr, fmt, ap); | |
+ va_end(ap); | |
+ fputs(": ", stderr); | |
+ } | |
+ fprintf(stderr, "%s\n", strerror(saved_errno)); | |
+ | |
+ exit(exitstatus); | |
+} | |
+ | |
+/* print to stderr and exit(). | |
+ * Unlike BSD errx() it does not prefix __progname */ | |
+void | |
+errx(int exitstatus, const char *fmt, ...) | |
+{ | |
+ va_list ap; | |
+ | |
+ if (fmt) { | |
+ va_start(ap, fmt); | |
+ vfprintf(stderr, fmt, ap); | |
+ va_end(ap); | |
+ } | |
+ fputs("\n", stderr); | |
+ | |
+ exit(exitstatus); | |
+} | |
void | |
die(int code, const char *fmt, ...) | |
@@ -121,14 +189,13 @@ edial(const char *host, const char *port) | |
struct addrinfo hints, *res, *res0; | |
int error, save_errno, s; | |
const char *cause = NULL; | |
- struct timeval timeout; | |
memset(&hints, 0, sizeof(hints)); | |
hints.ai_family = AF_UNSPEC; | |
hints.ai_socktype = SOCK_STREAM; | |
hints.ai_flags = AI_NUMERICSERV; /* numeric port only */ | |
if ((error = getaddrinfo(host, port, &hints, &res0))) | |
- die(500, "%s: %s: %s:%s\n", __func__, gai_strerror(error), hos… | |
+ die(500, "%s: %s: %s:%s", __func__, gai_strerror(error), host,… | |
s = -1; | |
for (res = res0; res; res = res->ai_next) { | |
s = socket(res->ai_family, res->ai_socktype, | |
@@ -138,16 +205,6 @@ edial(const char *host, const char *port) | |
continue; | |
} | |
- timeout.tv_sec = MAX_RESPONSETIMEOUT; | |
- timeout.tv_usec = 0; | |
- if (setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(ti… | |
- die(500, "%s: setsockopt: %s\n", __func__, strerror(er… | |
- | |
- timeout.tv_sec = MAX_RESPONSETIMEOUT; | |
- timeout.tv_usec = 0; | |
- if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(ti… | |
- die(500, "%s: setsockopt: %s\n", __func__, strerror(er… | |
- | |
if (connect(s, res->ai_addr, res->ai_addrlen) == -1) { | |
cause = "connect"; | |
save_errno = errno; | |
@@ -159,12 +216,199 @@ edial(const char *host, const char *port) | |
break; | |
} | |
if (s == -1) | |
- die(500, "%s: %s: %s:%s\n", __func__, cause, host, port); | |
+ die(500, "%s: %s: %s:%s", __func__, cause, host, port); | |
freeaddrinfo(res0); | |
return s; | |
} | |
+void | |
+setup_plain(void) | |
+{ | |
+ if (pledge("stdio dns inet", NULL) == -1) | |
+ err(1, "pledge"); | |
+ | |
+ sock = edial(u.host, u.port); | |
+} | |
+ | |
+#ifdef USE_TLS | |
+void | |
+setup_tls(void) | |
+{ | |
+ if (tls_init()) | |
+ errx(1, "tls_init failed"); | |
+ if (!(tls_config = tls_config_new())) | |
+ errx(1, "tls config failed"); | |
+ if (unveil(TLS_CA_CERT_FILE, "r") == -1) | |
+ err(1, "unveil: %s", TLS_CA_CERT_FILE); | |
+#if 0 | |
+ if (tls_config_set_ca_file(tls_config, TLS_CA_CERT_FILE) == -1) | |
+ errx(1, "tls_config_set_ca_file: %s: %s", TLS_CA_CERT_FILE, | |
+ tls_config_error(tls_config)); | |
+#endif | |
+ | |
+ if (pledge("stdio dns inet rpath", NULL) == -1) | |
+ err(1, "pledge"); | |
+ | |
+ if (!(t = tls_client())) | |
+ errx(1, "tls_client: %s", tls_error(t)); | |
+ if (tls_configure(t, tls_config)) | |
+ errx(1, "tls_configure: %s", tls_error(t)); | |
+ | |
+ sock = edial(u.host, u.port); | |
+ if (tls_connect_socket(t, sock, u.host) == -1) | |
+ die(500, "tls_connect: %s", tls_error(t)); | |
+} | |
+ | |
+ssize_t | |
+tls_writebuf(const char *buf, size_t buflen) | |
+{ | |
+ const char *errstr; | |
+ const char *p; | |
+ size_t len; | |
+ ssize_t r, written = 0; | |
+ | |
+ for (len = buflen, p = buf; len > 0; ) { | |
+ r = tls_write(t, p, len); | |
+ if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) { | |
+ continue; | |
+ } else if (r == -1) { | |
+ errstr = tls_error(t); | |
+ fprintf(stderr, "tls_write: %s\n", errstr ? errstr : "… | |
+ return -1; | |
+ } | |
+ p += r; | |
+ len -= r; | |
+ written += r; | |
+ } | |
+ return written; | |
+} | |
+ | |
+ssize_t | |
+tls_readbuf(char *buf, size_t bufsiz) | |
+{ | |
+ const char *errstr; | |
+ ssize_t r, len; | |
+ | |
+ for (len = 0; bufsiz > 0;) { | |
+ r = tls_read(t, buf + len, bufsiz); | |
+ if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) { | |
+ continue; | |
+ } else if (r == 0) { | |
+ break; | |
+ } else if (r == -1) { | |
+ errstr = tls_error(t); | |
+ fprintf(stderr, "tls_read: %s\n", errstr ? errstr : ""… | |
+ return -1; | |
+ } | |
+ len += r; | |
+ bufsiz -= r; | |
+ } | |
+ return len; | |
+} | |
+#endif | |
+ | |
+ssize_t | |
+plain_writebuf(const char *buf, size_t buflen) | |
+{ | |
+ ssize_t r; | |
+ | |
+ if ((r = write(sock, buf, buflen)) == -1) | |
+ fprintf(stderr, "write: %s\n", strerror(errno)); | |
+ | |
+ return r; | |
+} | |
+ | |
+ssize_t | |
+plain_readbuf(char *buf, size_t bufsiz) | |
+{ | |
+ ssize_t r, len; | |
+ | |
+ for (len = 0; bufsiz > 0;) { | |
+ r = read(sock, buf + len, bufsiz); | |
+ if (r == 0) { | |
+ break; | |
+ } else if (r == -1) { | |
+ fprintf(stderr, "read: %s\n", strerror(errno)); | |
+ return -1; | |
+ } | |
+ len += r; | |
+ bufsiz -= r; | |
+ } | |
+ return len; | |
+} | |
+ | |
+struct linebuf { | |
+ /* line buffer */ | |
+ char line[2048]; | |
+ size_t linelen; | |
+ size_t lineoff; | |
+ /* read buffer */ | |
+ char buf[4096]; | |
+ char *bufoff, *bufend; | |
+ int err; | |
+}; | |
+ | |
+void | |
+linebuf_init(struct linebuf *b) | |
+{ | |
+ memset(b, 0, sizeof(struct linebuf)); | |
+} | |
+ | |
+ssize_t | |
+linebuf_get(struct linebuf *b) | |
+{ | |
+ size_t len; | |
+ ssize_t n; | |
+ char *p; | |
+ | |
+ while (!(b->err)) { | |
+ /* need to read more */ | |
+ if (b->bufoff >= b->bufend) { | |
+ b->bufoff = b->buf; | |
+ b->bufend = b->buf; | |
+ n = readbuf(b->buf, sizeof(b->buf)); | |
+ if (n == -1) | |
+ b->err = EIO; | |
+ | |
+ /* use remaining data even if not terminated by a newl… | |
+ if (n == 0 && b->linelen > 0) | |
+ return b->linelen; | |
+ | |
+ if (n > 0) | |
+ b->bufend = b->buf + n; | |
+ else | |
+ return n; | |
+ } | |
+ | |
+ /* search first newline */ | |
+ if ((p = memchr(b->bufoff, '\n', b->bufend - b->bufoff))) { | |
+ len = (p - b->bufoff); | |
+ } else { | |
+ /* copy remaining data into line buffer and read more … | |
+ len = (b->bufend - b->bufoff); | |
+ } | |
+ | |
+ if (b->lineoff + len + 1 >= sizeof(b->line)) { | |
+ b->err = ENOMEM; | |
+ return -1; | |
+ } | |
+ memcpy(b->line + b->lineoff, b->bufoff, len); | |
+ b->lineoff += len; | |
+ b->linelen = b->lineoff; | |
+ b->line[b->linelen] = '\0'; | |
+ | |
+ if (p) { | |
+ b->bufoff = p + 1; /* after newline */ | |
+ b->lineoff = 0; /* reset line: start at beginning */ | |
+ return b->linelen; | |
+ } else { | |
+ b->bufoff = b->bufend; /* read more */ | |
+ } | |
+ } | |
+ return -1; /* UNREACHED */ | |
+} | |
+ | |
int | |
isblacklisted(const char *host, const char *port, const char *path) | |
{ | |
@@ -210,18 +454,16 @@ void | |
servefile(const char *server, const char *port, const char *path, const char *… | |
{ | |
char buf[1024]; | |
- int r, w, fd; | |
+ int r, w; | |
size_t totalsiz = 0; | |
- fd = edial(server, port); | |
+ w = snprintf(buf, sizeof(buf), "%s%s%s\r\n", path, query[0] ? "?" : ""… | |
+ if (w < 0 || (size_t)w >= sizeof(buf)) | |
+ die(500, "servefile: path too long\n"); | |
+ if (writebuf(buf, w) == -1) | |
+ die(500, "servefile: writebuf failed\n"); | |
- if (pledge("stdio", NULL) == -1) | |
- die(500, "pledge: %s\n", strerror(errno)); | |
- | |
- if ((w = dprintf(fd, "%s%s%s\r\n", path, query[0] ? "?" : "", query)) … | |
- die(500, "dprintf: %s\n", strerror(errno)); | |
- | |
- while ((r = read(fd, buf, sizeof(buf))) > 0) { | |
+ while ((r = readbuf(buf, sizeof(buf))) > 0) { | |
/* too big total response */ | |
totalsiz += r; | |
if (totalsiz > MAX_RESPONSESIZ) { | |
@@ -234,55 +476,57 @@ servefile(const char *server, const char *port, const cha… | |
} | |
if (r == -1) | |
die(500, "read: %s\n", strerror(errno)); | |
- close(fd); | |
} | |
void | |
servedir(const char *server, const char *port, const char *path, const char *q… | |
{ | |
struct visited v; | |
- FILE *fp; | |
- char line[1024], uri[1024], primarytype; | |
+ struct linebuf lb; | |
+ const char *prefix = ""; | |
+ char buf[1024], uri[2048]; | |
+ char *line; | |
size_t totalsiz, linenr; | |
ssize_t n; | |
- int fd, r, i, len; | |
+ char primarytype = '\0'; | |
+ int i, len, w; | |
- fd = edial(server, port); | |
- | |
- if (pledge("stdio", NULL) == -1) | |
- die(500, "pledge: %s\n", strerror(errno)); | |
+#ifdef USE_TLS | |
+ if (usetls) | |
+ prefix = "gophers://"; | |
+#endif | |
if (param[0]) | |
- r = dprintf(fd, "%s%s%s\t%s\r\n", path, query[0] ? "?" : "", q… | |
+ w = snprintf(buf, sizeof(buf), "%s%s%s\t%s\r\n", path, query[0… | |
else | |
- r = dprintf(fd, "%s%s%s\r\n", path, query[0] ? "?" : "", query… | |
- if (r == -1) | |
- die(500, "write: %s\n", strerror(errno)); | |
+ w = snprintf(buf, sizeof(buf), "%s%s%s\r\n", path, query[0] ? … | |
- if (!(fp = fdopen(fd, "rb+"))) | |
- die(500, "fdopen: %s\n", strerror(errno)); | |
+ if (w < 0 || (size_t)w >= sizeof(buf)) | |
+ die(500, "servedir: path too long\n"); | |
+ if (writebuf(buf, w) == -1) | |
+ die(500, "servedir: writebuf failed\n"); | |
+ | |
+ linebuf_init(&lb); | |
+ line = lb.line; | |
totalsiz = 0; | |
- primarytype = '\0'; | |
- for (linenr = 1; fgets(line, sizeof(line), fp); linenr++) { | |
- n = strcspn(line, "\n"); | |
- if (line[n] != '\n') | |
- die(500, "%s:%s %s:%d: line too long\n", | |
- server, port, path, linenr); | |
- if (n && line[n] == '\n') | |
- line[n] = '\0'; | |
- if (n && line[n - 1] == '\r') | |
- line[--n] = '\0'; | |
- if (n == 1 && line[0] == '.') | |
- break; | |
+ for (linenr = 1; (n = linebuf_get(&lb)) > 0; linenr++) { | |
/* too big total response */ | |
- totalsiz += n; | |
+ if (n > 0) | |
+ totalsiz += n; | |
if (totalsiz > MAX_RESPONSESIZ) { | |
dprintf(1, "--- transfer too big, truncated ---\n"); | |
break; | |
} | |
+ if (n > 0 && line[n - 1] == '\n') | |
+ line[--n] = '\0'; | |
+ if (n > 0 && line[n - 1] == '\r') | |
+ line[--n] = '\0'; | |
+ if (n == 1 && line[0] == '.') | |
+ break; | |
+ | |
memset(&v, 0, sizeof(v)); | |
v._type = line[0]; | |
@@ -349,11 +593,11 @@ servedir(const char *server, const char *port, const char… | |
} | |
if (!strcmp(v.port, "70")) | |
- snprintf(uri, sizeof(uri), "%s/%c%s", | |
- v.server, primarytype, v.path); | |
+ snprintf(uri, sizeof(uri), "%s%s/%c%s", | |
+ prefix, v.server, primarytype, v.path); | |
else | |
- snprintf(uri, sizeof(uri), "%s:%s/%c%s", | |
- v.server, v.port, primarytype, v.path); | |
+ snprintf(uri, sizeof(uri), "%s%s:%s/%c%s", | |
+ prefix, v.server, v.port, primarytype, v.path); | |
switch (primarytype) { | |
case 'i': /* info */ | |
@@ -388,7 +632,7 @@ servedir(const char *server, const char *port, const char *… | |
xmlencode(v.username); | |
fputs("</a>", stdout); | |
break; | |
- case 'I': /* image: show inline */ | |
+ case 'I': /* image: show inline */ | |
fputs(typestr(v._type), stdout); | |
fputs(" <a href=\"?q=", stdout); | |
encodeparam(uri); | |
@@ -416,9 +660,8 @@ servedir(const char *server, const char *port, const char *… | |
} | |
putchar('\n'); | |
} | |
- if (ferror(fp)) | |
- die(500, "fgets: %s\n", strerror(errno)); | |
- fclose(fp); | |
+ if (lb.err) | |
+ die(500, "%s:%s after line %d: error reading line\n", server, … | |
} | |
int | |
@@ -621,14 +864,18 @@ parsepath: | |
int | |
main(void) | |
{ | |
- struct uri u; | |
const char *p, *qs, *path, *showuri = ""; | |
char query[1024] = "", param[1024] = "", fulluri[4096]; | |
int r, _type = '1'; | |
- if (pledge("stdio inet dns", NULL) == -1) | |
+ if (pledge("stdio inet dns rpath unveil", NULL) == -1) | |
die(500, "pledge: %s\n", strerror(errno)); | |
+#ifdef MAX_RESPONSETIMEOUT | |
+ signal(SIGALRM, sighandler); | |
+ alarm(MAX_RESPONSETIMEOUT); | |
+#endif | |
+ | |
if (!(qs = getenv("QUERY_STRING"))) | |
qs = ""; | |
if ((p = getparam(qs, "q"))) { | |
@@ -648,8 +895,12 @@ main(void) | |
showuri = query + sizeof("gopher://") - 1; | |
r = snprintf(fulluri, sizeof(fulluri), "%s", query); | |
} else if (!strncmp(query, "gophers://", sizeof("gophers://") … | |
- showuri = query + sizeof("gophers://") - 1; | |
+ /* if "gophers://" is used then keep it so TLS is kept… | |
+ showuri = query; | |
r = snprintf(fulluri, sizeof(fulluri), "%s", query); | |
+#ifdef USE_TLS | |
+ usetls = 1; | |
+#endif | |
} else { | |
showuri = query; | |
if (uri_hasscheme(query)) | |
@@ -687,6 +938,23 @@ main(void) | |
if (isblacklisted(u.host, u.port, path)) | |
die(403, "%s:%s %s: blacklisted\n", u.host, u.port, pa… | |
+#ifdef USE_TLS | |
+ /* setup TLS or plain connection */ | |
+ if (usetls) { | |
+ setup_tls(); | |
+ readbuf = tls_readbuf; | |
+ writebuf = tls_writebuf; | |
+ } else | |
+#endif | |
+ { | |
+ setup_plain(); | |
+ readbuf = plain_readbuf; | |
+ writebuf = plain_writebuf; | |
+ } | |
+ | |
+ if (pledge("stdio", NULL) == -1) | |
+ err(1, "pledge"); | |
+ | |
headerset = 1; | |
switch (_type) { | |
case '1': | |
@@ -695,11 +963,11 @@ main(void) | |
case '0': | |
dprintf(1, "Content-Type: text/plain; charset=utf-8\r\… | |
servefile(u.host, u.port, path, u.query); | |
- return 0; | |
+ goto cleanup; | |
case 'g': | |
dprintf(1, "Content-Type: image/gif\r\n\r\n"); | |
servefile(u.host, u.port, path, u.query); | |
- return 0; | |
+ goto cleanup; | |
case 'I': | |
/* try to set Content-Type based on extension */ | |
if ((p = strrchr(path, '.'))) { | |
@@ -713,18 +981,18 @@ main(void) | |
} | |
write(1, "\r\n", 2); | |
servefile(u.host, u.port, path, u.query); | |
- return 0; | |
+ goto cleanup; | |
case '9': | |
/* try to detect filename */ | |
if ((p = strrchr(path, '/'))) | |
dprintf(1, "Content-Disposition: attachment; f… | |
dprintf(1, "Content-Type: application/octet-stream\r\n… | |
servefile(u.host, u.port, path, u.query); | |
- return 0; | |
+ goto cleanup; | |
default: | |
write(1, "\r\n", 2); | |
servefile(u.host, u.port, path, u.query); | |
- return 0; | |
+ goto cleanup; | |
} | |
} | |
@@ -767,5 +1035,16 @@ main(void) | |
fputs("</pre>\n</body>\n</html>\n", stdout); | |
+cleanup: | |
+#ifdef USE_TLS | |
+ /* cleanup TLS and plain connection */ | |
+ if (t) { | |
+ tls_close(t); | |
+ tls_free(t); | |
+ } | |
+#endif | |
+ if (sock != -1) | |
+ close(sock); | |
+ | |
return 0; | |
} |