OpenBSD uses its own http daemon. It's small, simple, and secure. Many OpenBSD daemons run as unprivileged users and chrooted to reduce the opportunity to exploit any vulnerabilites. The http daemon runs by default as user www, chrooted into /var/www.
For static HTML this is simple.
Create a config file /etc/httpd.conf.
server "v240.home.arpa" { listen on * port 80 root "/htdocs" directory auto index log style combined }
Start the webserver.
rcctl enable httpd rcctl start httpd
Just put your HTML into /var/www/htdocs and you're good to go. For interactive stuff you need a bit more work. The httpd doesn't support CGI directly. It uses FCGI. Swonk read 'Httpd and Relayd Mastery' by Michael W. Lucas to find out how to use FCGI. This got as far as using the slowcgi server to run traditional CGI scripts, but barely touched on using FCGI directly. Running the CGI scripts inside the chroot requires plenty of extra setup and Swonk is a lazy dog so he started looking for a more suitable way of running interactive programs. Swonk started with the basic information from the book and started digging.
FCGI communicates between the http daemon and the fcgi program using a socket. The first step is to tell httpd where the socket is. In /etc/httpd.conf Swonk added:
location "/test.fcgi" { fastcgi socket "/run/fcgi/test.sock" }
This tells the httpd that when it gets a request for /test.fcgi it should look to the socket in /run/fcgi/test.sock. As the httpd is chrooted into /var/www the socket is actually /var/www/run/fcgi/test.sock. Swonk created the /var/www/run/fcgi directory for the socket and made it owned by the user of the fcgi program and read/write to the www group. The socket must be read/write to both the httpd and the fcgi program.
$ ls -l /var/www/run/ total 4 drwxrwxr-x 2 swonk www 512 Sep 25 10:15 fcgi srw-rw---- 1 www www 0 Jul 19 17:50 slowcgi.sock $
Now all we need is a fcgi program.
#! /usr/bin/perl -w use FCGI; use strict; my $sockname = '/var/www/run/fcgi/test.sock'; my $socket = FCGI::OpenSocket($sockname, 3); my $mode = 0660; chmod $mode, $sockname; my $count = 0; my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket); while($request->Accept() >= 0) { print q{Content-type: text/html <html> <head> <title>FCGI Test</title> </head> <body> <h2>Counter</h2> <p> }; print $count++; print q{</p> <h2>Environment</h2> <table>}; foreach my $key ( sort keys %ENV ){ print q{<tr><td>}, $key, q{</td><td>}, $ENV{$key}, q{</td></tr>}; } print q{ </table> </body> </html> }; } FCGI::CloseSocket($socket);
When this program is run it opens a socket and waits for a request. The httpd gets a request and passes it to the fcgi program. The program returns some basic HTML, including the value of the counter and a dump of the environment passed to the program. Refreshing the browser increments the counter so you can see that it's doing something.
This is all you need to get basic reporting stuff working. It does pay to be careful because the FCGI program is not running inside the chroot with httpd. It can access the whole system it is running on. Unlike a simple CGI script the FCGI program is a long running program and you need to pay much more attention to resource usage, memory leaks, etc than is needed with the one-shot CGI script.
The next step is accepting input. When I first tried this I searched for a good way of parsing the query string. After some frustration I found you can pass the environment to CGI and it does all the work for you.
#! /usr/bin/perl -w use FCGI; use CGI; use strict; my $sockname = '/var/www/run/fcgi/test.sock'; my $socket = FCGI::OpenSocket($sockname, 3); my $mode = 0660; chmod $mode, $sockname; my $count = 0; my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket); while($request->Accept() >= 0) { print q{Content-type: text/html <html> <head> <title>FCGI Test</title> </head> <body> <h2>Counter</h2> <p> }; print $count++; print q{</p> <h2>Environment</h2> <table>}; foreach my $key ( sort keys %ENV ){ print q{<tr><td>}, $key, q{</td><td>}, $ENV{$key}, q{</td></tr>}; } print q{ </table> <h2>Query String</h2> <table>}; my $cgi = CGI->new($ENV{'QUERY_STRING'}); foreach my $name ( $cgi->param() ){ print q{<tr><td>}, $name, q{</td><td>}, $cgi->param($name), q{</td></tr>}; } <form> <input type="text" name ="test1"> <input type="text" name ="test2"> <input type="submit"> </form> </body> </html> }; } FCGI::CloseSocket($socket);
Next I tried using POST instead of GET as the form method. No joy. A quick hunt around the Internet found a solution. Just check the REQUEST_METHOD and stuff the appropriate data into the variable that gets passed to CGI.
#! /usr/bin/perl -w use FCGI; use CGI; use strict; my $sockname = '/var/www/run/fcgi/test.sock'; my $socket = FCGI::OpenSocket($sockname, 3); my $mode = 0660; chmod $mode, $sockname; my $count = 0; my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket); while($request->Accept() >= 0) { my $form = ""; if ( $ENV{'REQUEST_METHOD'} eq "GET" ) { $form = $ENV{'QUERY_STRING'}; } elsif ( $ENV{'REQUEST_METHOD'} eq "POST" ) { read(STDIN,$form, $ENV{'CONTENT_LENGTH'}); } print q{Content-type: text/html <html> <head> <title>FCGI Test</title> </head> <body> <h2>Counter</h2> <p> }; print $count++; print q{</p> <h2>Environment</h2> <table>}; foreach my $key ( sort keys %ENV ){ print q{<tr><td>}, $key, q{</td><td>}, $ENV{$key}, q{</td></tr>}; } print q{ </table> <h2>Query String</h2> <table>}; my $cgi = CGI->new($form}); foreach my $name ( $cgi->param() ){ print q{<tr><td>}, $name, q{</td><td>}, $cgi->param($name), q{</td></tr>}; } <form method="POST"> <input type="text" name ="test1"> <input type="text" name ="test2"> <input type="submit"> </form> </body> </html> }; } FCGI::CloseSocket($socket);
Now I can convert my old perl CGI scripts to new FCGI programs to run under the OpenBSD httpd using FCGI.
Now a new project called for the use of javascript and JSON, so Swonk had to learn more new tricks. Generating JSON from simple Perl programs using the JSON module is straight forward but accepting input using CGI seemed clumsy. Much better to use the JSON module to parse the input too.
#! /usr/bin/perl -w use FCGI; use JSON; use DBI; use strict; my $sockname = '/var/www/run/fcgi/test.sock'; my $socket = FCGI::OpenSocket($sockname, 3); my $mode = 0660; chmod $mode, $sockname; my $dbconnect='dbi:Pg:dbname=swonkdog;host=localhost'; my $dbuser='swonk'; my $dbpass='test'; my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket); while($request->Accept() >= 0) { my $dbh = DBI->connect($dbconnect, $dbuser, $dbpass) or die "Connect failed: " . $DBI::errstr; my $json = JSON->new; my @flights = (); my $form = ""; my $offset = 0; my $rows = 20; if ( defined $ENV{'REQUEST_METHOD'} and $ENV{'REQUEST_METHOD'} eq "POST" ) { read(STDIN,$form, $ENV{'CONTENT_LENGTH'}); my $formdata = $json->decode($form); $offset = $formdata->{'offset'}; } my $sth = $dbh->prepare( q{select * from winch order by day desc limit ? offset ?} ); $sth->execute($limit,$offset); while ( my $row = $sth->fetchrow_hashref ) { my %launch; $launch{'Day'} = $row->{'day'}; $launch{'Launches'} = $row->{'launches'}; push @flights, \%launch; } my %log; $log{'Launches'} = \@flights; print qq(Content-type: application/json\n\n); print $json->pretty->encode(\%log); } FCGI::CloseSocket($socket);