On Sun, Aug 07, 2022 at 11:10:22AM +0200, Omar Polo wrote: > I'm not sure httpd(8) handles correctly when the fastcgi application > (e.g. slowcgi) closes the connection prematurely. > > To verify it, I'm playing with three simple CGI scripts running under > slowcgi with a very low timeout (-t2). The scripts test "hangs" > (which slowcgi turns into hard disconnections) in different places: > > /* slow.c -- hang before replying */ > sleep(10); > puts("Content-Type: text/plain\n"); > puts("hello, world"); > return 0; > > /* slow2.c -- hang during headers */ > puts("Content-Type: text/plain"); > fflush(stdout); > sleep(10); > puts(""); > puts("hello, world"); > return 0; > > /* slow3.c -- hang during the body */ > puts("Content-Type: text/plain\n"); > printf("hello, (wait for it...)"); > fflush(stdout); > sleep(10); > puts("world!"); > return 0; > > Those sleep calls are meant to hit slowcgi_timeout which abruptly > closes the connection with httpd(8). > > httpd handles the fastcgi termination via server_file_error which > assumes the request' headers and body were already sent and so goes on > to set up for the next request (if clt_persist.) This leaves the HTTP > client hanging and waiting for a response that won't arrive, until > they give up when the fastcgi reply wasn't complete. > > Diff below (first three hunks) addresses this issue by introducing a > server_fcgi_error callback that handles the "no header" and > "incomplete data" cases before calling server_file_error. With it, > the first two scripts become an httpd' "500 error" page and the third > is correctly closed after the first line. > > I'm also bundling another small change for slowcgi to make it send a > FCGI_END_REQUEST record instead of abruptly closing the request. I > think it's better to let the fastcgi server know that the request is > over, but the slowcgi/httpd combo works even without it. > > > diff refs/heads/wip refs/heads/fastcgi > commit - 3aac3f8529325cbd58806247542caabd23befb6e > commit + cab6b39fe822d105a2dc852f2435693a6eff834f > blob - 381fade2924c4b5cea77cd9cd6500e75d4d59257 > blob + b6541b7c68235ac1dfc5a9d0243db988e5932a7f > --- usr.sbin/httpd/server_fcgi.c > +++ usr.sbin/httpd/server_fcgi.c > @@ -77,6 +77,7 @@ struct server_fcgi_param { > }; > > int server_fcgi_header(struct client *, unsigned int); > +void server_fcgi_error(struct bufferevent *, short, void *); > void server_fcgi_read(struct bufferevent *, void *); > int server_fcgi_writeheader(struct client *, struct kv *, void *); > int server_fcgi_writechunk(struct client *); > @@ -133,7 +134,7 @@ server_fcgi(struct httpd *env, struct client *clt) > > clt->clt_srvbev_throttled = 0; > clt->clt_srvbev = bufferevent_new(fd, server_fcgi_read, > - NULL, server_file_error, clt); > + NULL, server_fcgi_error, clt); > if (clt->clt_srvbev == NULL) { > errstr = "failed to allocate fcgi buffer event"; > goto fail; > @@ -482,6 +483,23 @@ fcgi_add_param(struct server_fcgi_param *p, const char > } > > void > +server_fcgi_error(struct bufferevent *bev, short error, void *arg) > +{ > + struct client *clt = arg; > + > + if ((error & EVBUFFER_EOF) && !clt->clt_fcgi.headersdone) { > + server_abort_http(clt, 500, "malformed or no headers"); > + return; > + } > + > + /* send the end marker if not already */ > + if (clt->clt_fcgi.chunked && !clt->clt_fcgi.end++) > + server_bufferevent_print(clt, "0\r\n\r\n"); > + > + server_file_error(bev, error, arg); > +} > + > +void > server_fcgi_read(struct bufferevent *bev, void *arg) > { > uint8_t buf[FCGI_RECORD_SIZE];
I already gave an OK for this. It still is OK. > blob - ddf83f965d0e6a99ada695694bea77b775bae2aa > blob + 1d577ba63efca388ca3644d1a52d9b3d9f246014 > --- usr.sbin/slowcgi/slowcgi.c > +++ usr.sbin/slowcgi/slowcgi.c > @@ -515,7 +515,34 @@ slowcgi_accept(int fd, short events, void *arg) > void > slowcgi_timeout(int fd, short events, void *arg) > { > - cleanup_request((struct request*) arg); > + struct request *c; > + > + c = arg; > + > + if (c->script_flags & (STDOUT_DONE | STDERR_DONE | SCRIPT_DONE)) Is this the correct check? If we hit the timeout we want to close all pipes to the CGI process and hope it dies because of that. Now one of STDOUT_DONE or STDERR_DONE may have happened but the script is still running. I think it would be better to use: if (c->script_flags == (STDOUT_DONE | STDERR_DONE | SCRIPT_DONE)) return; here. Should slowcgi kill the command if SCRIPT_DONE is not set? > + return; > + > + if (!c->stdout_fd_closed) { > + c->stdout_fd_closed = 1; > + close(EVENT_FD(&c->script_ev)); > + event_del(&c->script_ev); > + } > + > + if (!c->stderr_fd_closed) { > + c->stderr_fd_closed = 1; > + close(EVENT_FD(&c->script_err_ev)); > + event_del(&c->script_err_ev); > + } > + > + if (!c->stdin_fd_closed) { > + c->stdin_fd_closed = 1; > + close(EVENT_FD(&c->script_stdin_ev)); > + event_del(&c->script_stdin_ev); > + } > + > + c->script_status = SIGALRM; > + c->script_flags = STDOUT_DONE | STDERR_DONE | SCRIPT_DONE; > + create_end_record(c); > } > > void > Apart from that I like this a lot. slowcgi is a bit sloppy at implementing the fcgi protocol properly. This is one bit of the puzzle for sure. -- :wq Claudio