@ -1,9 +1,11 @@
# include <sys/stat.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <sys/wait.h>
# include <dirent.h>
# include <err.h>
# include <errno.h>
# include <fcntl.h>
# include <limits.h>
# include <pwd.h>
# include <stdarg.h>
@ -15,6 +17,7 @@
# include "mimes.h"
# include "opts.h"
# include "utils.h"
# define GEMINI_PART 9
# define GEMINI_REQUEST_MAX 1024 /* see https://gemini.circumlunar.space/docs/specification.html */
@ -22,45 +25,11 @@
void autoindex ( const char * ) ;
void cgi ( const char * cgicmd ) ;
void display_file ( const char * ) ;
void status ( const int , const char * ) ;
void status_redirect ( const int , const char * ) ;
void drop_privileges ( const char * , const char * ) ;
void eunveil ( const char * , const char * ) ;
size_t estrlcat ( char * , const char * , size_t ) ;
size_t estrlcpy ( char * , const char * , size_t ) ;
void
eunveil ( const char * path , const char * permissions )
{
if ( unveil ( path , permissions ) = = - 1 ) {
syslog ( LOG_DAEMON , " unveil on %s failed " , path ) ;
err ( 1 , " unveil " ) ;
}
}
size_t
estrlcpy ( char * dst , const char * src , size_t dstsize )
{
size_t n = 0 ;
n = strlcpy ( dst , src , dstsize ) ;
if ( n > = dstsize ) {
err ( 1 , " strlcyp failed for %s = %s " , dst , src ) ;
}
return n ;
}
size_t
estrlcat ( char * dst , const char * src , size_t dstsize )
{
size_t size ;
if ( ( size = strlcat ( dst , src , dstsize ) ) > = dstsize )
err ( 1 , " strlcat on %s + %s " , dst , src ) ;
return size ;
}
void
drop_privileges ( const char * user , const char * path )
@ -76,31 +45,26 @@ drop_privileges(const char *user, const char *path)
/* is root? */
if ( getuid ( ) ! = 0 ) {
syslog ( LOG_DAEMON , " chroot requires program to be run as root " ) ;
errx ( 1 , " chroot requires root user " ) ;
errlog ( " chroot requires program to be run as root " ) ;
}
/* search user uid from name */
if ( ( pw = getpwnam ( user ) ) = = NULL ) {
syslog ( LOG_DAEMON , " the user %s can't be found on the system " , user ) ;
err ( 1 , " finding user " ) ;
errlog ( " the user %s can't be found on the system " , user ) ;
}
/* chroot worked? */
if ( chroot ( path ) ! = 0 ) {
syslog ( LOG_DAEMON , " the chroot_dir %s can't be used for chroot " , path ) ;
err ( 1 , " chroot " ) ;
errlog ( " the chroot_dir %s can't be used for chroot " , path ) ;
}
chrooted = 1 ;
if ( chdir ( " / " ) = = - 1 ) {
syslog ( LOG_DAEMON , " failed to chdir( \" / \" ) " ) ;
err ( 1 , " chdir " ) ;
errlog ( " failed to chdir( \" / \" ) " ) ;
}
/* drop privileges */
if ( setgroups ( 1 , & pw - > pw_gid ) | |
setresgid ( pw - > pw_gid , pw - > pw_gid , pw - > pw_gid ) | |
setresuid ( pw - > pw_uid , pw - > pw_uid , pw - > pw_uid ) ) {
syslog( LOG_DAEMON , " dropping privileges to user %s (uid=%i) failed " ,
errlog( " dropping privileges to user %s (uid=%i) failed " ,
user , pw - > pw_uid ) ;
err ( 1 , " Can't drop privileges " ) ;
}
}
# ifdef __OpenBSD__
@ -112,13 +76,20 @@ drop_privileges(const char *user, const char *path)
} else {
eunveil ( path , " r " ) ;
}
if ( strlen ( cgibin ) > 0 ) {
char cgifullpath [ PATH_MAX ] = { ' \0 ' } ;
estrlcpy ( cgifullpath , path , sizeof ( cgifullpath ) ) ;
estrlcat ( cgifullpath , cgibin , sizeof ( cgifullpath ) ) ;
eunveil ( cgifullpath , " rx " ) ;
}
/*
* prevent system calls other parsing queryfor fread file and
* write to stdio
*/
if ( pledge ( " stdio rpath " , NULL ) = = - 1 ) {
syslog ( LOG_DAEMON , " pledge call failed " ) ;
err ( 1 , " pledge " ) ;
if ( strlen ( cgibin ) > 0 ) {
epledge ( " stdio rpath exec proc " , NULL ) ;
} else {
epledge ( " stdio rpath " , NULL ) ;
}
# endif
}
@ -168,8 +139,8 @@ display_file(const char *uri)
if ( fp [ strlen ( fp ) - 1 ] ! = ' / ' ) {
/* no ending "/", redirect to "path/" */
char new_uri [ PATH_MAX ] = { ' \0 ' } ;
estrlcpy ( new_uri , uri , sizeof ( fp ) ) ;
estrlcat ( new_uri , " / " , sizeof ( fp ) ) ;
estrlcpy ( new_uri , uri , sizeof ( new_uri ) ) ;
estrlcat ( new_uri , " / " , sizeof ( new_uri ) ) ;
status_redirect ( 31 , new_uri ) ;
return ;
@ -199,8 +170,8 @@ display_file(const char *uri)
status ( 20 , file_mime ) ;
/* read the file and write it to stdout */
while ( ( nread = fread ( buffer , sizeof ( char ) , sizeof ( buffer ) , fd ) ) ! = 0 )
fwrite ( buffer , sizeof ( char ) , nread , stdout ) ;
while ( ( nread = fread ( buffer , 1 , sizeof ( buffer ) , fd ) ) ! = 0 )
fwrite ( buffer , 1 , nread , stdout ) ;
goto closefd ;
syslog ( LOG_DAEMON , " path served %s " , fp ) ;
@ -257,6 +228,59 @@ autoindex(const char *path)
closedir ( fd ) ;
}
void
cgi ( const char * cgicmd )
{
int pipedes [ 2 ] = { 0 } ;
pid_t pid ;
if ( pipe ( pipedes ) ! = 0 ) {
err ( 1 , " pipe failed " ) ;
}
pid = fork ( ) ;
if ( pid < 0 ) {
close ( pipedes [ 0 ] ) ;
close ( pipedes [ 1 ] ) ;
err ( 1 , " fork failed " ) ;
}
if ( pid > 0 ) { /* parent */
char buf [ 3 ] ;
size_t nread = 0 ;
FILE * output = NULL ;
close ( pipedes [ 1 ] ) ; /* make sure entry is closed so fread() gets EOF */
/* use fread/fwrite because are buffered */
output = fdopen ( pipedes [ 0 ] , " r " ) ;
if ( output = = NULL ) {
err ( 1 , " fdopen failed " ) ;
}
/* read pipe output */
while ( ( nread = fread ( buf , 1 , sizeof ( buf ) , output ) ) ! = 0 ) {
fwrite ( buf , 1 , nread , stdout ) ;
}
close ( pipedes [ 0 ] ) ;
fclose ( output ) ;
wait ( NULL ) ; /* wait for child to terminate */
exit ( 0 ) ;
} else if ( pid = = 0 ) { /* child */
dup2 ( pipedes [ 1 ] , STDOUT_FILENO ) ; /* set pipe output equal to stdout */
close ( pipedes [ 1 ] ) ; /* no need this file descriptor : it is now stdout */
execlp ( cgicmd , cgicmd , NULL ) ;
/* if execlp is ok, this will never be reached */
status ( 42 , " text/plain " ) ;
errlog ( " error when trying to execlp %s " , cgicmd ) ;
}
}
int
main ( int argc , char * * argv )
{
@ -268,7 +292,7 @@ main(int argc, char **argv)
int option = 0 ;
char * pos = NULL ;
while ( ( option = getopt ( argc , argv , " :d:l:m:u: vi" ) ) ! = - 1 ) {
while ( ( option = getopt ( argc , argv , " :d:l:m:u: c: vi" ) ) ! = - 1 ) {
switch ( option ) {
case ' d ' :
estrlcpy ( chroot_dir , optarg , sizeof ( chroot_dir ) ) ;
@ -283,6 +307,9 @@ main(int argc, char **argv)
case ' u ' :
estrlcpy ( user , optarg , sizeof ( user ) ) ;
break ;
case ' c ' :
estrlcpy ( cgibin , optarg , sizeof ( cgibin ) ) ;
break ;
case ' v ' :
virtualhost = 1 ;
break ;
@ -303,8 +330,7 @@ main(int argc, char **argv)
*/
if ( fgets ( request , GEMINI_REQUEST_MAX , stdin ) = = NULL ) {
status ( 59 , " request is too long (1024 max) " ) ;
syslog ( LOG_DAEMON , " request is too long (1024 max): %s " , request ) ;
exit ( 1 ) ;
errlog ( " request is too long (1024 max): %s " , request ) ;
}
/* remove \r\n at the end of string */
@ -318,9 +344,8 @@ main(int argc, char **argv)
*/
if ( strncmp ( request , " gemini:// " , GEMINI_PART ) ! = 0 ) {
/* error code url malformed */
syslog( LOG_DAEMON , " request «%s» doesn't match gemini:// " ,
errlog( " request «%s» doesn't match gemini:// " ,
request ) ;
exit ( 1 ) ;
}
syslog ( LOG_DAEMON , " request %s " , request ) ;
@ -364,8 +389,47 @@ main(int argc, char **argv)
estrlcpy ( uri , new_uri , sizeof ( uri ) ) ;
}
/* open file and send it to stdout */
display_file ( uri ) ;
/* check if uri is cgibin */
if ( ( strlen ( cgibin ) > 0 ) & &
( strncmp ( uri , cgibin , strlen ( cgibin ) ) = = 0 )
) {
char cgipath [ PATH_MAX ] = { ' \0 ' } ;
estrlcpy ( cgipath , chroot_dir , sizeof ( cgipath ) ) ;
estrlcat ( cgipath , uri , sizeof ( cgipath ) ) ;
/* set env variables for CGI */
/* see https://lists.orbitalfox.eu/archives/gemini/2020/000315.html */
esetenv ( " GATEWAY_INTERFACE " , " CGI/1.1 " , 1 ) ;
esetenv ( " SERVER_PROTOCOL " , " GEMINI " , 1 ) ;
esetenv ( " SERVER_SOFTWARE " , " vger/1 " , 1 ) ;
/* look for "?" to set query */
pos = strchr ( cgipath , ' ? ' ) ;
if ( pos ! = NULL ) {
char query [ PATH_MAX ] = { ' \0 ' } ;
estrlcpy ( query , pos + 1 , sizeof ( query ) ) ;
esetenv ( " QUERY_STRING " , query , 1 ) ;
pos [ 0 ] = ' \0 ' ;
}
/* look for an extension to find PATH_INFO */
pos = strrchr ( cgipath , ' . ' ) ;
if ( pos ! = NULL ) {
/* found a dot */
pos = strchr ( pos , ' / ' ) ;
if ( pos ! = NULL ) {
setenv ( " PATH_INFO " , pos , 1 ) ;
pos [ 0 ] = ' \0 ' ; /* keep only script name */
}
}
esetenv ( " SCRIPT_NAME " , cgipath , 1 ) ;
esetenv ( " SERVER_NAME " , hostname , 1 ) ;
cgi ( cgipath ) ;
} else {
//TODO: percent decoding here
/* open file and send it to stdout */
display_file ( uri ) ;
}
return ( 0 ) ;
}