make got-read-gotconfig clear its imsgbuf before exit in an error case
[got-portable.git] / regress / gotwebd / gotwebd_test.c
blobc860ec205f79a0008dd231920f23f8fb1ba69716
1 /*
2 * Copyright (c) 2024 Mark Jamsek <mark@jamsek.dev>
3 * Copyright (c) 2014 Florian Obser <florian@openbsd.org>
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18 #include <sys/socket.h>
19 #include <sys/un.h>
21 #include <err.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <unistd.h>
27 #include "got_error.h"
28 #include "got_lib_poll.h"
30 #ifndef nitems
31 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
32 #endif
34 #define GOTWEBD_TEST_HARNESS "gotwebd_test_harness"
37 * Socket path should be passed on the command line or set as an envvar.
38 * Query string and request method can be passed on the command line;
39 * if not provided, use the index summary page and GET request method.
41 #define GOTWEBD_TEST_QUERYSTRING "action=summary&path=repo.git"
43 #define GOTWEBD_TEST_PATH_INFO "/"GOTWEBD_TEST_HARNESS"/"
44 #define GOTWEBD_TEST_REMOTE_ADDR "::1"
45 #define GOTWEBD_TEST_REMOTE_PORT "32768"
46 #define GOTWEBD_TEST_SERVER_ADDR "::1"
47 #define GOTWEBD_TEST_SERVER_PORT "80"
48 #define GOTWEBD_TEST_SERVER_NAME "gotwebd"
49 #define GOTWEBD_TEST_SCRIPT_NAME GOTWEBD_TEST_HARNESS
50 #define GOTWEBD_TEST_REQUEST_URI "/"GOTWEBD_TEST_HARNESS"/"
51 #define GOTWEBD_TEST_DOCUMENT_URI "/"GOTWEBD_TEST_HARNESS"/"
52 #define GOTWEBD_TEST_DOCUMENT_ROOT "/cgi-bin/"GOTWEBD_TEST_HARNESS
53 #define GOTWEBD_TEST_REQUEST_METHOD "GET"
54 #define GOTWEBD_TEST_SCRIPT_FILENAME "/cgi-bin/"GOTWEBD_TEST_HARNESS
55 #define GOTWEBD_TEST_SERVER_PROTOCOL "HTTP/1.1"
56 #define GOTWEBD_TEST_SERVER_SOFTWARE GOTWEBD_TEST_HARNESS
57 #define GOTWEBD_TEST_GATEWAY_INTERFACE "CGI/1.1"
59 #define PARAM(_p) { #_p, GOTWEBD_TEST_##_p }
61 static const char *mock_params[][2] = {
62 PARAM(PATH_INFO),
63 PARAM(REMOTE_ADDR),
64 PARAM(REMOTE_PORT),
65 PARAM(SERVER_ADDR),
66 PARAM(SERVER_PORT),
67 PARAM(SERVER_NAME),
68 PARAM(SCRIPT_NAME),
69 PARAM(REQUEST_URI),
70 PARAM(DOCUMENT_URI),
71 PARAM(DOCUMENT_ROOT),
72 PARAM(REQUEST_METHOD),
73 PARAM(SCRIPT_FILENAME),
74 PARAM(SERVER_PROTOCOL),
75 PARAM(SERVER_SOFTWARE),
76 PARAM(GATEWAY_INTERFACE)
79 #undef PARAM
81 #define FCGI_CONTENT_SIZE 65535
82 #define FCGI_PADDING_SIZE 255
83 #define FCGI_RECORD_SIZE \
84 (sizeof(struct fcgi_record_header) + FCGI_CONTENT_SIZE + FCGI_PADDING_SIZE)
86 #define FCGI_BEGIN_REQUEST 1
87 #define FCGI_ABORT_REQUEST 2
88 #define FCGI_END_REQUEST 3
89 #define FCGI_PARAMS 4
90 #define FCGI_STDIN 5
91 #define FCGI_STDOUT 6
92 #define FCGI_STDERR 7
93 #define FCGI_DATA 8
94 #define FCGI_GET_VALUES 9
95 #define FCGI_GET_VALUES_RESULT 10
96 #define FCGI_UNKNOWN_TYPE 11
97 #define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)
99 #define FCGI_RESPONDER 1
101 struct fcgi_record_header {
102 uint8_t version;
103 uint8_t type;
104 uint16_t id;
105 uint16_t content_len;
106 uint8_t padding_len;
107 uint8_t reserved;
108 }__attribute__((__packed__));
110 struct fcgi_begin_request_body {
111 uint16_t role;
112 uint8_t flags;
113 uint8_t reserved[5];
114 }__attribute__((__packed__));
116 struct server_fcgi_param {
117 int total_len;
118 uint8_t buf[FCGI_RECORD_SIZE];
121 enum fcgistate {
122 FCGI_READ_HEADER,
123 FCGI_READ_CONTENT,
124 FCGI_READ_PADDING
127 struct fcgi_data {
128 enum fcgistate state;
129 int toread;
130 int padding_len;
131 int type;
132 int status;
135 __dead static void
136 usage(void)
138 fprintf(stderr, "usage: %s [-m method] [-q query] [-s socket]\n",
139 getprogname());
140 exit(1);
143 static const struct got_error *
144 fcgi_writechunk(int type, uint8_t *dat, size_t datlen)
146 if (type == FCGI_END_REQUEST)
147 datlen = 0;
149 if (datlen > 0) {
150 if (write(STDOUT_FILENO, dat, datlen) == -1)
151 return got_error_from_errno("write");
152 } else if (fputs("\r\n", stdout) == EOF)
153 return got_error_from_errno("fputs");
155 return NULL;
158 static const struct got_error *
159 fcgi_read(int fd, struct fcgi_data *fcgi)
161 const struct got_error *err;
162 struct fcgi_record_header *h;
163 char buf[FCGI_RECORD_SIZE];
164 size_t len;
166 do {
167 if (fcgi->toread > sizeof(buf)) {
168 /* cannot happen with gotwebd response */
169 return got_error_msg(GOT_ERR_NO_SPACE,
170 "bad fcgi response size");
173 err = got_poll_read_full(fd, &len, buf,
174 fcgi->toread, fcgi->toread);
175 if (err != NULL) {
176 if (err->code != GOT_ERR_EOF)
177 return err;
178 err = NULL;
179 break;
182 fcgi->toread -= len;
183 if (fcgi->toread != 0)
184 return got_error_msg(GOT_ERR_BAD_PACKET,
185 "short fcgi response");
187 switch (fcgi->state) {
188 case FCGI_READ_HEADER:
189 h = (struct fcgi_record_header *)buf;
190 fcgi->type = h->type;
191 fcgi->state = FCGI_READ_CONTENT;
192 fcgi->padding_len = h->padding_len;
193 fcgi->toread = ntohs(h->content_len);
195 if (fcgi->toread != 0)
196 break;
198 /* fallthrough if content_len == 0 */
199 case FCGI_READ_CONTENT:
200 switch (fcgi->type) {
201 case FCGI_STDERR: /* gotwebd doesn't send STDERR */
202 case FCGI_STDOUT:
203 case FCGI_END_REQUEST:
204 err = fcgi_writechunk(fcgi->type, buf, len);
205 if (err != NULL)
206 return err;
207 break;
209 if (fcgi->padding_len == 0) {
210 fcgi->state = FCGI_READ_HEADER;
211 fcgi->toread = sizeof(*h);
212 } else {
213 fcgi->state = FCGI_READ_PADDING;
214 fcgi->toread = fcgi->padding_len;
216 break;
217 case FCGI_READ_PADDING:
218 fcgi->state = FCGI_READ_HEADER;
219 fcgi->toread = sizeof(*h);
220 break;
221 default:
222 /* should not happen with gotwebd */
223 return got_error_msg(GOT_ERR_RANGE, "bad fcgi state");
225 } while (len > 0);
227 return NULL;
230 static const struct got_error *
231 fcgi_add_stdin(int fd)
233 struct fcgi_record_header h;
235 memset(&h, 0, sizeof(h));
236 h.version = 1;
237 h.type = FCGI_STDIN;
238 h.id = htons(1);
239 h.padding_len = 0;
240 h.content_len = 0;
242 return got_poll_write_full(fd, &h, sizeof(h));
245 static const struct got_error *
246 fcgi_add_param(int fd, struct server_fcgi_param *p,
247 const char *key, const char *val)
249 struct fcgi_record_header *h;
250 int len, key_len, val_len;
251 uint8_t *param;
253 key_len = strlen(key);
254 val_len = strlen(val);
255 len = key_len + val_len;
256 len += key_len > 127 ? 4 : 1;
257 len += val_len > 127 ? 4 : 1;
259 if (len > FCGI_CONTENT_SIZE)
260 return got_error_msg(GOT_ERR_RANGE, "parameter too large");
262 if (p->total_len + len > FCGI_CONTENT_SIZE) {
263 const struct got_error *err;
265 err = got_poll_write_full(fd, p->buf,
266 sizeof(*h) + p->total_len);
267 if (err != NULL)
268 return err;
269 p->total_len = 0;
272 h = (struct fcgi_record_header *)p->buf;
273 param = p->buf + sizeof(*h) + p->total_len;
275 if (key_len > 127) {
276 *param++ = ((key_len >> 24) & 0xff) | 0x80;
277 *param++ = ((key_len >> 16) & 0xff);
278 *param++ = ((key_len >> 8) & 0xff);
279 *param++ = (key_len & 0xff);
280 } else
281 *param++ = key_len;
283 if (val_len > 127) {
284 *param++ = ((val_len >> 24) & 0xff) | 0x80;
285 *param++ = ((val_len >> 16) & 0xff);
286 *param++ = ((val_len >> 8) & 0xff);
287 *param++ = (val_len & 0xff);
288 } else
289 *param++ = val_len;
291 memcpy(param, key, key_len);
292 param += key_len;
293 memcpy(param, val, val_len);
295 p->total_len += len;
297 h->content_len = htons(p->total_len);
298 return NULL;
301 static const struct got_error *
302 fcgi_send_params(int fd, struct server_fcgi_param *param,
303 const char *meth, const char *qs)
305 const struct got_error *err;
306 struct fcgi_record_header *h;
307 const char *k, *v;
308 int i;
310 h = (struct fcgi_record_header *)&param->buf;
311 h->type = FCGI_PARAMS;
312 h->content_len = 0;
314 for (i = 0; i < nitems(mock_params); ++i) {
315 k = mock_params[i][0];
316 v = mock_params[i][1];
317 if ((err = fcgi_add_param(fd, param, k, v)) != NULL)
318 return err;
320 if (qs == NULL)
321 qs = GOTWEBD_TEST_QUERYSTRING;
322 if ((err = fcgi_add_param(fd, param, "QUERY_STRING", qs)) != NULL)
323 return err;
324 if (meth == NULL)
325 meth = GOTWEBD_TEST_REQUEST_METHOD;
326 if ((err = fcgi_add_param(fd, param, "REQUEST_METHOD", meth)) != NULL)
327 return err;
329 err = got_poll_write_full(fd, param->buf,
330 sizeof(*h) + ntohs(h->content_len));
331 if (err != NULL)
332 return err;
334 /* send "no more params" message */
335 h->content_len = 0;
336 return got_poll_write_full(fd, param->buf, sizeof(*h));
339 static const struct got_error *
340 fcgi(const char *sock, const char *meth, const char *qs)
342 const struct got_error *err;
343 struct server_fcgi_param param;
344 struct fcgi_record_header *h;
345 struct fcgi_begin_request_body *begin;
346 struct fcgi_data fcgi;
347 struct sockaddr_un sun;
348 int fd = -1;
350 if ((fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0)) == -1)
351 return got_error_from_errno("socket");
353 memset(&sun, 0, sizeof(sun));
354 sun.sun_family = AF_UNIX;
356 if (strlcpy(sun.sun_path, sock, sizeof(sun.sun_path))
357 >= sizeof(sun.sun_path)) {
358 err = got_error_fmt(GOT_ERR_NO_SPACE,
359 "socket path too long: %s", sock);
360 goto done;
363 if ((connect(fd, (struct sockaddr *)&sun, sizeof(sun))) == -1) {
364 err = got_error_from_errno_fmt("connect: %s", sock);
365 goto done;
368 if (pledge("stdio", NULL) == -1) {
369 err = got_error_from_errno("pledge");
370 goto done;
373 memset(&fcgi, 0, sizeof(fcgi));
375 fcgi.state = FCGI_READ_HEADER;
376 fcgi.toread = sizeof(*h);
377 fcgi.status = 200;
379 memset(&param, 0, sizeof(param));
381 h = (struct fcgi_record_header *)&param.buf;
382 h->version = 1;
383 h->type = FCGI_BEGIN_REQUEST;
384 h->id = htons(1);
385 h->content_len = htons(sizeof(*begin));
386 h->padding_len = 0;
388 begin = (struct fcgi_begin_request_body *)&param.buf[sizeof(*h)];
389 begin->role = htons(FCGI_RESPONDER);
391 err = got_poll_write_full(fd, param.buf, sizeof(*h) + sizeof(*begin));
392 if (err != NULL)
393 goto done;
395 if ((err = fcgi_send_params(fd, &param, meth, qs)) != NULL)
396 goto done;
398 if ((err = fcgi_add_stdin(fd)) != NULL)
399 goto done;
401 err = fcgi_read(fd, &fcgi);
403 done:
404 if (fd != -1 && close(fd) == EOF && err == NULL)
405 err = got_error_from_errno("close");
406 return err;
410 main(int argc, char *argv[])
412 const struct got_error *error;
413 const char *meth = NULL, *qs = NULL, *sock = NULL;
414 int ch;
416 while ((ch = getopt(argc, argv, "m:q:s:")) != -1) {
417 switch (ch) {
418 case 'm':
419 meth = optarg;
420 break;
421 case 'q':
422 qs = optarg;
423 break;
424 case 's':
425 sock = optarg;
426 break;
427 default:
428 usage();
429 /* NOTREACHED */
433 argc -= optind;
434 argv += optind;
436 if (argc != 0)
437 usage();
439 if (sock == NULL) {
440 sock = getenv("GOTWEBD_TEST_SOCK");
441 if (sock == NULL)
442 errx(1, "socket path not provided");
445 if (unveil(sock, "rw") != 0)
446 err(1, "unveil");
447 if (pledge("stdio unix", NULL) == -1)
448 err(1, "pledge");
450 error = fcgi(sock, meth, qs);
451 if (error != NULL)
452 errx(1, "%s", error->msg);
454 return 0;