@ -21,25 +21,46 @@
# include "utils.h"
# include "vger.h"
void
echdir( const char * path )
stop( const int r , const char * fmt , . . . )
{
if ( chdir ( path ) = = - 1 ) {
switch ( errno ) {
case ENOTDIR : /* FALLTHROUGH */
case ENOENT :
status_error ( 51 , " file not found " ) ;
break ;
case EACCES :
status_error ( 50 , " Forbidden path " ) ;
break ;
default :
status_error ( 50 , " Internal server error " ) ;
break ;
}
errlog ( " failed to chdir(%s) " , path ) ;
va_list ap , ap2 ;
fflush ( stdout ) ; /* ensure all data is sent */
/* log the request and retcode */
syslog ( LOG_DAEMON , " \" %s \" %i %zd " , _request , _retcode , _datasiz ) ;
if ( r ! = EXIT_SUCCESS ) {
/* log and print error */
va_copy ( ap2 , ap ) ;
va_start ( ap , fmt ) ;
vsyslog ( LOG_ERR , fmt , ap ) ;
va_end ( ap ) ;
va_start ( ap2 , fmt ) ;
vfprintf ( stderr , fmt , ap2 ) ;
va_end ( ap2 ) ;
}
exit ( r ) ;
}
void
status ( const int code , const char * fmt , . . . )
{
va_list ap ;
_datasiz + = fprintf ( stdout , " %i " , code ) ;
va_start ( ap , fmt ) ;
_datasiz + = vfprintf ( stdout , fmt , ap ) ;
va_end ( ap ) ;
_datasiz + = fprintf ( stdout , " \r \n " ) ; /* make sure status end correctly */
_retcode = code ; /* store return code for logs */
}
int
@ -77,27 +98,36 @@ uridecode(char *uri)
}
void
drop_privileges ( const char * user , const char * path )
drop_privileges ( const char * user , const char * chroot_dir, const char * cgi_dir )
{
struct passwd * pw ;
struct passwd * pw ;
/*
* use chroot ( ) if an user is specified requires root user to be
* running the program to run chroot ( ) and then drop privileges
*/
if ( strlen ( user ) > 0 ) {
if ( * user ) {
/* is root? */
if ( getuid ( ) ! = 0 )
errlog ( " chroot requires program to be run as root " ) ;
if ( getuid ( ) ! = 0 ) {
status ( 41 , " privileges issue, see logs " ) ;
stop ( EXIT_FAILURE , " %s " ,
" chroot requires program to be run as root " ) ;
}
/* search user uid from name */
if ( ( pw = getpwnam ( user ) ) = = NULL )
errlog ( " the user %s can't be found on the system " , user ) ;
if ( ( pw = getpwnam ( user ) ) = = NULL ) {
status ( 41 , " privileges issue, see logs " ) ;
stop ( EXIT_FAILURE ,
" the user %s can't be found on the system " , user ) ;
}
/* chroot worked? */
if ( chroot ( path ) ! = 0 )
errlog ( " the chroot_dir %s can't be used for chroot " , path ) ;
if ( chroot ( chroot_dir ) ! = 0 ) {
status ( 41 , " privileges issue, see logs " ) ;
stop ( EXIT_FAILURE ,
" the chroot_dir %s can't be used for chroot " , chroot_dir ) ;
}
chrooted = 1 ;
echdir ( " / " ) ;
@ -105,158 +135,159 @@ drop_privileges(const char *user, const char *path)
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 ) ) {
errlog ( " dropping privileges to user %s (uid=%i) failed " ,
user , pw - > pw_uid ) ;
status ( 41 , " privileges issue, see logs " ) ;
stop ( EXIT_FAILURE ,
" dropping privileges to user %s (uid=%i) failed " , \
user , pw - > pw_uid ) ;
}
}
# ifdef __OpenBSD__
/*
* prevent access to files other than the one in path
* prevent access to files other than the one in chroot_dir
*/
if ( chrooted )
eunveil ( " / " , " r " ) ;
eunveil ( " / " , " r " ) ;
else
eunveil ( path , " r " ) ;
eunveil ( chroot_dir , " r " ) ;
/* permission to execute what's inside cgi dir */
if ( strlen ( cgidir ) > 0 )
eunveil ( cgi dir, " rx " ) ;
/* permission to execute what's inside cgi _ dir */
if ( * cgi_dir )
eunveil ( cgi_ dir, " rx " ) ;
eunveil ( NULL , NULL ) ; /* no more call to unveil() */
/* promise permissions */
if ( strlen ( cgidir ) > 0 )
epledge ( " stdio rpath exec " , NULL ) ;
if ( * cgi_dir )
epledge ( " stdio rpath exec " , NULL ) ;
else
epledge ( " stdio rpath " , NULL ) ;
epledge ( " stdio rpath " , NULL ) ;
# endif
if ( ! chrooted )
echdir ( path ) ; /* move to the gemini data directory */
}
void
status ( const int code , const char * file_mime )
{
if ( strcmp ( file_mime , " text/gemini " ) = = 0 )
printf ( " %i %s; %s \r \n " , code , file_mime , lang ) ;
else
printf ( " %i %s \r \n " , code , file_mime ) ;
}
void
status_redirect ( const int code , const char * url )
{
printf ( " %i %s \r \n " ,
code , url ) ;
}
void
status_error ( const int code , const char * reason )
{
printf ( " %i %s \r \n " ,
code , reason ) ;
echdir ( chroot_dir ) ; /* move to the gemini data directory */
}
void
ssize_t
display_file ( const char * fname )
{
FILE * fd = NULL ;
struct stat sb = { 0 } ;
ssize_t nread = 0 ;
const char * file_mime ;
char * buffer [ BUFSIZ ] ;
char target [ FILENAME_MAX ] = { ' \0 ' } ;
char tmp [ PATH_MAX ] = { ' \0 ' } ; /* used to build
* temporary path */
/*
* special case : fname empty . The user requested just the directory
* name
* special case : fname empty . The user requested just a dir name
*/
if ( strlen ( fname ) = = 0 ) {
if ( stat ( " index.gmi " , & sb ) = = 0 ) {
/* there is index.gmi in the current directory */
display_file ( " index.gmi " ) ;
return ;
} else if ( doautoidx ) {
/* no index.gmi, so display autoindex if enabled */
autoindex ( " . " ) ;
return ;
} else {
goto err ;
}
if ( ( strlen ( fname ) = = 0 ) & & ( doautoidx ) ) {
/* no index.gmi, so display autoindex if enabled */
_datasiz + = autoindex ( " . " ) ;
return _datasiz ;
}
/* this is to check if path exists and obtain metadata later */
if ( stat ( fname , & sb ) = = - 1 ) {
/*
* check if fname is a symbolic link if so , redirect using
* its target
*/
if ( lstat ( fname , & sb ) ! = - 1 & & S_ISLNK ( sb . st_mode ) = = 1 )
goto redirect ;
/* open the file requested */
if ( ( fd = fopen ( fname , " r " ) ) ! = NULL ) {
file_mime = get_file_mime ( fname , default_mime ) ;
if ( strcmp ( file_mime , " text/gemini " ) = = 0 )
status ( 20 , " %s; %s " , file_mime , lang ) ;
else
goto err ;
}
/* check if directory */
if ( S_ISDIR ( sb . st_mode ) ! = 0 ) {
/* no ending "/", redirect to "fname/" */
estrlcpy ( tmp , fname , sizeof ( tmp ) ) ;
estrlcat ( tmp , " / " , sizeof ( tmp ) ) ;
status_redirect ( 31 , tmp ) ;
return ;
status ( 20 , " %s " , file_mime ) ;
_datasiz + = print_file ( fd ) ;
fclose ( fd ) ; /* close file descriptor */
} else {
/* return an error code and no content.
* seems unlikely to happen unless the file vanished
* since we checked with stat ( ) if it exists
*/
status ( 51 , " %s " , " file not found and may have vanished " ) ;
}
/* open the file requested */
if ( ( fd = fopen ( fname , " r " ) ) = = NULL )
goto err ;
file_mime = get_file_mime ( fname , default_mime ) ;
return _datasiz ;
}
status ( 20 , file_mime ) ;
int
do_cgi ( const char * chroot_dir , const char * cgi_dir , const char * path , const char * hostname , const char * query )
{
/* read the file byte after byte in buffer and write it to stdout */
while ( ( nread = fread ( buffer , 1 , sizeof ( buffer ) , fd ) ) ! = 0 )
fwrite ( buffer , 1 , nread , stdout ) ;
goto closefd ; /* close file descriptor */
syslog ( LOG_DAEMON , " path served %s " , fname ) ;
/* WARNING : this function is fragile since it
* compares path using the string to access them .
* It would be preferable to use stat ( ) to check
* if two path refer to the same inode
*/
char cgirp [ PATH_MAX ] = { ' \0 ' } ; /* cgi dir path in chroot */
char cgifp [ PATH_MAX ] = { ' \0 ' } ; /* cgi file to execute */
char * path_info = NULL ;
/* check if path starts with cgi_dir
* compare beginning of path with cgi_dir
* path + 2 : skip " ./ "
* cgi_dir + strlen ( chrootdir ) ( skip chrootdir )
*/
estrlcpy ( cgirp , cgi_dir + strlen ( chroot_dir ) , sizeof ( cgirp ) ) ;
/* ensure there is no leading / if user didn't end chrootdir with */
while ( * cgirp = = ' / ' )
estrlcpy ( cgirp , cgirp + 1 , sizeof ( cgirp ) ) ;
return ;
if ( strncmp ( cgirp , path + 2 , strlen ( cgirp ) ) ! = 0 )
return 1 ; /* not in cgi_dir, go to display_file */
err :
/* return an error code and no content */
status_error ( 51 , " file not found " ) ;
syslog ( LOG_DAEMON , " path invalid %s " , fname ) ;
goto closefd ;
/* 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 ) ;
if ( * query )
esetenv ( " QUERY_STRING " , query , 1 ) ;
redirect :
/* read symbolic link target to redirect */
if ( readlink ( fname , target , FILENAME_MAX ) = = - 1 )
goto err ;
/*
* if in cgi_dir , only the first file after cgi_dir / FILE
* is to be executed
* the rest is PATH_INFO
*/
status_redirect ( 30 , target ) ;
syslog ( LOG_DAEMON , " redirection from %s to %s " , fname , target ) ;
/* find next item after cgi_dir in path:
* path + 2 ( skip " ./ " ) + strlen ( cgirp ) + 1 ( skip ' / ' )
*/
closefd :
if ( S_ISREG ( sb . st_mode ) ! = 0 )
fclose ( fd ) ;
/* cgi file to execute */
estrlcpy ( cgifp , path + 2 + strlen ( cgirp ) + 1 , sizeof ( cgifp ) ) ;
if ( ! ( * cgifp ) ) /* problem with cgi file, abort */
return 1 ;
/* check if there is something after cgi file for PATH_INFO */
path_info = strchr ( cgifp , ' / ' ) ;
if ( path_info ! = NULL ) {
esetenv ( " PATH_INFO " , path_info , 1 ) ;
* path_info = ' \0 ' ; /* stop cgifp before PATH_INFO */
}
esetenv ( " SCRIPT_NAME " , cgifp , 1 ) ;
esetenv ( " SERVER_NAME " , hostname , 1 ) ;
echdir ( cgirp ) ;
cgi ( cgifp ) ;
return 0 ;
}
void
ssize_t
autoindex ( const char * path )
{
/* display liks to files in path + a link to parent (..) */
/* display li st of files in path + a link to parent (..) */
int n = 0 ;
struct dirent * * namelist ; /* this must be freed at last */
syslog ( LOG_DAEMON , " autoindex: %s " , path ) ;
size_t bs = 0 ;
/* use alphasort to always have the same order on every system */
if ( ( n = scandir ( path , & namelist , NULL , alphasort ) ) < 0 ) {
status_error ( 50 , " Internal server error " ) ;
errlog ( " Can't scan %s " , path ) ;
status ( 50 , " Can't scan %s " , path ) ;
} else {
status ( 20 , " text/gemini " ) ;
printf( " => .. ../ \n " ) ; /* display link to parent */
bs + = f printf( stdout , " => .. ../ \n " ) ; /* display link to parent */
for ( int j = 0 ; j < n ; j + + ) {
/* skip self and parent */
if ( ( strcmp ( namelist [ j ] - > d_name , " . " ) = = 0 ) | |
@ -264,25 +295,33 @@ autoindex(const char *path)
continue ;
}
/* add "/" at the end of a directory path */
if ( namelist [ j ] - > d_type = = DT_DIR )
printf ( " => ./%s/ %s/ \n " , namelist [ j ] - > d_name , namelist [ j ] - > d_name ) ;
else
printf ( " => ./%s %s \n " , namelist [ j ] - > d_name , namelist [ j ] - > d_name ) ;
if ( namelist [ j ] - > d_type = = DT_DIR ) {
bs + = fprintf ( stdout , " => ./%s/ %s/ \n " ,
namelist [ j ] - > d_name , namelist [ j ] - > d_name ) ;
} else {
bs + = fprintf ( stdout , " => ./%s %s \n " ,
namelist [ j ] - > d_name , namelist [ j ] - > d_name ) ;
}
free ( namelist [ j ] ) ;
}
free ( namelist ) ;
}
return bs ;
}
void
cgi ( const char * cgicmd )
{
/* TODO? cgi currently return the wrong data size unless we switch from execl to popen */
/* run cgicmd replacing current process */
_datasiz = - 1 ; /* bytes sent by cgi are unknown */
execl ( cgicmd , cgicmd , NULL ) ;
/* if execl is ok, this will never be reached */
status ( 42 , " Couldn't execute CGI script " ) ;
errlog( " error when trying to execl %s " , cgicmd ) ;
exit ( 1 ) ;
status ( 42 , " error when trying run cgi " ) ;
stop( EXIT_FAILURE , " error when trying to execl %s " , cgicmd ) ;
}
void
@ -295,3 +334,172 @@ strip_trailing_slash(char *path)
while ( path [ end ] = = ' / ' )
path [ end - - ] = ' \0 ' ;
}
char *
check_request ( char * request )
{
/*
* read the request , check for errors and sanitize the input
*/
char * pos = NULL ;
/* read 1024 +1 chars from stdin to get the request (1024 + \0) */
if ( fgets ( request , GEMINI_REQUEST_MAX , stdin ) = = NULL ) {
/* EOF reached before reading anything */
if ( feof ( stdin ) ) {
status ( 59 , " %s " , " request is too short and probably empty " ) ;
stop ( EXIT_FAILURE , " %s " , " request is too short and probably empty " ) ;
/* error before reading anything */
} else if ( ferror ( stdin ) ) {
status ( 59 , " Error while reading request: %s " , request ) ;
stop ( EXIT_FAILURE , " Error while reading request: %s " , request ) ;
}
}
/* check if string ends with '\n', or to long */
if ( request [ strnlen ( request , GEMINI_REQUEST_MAX ) - 1 ] ! = ' \n ' ) {
status ( 59 , " request is too long (1024 max): %s " , request ) ;
stop ( EXIT_FAILURE , " request is too long (1024 max): %s " , request ) ;
}
/* remove \r\n at the end of string */
request [ strcspn ( request , " \r \n " ) ] = ' \0 ' ;
/*
* check if the beginning of the request starts with
* gemini : //
*/
if ( strncmp ( request , " gemini:// " , GEMINI_PART ) ! = 0 ) {
/* error code url malformed */
status ( 59 , " request «%s» doesn't match gemini:// " , request ) ;
stop ( EXIT_FAILURE , " request «%s» doesn't match gemini:// " , request ) ;
}
/* save request for logs */
estrlcpy ( _request , request , sizeof ( _request ) ) ;
/* remove the gemini:// part */
memmove ( request , request + GEMINI_PART , strlen ( request ) + 1 - GEMINI_PART ) ;
/* remove all "/.." for safety reasons */
while ( ( pos = strstr ( request , " /.. " ) ) ! = NULL )
memmove ( request , pos + 3 , strlen ( pos ) + 1 - 3 ) ; /* "/.." = 3 */
return request ;
}
char *
get_hostname ( const char * request , char * hstnm , size_t hstnmsiz )
{
char * pos = NULL ;
/* first make a copy of request */
estrlcpy ( hstnm , request , hstnmsiz ) ;
/* look for hostname : stops at first '/' if any */
if ( ( pos = strchr ( hstnm , ' / ' ) ) ! = NULL )
pos [ 0 ] = ' \0 ' ; /* end string at the end of hostname */
/* check if client added :port at end of hostname and remove it */
if ( ( pos = strchr ( hstnm , ' : ' ) ) ! = NULL )
pos [ 0 ] = ' \0 ' ; /* end string at : */
return hstnm ;
}
char *
get_path ( const char * request , char * path , size_t pathsiz , int virtualhost , const char * hostname )
{
char * pos = NULL ;
/* path must be relative to chroot */
estrlcpy ( path , " ./ " , pathsiz ) ;
/* path is in a subdir named hostname */
if ( virtualhost ) {
estrlcat ( path , hostname , pathsiz ) ;
estrlcat ( path , " / " , pathsiz ) ;
}
/* path is after hostname/ */
pos = strchr ( request , ' / ' ) ;
if ( pos ! = NULL ) /* append the path. pos +1 to remove leading '/' */
estrlcat ( path , pos + 1 , pathsiz ) ;
return path ;
}
void
check_path ( char * path , size_t pathsiz , const char * hstnm , int virtualhost )
{
struct stat sb = { 0 } ;
char tmp [ PATH_MAX ] = { ' \0 ' } ;
if ( stat ( path , & sb ) = = - 1 ) {
if ( lstat ( path , & sb ) ! = - 1 & & S_ISLNK ( sb . st_mode ) = = 1 ) {
if ( readlink ( path , tmp , sizeof ( tmp ) ) > 0 ) {
status ( 30 , " %s " , tmp ) ;
stop ( EXIT_SUCCESS , NULL ) ;
}
}
status ( 51 , " %s " , " file not found " ) ;
stop ( EXIT_SUCCESS , NULL ) ;
}
if ( S_ISDIR ( sb . st_mode ) ) {
/* check if dir path end with "/" */
if ( path [ strlen ( path ) - 1 ] ! = ' / ' ) {
/* redirect to the dir with appropriate ending '/' */
/* remove leading '.' for redirection*/
if ( virtualhost ) /* remove ./host.name */
memmove ( path , path + 2 + strlen ( hstnm ) ,
strlen ( path + 2 ) + strlen ( hstnm ) + 1 ) ;
else
memmove ( path , path + 1 ,
strlen ( path + 1 ) + 1 ) ; /* +1 for \0 */
estrlcat ( path , " / " , pathsiz ) ;
status ( 31 , " %s " , path ) ;
stop ( EXIT_SUCCESS , NULL ) ;
}
/* check if DEFAULT_INDEX exists in directory */
estrlcpy ( tmp , path , sizeof ( tmp ) ) ;
estrlcat ( tmp , " / " , sizeof ( tmp ) ) ;
estrlcat ( tmp , DEFAULT_INDEX , sizeof ( tmp ) ) ;
if ( stat ( tmp , & sb ) = = 0 )
estrlcpy ( path , tmp , pathsiz ) ;
}
}
void
get_dir_file ( char * path , char * dir , size_t dirsiz , char * file , size_t filesiz )
{
char * pos = NULL ;
pos = strrchr ( path , ' / ' ) ;
if ( pos ! = NULL ) {
estrlcpy ( file , pos + 1 , filesiz ) ; /* +1 : not heading / */
pos [ 0 ] = ' \0 ' ; /* stop path at file */
estrlcpy ( dir , path , dirsiz ) ;
} else {
estrlcpy ( file , path , filesiz ) ;
}
}
char *
get_query ( char * path , char * query , size_t querysiz )
{
char * pos = NULL ;
/* remove a query string before percent decoding */
/* look for "?" if any to set query for cgi, remove it */
pos = strchr ( path , ' ? ' ) ;
if ( pos ! = NULL ) {
estrlcpy ( query , pos + 1 , querysiz ) ;
pos [ 0 ] = ' \0 ' ; /* path end where query begins */
}
return query ;
}