Touch up gopher support. Rewrite the main loop in main().

Update changes. Add README.
This commit is contained in:
Mid Favila 2022-12-07 11:41:26 -04:00
parent f27c613480
commit f93a73148f
4 changed files with 224 additions and 157 deletions

10
CHANGES
View File

@ -1,4 +1,12 @@
2021/11/29 - v0.1b
2022/12/7 - v0.2b
-It did segfault. Segfaults less now.
-Still has TLS.
-Still smaller and more readable than hurl.
-Has Gopher now.
-Maybe works on BSD.
-Add an actual README.
2022/11/29 - v0.1b
Features:
-Doesn't segfault. Probably.
-Has TLS.

43
README
View File

@ -0,0 +1,43 @@
What is it?
===========
Terrible attempt at writing a smaller replacement for cURL. Will include
a library in the future to make network operations in C convenient.
Why?
====
Because there weren't any HTTPS clients that were small, readable, and
portable with the features I needed. Also I needed to learn sockets.
Okay, but you should still use cURL in your other projects.
===========================================================
No, I shouldn't. cURL is huge, doesn't build with smaller compilers, and
has way too many features. Apportate is a cantrip, cURL is a level nine
spell.
What's apportate's featureset, anyway?
======================================
Right now, it retrieves data over HTTP or Gopher, and can support TLS,
at least when it comes to HTTPS. I'd like a more general approach to
TLS support in the future.
Its source code is small (< 400 lines of C total according to cloc right
now) and uses only POSIX routines -- there's no temptation to use GNU
or BSD features because I develop against musl and tcc, and I don't even
have the Linux manpages on any of my systems.
Furthermore, it has a relatively well-modularized design, at least for
my current ability. It could be much better, if I had more experience,
but it's still quite easy to add support for new protocols. For example,
adding support for Gopher took me less than five minutes, and required
no real edits to main aside from a kludge to avoid seeking for an HTTP
response header terminator.
Apportate also aims to have actually useful diagnostics; that is,
compared to other tools, apportate aims to only provide useful error
output. In the case of success, it follows the Rule of Silence; on
unrecoverable errors, it aborts immediately. It supports multiple levels
of verbosity, and exposes almost all of its internals.
Its simple design and use should also make it relatively convenient
for inclusion in shell scripts.

View File

@ -1 +1 @@
int reqgen_goph(const char *path, char **nbuf);
int reqgen_gopher(const char *path, char **nbuf);

View File

@ -4,6 +4,7 @@
enum
{
/* display a status bar */
/* currently not implemented */
B,
/* put the file somewhere other than the current directory */
O,
@ -12,11 +13,12 @@ enum
Q,
/* increase the verbosity level. higher levels imply you want the content lower levels would provide */
/* all verbose output
is sent to stderr for ease of capture */
is sent to stderr for ease of capture */
/* 0: no informational, statistical or debug output */
/* 1: informational output (Sending request FOO to host BAR, Received response header BAZ, etc) */
/* 2: statistical. informational+size of header and body in bytes, round-trip time, etc */
/* 3+: debug. statistical+all internal state changes */
/* currently we only implement 1 */
V
};
@ -26,175 +28,189 @@ char param[4] = {0};
char *outpath;
int main(int argc, char **argv)
{
int i, translen, gotheader;
int sockd;
char recvbuf[BUFSIZ+1] = {0};
char *sendbufp, *offsetp, *errstr;
FILE *filed;
struct tls *tlsc;
uri uristruct;
i = translen = gotheader = sockd = 0;
sendbufp = offsetp = 0;
filed = 0;
for(i = 0; (i = getopt(argc, argv, "bo:qv")) != -1; i = 0)
{
switch(i)
{
case 'b':
param[B] = 1;
break;
int i, translen, gotheader;
int sockd;
char *recvbuf;
char *sendbufp, *offsetp, *errstr;
FILE *filed;
struct tls *tlsc;
uri uristruct;
case 'o':
if(!param[Q])
{
param[O] = 1;
}
outpath = optarg;
break;
i = translen = sockd = 0;
sendbufp = offsetp = 0;
if( (recvbuf = malloc(BUFSIZ+1)) == NULL)
{
errstr = "failed to init";
goto err;
}
case 'q':
param[Q] = 1;
param[B] = 0;
param[V] = 0;
break;
case 'v':
/* we handle v differently because we want to support different levels of verbosity */
if(!param[Q])
{
param[V]++;
}
break;
default:
errstr = "apport: uri";
goto err;
}
}
filed = 0;
tlsc = NULL;
/* we should probably modify uri_parse to return a zero value on failure... */
if(uri_parse(argv[optind], &uristruct))
{
errstr = "invalid URI.";
goto err;
}
for(i = 0; (i = getopt(argc, argv, "bo:qv")) != -1; i = 0)
{
switch(i)
{
case 'b':
param[B] = 1;
break;
/* if outpath isn't set because we haven't received a -o param, */
/* then we should set the outpath to the final component of the */
/* URI. */
/* If we can't do that, we should just set it to 'default'. */
if(outpath == NULL && !strlen((outpath = (1 + strrchr(uristruct.path, '/')))))
{
outpath = "default";
}
case 'o':
param[O] = 1;
outpath = optarg;
break;
/* generate our request */
if( !(sendbufp = reqgen(&uristruct)))
{
errstr = "Unknown protocol.";
goto err;
}
case 'q':
param[Q] = 1;
param[B] = 0;
param[V] = 0;
break;
if(param[V])
{
fprintf(stderr, "request: %s\n", sendbufp);
}
case 'v':
/* we handle v differently because we want to support different levels of verbosity */
if(!param[Q])
{
param[V]++;
}
break;
/* having a routine or a global variable to track whether we need TLS would be */
/* nice to add in the future */
/* Note: at the moment, if the TLS pointer passed is non-NULL, */
/* dial's return code can only be treated as an indicator of success. */
if(!strcmp(uristruct.proto, "https"))
{
if(! (sockd = dial(uristruct.fqdn, uristruct.proto, &tlsc)))
{
errstr = "Couldn't connect to host using the specified protocol.";
goto err;
}
}
else
{
if(! (sockd = dial(uristruct.fqdn, uristruct.proto, NULL)))
{
errstr = "Couldn't connect to host using the specified protocol.";
goto err;
}
}
if(! (filed = fopen(outpath, "w")))
{
errstr = "Couldn't open file.";
goto err;
}
default:
errstr = "[-vb] [-q] [-o <file>] uri";
goto usage;
}
}
if(!strcmp("https", uristruct.proto))
{
tls_write(tlsc, sendbufp, strlen(sendbufp));
}
else
{
if(! (translen = send(sockd, sendbufp, strlen(sendbufp), 0)))
{
errstr = "Couldn't transmit request.";
goto err;
}
else if(param[V])
{
fprintf(stderr, "sent: %d bytes\n", translen);
}
}
/* we should probably modify uri_parse to return a zero value on failure... */
if(uri_parse(argv[optind], &uristruct))
{
errstr = "[-q|[-v -b]] [-o <file>] uri";
goto usage;
}
for(gotheader = 0, translen = 1; translen; memset(recvbuf, 0, BUFSIZ+1))
{
/* using an abstraction over tls_read/write and send/recv would eliminate */
/* a lot of redundant code here. */
if(!strcmp("https", uristruct.proto))
{
translen = tls_read(tlsc, recvbuf, BUFSIZ);
}
else
{
translen = recv(sockd, recvbuf, BUFSIZ, 0);
}
/* if outpath isn't set because we haven't received a -o param, */
/* then we should set the outpath to the final component of the */
/* URI. */
/* If we can't do that, we should just set it to 'default'. */
if(NULL == outpath && !strlen((outpath = (1 + strrchr(uristruct.path, '/')))))
{
outpath = "default";
}
if(!translen)
{
break;
}
/* generate our request */
if( !(sendbufp = reqgen(&uristruct)))
{
errstr = "Unknown protocol.";
goto err;
}
if(param[V])
{
printf("recv: %d bytes\n", translen);
}
if(!gotheader)
{
offsetp = (strstr(recvbuf, "\r\n\r\n") + 4);
i = (int) (offsetp - recvbuf);
gotheader = 1;
}
{
fprintf(stderr, "request: %s\n", sendbufp);
}
/* having a routine or a global variable to track whether we need TLS would be */
/* nice to add in the future */
/* could probably store whether tls is necessary in a char within uristruct... */
/* Note: at the moment, if the TLS pointer passed is non-NULL, */
/* dial's return code can only be treated as an indicator of success. */
if(!strcmp("https", uristruct.proto))
{
sockd = dial(uristruct.fqdn, uristruct.proto, &tlsc);
}
else
{
offsetp = recvbuf;
i = 0;
}
{
sockd = dial(uristruct.fqdn, uristruct.proto, NULL);
}
if(!sockd)
{
errstr = "Couldn't connect to the host using the specified protocol.";
goto err;
}
if(tlsc)
{
translen = tls_write(tlsc, sendbufp, strlen(sendbufp));
}
else
{
translen = send(sockd, sendbufp, strlen(sendbufp), 0);
}
if(translen)
{
if(param[V])
{
printf("send: %d bytes\n", translen);
}
}
else
{
errstr = "Couldn't transmit data.";
goto err;
}
/* now for a slightly more complex version of the same routine: until we've encountered the */
/* delimiter, "\r\n\r\n", don't write to disk. Once we have, calculate the size of the body, */
/* then write that number of bytes to disk starting from the offset. Then, write everything */
/* until end of transmission. */
for(gotheader = 0, offsetp = NULL, translen = 1; translen > 0; memset(recvbuf, 0, BUFSIZ+1))
{
if( NULL == tlsc)
{
printf("recv: %d bytes\n", translen = recv(sockd, recvbuf, BUFSIZ, 0));
}
else
{
printf("recv: %d bytes\n", translen = tls_read(tlsc, recvbuf, BUFSIZ));
}
/* if we haven't gotten the header and our delimiter isn't in the */
/* received string, we're getting a multipart header and need to */
/* skip over it. */
/* right now we assume HTTP/S -- this is undesirable... */
if( strcmp("gopher", uristruct.proto) && !gotheader)
{
if( NULL == (offsetp = strstr(recvbuf, "\r\n\r\n")))
{
printf("continuing...\n");
continue;
}
else
{
gotheader = 1;
offsetp += 4;
printf("got the header\n");
}
}
if( !filed && !(filed = fopen(outpath, "w")))
{
errstr = "couldn't open file";
goto err;
}
fwrite(offsetp, sizeof(char), (translen - (offsetp - recvbuf)), filed);
offsetp = recvbuf;
}
free(recvbuf);
close(sockd);
fclose(filed);
exit(EXIT_SUCCESS);
err:
fprintf(stderr, "%s: %s\n", argv[0], errstr);
exit(EXIT_FAILURE);
usage:
fprintf(stderr, "usage: %s: %s\n", argv[0], errstr);
exit(EXIT_FAILURE);
fwrite(offsetp, sizeof(char), translen-i, filed);
}
exit(EXIT_SUCCESS);
err:
fprintf(stderr, "%s: %s\n", argv[0], errstr);
exit(EXIT_FAILURE);
}