From 7543e84481af1fccb01bf3cedf2063961eeea919 Mon Sep 17 00:00:00 2001 From: Glenn Strauss Date: Sun, 14 Apr 2024 03:13:08 -0400 Subject: [PATCH] Revert "[multiple] remove long-deprecated modules" This reverts commit 86c2d3093686c4f945086c90af9b8a7900925b6b. --- INSTALL | 16 +- SConstruct | 31 +- configure.ac | 117 ++++- doc/config/conf.d/Makefile.am | 4 + doc/config/conf.d/cml.conf | 27 ++ doc/config/conf.d/geoip.conf | 28 ++ doc/config/conf.d/mysql_vhost.conf | 47 ++ doc/config/conf.d/trigger_b4_dl.conf | 70 +++ doc/config/modules.conf | 18 + doc/outdated/Makefile.am | 6 + doc/outdated/cml.txt | 261 +++++++++++ doc/outdated/geoip.txt | 148 +++++++ doc/outdated/mysqlvhost.txt | 58 +++ doc/outdated/trigger_b4_dl.txt | 57 +++ meson_options.txt | 15 + src/CMakeLists.txt | 65 ++- src/Makefile.am | 65 ++- src/SConscript | 13 + src/config.h.cmake | 7 + src/configfile.c | 10 + src/meson.build | 62 +++ src/mod_authn_mysql.c | 501 ++++++++++++++++++++++ src/mod_cml.c | 323 ++++++++++++++ src/mod_cml.h | 37 ++ src/mod_cml_funcs.c | 260 +++++++++++ src/mod_cml_funcs.h | 16 + src/mod_cml_lua.c | 320 ++++++++++++++ src/mod_flv_streaming.c | 161 +++++++ src/mod_geoip.c | 291 +++++++++++++ src/mod_mysql_vhost.c | 385 +++++++++++++++++ src/mod_trigger_b4_dl.c | 619 +++++++++++++++++++++++++++ src/server.c | 10 + 32 files changed, 4035 insertions(+), 13 deletions(-) create mode 100644 doc/config/conf.d/cml.conf create mode 100644 doc/config/conf.d/geoip.conf create mode 100644 doc/config/conf.d/mysql_vhost.conf create mode 100644 doc/config/conf.d/trigger_b4_dl.conf create mode 100644 doc/outdated/cml.txt create mode 100644 doc/outdated/geoip.txt create mode 100644 doc/outdated/mysqlvhost.txt create mode 100644 doc/outdated/trigger_b4_dl.txt create mode 100644 src/mod_authn_mysql.c create mode 100644 src/mod_cml.c create mode 100644 src/mod_cml.h create mode 100644 src/mod_cml_funcs.c create mode 100644 src/mod_cml_funcs.h create mode 100644 src/mod_cml_lua.c create mode 100644 src/mod_flv_streaming.c create mode 100644 src/mod_geoip.c create mode 100644 src/mod_mysql_vhost.c create mode 100644 src/mod_trigger_b4_dl.c diff --git a/INSTALL b/INSTALL index 21a22e12..c04cbfc8 100644 --- a/INSTALL +++ b/INSTALL @@ -60,6 +60,10 @@ optional packages for optional features :: bzip2-libs cyrus-sasl # SASL ./configure --with-sasl cyrus-sasl-devel + gdbm # GDBM ./configure --with-gdbm + gdbm-devel + GeoIP-devel # GeoIP ./configure --with-geoip + GeoIP gnutls # GnuTLS ./configure --with-gnutls gnutls-devel krb5-devel # Kerberos5 ./configure --with-krb5 @@ -75,9 +79,13 @@ optional packages for optional features :: libdeflate-devel libmaxminddb # MaxMindDB ./configure --with-maxminddb libmaxminddb-devel + libmemcached-devel # Memcached ./configure --with-memcache + libmemcached-libs libpq # Postgresql ./configure --with-pgsql libpq-devel libunwind-devel # libunwind ./configure --with-libunwind + libuuid # libuuid ./configure --with-webdav-locks + libuuid-devel libxml2 # libxml2 ./configure --with-webdav-props libxml2-devel libxml2-static @@ -97,9 +105,9 @@ optional packages for optional features :: openssl-libs pam # PAM ./configure --with-pam pam-devel - pcre # PCRE ./configure --with-pcre + pcre # PCRE ./configure --with-pcre # (default) pcre-devel - pcre2 # PCRE ./configure --with-pcre2 # (default) + pcre2 # PCRE ./configure --with-pcre2 pcre2-devel sqlite # SQLite ./configure --with-webdav-props sqlite-devel @@ -107,9 +115,9 @@ optional packages for optional features :: valgrind-devel wolfssl # wolfSSL ./configure --with-wolfssl wolfssl-devel - zlib # zlib ./configure --with-zlib # (default) + zlib # zlib ./configure --with-zlib zlib-devel - libzstd # zstd ./configure --with-zstd + zstd # zstd ./configure --with-zstd libzstd-devel more options: ./configure --help diff --git a/SConstruct b/SConstruct index 429fc5e2..de52b056 100644 --- a/SConstruct +++ b/SConstruct @@ -245,12 +245,15 @@ vars.AddVariables( PackageVariable('with_dbi', 'enable dbi support', 'no'), BoolVariable('with_fam', 'enable FAM/gamin support', 'no'), BoolVariable('with_libdeflate', 'enable libdeflate compression', 'no'), + BoolVariable('with_gdbm', 'enable gdbm support', 'no'), + BoolVariable('with_geoip', 'enable GeoIP support', 'no'), BoolVariable('with_maxminddb', 'enable MaxMind GeoIP2 support', 'no'), BoolVariable('with_krb5', 'enable krb5 auth support', 'no'), BoolVariable('with_ldap', 'enable ldap auth support', 'no'), # with_libev not supported # with_libunwind not supported - BoolVariable('with_lua', 'enable lua support', 'no'), + BoolVariable('with_lua', 'enable lua support for mod_cml', 'no'), + BoolVariable('with_memcached', 'enable memcached support', 'no'), PackageVariable('with_mysql', 'enable mysql support', 'no'), BoolVariable('with_openssl', 'enable openssl support', 'no'), PackageVariable('with_gnutls', 'enable GnuTLS support', 'no'), @@ -337,6 +340,7 @@ if 1: LIBDBI = '', LIBDEFLATE = '', LIBDL = '', + LIBGDBM = '', LIBGNUTLS = '', LIBGSSAPI_KRB5 = '', LIBKRB5 = '', @@ -346,6 +350,7 @@ if 1: LIBMBEDTLS = '', LIBMBEDX509 = '', LIBMBEDCRYPTO = '', + LIBMEMCACHED = '', LIBMYSQL = '', LIBNSS = '', LIBPAM = '', @@ -545,6 +550,22 @@ if 1: LIBDEFLATE = 'libdeflate', ) + if env['with_gdbm']: + if not autoconf.CheckLibWithHeader('gdbm', 'gdbm.h', 'C'): + fail("Couldn't find gdbm") + autoconf.env.Append( + CPPFLAGS = [ '-DHAVE_GDBM_H', '-DHAVE_GDBM' ], + LIBGDBM = 'gdbm', + ) + + if env['with_geoip']: + if not autoconf.CheckLibWithHeader('GeoIP', 'GeoIP.h', 'C'): + fail("Couldn't find geoip") + autoconf.env.Append( + CPPFLAGS = [ '-DHAVE_GEOIP' ], + LIBGEOIP = 'GeoIP', + ) + if env['with_maxminddb']: if not autoconf.CheckLibWithHeader('maxminddb', 'maxminddb.h', 'C'): fail("Couldn't find maxminddb") @@ -589,6 +610,14 @@ if 1: if not found_lua: fail("Couldn't find any lua implementation") + if env['with_memcached']: + if not autoconf.CheckLibWithHeader('memcached', 'libmemcached/memcached.h', 'C'): + fail("Couldn't find memcached") + autoconf.env.Append( + CPPFLAGS = [ '-DUSE_MEMCACHED' ], + LIBMEMCACHED = 'memcached', + ) + if env['with_mysql']: mysql_config = autoconf.checkProgram('mysql', 'mysql_config') if not autoconf.CheckParseConfigForLib('LIBMYSQL', mysql_config + ' --cflags --libs'): diff --git a/configure.ac b/configure.ac index ac001d91..ada91c6d 100644 --- a/configure.ac +++ b/configure.ac @@ -1272,6 +1272,66 @@ if test "$WITH_XXHASH" != no; then AC_SUBST([XXHASH_LIBS]) fi +dnl Check for gdbm +AC_MSG_NOTICE([----------------------------------------]) +AC_MSG_CHECKING([for gdbm]) +AC_ARG_WITH([gdbm], + [AS_HELP_STRING([--with-gdbm], [gdbm storage for mod_trigger_b4_dl])], + [WITH_GDBM=$withval], + [WITH_GDBM=no] +) +AC_MSG_RESULT([$WITH_GDBM]) + +if test "$WITH_GDBM" != no; then + if test "$WITH_GDBM" != yes; then + GDBM_LIB="-L$WITH_GDBM -lgdbm" + CPPFLAGS="$CPPFLAGS -I$WITH_GDBM" + else + AC_CHECK_LIB([gdbm], [gdbm_open], + [GDBM_LIB=-lgdbm], + [AC_MSG_ERROR([gdbm lib not found, install it or build without --with-gdbm])] + ) + AC_CHECK_HEADERS([gdbm.h], [], + [AC_MSG_ERROR([gdbm headers not found, install them or build without --with-gdbm])] + ) + fi + + AC_DEFINE([HAVE_GDBM], [1], [libgdbm]) + AC_DEFINE([HAVE_GDBM_H], [1]) + AC_SUBST([GDBM_LIB]) +fi +AM_CONDITIONAL([BUILD_WITH_GDBM], [test "$WITH_GDBM" != no]) + +dnl Check for GeoIP +AC_MSG_NOTICE([----------------------------------------]) +AC_MSG_CHECKING([for GeoIP]) +AC_ARG_WITH([geoip], + [AS_HELP_STRING([--with-geoip], [IP-based geolocation lookup])], + [WITH_GEOIP=$withval], + [WITH_GEOIP=no] +) +AC_MSG_RESULT([$WITH_GEOIP]) + +if test "$WITH_GEOIP" != no; then + if test "$WITH_GEOIP" != yes; then + GEOIP_LIB="-L$WITH_GEOIP -lGeoIP" + CPPFLAGS="$CPPFLAGS -I$WITH_GEOIP" + else + AC_CHECK_LIB([GeoIP], [GeoIP_country_name_by_addr], + [GEOIP_LIB=-lGeoIP], + [AC_MSG_ERROR([GeoIP lib not found, install it or build without --with-geoip])] + ) + AC_CHECK_HEADERS([GeoIP.h], [], + [AC_MSG_ERROR([GeoIP headers not found, install them or build without --with-geoip])] + ) + fi + + AC_DEFINE([HAVE_GEOIP], [1], [libGeoIP]) + AC_DEFINE([HAVE_GEOIP_H], [1]) + AC_SUBST([GEOIP_LIB]) +fi +AM_CONDITIONAL([BUILD_WITH_GEOIP], [test "$WITH_GEOIP" != no]) + dnl Check for maxminddb AC_MSG_NOTICE([----------------------------------------]) AC_MSG_CHECKING([for maxminddb]) @@ -1302,11 +1362,49 @@ if test "$WITH_MAXMINDDB" != no; then fi AM_CONDITIONAL([BUILD_WITH_MAXMINDDB], [test "$WITH_MAXMINDDB" != no]) +dnl Check for memcached +AC_MSG_NOTICE([----------------------------------------]) +AC_MSG_CHECKING([for memcached]) +AC_ARG_WITH([memcached], + [AS_HELP_STRING([--with-memcached], + [memcached storage for mod_trigger_b4_dl/mod_cml] + )], + [WITH_MEMCACHED=$withval], + [WITH_MEMCACHED=no] +) +AC_MSG_RESULT([$WITH_MEMCACHED]) + +if test "$WITH_MEMCACHED" != no; then + if test "$WITH_MEMCACHED" != yes; then + MEMCACHED_LIB="-L$WITH_MEMCACHED -lMEMCACHED" + CPPFLAGS="$CPPFLAGS -I$WITH_MEMCACHED" + else + AC_CHECK_LIB([memcached], [memcached], + [ MEMCACHED_LIB=-lmemcached ], + [AC_MSG_ERROR([memcached lib not found, install it or build without --with-memcached])] + ) + AC_CHECK_HEADERS([libmemcached/memcached.h], [], + [AC_MSG_ERROR([memcached headers not found, install them or build without --with-memcached])] + ) + fi + + AC_DEFINE([USE_MEMCACHED], [1], [libmemcached]) + AC_SUBST([MEMCACHED_LIB]) +fi +AM_CONDITIONAL([BUILD_WITH_MEMCACHED], [test "$WITH_MEMCACHED" != no]) + +BUILD_MOD_TRIGGER_B4_DL=no +if test "$WITH_MEMCACHED" != no || test "$WITH_GDBM" != no ; then + BUILD_MOD_TRIGGER_B4_DL=yes +fi + +AM_CONDITIONAL([BUILD_MOD_TRIGGER_B4_DL], [test "$BUILD_MOD_TRIGGER_B4_DL" != no]) + dnl Check for lua AC_MSG_NOTICE([----------------------------------------]) AC_MSG_CHECKING([if lua-support is requested]) AC_ARG_WITH([lua], - [AS_HELP_STRING([--with-lua], [lua engine for mod_magnet])], + [AS_HELP_STRING([--with-lua], [lua engine for mod_magnet/mod_cml])], [WITH_LUA=$withval], [WITH_LUA=no] ) @@ -1673,6 +1771,7 @@ do_build="\ mod_expire \ mod_extforward \ mod_fastcgi \ + mod_flv_streaming \ mod_indexfile \ mod_proxy \ mod_redirect \ @@ -1704,7 +1803,16 @@ lighty_track_feature() { lighty_track_feature "regex-conditionals" "" \ 'test "$WITH_PCRE" != no || test "$WITH_PCRE2" != no' -lighty_track_feature "mysql" "mod_vhostdb_mysql" \ +lighty_track_feature "storage-gdbm" "" \ + 'test "$WITH_GDBM" != no' + +lighty_track_feature "storage-memcached" "" \ + 'test "$WITH_MEMCACHED" != no' + +lighty_track_feature "" "mod_trigger_b4_dl" \ + 'test "$BUILD_MOD_TRIGGER_B4_DL" != no' + +lighty_track_feature "mysql" "mod_authn_mysql mod_mysql_vhost mod_vhostdb_mysql" \ 'test "$WITH_MYSQL" != no' lighty_track_feature "postgresql" "mod_vhostdb_pgsql" \ @@ -1713,9 +1821,12 @@ lighty_track_feature "postgresql" "mod_vhostdb_pgsql" \ lighty_track_feature "dbi" "mod_authn_dbi mod_vhostdb_dbi" \ 'test "$WITH_DBI" != no' -lighty_track_feature "lua" "mod_magnet" \ +lighty_track_feature "lua" "mod_cml mod_magnet" \ 'test "$WITH_LUA" != no' +lighty_track_feature "geoip" "mod_geoip" \ + 'test "$WITH_GEOIP" != no' + lighty_track_feature "maxminddb" "mod_maxminddb" \ 'test "$WITH_MAXMINDDB" != no' diff --git a/doc/config/conf.d/Makefile.am b/doc/config/conf.d/Makefile.am index a1eb33b9..0fda8ae0 100644 --- a/doc/config/conf.d/Makefile.am +++ b/doc/config/conf.d/Makefile.am @@ -1,20 +1,24 @@ EXTRA_DIST=access_log.conf \ auth.conf \ cgi.conf \ + cml.conf \ debug.conf \ deflate.conf \ dirlisting.conf \ evhost.conf \ expire.conf \ fastcgi.conf \ + geoip.conf \ magnet.conf \ mime.conf \ mod.template \ + mysql_vhost.conf \ proxy.conf \ rrdtool.conf \ scgi.conf \ simple_vhost.conf \ ssi.conf \ status.conf \ + trigger_b4_dl.conf \ userdir.conf \ webdav.conf diff --git a/doc/config/conf.d/cml.conf b/doc/config/conf.d/cml.conf new file mode 100644 index 00000000..6c539794 --- /dev/null +++ b/doc/config/conf.d/cml.conf @@ -0,0 +1,27 @@ +####################################################################### +## +## CML Module +## --------------- +## +## See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModCML +## +server.modules += ( "mod_cml" ) +index-file.names += ( "index.cml" ) + +## +## The file extension that is bound to the cml-module. +## +cml.extension = ".cml" + +## +## Memcached hosts used for memcache* functions. +## +#cml.memcache-hosts = ( "127.0.0.1:11211" ) + +## +## A cml file that is executed for each request. +## +#cml.power-magnet = server_root + "/htdocs/powermagnet.cml" + +## +####################################################################### diff --git a/doc/config/conf.d/geoip.conf b/doc/config/conf.d/geoip.conf new file mode 100644 index 00000000..d0da3067 --- /dev/null +++ b/doc/config/conf.d/geoip.conf @@ -0,0 +1,28 @@ +####################################################################### +## +## GeoIP Module +## --------------- +## +## See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModGeoip +## +## mod_geoip is a module for fast ip/location lookups. It uses MaxMind +## GeoIP / GeoCity databases. If the ip was found in the database the +## module sets the appropriate environments variables to the request, +## thus making other modules/fcgi be informed. +## +server.modules += ( "mod_geoip" ) + +## +## mod_geoip will determine the database type automatically so if you +## enter GeoCity databse path it will load GeoCity Env. +## +#geoip.db-filename = "/path/to/GeoLiteCity.dat" + +## +## If enabled, mod_geoip will load the database binary file to memory +## for very fast lookups. The only penalty is memory usage. +## +#geoip.memory-cache = "disable" + +## +####################################################################### diff --git a/doc/config/conf.d/mysql_vhost.conf b/doc/config/conf.d/mysql_vhost.conf new file mode 100644 index 00000000..cb63083d --- /dev/null +++ b/doc/config/conf.d/mysql_vhost.conf @@ -0,0 +1,47 @@ +####################################################################### +## +## Virtual hosting with MySQL +## ---------------------------- +## +## See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModMySQLVhost +## +server.modules += ( "mod_mysql_vhost" ) + +## +## Either set the the socket or host (and port) +## +## Local path to the mysql socket +## +#mysql-vhost.sock = "/var/lib/mysql/mysql.sock" + +## +## Host of the MySQL server. +## +#mysql-vhost.hostname = "localhost" + +## +## Optional: port to use. +## +#mysql-vhost.port = 3306 + +## +## Name of the database +## +mysql-vhost.db = "lighttpd" + +## +## SQL User/Password for the connection +## +mysql-vhost.user = "lighttpd" +mysql-vhost.pass = "secret" + +## +## The query to get the needed informations from the database. +## +## It doesnt matter how you name the fields the first field is always used +## as the document root. +## +mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?'" + +## +####################################################################### diff --git a/doc/config/conf.d/trigger_b4_dl.conf b/doc/config/conf.d/trigger_b4_dl.conf new file mode 100644 index 00000000..9cf48354 --- /dev/null +++ b/doc/config/conf.d/trigger_b4_dl.conf @@ -0,0 +1,70 @@ +####################################################################### +## +## Trigger before download +## --------------- +## +## - if user requests ''download-url'' directly, the request is denied +## and he is redirected to ''deny-url' +## - if user visits ''trigger-url'' before requesting ''download-url'', +## access is granted +## - if user visits ''download-url'' again after ''trigger-timeout'' has +## elapsed, the request is denied and he is redirected to ''deny-url'' +## +## See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModTriggerBeforeDownload +## +server.modules += ( "mod_mod_trigger_b4_dl" ) + +## +## To store the trigger state you can either use a local GDBM +## file or memcached(s). +## + +## +## Path to the local GDBM file. +## +trigger-before-download.gdbm-filename = home_dir + "/trigger.db" + +## +## List of memcached servers. +## +#trigger-before-download.memcache-hosts = ( "127.0.0.1:11211" ) + +## +## URL prefix a visitor has to visit before downloading is allowed +## +trigger-before-download.trigger-url = "^/trigger/" + +## +## URL Prefix of the proteced area. +## +trigger-before-download.download-url = "^/download/" + +## +## +## The deny url. +## +trigger-before-download.deny-url = "http://www.example.com/index.html" + +## +## How long the "ticket" of the user will be valid. +## +## Value in seconds. +## +trigger-before-download.trigger-timeout = 10 + +## +## Normally the memcached key will be the remote ip of the request +## If you store other data in the memcached aswell and want to avoid +## key collisions you can configure a memcache-namespace. +## +## The key for the request will be memcache-namespace + remote_ip than. +## +#trigger-before-download.memcache-namespace = "t4bdl_" + +## +## If set to 1, the module will log some debug informations. +## +#trigger-before-download.debug = 0 + +## +####################################################################### diff --git a/doc/config/modules.conf b/doc/config/modules.conf index 75235b55..f660118e 100644 --- a/doc/config/modules.conf +++ b/doc/config/modules.conf @@ -21,8 +21,11 @@ ## - mod_deflate -> conf.d/deflate.conf ## - mod_status -> conf.d/status.conf ## - mod_webdav -> conf.d/webdav.conf +## - mod_cml -> conf.d/cml.conf ## - mod_evhost -> conf.d/evhost.conf ## - mod_simple_vhost -> conf.d/simple_vhost.conf +## - mod_mysql_vhost -> conf.d/mysql_vhost.conf +## - mod_trigger_b4_dl -> conf.d/trigger_b4_dl.conf ## - mod_userdir -> conf.d/userdir.conf ## - mod_rrdtool -> conf.d/rrdtool.conf ## - mod_ssi -> conf.d/ssi.conf @@ -77,6 +80,11 @@ server.modules = ( ## #include conf_dir + "/conf.d/magnet.conf" +## +## mod_geoip +## +#include conf_dir + "/conf.d/geoip.conf" + ## ## mod_ssi ## @@ -97,6 +105,11 @@ server.modules = ( ## #include conf_dir + "/conf.d/userdir.conf" +## +## mod_cml +## +#include conf_dir + "/conf.d/cml.conf" + ## ## mod_rrdtool ## @@ -157,5 +170,10 @@ server.modules = ( ## #include conf_dir + "/conf.d/simple_vhost.conf" +## +## mod_mysql_vhost +## +#include conf_dir + "/conf.d/mysql_vhost.conf" + ## ####################################################################### diff --git a/doc/outdated/Makefile.am b/doc/outdated/Makefile.am index b901f501..0cdd6e46 100644 --- a/doc/outdated/Makefile.am +++ b/doc/outdated/Makefile.am @@ -21,11 +21,14 @@ state.txt \ rrdtool.txt \ alias.txt \ userdir.txt \ +mysqlvhost.txt \ access.txt \ traffic-shaping.txt \ setenv.txt \ status.txt \ scgi.txt \ +cml.txt \ +trigger_b4_dl.txt \ webdav.txt \ expire.txt \ dirlisting.txt \ @@ -55,11 +58,14 @@ HTMLDOCS=accesslog.html \ rrdtool.html \ alias.html \ userdir.html \ + mysqlvhost.html \ access.html \ traffic-shaping.html \ setenv.html \ status.html \ scgi.html \ + cml.html \ + trigger_b4_dl.html \ webdav.html \ expire.html \ dirlisting.html \ diff --git a/doc/outdated/cml.txt b/doc/outdated/cml.txt new file mode 100644 index 00000000..b9ae92c5 --- /dev/null +++ b/doc/outdated/cml.txt @@ -0,0 +1,261 @@ +========================= +CML (Cache Meta Language) +========================= + +--------------- +Module: mod_cml +--------------- + +:Author: Jan Kneschke +:Date: $Date: 2004/11/03 22:26:05 $ +:Revision: $Revision: 1.2 $ + +:abstract: + CML is a Meta language to describe the dependencies of a page at one side and building a page from its fragments on the other side using LUA. + +.. meta:: + :keywords: lighttpd, cml, lua + +.. contents:: Table of Contents + +Description +=========== + +CML (Cache Meta Language) wants to solves several problems: + + * dynamic content needs caching to perform + * checking if the content is dirty inside of the application is usually more expensive than sending out the cached data + * a dynamic page is usually fragmented and the fragments have different livetimes + * the different fragments can be cached independently + +Cache Decision +-------------- + +A simple example should show how to a content caching the very simple way in PHP. + +jan.kneschke.de has a very simple design: + + * the layout is taken from a template in templates/jk.tmpl + * the menu is generated from a menu.csv file + * the content is coming from files on the local directory named content-1, content-2 and so on + +The page content is static as long non of the those tree items changes. A change in the layout +is affecting all pages, a change of menu.csv too, a change of content-x file only affects the +cached page itself. + +If we model this in PHP we get: :: + + $v) { + if (isset($v["file"]) && + filemtime($v["file"]) > $cachemtime) { + return 0; + } + } + + if (filemtime("/menu/menu.csv") > $cachemtime) { + return 0; + } + if (filemtime("/templates/jk.tmpl") > $cachemtime) { + return 0; + } + } + + if (is_cachable(...), $cachefile) { + readfile($cachefile); + exit(); + } else { + # generate content and write it to $cachefile + } + ?> + +Quite simple. No magic involved. If the one of the files is new than the cached +content, the content is dirty and has to be regenerated. + +Now let take a look at the numbers: + + * 150 req/s for a Cache-Hit + * 100 req/s for a Cache-Miss + +As you can see the increase is not as good as it could be. The main reason as the overhead +of the PHP interpreter to start up (a byte-code cache has been used here). + +Moving these decisions out of the PHP script into a server module will remove the need +to start PHP for a cache-hit. + +To transform this example into a CML you need 'index.cml' in the list of indexfiles +and the following index.cml file: :: + + output_contenttype = "text/html" + + b = request["DOCUMENT_ROOT"] + cwd = request["CWD"] + + output_include = { b .. "_cache.html" } + + trigger_handler = "index.php" + + if file_mtime(b .. "../lib/php/menu.csv") > file_mtime(cwd .. "_cache.html") or + file_mtime(b .. "templates/jk.tmpl") > file_mtime(cwd .. "_cache.html") or + file_mtime(b .. "content.html") > file_mtime(cwd .. "_cache.html") then + return CACHE_MISS + else + return CACHE_HIT + end + +Numbers again: + + * 4900 req/s for Cache-Hit + * 100 req/s for Cache-Miss + +Content Assembling +------------------ + +Sometimes the different fragment are already generated externally. You have to cat them together: :: + + + +We we can do the same several times faster directly in the webserver. + +Don't forget: Webserver are built to send out static content, that is what they can do best. + +The index.cml for this looks like: :: + + output_contenttype = "text/html" + + cwd = request["CWD"] + + output_include = { cwd .. "head.html", + cwd .. "menu.html", + cwd .. "spacer.html", + cwd .. "db-content.html", + cwd .. "spacer2.html", + cwd .. "news.html", + cwd .. "footer.html" } + + return CACHE_HIT + +Now we get about 10000 req/s instead of 600 req/s. + +Power Magnet +------------ + +Next to all the features about Cache Decisions CML can do more. Starting +with lighttpd 1.4.9 a power-magnet was added which attracts each request +and allows you to manipulate the request for your needs. + +We want to display a maintenance page by putting a file in a specified +place: + +We enable the power magnet: :: + + cml.power-magnet = "/home/www/power-magnet.cml" + +and create /home/www/power-magnet.cml with: :: + + dr = request["DOCUMENT_ROOT"] + + if file_isreg(dr .. 'maintenance.html') then + output_include = { 'maintenance.html' } + return CACHE_HIT + end + + return CACHE_MISS + +For each requested file the /home/www/power-magnet.cml is executed which +checks if maintenance.html exists in the docroot and displays it +instead of handling the usual request. + +Another example, create thumbnail for requested image and serve it instead +of sending the big image: :: + + ## image-url is /album/baltic_winter_2005.jpg + ## no params -> 640x480 is served + ## /album/baltic_winter_2005.jpg/orig for full size + ## /album/baltic_winter_2005.jpg/thumb for thumbnail + + dr = request["DOCUMENT_ROOT"] + sn = request["SCRIPT_NAME"] + + ## to be continued :) ... + + trigger_handler = '/gen_image.php' + + return CACHE_MISS + + +Installation +============ + +You need `lua `_ and should install `memcached `_ and have to configure lighttpd with: :: + + ./configure ... --with-lua --with-memcached + +To use the plugin you have to load it: :: + + server.modules = ( ..., "mod_cml", ... ) + +Options +======= + +:cml.extension: + the file extension that is bound to the cml-module +:cml.memcache-hosts: + hosts for the memcache.* functions +:cml.memcache-namespace: + (not used yet) +:cml.power-magnet: + a cml file that is executed for each request + +Language +======== + +The language used for CML is provided by `LUA `_. + +Additionally to the functions provided by lua mod_cml provides: :: + + tables: + + request + - REQUEST_URI + - SCRIPT_NAME + - SCRIPT_FILENAME + - DOCUMENT_ROOT + - PATH_INFO + - CWD + - BASEURI + + get + - parameters from the query-string + + functions: + string md5(string) + number file_mtime(string) + string memcache_get_string(string) + number memcache_get_long(string) + boolean memcache_exists(string) + + +What ever your script does, it has to return either CACHE_HIT or CACHE_MISS. +It case a error occurs check the error-log, the user will get a error 500. If you don't like +the standard error-page use ``server.errorfile-prefix``. + diff --git a/doc/outdated/geoip.txt b/doc/outdated/geoip.txt new file mode 100644 index 00000000..a3d39bc3 --- /dev/null +++ b/doc/outdated/geoip.txt @@ -0,0 +1,148 @@ +{{{ +#!rst +============================== +ip based geographic lookups... +============================== + +----------------- +Module: mod_geoip +----------------- + + + +.. contents:: Table of Contents + +Requirements +============ + +:Packages: GeoIP C API & Library (http://www.maxmind.com/download/geoip/api/c/) + +Overview +======== + +mod_geoip is a module for fast ip/location lookups. It uses MaxMind GeoIP / +GeoCity databases. +If the ip was found in the database the module sets the appropriate +environments variables to the request, thus making other modules/fcgi be +informed. + +.. note:: + + Currently only country/city databases are supported because they have a free + version that i can test. + +Configuration Options +======================== + +mod_geoip uses two configuration options. + +1) geoip.db-filename = +2) geoip.memory-cache = : default disabled + +if enabled, mod_geoip will load the database binary file to +memory for very fast lookups. the only penalty is memory usage. + +.. note:: + + mod_geoip will determine the database type automatically so if you enter + GeoCity database path it will load GeoCity Env. + +Environment +=========== + +Every database sets it's own ENV: + +GeoIP (Country): +---------------- + +:: + + GEOIP_COUNTRY_CODE + GEOIP_COUNTRY_CODE3 + GEOIP_COUNTRY_NAME + +GeoCity: +-------- + +:: + + GEOIP_COUNTRY_CODE + GEOIP_COUNTRY_CODE3 + GEOIP_COUNTRY_NAME + GEOIP_CITY_NAME + GEOIP_CITY_POSTAL_CODE + GEOIP_CITY_LATITUDE + GEOIP_CITY_LONG_LATITUDE + GEOIP_CITY_DMA_CODE + GEOIP_CITY_AREA_CODE + +Examples +======== + +mod_geoip + php +--------------- + +when using fastcgi (not only php) you can access mod_geoip env and do as you +please. this example just prints all mod_geoip envs to the client, html. + +Config-file :: + + geoip.db-filename = "/your-geoip-db-path/GeoCityLite.dat" + geoip.memory-cache = "enable" + +index.php :: + + \n\n\t
\n"; + echo 'Country Code: ' . $country_code . '
'; + echo 'Country Code 3: ' . $country_code3 . '
'; + echo 'Country Name: ' . $country_name . '
'; + echo '
'; + echo 'City Region: ' . $city_region . '
'; + echo 'City Name: ' . $city_name . '
'; + echo 'City Postal Code: ' . $city_postal_code . '
'; + echo 'City Latitude: ' . $city_latitude . '
'; + echo 'City Long Latitude: ' . $city_long_latitude . '
'; + echo 'City DMA Code: ' . $city_dma_code . '
'; + echo 'City Area Code: ' . $city_area_code . '
'; + echo "\n"; + ?> + +country based redirect +---------------------- + +Config-file :: + + $HTTP["host"] =~ "www.domain.com" { + url.rewrite = ( "" => "/redirect.php") + } + +redirect.php :: + + + +.. note:: + + Currently it is not possible to redirect based on mod_geoip directly in +lighttpd config file. But i believe with the release of lighttpd mod_magnet +it would be. (mod_magnet will be available in lighttpd 1.4.12+) + +Downloads +========= +mod_geoip.c (http://trac.lighttpd.net/trac/attachment/wiki/Docs/ModGeoip/mod_geoip.c) +}}} diff --git a/doc/outdated/mysqlvhost.txt b/doc/outdated/mysqlvhost.txt new file mode 100644 index 00000000..9a869a19 --- /dev/null +++ b/doc/outdated/mysqlvhost.txt @@ -0,0 +1,58 @@ +==================== +MySQL-based vhosting +==================== + +----------------------- +Module: mod_mysql_vhost +----------------------- + +:Author: ada@riksnet.se +:Date: $Date: 2004/08/29 09:43:49 $ +:Revision: $Revision: 1.1 $ + +:abstract: + This module provides virtual hosts (vhosts) based on a MySQL table + +.. meta:: + :keywords: lighttpd, mysql, vhost + +.. contents:: Table of Contents + +Description +=========== + +With MySQL-based vhosting you can store the path to a given host's +document root in a MySQL database. + +.. note:: Keep in mind that only one vhost module should be active at a time. + Don't mix mod_simple_vhost with mod_mysql_vhost. + +Options +======= + +Example: :: + + mysql-vhost.db = "lighttpd" + mysql-vhost.user = "lighttpd" + mysql-vhost.pass = "secret" + mysql-vhost.sock = "/var/mysql.lighttpd.sock" + mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?'" + + +MySQL setup: :: + + GRANT SELECT ON lighttpd.* TO lighttpd@localhost IDENTIFIED BY 'secret'; + + CREATE DATABASE lighttpd; + + USE lighttpd; + + CREATE TABLE domains ( + domain varchar(64) not null primary key, + docroot varchar(128) not null + ); + + INSERT INTO domains VALUES ('host.dom.ain','/http/host.dom.ain/'); + + + diff --git a/doc/outdated/trigger_b4_dl.txt b/doc/outdated/trigger_b4_dl.txt new file mode 100644 index 00000000..0bfd099a --- /dev/null +++ b/doc/outdated/trigger_b4_dl.txt @@ -0,0 +1,57 @@ +======================= +Trigger before Download +======================= + +------------------------- +Module: mod_trigger_b4_dl +------------------------- + +:Author: Jan Kneschke +:Date: $Date: 2004/11/03 22:26:05 $ +:Revision: $Revision: 1.2 $ + +:abstract: + another anti-hot-linking module + +.. meta:: + :keywords: lighttpd, hot-linking, deep-linking + +.. contents:: Table of Contents + +Description +=========== + +Anti Hotlinking: + + * if user requests ''download-url'' directly, the request is denied and he is redirected to ''deny-url' + * if user visits ''trigger-url'' before requesting ''download-url'', access is granted + * if user visits ''download-url'' again after ''trigger-timeout'' has elapsed, the request is denied and he is redirected to ''deny-url'' + +The trigger information is either stored locally in a gdbm file or remotely in memcached. + +Requirements +------------ + + * libpcre + * libgdbm or libmemcached + +Options +======= + +:: + + trigger-before-download.gdbm-filename = "/home/weigon/testbase/trigger.db" + trigger-before-download.memcache-hosts = ( "127.0.0.1:11211" ) + trigger-before-download.trigger-url = "^/trigger/" + trigger-before-download.download-url = "^/download/" + trigger-before-download.deny-url = "http://192.168.1.5:1025/index.html" + trigger-before-download.trigger-timeout = 10 + +If both trigger-before-download.gdbm-filename and +trigger-before-download.memcache-hosts is set gdbm will take precedence. + +Installation +============ + +memcached should be started with the option -M as we don't want to remove entry if the memory is full. + diff --git a/meson_options.txt b/meson_options.txt index 41b2311f..ab210ebe 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -23,6 +23,16 @@ option('with_fam', value: 'disabled', description: 'fam/gamin for reducing number of stat() calls [default: off]', ) +option('with_gdbm', + type: 'boolean', + value: false, + description: 'gdbm storage for mod_trigger_b4_dl [default: off]', +) +option('with_geoip', + type: 'boolean', + value: false, + description: 'with GeoIP-support mod_geoip [default: off]', +) option('with_gnutls', type: 'boolean', value: false, @@ -63,6 +73,11 @@ option('with_mbedtls', value: false, description: 'with mbedTLS-support [default: off]', ) +option('with_memcached', + type: 'boolean', + value: false, + description: 'memcached storage for mod_trigger_b4_dl [default: off]', +) option('with_mysql', type: 'feature', value: 'disabled', diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fafc4ef1..8cedebc2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -41,8 +41,11 @@ option(WITH_LUA_VERSION "specify lua version for mod_magnet") # option(WITH_VALGRIND "with internal support for valgrind [default: off]") option(WITH_FAM "fam/gamin for reducing number of stat() calls [default: off]") option(WITH_LIBDEFLATE "with libdeflate-support for mod_deflate [default: off]") +option(WITH_GDBM "gdbm storage for mod_trigger_b4_dl [default: off]") +option(WITH_MEMCACHED "memcached storage for mod_trigger_b4_dl [default: off]") option(WITH_LIBEV "libev support for fdevent handlers [default: off]") option(WITH_LIBUNWIND "with libunwind to print backtraces in asserts [default: off]") +option(WITH_GEOIP "with GeoIP-support mod_geoip [default: off]") option(WITH_MAXMINDDB "with MaxMind GeoIP2-support mod_maxminddb [default: off]") option(WITH_SASL "with SASL-support for mod_authn_sasl [default: off]") option(WITH_XXHASH "with system-provided xxhash [default: off]") @@ -782,6 +785,28 @@ else() unset(HAVE_FAMNOEXISTS) endif() +if(WITH_GDBM) + check_include_files(gdbm.h HAVE_GDBM_H) + check_library_exists(gdbm gdbm_open "" HAVE_GDBM) +else() + unset(HAVE_GDBM_H) + unset(HAVE_GDBM) +endif() + +if(WITH_MEMCACHED) + check_include_files(libmemcached/memcached.h HAVE_LIBMEMCACHED_MEMCACHED_H) + check_library_exists(memcached memcached "" HAVE_LIBMEMCACHED) + if(HAVE_LIBMEMCACHED_MEMCACHED_H AND HAVE_LIBMEMCACHED) + set(USE_MEMCACHED 1) + else() + message(FATAL_ERROR "didn't find libmemcached") + endif() +endif() + +if(WITH_GEOIP) + check_library_exists(geoip GeoIP_country_name_by_addr "" HAVE_GEOIP) +endif() + if(WITH_MAXMINDDB) check_library_exists(maxminddb MMDB_open "" HAVE_MAXMINDDB) endif() @@ -944,6 +969,7 @@ add_and_install_library(mod_dirlisting mod_dirlisting.c) add_and_install_library(mod_evasive mod_evasive.c) add_and_install_library(mod_extforward mod_extforward.c) add_and_install_library(mod_h2 "h2.c;ls-hpack/lshpack.c;algo_xxhash.c") +add_and_install_library(mod_flv_streaming mod_flv_streaming.c) add_and_install_library(mod_proxy mod_proxy.c) add_and_install_library(mod_rrdtool mod_rrdtool.c) add_and_install_library(mod_secdownload "mod_secdownload.c;algo_hmac.c") @@ -1020,10 +1046,26 @@ if(HAVE_PCRE) add_target_properties(test_mod COMPILE_FLAGS ${PCRE_CFLAGS}) endif() +if(HAVE_PCRE AND (WITH_MEMCACHED OR WITH_GDBM)) + add_and_install_library(mod_trigger_b4_dl mod_trigger_b4_dl.c) +endif() + if(WITH_LUA) add_and_install_library(mod_magnet "mod_magnet.c;mod_magnet_cache.c;algo_hmac.c") target_link_libraries(mod_magnet ${LUA_LDFLAGS} ${CRYPTO_LIBRARY}) add_target_properties(mod_magnet COMPILE_FLAGS ${LUA_CFLAGS}) + + add_and_install_library(mod_cml "mod_cml.c;mod_cml_lua.c;mod_cml_funcs.c") + target_link_libraries(mod_cml ${LUA_LDFLAGS} ${CRYPTO_LIBRARY}) + add_target_properties(mod_cml COMPILE_FLAGS ${LUA_CFLAGS}) + if(WITH_MEMCACHED) + target_link_libraries(mod_cml memcached) + endif() +endif() + +if(WITH_GEOIP) + add_and_install_library(mod_geoip mod_geoip.c) + target_link_libraries(mod_geoip GeoIP) endif() if(WITH_MAXMINDDB) @@ -1031,10 +1073,19 @@ if(WITH_MAXMINDDB) target_link_libraries(mod_maxminddb maxminddb) endif() -if(HAVE_MYSQL) +if(HAVE_MYSQL_H AND HAVE_MYSQL) + add_and_install_library(mod_mysql_vhost "mod_mysql_vhost.c") + target_link_libraries(mod_mysql_vhost mysqlclient) add_and_install_library(mod_vhostdb_mysql "mod_vhostdb_mysql.c") - target_link_libraries(mod_vhostdb_mysql ${MYSQL_LDFLAGS}) - add_target_properties(mod_vhostdb_mysql COMPILE_FLAGS ${MYSQL_CFLAGS}) + target_link_libraries(mod_vhostdb_mysql mysqlclient) + include_directories(/usr/include/mysql) + + add_and_install_library(mod_authn_mysql "mod_authn_mysql.c") + set(L_MOD_AUTHN_MYSQL ${CRYPTO_LIBRARY}) + if(HAVE_LIBCRYPT) + set(L_MOD_AUTHN_MYSQL ${L_MOD_AUTHN_MYSQL} crypt) + endif() + target_link_libraries(mod_authn_mysql ${L_MOD_AUTHN_MYSQL} mysqlclient) endif() if(HAVE_PGSQL) @@ -1137,6 +1188,14 @@ if(HAVE_LIBFAM) target_link_libraries(test_mod fam) endif() +if(HAVE_GDBM_H) + target_link_libraries(mod_trigger_b4_dl gdbm) +endif() + +if(WITH_MEMCACHED) + target_link_libraries(mod_trigger_b4_dl memcached) +endif() + if(HAVE_XATTR) target_link_libraries(lighttpd attr) target_link_libraries(test_mod attr) diff --git a/src/Makefile.am b/src/Makefile.am index 7b71bbc6..88be1caa 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -127,6 +127,18 @@ mod_h2_la_SOURCES = h2.c ls-hpack/lshpack.c algo_xxhash.c mod_h2_la_LDFLAGS = $(common_module_ldflags) mod_h2_la_LIBADD = $(common_libadd) $(XXHASH_LIBS) +lib_LTLIBRARIES += mod_flv_streaming.la +mod_flv_streaming_la_SOURCES = mod_flv_streaming.c +mod_flv_streaming_la_LDFLAGS = $(common_module_ldflags) +mod_flv_streaming_la_LIBADD = $(common_libadd) + +if BUILD_WITH_GEOIP +lib_LTLIBRARIES += mod_geoip.la +mod_geoip_la_SOURCES = mod_geoip.c +mod_geoip_la_LDFLAGS = $(common_module_ldflags) +mod_geoip_la_LIBADD = $(common_libadd) $(GEOIP_LIB) +endif + if BUILD_WITH_MAXMINDDB lib_LTLIBRARIES += mod_maxminddb.la mod_maxminddb_la_SOURCES = mod_maxminddb.c @@ -153,6 +165,21 @@ mod_magnet_la_LDFLAGS = $(common_module_ldflags) mod_magnet_la_LIBADD = $(common_libadd) $(LUA_LIBS) $(CRYPTO_LIB) -lm endif +if BUILD_WITH_LUA +lib_LTLIBRARIES += mod_cml.la +mod_cml_la_SOURCES = mod_cml.c mod_cml_lua.c mod_cml_funcs.c +mod_cml_la_CFLAGS = $(AM_CFLAGS) $(LUA_CFLAGS) +mod_cml_la_LDFLAGS = $(common_module_ldflags) +mod_cml_la_LIBADD = $(MEMCACHED_LIB) $(common_libadd) $(LUA_LIBS) $(CRYPTO_LIB) -lm +endif + +if BUILD_MOD_TRIGGER_B4_DL +lib_LTLIBRARIES += mod_trigger_b4_dl.la +mod_trigger_b4_dl_la_SOURCES = mod_trigger_b4_dl.c +mod_trigger_b4_dl_la_LDFLAGS = $(common_module_ldflags) +mod_trigger_b4_dl_la_LIBADD = $(GDBM_LIB) $(MEMCACHED_LIB) $(common_libadd) +endif + lib_LTLIBRARIES += mod_vhostdb.la mod_vhostdb_la_SOURCES = mod_vhostdb.c if !NO_RDYNAMIC @@ -168,6 +195,14 @@ mod_vhostdb_ldap_la_LDFLAGS = $(common_module_ldflags) mod_vhostdb_ldap_la_LIBADD = $(LDAP_LIB) $(LBER_LIB) $(common_libadd) endif +if BUILD_WITH_MYSQL +lib_LTLIBRARIES += mod_mysql_vhost.la +mod_mysql_vhost_la_SOURCES = mod_mysql_vhost.c +mod_mysql_vhost_la_LDFLAGS = $(common_module_ldflags) +mod_mysql_vhost_la_LIBADD = $(MYSQL_LIBS) $(common_libadd) +mod_mysql_vhost_la_CPPFLAGS = $(MYSQL_CFLAGS) +endif + if BUILD_WITH_MYSQL lib_LTLIBRARIES += mod_vhostdb_mysql.la mod_vhostdb_mysql_la_SOURCES = mod_vhostdb_mysql.c @@ -294,6 +329,14 @@ mod_authn_pam_la_LDFLAGS = $(common_module_ldflags) mod_authn_pam_la_LIBADD = $(PAM_LIB) $(common_libadd) endif +if BUILD_WITH_MYSQL +lib_LTLIBRARIES += mod_authn_mysql.la +mod_authn_mysql_la_SOURCES = mod_authn_mysql.c +mod_authn_mysql_la_LDFLAGS = $(common_module_ldflags) +mod_authn_mysql_la_LIBADD = $(CRYPT_LIB) $(MYSQL_LIBS) $(CRYPTO_LIB) $(common_libadd) +mod_authn_mysql_la_CPPFLAGS = $(MYSQL_CFLAGS) +endif + if BUILD_WITH_SASL lib_LTLIBRARIES += mod_authn_sasl.la mod_authn_sasl_la_SOURCES = mod_authn_sasl.c @@ -385,6 +428,7 @@ hdr = base64.h buffer.h burl.h network.h log.h http_kv.h keyvalue.h \ sys-socket.h sys-stat.h sys-strings.h \ sys-time.h sys-unistd.h sys-wait.h \ sock_addr.h \ + mod_cml.h mod_cml_funcs.h \ mod_auth_api.h \ mod_magnet_cache.h \ mod_vhostdb_api.h \ @@ -419,6 +463,7 @@ lighttpd_SOURCES = \ mod_expire.c \ mod_extforward.c \ mod_fastcgi.c \ + mod_flv_streaming.c \ mod_indexfile.c \ mod_proxy.c \ mod_redirect.c \ @@ -453,12 +498,18 @@ lighttpd_LDFLAGS = -export-dynamic lighttpd_SOURCES += h2.c ls-hpack/lshpack.c algo_xxhash.c lighttpd_LDADD += $(XXHASH_LIBS) +if BUILD_WITH_GEOIP +lighttpd_SOURCES += mod_geoip.c +lighttpd_LDADD += $(GEOIP_LIB) +endif if BUILD_WITH_MAXMINDDB lighttpd_SOURCES += mod_maxminddb.c lighttpd_LDADD += $(MAXMINDDB_LIB) endif if BUILD_WITH_LUA -lighttpd_SOURCES += mod_magnet.c mod_magnet_cache.c algo_hmac.c +lighttpd_SOURCES += mod_cml.c mod_cml_lua.c mod_cml_funcs.c \ + mod_magnet.c mod_magnet_cache.c + #algo_hmac.c lighttpd_CPPFLAGS += $(LUA_CFLAGS) lighttpd_LDADD += $(LUA_LIBS) -lm endif @@ -475,7 +526,7 @@ lighttpd_SOURCES += mod_authn_pam.c lighttpd_LDADD += $(PAM_LIB) endif if BUILD_WITH_MYSQL -lighttpd_SOURCES += mod_vhostdb_mysql.c +lighttpd_SOURCES += mod_authn_mysql.c mod_mysql_vhost.c mod_vhostdb_mysql.c lighttpd_CPPFLAGS += $(MYSQL_CFLAGS) lighttpd_LDADD += $(MYSQL_LIBS) endif @@ -513,6 +564,16 @@ lighttpd_SOURCES += mod_wolfssl.c lighttpd_CPPFLAGS += $(WOLFSSL_CFLAGS) lighttpd_LDADD += $(WOLFSSL_LIBS) endif +if BUILD_WITH_MEMCACHED +lighttpd_CPPFLAGS += $(MEMCACHED_CFLAGS) +lighttpd_LDADD += $(MEMCACHED_LIB) +endif +if BUILD_WITH_GDBM +lighttpd_LDADD += $(GDBM_LIB) +endif +if BUILD_MOD_TRIGGER_B4_DL +lighttpd_SOURCES += mod_trigger_b4_dl.c +endif else diff --git a/src/SConscript b/src/SConscript index 6c7edfc9..8add2698 100644 --- a/src/SConscript +++ b/src/SConscript @@ -119,6 +119,7 @@ modules = { 'mod_evasive' : { 'src' : [ 'mod_evasive.c' ] }, 'mod_extforward' : { 'src' : [ 'mod_extforward.c' ] }, 'mod_h2' : { 'src' : [ 'h2.c', 'ls-hpack/lshpack.c', 'algo_xxhash.c' ], 'lib' : [ env['LIBXXHASH'] ] }, + 'mod_flv_streaming' : { 'src' : [ 'mod_flv_streaming.c' ] }, 'mod_proxy' : { 'src' : [ 'mod_proxy.c' ] }, 'mod_rrdtool' : { 'src' : [ 'mod_rrdtool.c' ] }, 'mod_secdownload' : { 'src' : [ 'mod_secdownload.c', 'algo_hmac.c' ], 'lib' : [ env['LIBCRYPTO'] ] }, @@ -133,6 +134,9 @@ modules = { 'mod_wstunnel' : { 'src' : [ 'mod_wstunnel.c' ], 'lib' : [ env['LIBCRYPTO'] ] }, } +if env['with_geoip']: + modules['mod_geoip'] = { 'src' : [ 'mod_geoip.c' ], 'lib' : [ env['LIBGEOIP'] ] } + if env['with_maxminddb']: modules['mod_maxminddb'] = { 'src' : [ 'mod_maxminddb.c' ], 'lib' : [ env['LIBMAXMINDDB'] ] } @@ -148,11 +152,20 @@ if env['with_lua']: 'src' : [ 'mod_magnet.c', 'mod_magnet_cache.c', 'algo_hmac.c' ], 'lib' : [ env['LIBLUA'], env['LIBCRYPTO'] ] } + modules['mod_cml'] = { + 'src' : [ 'mod_cml_lua.c', 'mod_cml.c', 'mod_cml_funcs.c' ], + 'lib' : [ env['LIBMEMCACHED'], env['LIBLUA'], env['LIBCRYPTO'] ] + } if env['with_pam']: modules['mod_authn_pam'] = { 'src' : [ 'mod_authn_pam.c' ], 'lib' : [ env['LIBPAM'] ] } +if env['with_pcre'] and (env['with_memcached'] or env['with_gdbm']): + modules['mod_trigger_b4_dl'] = { 'src' : [ 'mod_trigger_b4_dl.c' ], 'lib' : [ env['LIBMEMCACHED'], env['LIBGDBM'] ] } + if env['with_mysql']: + modules['mod_authn_mysql'] = { 'src' : [ 'mod_authn_mysql.c' ], 'lib' : [ env['LIBCRYPT'], env['LIBMYSQL'], env['LIBCRYPTO'] ] } + modules['mod_mysql_vhost'] = { 'src' : [ 'mod_mysql_vhost.c' ], 'lib' : [ env['LIBMYSQL'] ] } modules['mod_vhostdb_mysql'] = { 'src' : [ 'mod_vhostdb_mysql.c' ], 'lib' : [ env['LIBMYSQL'] ] } if env['with_pgsql']: diff --git a/src/config.h.cmake b/src/config.h.cmake index 4bb24138..1eb399ba 100644 --- a/src/config.h.cmake +++ b/src/config.h.cmake @@ -138,6 +138,13 @@ /* lua */ #cmakedefine HAVE_LUA_H +/* gdbm */ +#cmakedefine HAVE_GDBM_H +#cmakedefine HAVE_GDBM + +/* memcache */ +#cmakedefine USE_MEMCACHED + /* inotify */ #cmakedefine HAVE_INOTIFY_INIT #cmakedefine HAVE_SYS_INOTIFY_H diff --git a/src/configfile.c b/src/configfile.c index e7d34841..7a789b73 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -398,6 +398,7 @@ static void config_compat_module_load (server *srv) { int append_mod_staticfile = 1; int append_mod_authn_file = 1; int append_mod_authn_ldap = 1; + int append_mod_authn_mysql = 1; int append_mod_openssl = 1; int contains_mod_auth = 0; int prepend_mod_auth = 0; @@ -440,6 +441,8 @@ static void config_compat_module_load (server *srv) { append_mod_authn_file = 0; else if (buffer_eq_slen(m, CONST_STR_LEN("mod_authn_ldap"))) append_mod_authn_ldap = 0; + else if (buffer_eq_slen(m, CONST_STR_LEN("mod_authn_mysql"))) + append_mod_authn_mysql = 0; } else if (0 == strncmp(m->ptr, "mod_vhostdb", sizeof("mod_vhostdb")-1)) { if (buffer_eq_slen(m, CONST_STR_LEN("mod_vhostdb"))) @@ -532,6 +535,13 @@ static void config_compat_module_load (server *srv) { config_warn_authn_module(srv, CONST_STR_LEN("ldap"), "ldap"); #endif } + if (append_mod_authn_mysql) { + #if defined(HAVE_MYSQL) + if (config_has_opt_and_value(srv, CONST_STR_LEN("auth.backend"), + CONST_STR_LEN("mysql"))) + config_warn_authn_module(srv, CONST_STR_LEN("mysql"), "mysql"); + #endif + } } if (prepend_mod_auth) { diff --git a/src/meson.build b/src/meson.build index 841d17e6..078bd262 100644 --- a/src/meson.build +++ b/src/meson.build @@ -271,6 +271,42 @@ conf_data.set('HAVE_LIBDEFLATE', libdeflate.found()) libmaxminddb = dependency('libmaxminddb', required: get_option('with_maxminddb')) +libgeoip = [] +if get_option('with_geoip') + libgeoip = dependency('geoip', required: false) + if libgeoip.found() + libgeoip = [ libgeoip ] + else + libgeoip = [ compiler.find_library('GeoIP') ] + if not(compiler.has_function('GeoIP_country_name_by_addr', args: defs, dependencies: libgeoip)) + error('Couldn\'t find GeoIP_country_name_by_addr in lib GeoIP') + endif + endif +endif + +libgdbm = [] +if get_option('with_gdbm') + libgdbm = [ compiler.find_library('gdbm') ] + if not(compiler.has_function('gdbm_open', args: defs, dependencies: libgdbm, prefix: '#include ')) + error('Couldn\'t find gdbm.h or gdbm_open in lib gdbm') + endif + conf_data.set('HAVE_GDBM_H', true) + conf_data.set('HAVE_GDBM', true) +endif + +libkrb5 = [] +libgssapi_krb5 = [] +if get_option('with_krb5') + libkrb5 = dependency('krb5', required: false) + if libkrb5.found() + libkrb5 = [ libkrb5 ] + else + libkrb5 = [ compiler.find_library('krb5') ] + if not(compiler.has_function('krb5_init_context', args: defs, dependencies: libkrb5)) + error('Couldn\'t find krb5_init_context in lib krb5') + endif + endif + libkrb5 = dependency('krb5', required: get_option('with_krb5')) libgssapi_krb5 = dependency('krb5-gssapi', required: get_option('with_krb5')) conf_data.set('HAVE_KRB5', libkrb5.found() and libgssapi_krb5.found()) @@ -320,6 +356,15 @@ if get_option('with_lua') conf_data.set('HAVE_LUA_H', true) endif +libmemcached = [] +if get_option('with_memcached') + # manual search: + # header: libmemcached/memcached.h + # function: memcached (-lmemcached) + libmemcached = [ dependency('libmemcached') ] + conf_data.set('USE_MEMCACHED', true) +endif + libmysqlclient = dependency('libmariadb', required: get_option('with_mysql')) conf_data.set('HAVE_MYSQL', libmysqlclient.found()) @@ -771,6 +816,8 @@ modules = [ [ 'mod_extforward', [ 'mod_extforward.c' ] ], [ 'mod_h2', [ 'h2.c', 'ls-hpack/lshpack.c', 'algo_xxhash.c' ], [ libxxhash ] ], [ 'mod_proxy', [ 'mod_proxy.c' ], socket_libs ], + [ 'mod_flv_streaming', [ 'mod_flv_streaming.c' ] ], + [ 'mod_proxy', [ 'mod_proxy.c' ], libws2_32 ], [ 'mod_rrdtool', [ 'mod_rrdtool.c' ] ], [ 'mod_secdownload', [ 'mod_secdownload.c', 'algo_hmac.c' ], libcrypto ], [ 'mod_sockproxy', [ 'mod_sockproxy.c' ] ], @@ -785,8 +832,15 @@ modules = [ ] endif +if get_option('with_pcre') and (get_option('with_memcached') or get_option('with_gdbm')) + modules += [ + [ 'mod_trigger_b4_dl', [ 'mod_trigger_b4_dl.c' ], libmemcached + libgdbm ], + ] +endif + if get_option('with_lua') modules += [ + [ 'mod_cml', [ 'mod_cml.c', 'mod_cml_lua.c', 'mod_cml_funcs.c' ], liblua + libmemcached + libcrypto ], [ 'mod_magnet', [ 'mod_magnet.c', 'mod_magnet_cache.c', 'algo_hmac.c' ], liblua + libcrypto ], ] endif @@ -797,8 +851,16 @@ if libmaxminddb.found() ] endif +if get_option('with_geoip') + modules += [ + [ 'mod_geoip', [ 'mod_geoip.c' ], libgeoip ], + ] +endif + if libmysqlclient.found() modules += [ + [ 'mod_authn_mysql', [ 'mod_authn_mysql.c' ], libcrypt + libmysqlclient + libcrypto ], + [ 'mod_mysql_vhost', [ 'mod_mysql_vhost.c' ], libmysqlclient ], [ 'mod_vhostdb_mysql', [ 'mod_vhostdb_mysql.c' ], libmysqlclient ], ] endif diff --git a/src/mod_authn_mysql.c b/src/mod_authn_mysql.c new file mode 100644 index 00000000..a061250a --- /dev/null +++ b/src/mod_authn_mysql.c @@ -0,0 +1,501 @@ +/* mod_authn_mysql + * + * KNOWN LIMITATIONS: + * - no mechanism provided to configure SSL connection to a remote MySQL db + * + * FUTURE POTENTIAL PERFORMANCE ENHANCEMENTS: + * - database response is not cached + * TODO: db response caching (for limited time) to reduce load on db + * (only cache successful logins to prevent cache bloat?) + * (or limit number of entries (size) of cache) + * (maybe have negative cache (limited size) of names not found in database) + * - database query is synchronous and blocks waiting for response + * TODO: https://mariadb.com/kb/en/mariadb/using-the-non-blocking-library/ + * - opens and closes connection to MySQL db for each request (inefficient) + * (fixed) one-element cache for persistent connection open to last used db + * TODO: db connection pool (if asynchronous requests) + */ +#include "first.h" + +#if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT) +#ifndef _XOPEN_CRYPT +#define _XOPEN_CRYPT 1 +#endif +#include /* crypt() */ +#endif +#ifdef HAVE_CRYPT_H +#include +#endif + +#include +#include + +#include + +#include "mod_auth_api.h" +#include "sys-crypto-md.h" + +#include "base.h" +#include "ck.h" +#include "log.h" +#include "plugin.h" + +typedef struct { + int auth_mysql_port; + const char *auth_mysql_host; + const char *auth_mysql_user; + const char *auth_mysql_pass; + const char *auth_mysql_db; + const char *auth_mysql_socket; + const char *auth_mysql_users_table; + const char *auth_mysql_col_user; + const char *auth_mysql_col_pass; + const char *auth_mysql_col_realm; + log_error_st *errh; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_config defaults; + plugin_config conf; + + MYSQL *mysql_conn; + const char *mysql_conn_host; + const char *mysql_conn_user; + const char *mysql_conn_pass; + const char *mysql_conn_db; + int mysql_conn_port; +} plugin_data; + +static void mod_authn_mysql_sock_close(void *p_d) { + plugin_data * const p = p_d; + if (NULL != p->mysql_conn) { + mysql_close(p->mysql_conn); + p->mysql_conn = NULL; + } +} + +static MYSQL * mod_authn_mysql_sock_connect(plugin_data *p) { + plugin_config * const pconf = &p->conf; + if (NULL != p->mysql_conn) { + /* reuse open db connection if same ptrs to host user pass db port */ + if ( p->mysql_conn_host == pconf->auth_mysql_host + && p->mysql_conn_user == pconf->auth_mysql_user + && p->mysql_conn_pass == pconf->auth_mysql_pass + && p->mysql_conn_db == pconf->auth_mysql_db + && p->mysql_conn_port == pconf->auth_mysql_port) { + return p->mysql_conn; + } + mod_authn_mysql_sock_close(p); + } + + /* !! mysql_init() is not thread safe !! (see MySQL doc) */ + p->mysql_conn = mysql_init(NULL); + if (mysql_real_connect(p->mysql_conn, + pconf->auth_mysql_host, + pconf->auth_mysql_user, + pconf->auth_mysql_pass, + pconf->auth_mysql_db, + pconf->auth_mysql_port, + (pconf->auth_mysql_socket && *pconf->auth_mysql_socket) + ? pconf->auth_mysql_socket + : NULL, + CLIENT_IGNORE_SIGPIPE)) { + /* (copy ptrs to plugin data (has lifetime until server shutdown)) */ + p->mysql_conn_host = pconf->auth_mysql_host; + p->mysql_conn_user = pconf->auth_mysql_user; + p->mysql_conn_pass = pconf->auth_mysql_pass; + p->mysql_conn_db = pconf->auth_mysql_db; + p->mysql_conn_port = pconf->auth_mysql_port; + return p->mysql_conn; + } + else { + /*(note: any of these params might be NULL)*/ + log_error(pconf->errh, __FILE__, __LINE__, + "opening connection to mysql: %s user: %s db: %s failed: %s", + pconf->auth_mysql_host ? pconf->auth_mysql_host : "", + pconf->auth_mysql_user ? pconf->auth_mysql_user : "", + /*"pass:",*//*(omit pass from logs)*/ + /*p->conf.auth_mysql_pass ? p->conf.auth_mysql_pass : "",*/ + pconf->auth_mysql_db ? pconf->auth_mysql_db : "", + mysql_error(p->mysql_conn)); + mod_authn_mysql_sock_close(p); + return NULL; + } +} + +static MYSQL * mod_authn_mysql_sock_acquire(plugin_data *p) { + return mod_authn_mysql_sock_connect(p); +} + +static void mod_authn_mysql_sock_release(plugin_data *p) { + UNUSED(p); + /*(empty; leave db connection open)*/ + /* Note: mod_authn_mysql_result() calls mod_authn_mysql_sock_error() + * on error, so take that into account if making changes here. + * Must check if (NULL == p->mysql_conn) */ +} + +__attribute_cold__ +static void mod_authn_mysql_sock_error(plugin_data *p) { + mod_authn_mysql_sock_close(p); +} + +static handler_t mod_authn_mysql_basic(request_st *r, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw); +static handler_t mod_authn_mysql_digest(request_st *r, void *p_d, http_auth_info_t *dig); + +INIT_FUNC(mod_authn_mysql_init) { + static http_auth_backend_t http_auth_backend_mysql = + { "mysql", mod_authn_mysql_basic, mod_authn_mysql_digest, NULL }; + plugin_data *p = calloc(1, sizeof(*p)); + + /* register http_auth_backend_mysql */ + http_auth_backend_mysql.p_d = p; + http_auth_backend_set(&http_auth_backend_mysql); + + return p; +} + +static void mod_authn_mysql_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { + switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ + case 0: /* auth.backend.mysql.host */ + pconf->auth_mysql_host = cpv->v.b->ptr; + break; + case 1: /* auth.backend.mysql.user */ + pconf->auth_mysql_user = cpv->v.b->ptr; + break; + case 2: /* auth.backend.mysql.pass */ + pconf->auth_mysql_pass = cpv->v.b->ptr; + break; + case 3: /* auth.backend.mysql.db */ + pconf->auth_mysql_db = cpv->v.b->ptr; + break; + case 4: /* auth.backend.mysql.port */ + pconf->auth_mysql_port = (int)cpv->v.shrt; + break; + case 5: /* auth.backend.mysql.socket */ + pconf->auth_mysql_socket = cpv->v.b->ptr; + break; + case 6: /* auth.backend.mysql.users_table */ + pconf->auth_mysql_users_table = cpv->v.b->ptr; + break; + case 7: /* auth.backend.mysql.col_user */ + pconf->auth_mysql_col_user = cpv->v.b->ptr; + break; + case 8: /* auth.backend.mysql.col_pass */ + pconf->auth_mysql_col_pass = cpv->v.b->ptr; + break; + case 9: /* auth.backend.mysql.col_realm */ + pconf->auth_mysql_col_realm = cpv->v.b->ptr; + break; + default:/* should not happen */ + return; + } +} + +static void mod_authn_mysql_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { + do { + mod_authn_mysql_merge_config_cpv(pconf, cpv); + } while ((++cpv)->k_id != -1); +} + +static void mod_authn_mysql_patch_config(request_st * const r, plugin_data * const p) { + memcpy(&p->conf, &p->defaults, sizeof(plugin_config)); + for (int i = 1, used = p->nconfig; i < used; ++i) { + if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) + mod_authn_mysql_merge_config(&p->conf, + p->cvlist + p->cvlist[i].v.u2[0]); + } +} + +SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) { + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("auth.backend.mysql.host"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.user"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.pass"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.db"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.port"), + T_CONFIG_SHORT, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.socket"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.users_table"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.col_user"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.col_pass"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("auth.backend.mysql.col_realm"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + + plugin_data * const p = p_d; + if (!config_plugin_values_init(srv, p, cpk, "mod_authn_mysql")) + return HANDLER_ERROR; + + /* process and validate config directives + * (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* auth.backend.mysql.host */ + case 1: /* auth.backend.mysql.user */ + case 2: /* auth.backend.mysql.pass */ + case 3: /* auth.backend.mysql.db */ + case 4: /* auth.backend.mysql.port */ + case 5: /* auth.backend.mysql.socket */ + case 6: /* auth.backend.mysql.users_table */ + break; + case 7: /* auth.backend.mysql.col_user */ + case 8: /* auth.backend.mysql.col_pass */ + case 9: /* auth.backend.mysql.col_realm */ + if (buffer_is_blank(cpv->v.b)) { + log_error(srv->errh, __FILE__, __LINE__, + "%s must not be blank", cpk[cpv->k_id].k); + return HANDLER_ERROR; + } + break; + default:/* should not happen */ + break; + } + } + } + + p->defaults.auth_mysql_col_user = "user"; + p->defaults.auth_mysql_col_pass = "password"; + p->defaults.auth_mysql_col_realm = "realm"; + p->defaults.errh = srv->errh; + + /* initialize p->defaults from global config context */ + if (p->nconfig > 0 && p->cvlist->v.u2[1]) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; + if (-1 != cpv->k_id) + mod_authn_mysql_merge_config(&p->defaults, cpv); + } + + log_error(srv->errh, __FILE__, __LINE__, + "Warning: mod_%s is deprecated " + "and will be removed from a future lighttpd release in early 2022. " + "https://wiki.lighttpd.net/Docs_ModAuth#mysql-mod_authn_mysql-since-lighttpd-1442", + p->self->name); + + return HANDLER_GO_ON; +} + +static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) { + #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT) + if (userpwlen >= 3 && userpw[0] == '$') { + char *crypted = crypt(reqpw, userpw); + size_t crypwlen = (NULL != crypted) ? strlen(crypted) : 0; + int rc = (crypwlen == userpwlen) ? memcmp(crypted, userpw, crypwlen) : -1; + if (crypwlen >= 13) ck_memzero(crypted, crypwlen); + return rc; + } + else + #endif + if (32 == userpwlen) { + /* plain md5 */ + unsigned char HA1[MD5_DIGEST_LENGTH]; + unsigned char md5pw[MD5_DIGEST_LENGTH]; + MD5_once(HA1, reqpw, strlen(reqpw)); + + /*(compare 16-byte MD5 binary instead of converting to hex strings + * in order to then have to do case-insensitive hex str comparison)*/ + return (0 == li_hex2bin(md5pw, sizeof(md5pw), userpw, 32)) + ? ck_memeq_const_time_fixed_len(HA1, md5pw, sizeof(md5pw)) ? 0 : 1 + : -1; + } + + return -1; +} + +static int mod_authn_mysql_result(plugin_data *p, http_auth_info_t *ai, const char *pw) { + MYSQL_RES *result = mysql_store_result(p->mysql_conn); + int rc = -1; + my_ulonglong num_rows; + + if (NULL == result) { + /*(future: might log mysql_error() string)*/ + #if 0 + log_error(errh, __FILE__, __LINE__, + "mysql_store_result: %s", mysql_error(p->mysql_conn)); + #endif + mod_authn_mysql_sock_error(p); + return -1; + } + + num_rows = mysql_num_rows(result); + if (1 == num_rows) { + MYSQL_ROW row = mysql_fetch_row(result); + unsigned long *lengths = mysql_fetch_lengths(result); + if (NULL == lengths) { + /*(error; should not happen)*/ + } + else if (pw) { /* used with HTTP Basic auth */ + rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw); + } + else { /* used with HTTP Digest auth */ + /*(currently supports only single row, single digest algorithm)*/ + if (lengths[0] == (ai->dlen << 1)) { + rc = li_hex2bin(ai->digest, sizeof(ai->digest), + row[0], lengths[0]); + } + } + } + else if (0 == num_rows) { + /* user,realm not found */ + } + else { + /* (multiple rows returned, which should not happen) */ + /* (future: might log if multiple rows returned; unexpected result) */ + } + mysql_free_result(result); + return rc; +} + +static handler_t mod_authn_mysql_query(request_st * const r, void *p_d, http_auth_info_t * const ai, const char * const pw) { + plugin_data *p = (plugin_data *)p_d; + int rc = -1; + + mod_authn_mysql_patch_config(r, p); + p->conf.errh = r->conf.errh; + + if (NULL == p->conf.auth_mysql_users_table) { + /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/ + log_error(r->conf.errh, __FILE__, __LINE__, + "auth config missing auth.backend.mysql.users_table for uri: %s", + r->target.ptr); + return HANDLER_ERROR; + } + + do { + char uname[512], urealm[512]; + unsigned long mrc; + + if (ai->ulen > sizeof(uname)/2-1) + return HANDLER_ERROR; + if (ai->rlen > sizeof(urealm)/2-1) + return HANDLER_ERROR; + + if (!mod_authn_mysql_sock_acquire(p)) { + return HANDLER_ERROR; + } + + #if 0 + mrc = mysql_real_escape_string_quote(p->mysql_conn, uname, + ai->username, ai->ulen, '\''); + if ((unsigned long)~0 == mrc) break; + + mrc = mysql_real_escape_string_quote(p->mysql_conn, urealm, + ai->realm, ai->rlen, '\''); + if ((unsigned long)~0 == mrc) break; + #else + mrc = mysql_real_escape_string(p->mysql_conn, uname, + ai->username, ai->ulen); + if ((unsigned long)~0 == mrc) break; + + mrc = mysql_real_escape_string(p->mysql_conn, urealm, + ai->realm, ai->rlen); + if ((unsigned long)~0 == mrc) break; + #endif + + buffer * const tb = r->tmp_buf; + buffer_clear(tb); + struct const_iovec iov[] = { + { CONST_STR_LEN("SELECT ") } + ,{ p->conf.auth_mysql_col_pass, strlen(p->conf.auth_mysql_col_pass) } + ,{ CONST_STR_LEN(" FROM ") } + ,{ p->conf.auth_mysql_users_table, strlen(p->conf.auth_mysql_users_table) } + ,{ CONST_STR_LEN(" WHERE ") } + ,{ p->conf.auth_mysql_col_user, strlen(p->conf.auth_mysql_col_user) } + ,{ CONST_STR_LEN("='") } + ,{ uname, strlen(uname) } + ,{ CONST_STR_LEN("' AND ") } + ,{ p->conf.auth_mysql_col_realm, strlen(p->conf.auth_mysql_col_realm) } + ,{ CONST_STR_LEN("='") } + ,{ urealm, strlen(urealm) } + ,{ CONST_STR_LEN("'") } + }; + buffer_append_iovec(tb, iov, sizeof(iov)/sizeof(*iov)); + char * const q = tb->ptr; + + /* for now we stay synchronous */ + if (0 != mysql_query(p->mysql_conn, q)) { + /* reconnect to db and retry once if query error occurs */ + mod_authn_mysql_sock_error(p); + if (!mod_authn_mysql_sock_acquire(p)) { + rc = -1; + break; + } + if (0 != mysql_query(p->mysql_conn, q)) { + /*(note: any of these params might be bufs w/ b->ptr == NULL)*/ + log_error(r->conf.errh, __FILE__, __LINE__, + "mysql_query host: %s user: %s db: %s query: %s failed: %s", + p->conf.auth_mysql_host ? p->conf.auth_mysql_host : "", + p->conf.auth_mysql_user ? p->conf.auth_mysql_user : "", + /*"pass:",*//*(omit pass from logs)*/ + /*p->conf.auth_mysql_pass ? p->conf.auth_mysql_pass : "",*/ + p->conf.auth_mysql_db ? p->conf.auth_mysql_db : "", + q, mysql_error(p->mysql_conn)); + rc = -1; + break; + } + } + + rc = mod_authn_mysql_result(p, ai, pw); + + } while (0); + + mod_authn_mysql_sock_release(p); + + return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR; +} + +static handler_t mod_authn_mysql_basic(request_st * const r, void *p_d, const http_auth_require_t * const require, const buffer * const username, const char * const pw) { + handler_t rc; + http_auth_info_t ai; + ai.dalgo = HTTP_AUTH_DIGEST_NONE; + ai.dlen = 0; + ai.username = username->ptr; + ai.ulen = buffer_clen(username); + ai.realm = require->realm->ptr; + ai.rlen = buffer_clen(require->realm); + ai.userhash = 0; + rc = mod_authn_mysql_query(r, p_d, &ai, pw); + if (HANDLER_GO_ON != rc) return rc; + return http_auth_match_rules(require, username->ptr, NULL, NULL) + ? HANDLER_GO_ON /* access granted */ + : HANDLER_ERROR; +} + +static handler_t mod_authn_mysql_digest(request_st * const r, void *p_d, http_auth_info_t * const ai) { + return mod_authn_mysql_query(r, p_d, ai, NULL); +} + +int mod_authn_mysql_plugin_init(plugin *p); +int mod_authn_mysql_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = "authn_mysql"; + p->init = mod_authn_mysql_init; + p->set_defaults= mod_authn_mysql_set_defaults; + p->cleanup = mod_authn_mysql_sock_close; + + return 0; +} diff --git a/src/mod_cml.c b/src/mod_cml.c new file mode 100644 index 00000000..c0016971 --- /dev/null +++ b/src/mod_cml.c @@ -0,0 +1,323 @@ +#include "first.h" + +#include "mod_cml.h" + +#include "base.h" +#include "buffer.h" +#include "log.h" +#include "plugin.h" + +#include + +#include +#include + +INIT_FUNC(mod_cml_init) { + return calloc(1, sizeof(plugin_data)); +} + +FREE_FUNC(mod_cml_free) { + plugin_data * const p = p_d; + free(p->trigger_handler.ptr); + free(p->basedir.ptr); + free(p->baseurl.ptr); + if (NULL == p->cvlist) return; + #if defined(USE_MEMCACHED) + /* (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 1: /* cml.memcache-hosts */ + if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) + memcached_free(cpv->v.v); /* mod_cml_free_memcached() */ + break; + default: + break; + } + } + } + #endif +} + +static int mod_cml_init_memcached(server *srv, config_plugin_value_t * const cpv) { + const array * const mc_hosts = cpv->v.a; + if (0 == mc_hosts->used) { + cpv->v.v = NULL; + return 1; + } + + #if defined(USE_MEMCACHED) + + buffer * const opts = srv->tmp_buf; + buffer_clear(opts); + for (uint32_t k = 0; k < mc_hosts->used; ++k) { + const data_string * const ds = (const data_string *)mc_hosts->data[k]; + buffer_append_string_len(opts, CONST_STR_LEN(" --SERVER=")); + buffer_append_string_buffer(opts, &ds->value); + } + + cpv->v.v = memcached(opts->ptr+1, buffer_clen(opts)-1); + + if (cpv->v.v) { + cpv->vtype = T_CONFIG_LOCAL; + return 1; + } + else { + log_error(srv->errh, __FILE__, __LINE__, + "configuring memcached failed for option string: %s", opts->ptr); + return 0; + } + + #else + + log_error(srv->errh, __FILE__, __LINE__, + "memcache support is not compiled in but cml.memcache-hosts is set, " + "aborting"); + return 0; + + #endif +} + +static void mod_cml_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { + switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ + case 0: /* cml.extension */ + pconf->ext = cpv->v.b; + break; + case 1: /* cml.memcache-hosts *//* setdefaults inits memcached_st *memc */ + #if defined(USE_MEMCACHED) + if (cpv->vtype != T_CONFIG_LOCAL) break; + pconf->memc = cpv->v.v; + #endif + break; + case 2: /* cml.memcache-namespace */ + /*pconf->mc_namespace = cpv->v.b;*//*(unused)*/ + break; + case 3: /* cml.power-magnet */ + pconf->power_magnet = cpv->v.b; + break; + default:/* should not happen */ + return; + } +} + +static void mod_cml_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { + do { + mod_cml_merge_config_cpv(pconf, cpv); + } while ((++cpv)->k_id != -1); +} + +static void mod_cml_patch_config(request_st * const r, plugin_data * const p) { + memcpy(&p->conf, &p->defaults, sizeof(plugin_config)); + for (int i = 1, used = p->nconfig; i < used; ++i) { + if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) + mod_cml_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]); + } +} + +SETDEFAULTS_FUNC(mod_cml_set_defaults) { + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("cml.extension"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("cml.memcache-hosts"), + T_CONFIG_ARRAY_VLIST, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("cml.memcache-namespace"), /*(unused)*/ + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("cml.power-magnet"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + + plugin_data * const p = p_d; + if (!config_plugin_values_init(srv, p, cpk, "mod_cml")) + return HANDLER_ERROR; + + /* process and validate config directives + * (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* cml.extension */ + if (buffer_is_blank(cpv->v.b)) + cpv->v.b = NULL; + break; + case 1: /* cml.memcache-hosts */ /* config converted to memc handles */ + if (!mod_cml_init_memcached(srv, cpv)) { + return HANDLER_ERROR; + } + break; + case 2: /* cml.memcache-namespace *//*(unused)*/ + break; + case 3: /* cml.power-magnet */ + if (buffer_is_blank(cpv->v.b)) + cpv->v.b = NULL; + break; + default:/* should not happen */ + break; + } + } + } + + /* initialize p->defaults from global config context */ + if (p->nconfig > 0 && p->cvlist->v.u2[1]) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; + if (-1 != cpv->k_id) + mod_cml_merge_config(&p->defaults, cpv); + } + + log_error(srv->errh, __FILE__, __LINE__, + "Warning: mod_%s is deprecated " + "and will be removed from a future lighttpd release in early 2022. " + "https://wiki.lighttpd.net/Docs_ConfigurationOptions#Deprecated", + p->self->name); + + return HANDLER_GO_ON; +} + +static int cache_call_lua(request_st * const r, plugin_data * const p, const buffer * const cml_file) { + buffer *b; + char *c; + + /* cleanup basedir */ + b = &p->baseurl; + buffer_copy_buffer(b, &r->uri.path); + for (c = b->ptr + buffer_clen(b); c > b->ptr && *c != '/'; c--); + + if (*c == '/') { + buffer_truncate(b, c - b->ptr + 1); + } + + b = &p->basedir; + buffer_copy_buffer(b, &r->physical.path); + for (c = b->ptr + buffer_clen(b); c > b->ptr && *c != '/'; c--); + + if (*c == '/') { + buffer_truncate(b, c - b->ptr + 1); + } + + + /* prepare variables + * - cookie-based + * - get-param-based + */ + return cache_parse_lua(r, p, cml_file); +} + +URIHANDLER_FUNC(mod_cml_power_magnet) { + plugin_data *p = p_d; + + mod_cml_patch_config(r, p); + + if (!p->conf.power_magnet) return HANDLER_GO_ON; + + buffer_clear(&p->basedir); + buffer_clear(&p->baseurl); + buffer_clear(&p->trigger_handler); + + /* + * power-magnet: + * cml.power-magnet = server.docroot + "/rewrite.cml" + * + * is called on EACH request, take the original REQUEST_URI and modifies the + * request header as necessary. + * + * First use: + * if file_exists("/maintenance.html") { + * output_include = ( "/maintenance.html" ) + * return CACHE_HIT + * } + * + * as we only want to rewrite HTML like requests we should cover it in a conditional + * + * */ + + switch(cache_call_lua(r, p, p->conf.power_magnet)) { + case -1: + /* error */ + if (r->conf.log_request_handling) { + log_error(r->conf.errh, __FILE__, __LINE__, "cache-error"); + } + r->http_status = 500; + return HANDLER_COMEBACK; + case 0: + if (r->conf.log_request_handling) { + log_error(r->conf.errh, __FILE__, __LINE__, "cache-hit"); + } + /* cache-hit */ + return HANDLER_FINISHED; + case 1: + /* cache miss */ + return HANDLER_GO_ON; + default: + r->http_status = 500; + return HANDLER_COMEBACK; + } +} + +URIHANDLER_FUNC(mod_cml_is_handled) { + plugin_data *p = p_d; + + /* r->physical.path is non-empty for handle_subrequest_start */ + /*if (buffer_is_blank(&r->physical.path)) return HANDLER_ERROR;*/ + + mod_cml_patch_config(r, p); + if (!p->conf.ext) return HANDLER_GO_ON; + + const uint32_t elen = buffer_clen(p->conf.ext); + const uint32_t plen = buffer_clen(&r->physical.path); + if (plen < elen || 0 != memcmp(r->physical.path.ptr+plen-elen, p->conf.ext->ptr, elen)) { + return HANDLER_GO_ON; + } + + buffer_clear(&p->basedir); + buffer_clear(&p->baseurl); + buffer_clear(&p->trigger_handler); + + switch(cache_call_lua(r, p, &r->physical.path)) { + case -1: + /* error */ + if (r->conf.log_request_handling) { + log_error(r->conf.errh, __FILE__, __LINE__, "cache-error"); + } + r->http_status = 500; + return HANDLER_COMEBACK; + case 0: + if (r->conf.log_request_handling) { + log_error(r->conf.errh, __FILE__, __LINE__, "cache-hit"); + } + /* cache-hit */ + return HANDLER_FINISHED; + case 1: + if (r->conf.log_request_handling) { + log_error(r->conf.errh, __FILE__, __LINE__, "cache-miss"); + } + /* cache miss */ + return HANDLER_COMEBACK; + default: + r->http_status = 500; + return HANDLER_COMEBACK; + } +} + + +int mod_cml_plugin_init(plugin *p); +int mod_cml_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = "cache"; + + p->init = mod_cml_init; + p->cleanup = mod_cml_free; + p->set_defaults = mod_cml_set_defaults; + + p->handle_subrequest_start = mod_cml_is_handled; + p->handle_physical = mod_cml_power_magnet; + + return 0; +} diff --git a/src/mod_cml.h b/src/mod_cml.h new file mode 100644 index 00000000..cdd00e91 --- /dev/null +++ b/src/mod_cml.h @@ -0,0 +1,37 @@ +#ifndef _MOD_CACHE_H_ +#define _MOD_CACHE_H_ +#include "first.h" + +#include "base_decls.h" +#include "buffer.h" + +#include "plugin.h" + +#if defined(USE_MEMCACHED) +#include +#endif + +#define plugin_data mod_cache_plugin_data + +typedef struct { + const buffer *ext; + const buffer *power_magnet; + /*const buffer *mc_namespace;*//*(unused)*/ + #if defined(USE_MEMCACHED) + memcached_st *memc; + #endif +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_config defaults; + plugin_config conf; + + buffer basedir; + buffer baseurl; + buffer trigger_handler; +} plugin_data; + +int cache_parse_lua(request_st *r, plugin_data *p, const buffer *fn); + +#endif diff --git a/src/mod_cml_funcs.c b/src/mod_cml_funcs.c new file mode 100644 index 00000000..1a964bfe --- /dev/null +++ b/src/mod_cml_funcs.c @@ -0,0 +1,260 @@ +#include "first.h" + +#include + +#include +#include +#include +#include + +#include + +#include "sys-crypto-md.h" + +#include "mod_cml_funcs.h" +#include "mod_cml.h" + +#include "buffer.h" +#include "log.h" +#include "plugin.h" + +int f_crypto_md5(lua_State *L) { + int n = lua_gettop(L); + size_t s_len; + const char *s; + + if (n != 1) { + lua_pushstring(L, "md5: expected one argument"); + lua_error(L); + } + + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "md5: argument has to be a string"); + lua_error(L); + } + + s = lua_tolstring(L, 1, &s_len); + + char HA1[MD5_DIGEST_LENGTH]; + MD5_once((unsigned char *)HA1, s, s_len); + + char hex[MD5_DIGEST_LENGTH*2+1]; + li_tohex(hex, sizeof(hex), HA1, sizeof(HA1)); + + lua_pushstring(L, hex); + + return 1; +} + + +int f_file_mtime(lua_State *L) { + struct stat st; + int n = lua_gettop(L); + + if (n != 1) { + lua_pushstring(L, "file_mtime: expected one argument"); + lua_error(L); + } + + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "file_mtime: argument has to be a string"); + lua_error(L); + } + + if (-1 == stat(lua_tostring(L, 1), &st)) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, st.st_mtime); + + return 1; +} + +static int f_dir_files_iter(lua_State *L) { + DIR *d; + struct dirent *de; + + d = lua_touserdata(L, lua_upvalueindex(1)); + + if (NULL == (de = readdir(d))) { + /* EOF */ + closedir(d); + + return 0; + } else { + lua_pushstring(L, de->d_name); + return 1; + } +} + +int f_dir_files(lua_State *L) { + DIR *d; + int n = lua_gettop(L); + + if (n != 1) { + lua_pushstring(L, "dir_files: expected one argument"); + lua_error(L); + } + + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "dir_files: argument has to be a string"); + lua_error(L); + } + + /* check if there is a valid DIR handle on the stack */ + if (NULL == (d = opendir(lua_tostring(L, 1)))) { + lua_pushnil(L); + return 1; + } + + /* push d into userdata */ + lua_pushlightuserdata(L, d); + lua_pushcclosure(L, f_dir_files_iter, 1); + + return 1; +} + +int f_file_isreg(lua_State *L) { + struct stat st; + int n = lua_gettop(L); + + if (n != 1) { + lua_pushstring(L, "file_isreg: expected one argument"); + lua_error(L); + } + + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "file_isreg: argument has to be a string"); + lua_error(L); + } + + if (-1 == stat(lua_tostring(L, 1), &st)) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, S_ISREG(st.st_mode)); + + return 1; +} + +int f_file_isdir(lua_State *L) { + struct stat st; + int n = lua_gettop(L); + + if (n != 1) { + lua_pushstring(L, "file_isreg: expected one argument"); + lua_error(L); + } + + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "file_isreg: argument has to be a string"); + lua_error(L); + } + + if (-1 == stat(lua_tostring(L, 1), &st)) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, S_ISDIR(st.st_mode)); + + return 1; +} + + + +#ifdef USE_MEMCACHED +int f_memcache_exists(lua_State *L) { + size_t key_len; + const char *key; + memcached_st *memc; + + if (!lua_islightuserdata(L, lua_upvalueindex(1))) { + lua_pushstring(L, "where is my userdata ?"); + lua_error(L); + } + + memc = lua_touserdata(L, lua_upvalueindex(1)); + + if (1 != lua_gettop(L)) { + lua_pushstring(L, "expected one argument"); + lua_error(L); + } + + key = luaL_checklstring(L, 1, &key_len); + lua_pushboolean(L, (MEMCACHED_SUCCESS == memcached_exist(memc, key, key_len))); + return 1; +} + +int f_memcache_get_string(lua_State *L) { + size_t key_len, value_len; + char *value; + const char *key; + memcached_st *memc; + + if (!lua_islightuserdata(L, lua_upvalueindex(1))) { + lua_pushstring(L, "where is my userdata ?"); + lua_error(L); + } + + memc = lua_touserdata(L, lua_upvalueindex(1)); + + if (1 != lua_gettop(L)) { + lua_pushstring(L, "expected one argument"); + lua_error(L); + } + + key = luaL_checklstring(L, 1, &key_len); + if (NULL == (value = memcached_get(memc, key, key_len, &value_len, NULL, NULL))) { + lua_pushnil(L); + return 1; + } + + lua_pushlstring(L, value, value_len); + + free(value); + + return 1; +} + +int f_memcache_get_long(lua_State *L) { + size_t key_len, value_len; + char *value; + const char *key; + memcached_st *memc; + char *endptr; + long v; + + if (!lua_islightuserdata(L, lua_upvalueindex(1))) { + lua_pushstring(L, "where is my userdata ?"); + lua_error(L); + } + + memc = lua_touserdata(L, lua_upvalueindex(1)); + + if (1 != lua_gettop(L)) { + lua_pushstring(L, "expected one argument"); + lua_error(L); + } + + key = luaL_checklstring(L, 1, &key_len); + if (NULL == (value = memcached_get(memc, key, key_len, &value_len, NULL, NULL))) { + lua_pushnil(L); + return 1; + } + + errno = 0; + v = strtol(value, &endptr, 10); + if (0 == errno && *endptr == '\0') { + lua_pushinteger(L, v); + } else { + lua_pushnil(L); + } + + free(value); + + return 1; +} +#endif diff --git a/src/mod_cml_funcs.h b/src/mod_cml_funcs.h new file mode 100644 index 00000000..49243350 --- /dev/null +++ b/src/mod_cml_funcs.h @@ -0,0 +1,16 @@ +#ifndef _MOD_CML_FUNCS_H_ +#define _MOD_CML_FUNCS_H_ +#include "first.h" + +#include + +int f_crypto_md5(lua_State *L); +int f_file_mtime(lua_State *L); +int f_file_isreg(lua_State *L); +int f_file_isdir(lua_State *L); +int f_dir_files(lua_State *L); + +int f_memcache_exists(lua_State *L); +int f_memcache_get_string(lua_State *L); +int f_memcache_get_long(lua_State *L); +#endif diff --git a/src/mod_cml_lua.c b/src/mod_cml_lua.c new file mode 100644 index 00000000..b31c47f9 --- /dev/null +++ b/src/mod_cml_lua.c @@ -0,0 +1,320 @@ +#include "first.h" + +#include +#include +#include + +#include +#include + +#include "mod_cml_funcs.h" +#include "mod_cml.h" + +#include "chunk.h" +#include "log.h" +#include "http_header.h" +#include "request.h" +#include "response.h" +#include "stat_cache.h" + +#define HASHLEN 16 +typedef unsigned char HASH[HASHLEN]; +#define HASHHEXLEN 32 +typedef char HASHHEX[HASHHEXLEN+1]; + +static int lua_to_c_get_string(lua_State *L, const char *varname, buffer *b) { + int curelem = lua_gettop(L); + int result; + + lua_getglobal(L, varname); + + if (lua_isstring(L, curelem)) { + buffer_copy_string(b, lua_tostring(L, curelem)); + result = 0; + } else { + result = -1; + } + + lua_pop(L, 1); + force_assert(curelem == lua_gettop(L)); + return result; +} + +static int lua_to_c_is_table(lua_State *L, const char *varname) { + int curelem = lua_gettop(L); + int result; + + lua_getglobal(L, varname); + + result = lua_istable(L, curelem) ? 1 : 0; + + lua_pop(L, 1); + force_assert(curelem == lua_gettop(L)); + return result; +} + +static int c_to_lua_push(lua_State *L, int tbl, const char *key, size_t key_len, const char *val, size_t val_len) { + lua_pushlstring(L, key, key_len); + lua_pushlstring(L, val, val_len); + lua_settable(L, tbl); + + return 0; +} + +static int cache_export_get_params(lua_State *L, int tbl, buffer *qrystr) { + size_t is_key = 1; + size_t i, len, klen = 0; + char *key = NULL, *val = NULL; + + if (buffer_is_blank(qrystr)) return 0; + key = qrystr->ptr; + + /* we need the \0 */ + len = buffer_clen(qrystr); + for (i = 0; i <= len; i++) { + switch(qrystr->ptr[i]) { + case '=': + if (is_key) { + val = qrystr->ptr + i + 1; + klen = (size_t)(val - key - 1); + is_key = 0; + } + + break; + case '&': + case '\0': /* fin symbol */ + if (!is_key) { + /* we need at least a = since the last & */ + c_to_lua_push(L, tbl, + key, klen, + val, (size_t)(qrystr->ptr + i - val)); + } + + key = qrystr->ptr + i + 1; + val = NULL; + is_key = 1; + break; + } + } + + return 0; +} + +int cache_parse_lua(request_st * const r, plugin_data * const p, const buffer * const fn) { + lua_State *L; + int ret; + buffer *b; + + b = buffer_init(); + /* push the lua file to the interpreter and see what happends */ + L = luaL_newstate(); + luaL_openlibs(L); + + /* register functions */ + lua_register(L, "md5", f_crypto_md5); + lua_register(L, "file_mtime", f_file_mtime); + lua_register(L, "file_isreg", f_file_isreg); + lua_register(L, "file_isdir", f_file_isreg); + lua_register(L, "dir_files", f_dir_files); + +#ifdef USE_MEMCACHED + lua_pushlightuserdata(L, p->conf.memc); + lua_pushcclosure(L, f_memcache_get_long, 1); + lua_setglobal(L, "memcache_get_long"); + + lua_pushlightuserdata(L, p->conf.memc); + lua_pushcclosure(L, f_memcache_get_string, 1); + lua_setglobal(L, "memcache_get_string"); + + lua_pushlightuserdata(L, p->conf.memc); + lua_pushcclosure(L, f_memcache_exists, 1); + lua_setglobal(L, "memcache_exists"); +#endif + + /* register CGI environment */ + lua_newtable(L); + { + int header_tbl = lua_gettop(L); + + c_to_lua_push(L, header_tbl, CONST_STR_LEN("REQUEST_URI"), BUF_PTR_LEN(&r->target_orig)); + c_to_lua_push(L, header_tbl, CONST_STR_LEN("SCRIPT_NAME"), BUF_PTR_LEN(&r->uri.path)); + c_to_lua_push(L, header_tbl, CONST_STR_LEN("SCRIPT_FILENAME"), BUF_PTR_LEN(&r->physical.path)); + c_to_lua_push(L, header_tbl, CONST_STR_LEN("DOCUMENT_ROOT"), BUF_PTR_LEN(&r->physical.basedir)); + if (!buffer_is_blank(&r->pathinfo)) { + c_to_lua_push(L, header_tbl, CONST_STR_LEN("PATH_INFO"), BUF_PTR_LEN(&r->pathinfo)); + } + + c_to_lua_push(L, header_tbl, CONST_STR_LEN("CWD"), BUF_PTR_LEN(&p->basedir)); + c_to_lua_push(L, header_tbl, CONST_STR_LEN("BASEURL"), BUF_PTR_LEN(&p->baseurl)); + } + lua_setglobal(L, "request"); + + /* register GET parameter */ + lua_newtable(L); + cache_export_get_params(L, lua_gettop(L), &r->uri.query); + lua_setglobal(L, "get"); + + /* 2 default constants */ + lua_pushinteger(L, 0); + lua_setglobal(L, "CACHE_HIT"); + + lua_pushinteger(L, 1); + lua_setglobal(L, "CACHE_MISS"); + + /* load lua program */ + ret = luaL_loadfile(L, fn->ptr); + if (0 != ret) { + log_error(r->conf.errh, __FILE__, __LINE__, + "failed loading cml_lua script %s: %s", + fn->ptr, lua_tostring(L, -1)); + goto error; + } + + if (lua_pcall(L, 0, 1, 0)) { + log_error(r->conf.errh, __FILE__, __LINE__, + "failed running cml_lua script %s: %s", + fn->ptr, lua_tostring(L, -1)); + goto error; + } + + /* get return value */ + ret = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + + /* fetch the data from lua */ + lua_to_c_get_string(L, "trigger_handler", &p->trigger_handler); + + if (0 == lua_to_c_get_string(L, "output_contenttype", b)) { + http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type"), BUF_PTR_LEN(b)); + } + + if (ret == 0) { + /* up to now it is a cache-hit, check if all files exist */ + + int curelem; + unix_time64_t mtime = 0; + + if (!lua_to_c_is_table(L, "output_include")) { + log_error(r->conf.errh, __FILE__, __LINE__, + "output_include is missing or not a table"); + ret = -1; + + goto error; + } + + lua_getglobal(L, "output_include"); + curelem = lua_gettop(L); + + /* HOW-TO build a etag ? + * as we don't just have one file we have to take the stat() + * from all base files, merge them and build the etag from + * it later. + * + * The mtime of the content is the mtime of the freshest base file + * + * */ + + lua_pushnil(L); /* first key */ + while (lua_next(L, curelem) != 0) { + /* key' is at index -2 and value' at index -1 */ + + if (lua_isstring(L, -1)) { + size_t slen; + const char * const s = lua_tolstring(L, -1, &slen); + struct stat st; + int fd; + + /* the file is relative, make it absolute */ + if (s[0] != '/') { + buffer_copy_path_len2(b, BUF_PTR_LEN(&p->basedir), + s, slen); + } else { + buffer_copy_string_len(b, s, (uint32_t)slen); + } + + fd = stat_cache_open_rdonly_fstat(b, &st, r->conf.follow_symlink); + if (fd < 0) { + /* stat failed */ + + switch(errno) { + case ENOENT: + /* a file is missing, call the handler to generate it */ + if (!buffer_is_blank(&p->trigger_handler)) { + ret = 1; /* cache-miss */ + + log_error(r->conf.errh, __FILE__, __LINE__, + "a file is missing, calling handler"); + + break; + } else { + /* handler not set -> 500 */ + ret = -1; + + log_error(r->conf.errh, __FILE__, __LINE__, + "a file missing and no handler set"); + + break; + } + break; + default: + break; + } + } else { + chunkqueue_append_file_fd(&r->write_queue, b, fd, 0, st.st_size); + if (mtime < TIME64_CAST(st.st_mtime)) + mtime = TIME64_CAST(st.st_mtime); + } + } else { + /* not a string */ + ret = -1; + log_error(r->conf.errh, __FILE__, __LINE__, "not a string"); + break; + } + + lua_pop(L, 1); /* removes value'; keeps key' for next iteration */ + } + + lua_settop(L, curelem - 1); + + if (ret == 0) { + const buffer *vb = http_header_response_get(r, HTTP_HEADER_LAST_MODIFIED, CONST_STR_LEN("Last-Modified")); + if (NULL == vb) { /* no Last-Modified specified */ + if (0 == mtime) mtime = log_epoch_secs; /* default last-modified to now */ + vb = http_response_set_last_modified(r, mtime); + } + + r->resp_body_finished = 1; + + if (HANDLER_FINISHED == http_response_handle_cachable(r, vb, mtime)) { + /* ok, the client already has our content, + * no need to send it again */ + + chunkqueue_reset(&r->write_queue); + ret = 0; /* cache-hit */ + } + } else { + chunkqueue_reset(&r->write_queue); + } + } + + if (ret == 1 && !buffer_is_blank(&p->trigger_handler)) { + /* cache-miss */ + buffer_clear(&r->uri.path); + buffer_append_str2(&r->uri.path, + BUF_PTR_LEN(&p->baseurl), + BUF_PTR_LEN(&p->trigger_handler)); + + buffer_copy_path_len2(&r->physical.path, + BUF_PTR_LEN(&p->basedir), + BUF_PTR_LEN(&p->trigger_handler)); + + chunkqueue_reset(&r->write_queue); + } + +error: + lua_close(L); + + buffer_free(b); + + return ret /* cache-error */; +} diff --git a/src/mod_flv_streaming.c b/src/mod_flv_streaming.c new file mode 100644 index 00000000..c12d0f02 --- /dev/null +++ b/src/mod_flv_streaming.c @@ -0,0 +1,161 @@ +#include "first.h" + +#include "array.h" +#include "buffer.h" +#include "log.h" +#include "http_chunk.h" +#include "http_header.h" +#include "request.h" +#include "stat_cache.h" + +#include "plugin.h" + +#include +#include +#include + +typedef struct { + const array *extensions; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_config defaults; + plugin_config conf; +} plugin_data; + +INIT_FUNC(mod_flv_streaming_init) { + return calloc(1, sizeof(plugin_data)); +} + +static void mod_flv_streaming_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { + switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ + case 0: /* flv-streaming.extensions */ + pconf->extensions = cpv->v.a; + break; + default:/* should not happen */ + return; + } +} + +static void mod_flv_streaming_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { + do { + mod_flv_streaming_merge_config_cpv(pconf, cpv); + } while ((++cpv)->k_id != -1); +} + +static void mod_flv_streaming_patch_config(request_st * const r, plugin_data * const p) { + p->conf = p->defaults; /* copy small struct instead of memcpy() */ + /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ + for (int i = 1, used = p->nconfig; i < used; ++i) { + if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) + mod_flv_streaming_merge_config(&p->conf, + p->cvlist + p->cvlist[i].v.u2[0]); + } +} + +SETDEFAULTS_FUNC(mod_flv_streaming_set_defaults) { + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("flv-streaming.extensions"), + T_CONFIG_ARRAY_VLIST, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + + plugin_data * const p = p_d; + if (!config_plugin_values_init(srv, p, cpk, "mod_flv_streaming")) + return HANDLER_ERROR; + + /* initialize p->defaults from global config context */ + if (p->nconfig > 0 && p->cvlist->v.u2[1]) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; + if (-1 != cpv->k_id) + mod_flv_streaming_merge_config(&p->defaults, cpv); + } + + log_error(NULL, __FILE__, __LINE__, + "Warning: mod_%s is deprecated " + "and will be removed from a future lighttpd release in early 2022. " + "https://wiki.lighttpd.net/Docs_ConfigurationOptions#Deprecated", + p->self->name); + + return HANDLER_GO_ON; +} + +static off_t get_param_value(buffer *qb, const char *m, size_t mlen) { + const char * const q = qb->ptr; + size_t len = buffer_clen(qb); + if (len < mlen+2) return -1; + len -= (mlen+2); + for (size_t i = 0; i <= len; ++i) { + if (0 == memcmp(q+i, m, mlen) && q[i+mlen] == '=') { + char *err; + off_t n = strtoll(q+i+mlen+1, &err, 10); + return (*err == '\0' || *err == '&') ? n : -1; + } + do { ++i; } while (i < len && q[i] != '&'); + } + return -1; +} + +URIHANDLER_FUNC(mod_flv_streaming_path_handler) { + plugin_data *p = p_d; + + if (NULL != r->handler_module) return HANDLER_GO_ON; + if (buffer_is_blank(&r->physical.path)) return HANDLER_GO_ON; + + mod_flv_streaming_patch_config(r, p); + if (NULL == p->conf.extensions) return HANDLER_GO_ON; + + if (!array_match_value_suffix(p->conf.extensions, &r->physical.path)) { + /* not found */ + return HANDLER_GO_ON; + } + + off_t start = get_param_value(&r->uri.query, CONST_STR_LEN("start")); + off_t end = get_param_value(&r->uri.query, CONST_STR_LEN("end")); + off_t len = -1; + if (start < 0) start = 0; + if (start < end) + len = end - start + 1; + else if (0 == start) + return HANDLER_GO_ON; /* let mod_staticfile send whole file */ + + stat_cache_entry * const sce = + stat_cache_get_entry_open(&r->physical.path, r->conf.follow_symlink); + if (NULL == sce || (sce->fd < 0 && 0 != sce->st.st_size)) { + r->http_status = (errno == ENOENT) ? 404 : 403; + return HANDLER_FINISHED; + } + if (len == -1 || sce->st.st_size - start < len) + len = sce->st.st_size - start; + if (len <= 0) + return HANDLER_FINISHED; + + /* if there is a start=[0-9]+ in the header use it as start, + * otherwise set start to beginning of file */ + /* if there is a end=[0-9]+ in the header use it as end pos, + * otherwise send rest of file, starting from start */ + + /* let's build a flv header */ + http_chunk_append_mem(r, CONST_STR_LEN("FLV\x1\x1\0\0\0\x9\0\0\0\x9")); + http_chunk_append_file_ref_range(r, sce, start, len); + http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("video/x-flv")); + r->resp_body_finished = 1; + return HANDLER_FINISHED; +} + + +int mod_flv_streaming_plugin_init(plugin *p); +int mod_flv_streaming_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = "flv_streaming"; + + p->init = mod_flv_streaming_init; + p->handle_physical = mod_flv_streaming_path_handler; + p->set_defaults = mod_flv_streaming_set_defaults; + + return 0; +} diff --git a/src/mod_geoip.c b/src/mod_geoip.c new file mode 100644 index 00000000..10a8c38b --- /dev/null +++ b/src/mod_geoip.c @@ -0,0 +1,291 @@ +#include "first.h" + +#include +#include + +#include "base.h" +#include "log.h" +#include "buffer.h" +#include "http_header.h" +#include "plugin.h" + +#include +#include + +/** + * + * $mod_geoip.c (v2.0) (13.09.2006 00:29:11) + * + * Name: + * mod_geoip.c + * + * Description: + * GeoIP module (plugin) for lighttpd. + * the module loads a geoip database of type "country" or "city" and + * sets new ENV vars based on ip record lookups. + * + * country db env's: + * GEOIP_COUNTRY_CODE + * GEOIP_COUNTRY_CODE3 + * GEOIP_COUNTRY_NAME + * + * city db env's: + * GEOIP_COUNTRY_CODE + * GEOIP_COUNTRY_CODE3 + * GEOIP_COUNTRY_NAME + * GEOIP_CITY_NAME + * GEOIP_CITY_POSTAL_CODE + * GEOIP_CITY_LATITUDE + * GEOIP_CITY_LONG_LATITUDE + * GEOIP_CITY_DMA_CODE + * GEOIP_CITY_AREA_CODE + * + * Usage (configuration options): + * geoip.db-filename = + * geoip.memory-cache = : default disabled + * if enabled, mod_geoip will load the database binary file to + * memory for very fast lookups. the only penalty is memory usage. + * + * Author: + * Ami E. Bizamcher (amix) + * duke.amix@gmail.com + * + * Note: + * GeoIP Library and API must be installed! + * + * + * Fully-rewritten from original + * Copyright(c) 2016 Glenn Strauss gstrauss()gluelogic.com All rights reserved + * License: BSD 3-clause (same as lighttpd) + */ + + +typedef struct { + GeoIP *gi; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_config defaults; + plugin_config conf; +} plugin_data; + +INIT_FUNC(mod_geoip_init) { + return calloc(1, sizeof(plugin_data)); +} + +FREE_FUNC(mod_geoip_free) { + plugin_data * const p = p_d; + if (NULL == p->cvlist) return; + /* (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* geoip.db-filename */ + if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) + GeoIP_delete(cpv->v.v); + break; + default: + break; + } + } + } +} + +static int mod_geoip_open_db(server *srv, config_plugin_value_t * const cpv, int mem_cache) { + /* country db filename is required! */ + if (buffer_is_blank(cpv->v.b)) { + cpv->v.v = NULL; + return 1; + } + + /* let's start cooking */ + const int mode = (mem_cache) + ? GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE + : GEOIP_STANDARD | GEOIP_CHECK_CACHE; + + GeoIP *gi = GeoIP_open(cpv->v.b->ptr, mode); + if (NULL == gi) { + log_error(srv->errh, __FILE__, __LINE__, + "failed to open GeoIP database!!!"); + return 0; + } + + /* is the db supported ? */ + if ( gi->databaseType != GEOIP_COUNTRY_EDITION + && gi->databaseType != GEOIP_CITY_EDITION_REV0 + && gi->databaseType != GEOIP_CITY_EDITION_REV1) { + log_error(srv->errh, __FILE__, __LINE__, + "GeoIP database is of unsupported type!!!"); + GeoIP_delete(gi); + return 0; + } + + cpv->vtype = T_CONFIG_LOCAL; + cpv->v.v = gi; + return 1; +} + +static void mod_geoip_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { + switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ + case 0: /* geoip.db-filename */ + if (cpv->vtype != T_CONFIG_LOCAL) break; + pconf->gi = cpv->v.v; + break; + case 1: /* geoip.memory-cache */ + break; + default:/* should not happen */ + return; + } +} + +static void mod_geoip_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { + do { + mod_geoip_merge_config_cpv(pconf, cpv); + } while ((++cpv)->k_id != -1); +} + +static void mod_geoip_patch_config(request_st * const r, plugin_data * const p) { + p->conf = p->defaults; /* copy small struct instead of memcpy() */ + /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ + for (int i = 1, used = p->nconfig; i < used; ++i) { + if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) + mod_geoip_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]); + } +} + +SETDEFAULTS_FUNC(mod_geoip_set_defaults) { + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("geoip.db-filename"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("geoip.memory-cache"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + + plugin_data * const p = p_d; + if (!config_plugin_values_init(srv, p, cpk, "mod_geoip")) + return HANDLER_ERROR; + + /* process and validate config directives + * (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + config_plugin_value_t *fn = NULL; + int mem_cache = 0; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* geoip.db-filename */ + fn = cpv; + break; + case 1: /* geoip.memory-cache */ + mem_cache = cpv->v.u; + break; + default:/* should not happen */ + break; + } + } + if (fn) { + if (!mod_geoip_open_db(srv, fn, mem_cache)) + return HANDLER_ERROR; + } + } + + /* initialize p->defaults from global config context */ + if (p->nconfig > 0 && p->cvlist->v.u2[1]) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; + if (-1 != cpv->k_id) + mod_geoip_merge_config(&p->defaults, cpv); + } + + log_error(srv->errh, __FILE__, __LINE__, + "Warning: mod_%s is deprecated " + "and will be removed from a future lighttpd release in early 2022. " + "https://wiki.lighttpd.net/Docs_ConfigurationOptions#Deprecated", + p->self->name); + + return HANDLER_GO_ON; +} + +static handler_t mod_geoip_query (request_st * const r, plugin_data * const p) { + GeoIPRecord *gir; + const char *remote_ip = r->con->dst_addr_buf.ptr; + + if (NULL != http_header_env_get(r, CONST_STR_LEN("GEOIP_COUNTRY_CODE"))) { + return HANDLER_GO_ON; + } + + if (p->conf.gi->databaseType == GEOIP_COUNTRY_EDITION) { + const char *returnedCountry; + + if (NULL != (returnedCountry = GeoIP_country_code_by_addr(p->conf.gi, remote_ip))) { + http_header_env_set(r, CONST_STR_LEN("GEOIP_COUNTRY_CODE"), returnedCountry, strlen(returnedCountry)); + } + + if (NULL != (returnedCountry = GeoIP_country_code3_by_addr(p->conf.gi, remote_ip))) { + http_header_env_set(r, CONST_STR_LEN("GEOIP_COUNTRY_CODE3"), returnedCountry, strlen(returnedCountry)); + } + + if (NULL != (returnedCountry = GeoIP_country_name_by_addr(p->conf.gi, remote_ip))) { + http_header_env_set(r, CONST_STR_LEN("GEOIP_COUNTRY_NAME"), returnedCountry, strlen(returnedCountry)); + } + + return HANDLER_GO_ON; + } + + /* if we are here, geo city is in use */ + + if (NULL != (gir = GeoIP_record_by_addr(p->conf.gi, remote_ip))) { + + http_header_env_set(r, CONST_STR_LEN("GEOIP_COUNTRY_CODE"), gir->country_code, strlen(gir->country_code)); + http_header_env_set(r, CONST_STR_LEN("GEOIP_COUNTRY_CODE3"), gir->country_code3, strlen(gir->country_code3)); + http_header_env_set(r, CONST_STR_LEN("GEOIP_COUNTRY_NAME"), gir->country_name, strlen(gir->country_name)); + http_header_env_set(r, CONST_STR_LEN("GEOIP_CITY_REGION"), gir->region, strlen(gir->region)); + http_header_env_set(r, CONST_STR_LEN("GEOIP_CITY_NAME"), gir->city, strlen(gir->city)); + http_header_env_set(r, CONST_STR_LEN("GEOIP_CITY_POSTAL_CODE"), gir->postal_code, strlen(gir->postal_code)); + + buffer_append_int(http_header_env_set_ptr(r, CONST_STR_LEN("GEOIP_CITY_DMA_CODE")), (intmax_t)gir->dma_code); + buffer_append_int(http_header_env_set_ptr(r, CONST_STR_LEN("GEOIP_CITY_AREA_CODE")), (intmax_t)gir->area_code); + + { + char latitude[32]; + snprintf(latitude, sizeof(latitude), "%f", gir->latitude); + http_header_env_set(r, CONST_STR_LEN("GEOIP_CITY_LATITUDE"), latitude, strlen(latitude)); + } + + { + char long_latitude[32]; + snprintf(long_latitude, sizeof(long_latitude), "%f", gir->longitude); + http_header_env_set(r, CONST_STR_LEN("GEOIP_CITY_LONG_LATITUDE"), long_latitude, strlen(long_latitude)); + } + + GeoIPRecord_delete(gir); + } + + return HANDLER_GO_ON; +} + +REQUEST_FUNC(mod_geoip_handle_request_env) { + plugin_data *p = p_d; + mod_geoip_patch_config(r, p); + return (p->conf.gi) ? mod_geoip_query(r, p) : HANDLER_GO_ON; +} + + +int mod_geoip_plugin_init(plugin *p); +int mod_geoip_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = "geoip"; + + p->init = mod_geoip_init; + p->handle_request_env = mod_geoip_handle_request_env; + p->set_defaults = mod_geoip_set_defaults; + p->cleanup = mod_geoip_free; + + return 0; +} diff --git a/src/mod_mysql_vhost.c b/src/mod_mysql_vhost.c new file mode 100644 index 00000000..edb0e168 --- /dev/null +++ b/src/mod_mysql_vhost.c @@ -0,0 +1,385 @@ +#include "first.h" + +#include +#include +#include + +#include + +#include "base.h" +#include "plugin.h" +#include "fdevent.h" +#include "log.h" + +#include "stat_cache.h" + +/* + * Plugin for lighttpd to use MySQL + * for domain to directory lookups, + * i.e virtual hosts (vhosts). + * + * /ada@riksnet.se 2004-12-06 + */ + +typedef struct { + MYSQL *mysql; + const buffer *mysql_query; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_config defaults; + plugin_config conf; +} plugin_data; + +typedef struct { + buffer *server_name; + buffer *document_root; +} plugin_connection_data; + +INIT_FUNC(mod_mysql_vhost_init) { + return calloc(1, sizeof(plugin_data)); +} + +/* cleanup the mysql connections */ +FREE_FUNC(mod_mysql_vhost_cleanup) { + plugin_data * const p = p_d; + if (NULL == p->cvlist) return; + /* (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + if (cpv->vtype != T_CONFIG_LOCAL || NULL == cpv->v.v) continue; + switch (cpv->k_id) { + case 1: /* mysql-vhost.db */ + mysql_close(cpv->v.v); + break; + default: + break; + } + } + } +} + +static void* mod_mysql_vhost_connection_data(request_st * const r, void *p_d) +{ + plugin_data *p = p_d; + plugin_connection_data *c = r->plugin_ctx[p->id]; + + if (c) return c; + c = calloc(1, sizeof(*c)); + force_assert(c); + + c->server_name = buffer_init(); + c->document_root = buffer_init(); + + return r->plugin_ctx[p->id] = c; +} + +REQUEST_FUNC(mod_mysql_vhost_handle_request_reset) { + plugin_data *p = p_d; + plugin_connection_data *c = r->plugin_ctx[p->id]; + + if (!c) return HANDLER_GO_ON; + + buffer_free(c->server_name); + buffer_free(c->document_root); + + free(c); + + r->plugin_ctx[p->id] = NULL; + return HANDLER_GO_ON; +} + +static void mod_mysql_vhost_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { + switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ + case 0: /* mysql-vhost.sql */ + pconf->mysql_query = cpv->v.b; + break; + case 1: /* mysql-vhost.db */ + if (cpv->vtype == T_CONFIG_LOCAL) + pconf->mysql = cpv->v.v; + break; + case 2: /* mysql-vhost.user */ + case 3: /* mysql-vhost.pass */ + case 4: /* mysql-vhost.sock */ + case 5: /* mysql-vhost.hostname */ + case 6: /* mysql-vhost.port */ + break; + default:/* should not happen */ + return; + } +} + +static void mod_mysql_vhost_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { + do { + mod_mysql_vhost_merge_config_cpv(pconf, cpv); + } while ((++cpv)->k_id != -1); +} + +static void mod_mysql_vhost_patch_config(request_st * const r, plugin_data * const p) { + p->conf = p->defaults; /* copy small struct instead of memcpy() */ + /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ + for (int i = 1, used = p->nconfig; i < used; ++i) { + if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) + mod_mysql_vhost_merge_config(&p->conf, + p->cvlist + p->cvlist[i].v.u2[0]); + } +} + +static MYSQL * mod_mysql_vhost_db_setup (server *srv, const char *dbname, const char *user, const char *pass, const char *sock, const char *host, unsigned short port) { + /* required: + * - database + * - username + * + * optional: + * - password, default: empty + * - socket, default: mysql default + * - hostname, if set overrides socket + * - port, default: 3306 + */ + + MYSQL * const my = mysql_init(NULL); + if (NULL == my) { + log_error(srv->errh, __FILE__, __LINE__, + "mysql_init() failed, exiting..."); + return NULL; + } + + #if MYSQL_VERSION_ID >= 50013 + /* in mysql versions above 5.0.3 the reconnect flag is off by default */ + char reconnect = 1; + mysql_options(my, MYSQL_OPT_RECONNECT, &reconnect); + #endif + + unsigned long flags = 0; + #if MYSQL_VERSION_ID >= 40100 + /* CLIENT_MULTI_STATEMENTS first appeared in 4.1 */ + flags |= CLIENT_MULTI_STATEMENTS; + #endif + + if (!mysql_real_connect(my, host, user, pass, dbname, port, sock, flags)) { + log_error(srv->errh, __FILE__, __LINE__, "%s", mysql_error(my)); + mysql_close(my); + return NULL; + } + + fdevent_setfd_cloexec(my->net.fd); + return my; +} + +SETDEFAULTS_FUNC(mod_mysql_vhost_set_defaults) { + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("mysql-vhost.sql"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("mysql-vhost.db"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("mysql-vhost.user"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("mysql-vhost.pass"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("mysql-vhost.sock"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("mysql-vhost.hostname"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("mysql-vhost.port"), + T_CONFIG_SHORT, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + + log_error(srv->errh, __FILE__, __LINE__, + "mod_mysql_vhost is deprecated and will be removed in a future version; " + "please migrate to use mod_vhostdb_mysql"); + + plugin_data * const p = p_d; + if (!config_plugin_values_init(srv, p, cpk, "mod_mysql_vhost")) + return HANDLER_ERROR; + + /* process and validate config directives + * (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + const char *dbname=NULL, *user=NULL, *pass=NULL, *host=NULL, *sock=NULL; + unsigned short port = 0; + config_plugin_value_t *db = NULL; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* mysql_vhost.sql */ + if (buffer_is_blank(cpv->v.b)) + cpv->v.b = NULL; + break; + case 1: /* mysql_vhost.db */ + if (!buffer_is_blank(cpv->v.b)) { + db = cpv; + dbname = cpv->v.b->ptr; + } + break; + case 2: /* mysql_vhost.user */ + if (!buffer_is_blank(cpv->v.b)) + user = cpv->v.b->ptr; + break; + case 3: /* mysql_vhost.pass */ + if (!buffer_is_blank(cpv->v.b)) + pass = cpv->v.b->ptr; + break; + case 4: /* mysql_vhost.sock */ + if (!buffer_is_blank(cpv->v.b)) + sock = cpv->v.b->ptr; + break; + case 5: /* mysql_vhost.hostname */ + if (!buffer_is_blank(cpv->v.b)) + host = cpv->v.b->ptr; + break; + case 6: /* mysql_vhost.port */ + port = cpv->v.shrt; + break; + default:/* should not happen */ + break; + } + } + + if (dbname && user) { + cpv = db; + cpv->v.v = + mod_mysql_vhost_db_setup(srv,dbname,user,pass,sock,host,port); + if (NULL == db->v.v) return HANDLER_ERROR; + cpv->vtype = T_CONFIG_LOCAL; + } + } + + /* initialize p->defaults from global config context */ + if (p->nconfig > 0 && p->cvlist->v.u2[1]) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; + if (-1 != cpv->k_id) + mod_mysql_vhost_merge_config(&p->defaults, cpv); + } + + log_error(srv->errh, __FILE__, __LINE__, + "Warning: mod_%s is deprecated " + "and will be removed from a future lighttpd release in early 2022. " + "https://wiki.lighttpd.net/Docs_ConfigurationOptions#Deprecated", + p->self->name); + + return HANDLER_GO_ON; +} + +REQUEST_FUNC(mod_mysql_vhost_handle_docroot) { + plugin_data *p = p_d; + plugin_connection_data *c; + + unsigned cols; + MYSQL_ROW row; + MYSQL_RES *result = NULL; + + /* no host specified? */ + if (buffer_is_blank(&r->uri.authority)) return HANDLER_GO_ON; + + mod_mysql_vhost_patch_config(r, p); + + if (!p->conf.mysql) return HANDLER_GO_ON; + if (!p->conf.mysql_query) return HANDLER_GO_ON; + + /* sets up connection data if not done yet */ + c = mod_mysql_vhost_connection_data(r, p_d); + + /* check if cached this connection */ + if (buffer_is_equal(c->server_name, &r->uri.authority)) goto GO_ON; + + /* build and run SQL query */ + buffer * const b = r->tmp_buf; + buffer_clear(b); + for (const char *ptr = p->conf.mysql_query->ptr, *d; *ptr; ptr = d+1) { + if (NULL != (d = strchr(ptr, '?'))) { + /* escape the uri.authority */ + unsigned long to_len; + buffer_append_string_len(b, ptr, (size_t)(d - ptr)); + buffer_string_prepare_append(b, buffer_clen(&r->uri.authority) * 2); + to_len = mysql_real_escape_string(p->conf.mysql, + b->ptr + buffer_clen(b), + BUF_PTR_LEN(&r->uri.authority)); + if ((unsigned long)~0 == to_len) goto ERR500; + buffer_commit(b, to_len); + } else { + d = p->conf.mysql_query->ptr + buffer_clen(p->conf.mysql_query); + buffer_append_string_len(b, ptr, (size_t)(d - ptr)); + break; + } + } + if (mysql_real_query(p->conf.mysql, BUF_PTR_LEN(b))) { + log_error(r->conf.errh, __FILE__, __LINE__, "%s", mysql_error(p->conf.mysql)); + goto ERR500; + } + result = mysql_store_result(p->conf.mysql); + cols = mysql_num_fields(result); + row = mysql_fetch_row(result); + if (!row || cols < 1) { + /* no such virtual host */ + mysql_free_result(result); +#if MYSQL_VERSION_ID >= 40100 + while (mysql_next_result(p->conf.mysql) == 0); +#endif + return HANDLER_GO_ON; + } + + /* sanity check that really is a directory */ + buffer_copy_string(b, row[0]); + buffer_append_slash(b); + + if (!stat_cache_path_isdir(b)) { + log_perror(r->conf.errh, __FILE__, __LINE__, "%s", b->ptr); + goto ERR500; + } + + /* cache the data */ + buffer_copy_buffer(c->server_name, &r->uri.authority); + buffer_copy_buffer(c->document_root, b); + + mysql_free_result(result); +#if MYSQL_VERSION_ID >= 40100 + while (mysql_next_result(p->conf.mysql) == 0); +#endif + + /* fix virtual server and docroot */ +GO_ON: + if (!buffer_is_blank(c->server_name)) { + r->server_name = &r->server_name_buf; + buffer_copy_buffer(&r->server_name_buf, c->server_name); + } + buffer_copy_buffer(&r->physical.doc_root, c->document_root); + + return HANDLER_GO_ON; + +ERR500: + if (result) mysql_free_result(result); +#if MYSQL_VERSION_ID >= 40100 + while (mysql_next_result(p->conf.mysql) == 0); +#endif + r->http_status = 500; /* Internal Error */ + r->handler_module = NULL; + return HANDLER_FINISHED; +} + + +int mod_mysql_vhost_plugin_init(plugin *p); +int mod_mysql_vhost_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = "mysql_vhost"; + + p->init = mod_mysql_vhost_init; + p->cleanup = mod_mysql_vhost_cleanup; + p->handle_request_reset = mod_mysql_vhost_handle_request_reset; + + p->set_defaults = mod_mysql_vhost_set_defaults; + p->handle_docroot = mod_mysql_vhost_handle_docroot; + + return 0; +} diff --git a/src/mod_trigger_b4_dl.c b/src/mod_trigger_b4_dl.c new file mode 100644 index 00000000..4295d810 --- /dev/null +++ b/src/mod_trigger_b4_dl.c @@ -0,0 +1,619 @@ +#include "first.h" + +#include "plugin.h" + + +#if defined(HAVE_GDBM_H) || defined(USE_MEMCACHED) /* at least one required */ + + +#include "base.h" +#include "log.h" +#include "buffer.h" +#include "http_header.h" +#include "keyvalue.h" + +#include +#include +#include + +#if defined(HAVE_GDBM_H) +#include "fdevent.h" +# include +#endif + +#if defined(USE_MEMCACHED) +# include +#endif + +/** + * this is a trigger_b4_dl for a lighttpd plugin + * + */ + +typedef struct { + const buffer *deny_url; + pcre_keyvalue_buffer *trigger_regex; + pcre_keyvalue_buffer *download_regex; + #if defined(HAVE_GDBM_H) + GDBM_FILE db; + #endif + #if defined(USE_MEMCACHED) + memcached_st *memc; + const buffer *mc_namespace; + #endif + unsigned short trigger_timeout; + unsigned short debug; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_config defaults; + plugin_config conf; +} plugin_data; + +INIT_FUNC(mod_trigger_b4_dl_init) { + return calloc(1, sizeof(plugin_data)); +} + +FREE_FUNC(mod_trigger_b4_dl_free) { + plugin_data *p = p_d; + if (NULL == p->cvlist) return; + /* (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + if (cpv->vtype != T_CONFIG_LOCAL || NULL == cpv->v.v) continue; + switch (cpv->k_id) { + #if defined(HAVE_GDBM_H) + case 0: /* trigger-before-download.gdbm-filename */ + gdbm_close(cpv->v.v); + break; + #endif + case 1: /* trigger-before-download.trigger-url */ + pcre_keyvalue_buffer_free(cpv->v.v); + break; + case 2: /* trigger-before-download.download-url */ + pcre_keyvalue_buffer_free(cpv->v.v); + break; + #if defined(USE_MEMCACHED) + case 5: /* trigger-before-download.memcache-hosts */ + memcached_free(cpv->v.v); + break; + #endif + default: + break; + } + } + } +} + +static int mod_trigger_b4_dl_init_gdbm(server * const srv, config_plugin_value_t * const cpv) { + if (buffer_is_blank(cpv->v.b)) { + cpv->v.v = NULL; + return 1; + } + + #if defined(HAVE_GDBM_H) + + GDBM_FILE db = gdbm_open(cpv->v.b->ptr, 4096, GDBM_WRCREAT | GDBM_NOLOCK, + S_IRUSR | S_IWUSR, 0); + + if (db) { + cpv->v.v = db; + cpv->vtype = T_CONFIG_LOCAL; + fdevent_setfd_cloexec(gdbm_fdesc(db)); + return 1; + } + else { + log_error(srv->errh, __FILE__, __LINE__, + "gdbm-open failed %s", cpv->v.b->ptr); + return 0; + } + + #else + + UNUSED(srv); + return 1; + + #endif +} + +static int mod_trigger_b4_dl_init_memcached(server * const srv, config_plugin_value_t * const cpv) { + const array * const mc_hosts = cpv->v.a; + if (0 == mc_hosts->used) { + cpv->v.v = NULL; + return 1; + } + + #if defined(USE_MEMCACHED) + + buffer * const opts = srv->tmp_buf; + buffer_clear(opts); + for (uint32_t k = 0; k < mc_hosts->used; ++k) { + const data_string * const ds = (const data_string *)mc_hosts->data[k]; + buffer_append_str2(opts, CONST_STR_LEN(" --SERVER="), + BUF_PTR_LEN(&ds->value)); + } + + cpv->v.v = memcached(opts->ptr+1, buffer_clen(opts)-1); + + if (cpv->v.v) { + cpv->vtype = T_CONFIG_LOCAL; + return 1; + } + else { + log_error(srv->errh, __FILE__, __LINE__, + "configuring memcached failed for option string: %s", opts->ptr); + return 0; + } + + #else + + log_error(srv->errh, __FILE__, __LINE__, + "memcache support is not compiled in but " + "trigger-before-download.memcache-hosts is set; aborting"); + return 0; + + #endif +} + +static int mod_trigger_b4_dl_init_regex(server * const srv, config_plugin_value_t * const cpv, const char * const str) { + const buffer * const b = cpv->v.b; + if (buffer_is_blank(b)) { + cpv->v.v = NULL; + return 1; + } + + const int pcre_jit = config_feature_bool(srv, "server.pcre_jit", 1); + pcre_keyvalue_buffer * const kvb = pcre_keyvalue_buffer_init(); + buffer empty = { NULL, 0, 0 }; + if (!pcre_keyvalue_buffer_append(srv->errh, kvb, b, &empty, pcre_jit)) { + log_error(srv->errh, __FILE__, __LINE__, + "pcre_compile failed for %s %s", str, b->ptr); + pcre_keyvalue_buffer_free(kvb); + return 0; + } + cpv->v.v = kvb; + cpv->vtype = T_CONFIG_LOCAL; + return 1; +} + +#ifdef __COVERITY__ +#include "burl.h" +#endif + +static int mod_trigger_b4_dl_match(pcre_keyvalue_buffer * const kvb, const buffer * const input) { + /*(re-use keyvalue.[ch] for match-only; + * must have been configured with empty kvb 'value' during init)*/ + pcre_keyvalue_ctx ctx = { NULL, NULL, -1, 0, NULL, NULL }; + #ifdef __COVERITY__ + /*(again, must have been configured w/ empty kvb 'value' during init)*/ + struct cond_match_t cache; + memset(&cache, 0, sizeof(cache)); + struct burl_parts_t bp; + memset(&bp, 0, sizeof(bp)); + ctx.cache = &cache; + ctx.burl = &bp; + #endif + return HANDLER_GO_ON == pcre_keyvalue_buffer_process(kvb, &ctx, input, NULL) + && -1 != ctx.m; +} + +static void mod_trigger_b4_dl_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { + switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ + case 0: /* trigger-before-download.gdbm-filename */ + #if defined(HAVE_GDBM_H) + if (cpv->vtype != T_CONFIG_LOCAL) break; + pconf->db = cpv->v.v; + #endif + break; + case 1: /* trigger-before-download.trigger-url */ + if (cpv->vtype != T_CONFIG_LOCAL) break; + pconf->trigger_regex = cpv->v.v; + break; + case 2: /* trigger-before-download.download-url */ + if (cpv->vtype != T_CONFIG_LOCAL) break; + pconf->download_regex = cpv->v.v; + break; + case 3: /* trigger-before-download.deny-url */ + pconf->deny_url = cpv->v.b; + break; + case 4: /* trigger-before-download.trigger-timeout */ + pconf->trigger_timeout = cpv->v.shrt; + break; + case 5: /* trigger-before-download.memcache-hosts */ + #if defined(USE_MEMCACHED) + if (cpv->vtype != T_CONFIG_LOCAL) break; + pconf->memc = cpv->v.v; + #endif + break; + case 6: /* trigger-before-download.memcache-namespace */ + #if defined(USE_MEMCACHED) + pconf->mc_namespace = cpv->v.b; + #endif + break; + case 7: /* trigger-before-download.debug */ + pconf->debug = cpv->v.u; + break; + default:/* should not happen */ + return; + } +} + +static void mod_trigger_b4_dl_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { + do { + mod_trigger_b4_dl_merge_config_cpv(pconf, cpv); + } while ((++cpv)->k_id != -1); +} + +static void mod_trigger_b4_dl_patch_config(request_st * const r, plugin_data * const p) { + memcpy(&p->conf, &p->defaults, sizeof(plugin_config)); + for (int i = 1, used = p->nconfig; i < used; ++i) { + if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) + mod_trigger_b4_dl_merge_config(&p->conf, + p->cvlist + p->cvlist[i].v.u2[0]); + } +} + +SETDEFAULTS_FUNC(mod_trigger_b4_dl_set_defaults) { + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("trigger-before-download.gdbm-filename"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("trigger-before-download.trigger-url"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("trigger-before-download.download-url"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("trigger-before-download.deny-url"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("trigger-before-download.trigger-timeout"), + T_CONFIG_SHORT, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("trigger-before-download.memcache-hosts"), + T_CONFIG_ARRAY_VLIST, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("trigger-before-download.memcache-namespace"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("trigger-before-download.debug"), + T_CONFIG_SHORT, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + + plugin_data * const p = p_d; + if (!config_plugin_values_init(srv, p, cpk, "mod_trigger_b4_dl")) + return HANDLER_ERROR; + + /* process and validate config directives + * (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* trigger-before-download.gdbm-filename */ + if (!mod_trigger_b4_dl_init_gdbm(srv, cpv)) + return HANDLER_ERROR; + break; + case 1: /* trigger-before-download.trigger-url */ + if (!mod_trigger_b4_dl_init_regex(srv, cpv, "trigger-url")) + return HANDLER_ERROR; + break; + case 2: /* trigger-before-download.download-url */ + if (!mod_trigger_b4_dl_init_regex(srv, cpv, "download-url")) + return HANDLER_ERROR; + break; + case 3: /* trigger-before-download.deny-url */ + case 4: /* trigger-before-download.trigger-timeout */ + break; + case 5: /* trigger-before-download.memcache-hosts */ + if (!mod_trigger_b4_dl_init_memcached(srv, cpv)) + return HANDLER_ERROR; + break; + case 6: /* trigger-before-download.memcache-namespace */ + case 7: /* trigger-before-download.debug */ + break; + default:/* should not happen */ + break; + } + } + } + + /* initialize p->defaults from global config context */ + if (p->nconfig > 0 && p->cvlist->v.u2[1]) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; + if (-1 != cpv->k_id) + mod_trigger_b4_dl_merge_config(&p->defaults, cpv); + } + + return HANDLER_GO_ON; +} + +#if defined(USE_MEMCACHED) +static void mod_trigger_b4_dl_memcached_key(buffer * const b, const plugin_data * const p, const buffer * const remote_ip) { + buffer_clear(b); + if (p->conf.mc_namespace) + buffer_copy_buffer(b, p->conf.mc_namespace); + buffer_append_string_buffer(b, remote_ip); + + /* memcached can't handle spaces */ + for (size_t i = 0, len = buffer_clen(b); i < len; ++i) { + if (b->ptr[i] == ' ') b->ptr[i] = '-'; + } +} +#endif + +static handler_t mod_trigger_b4_dl_deny(request_st * const r, const plugin_data * const p) { + if (p->conf.deny_url) { + http_header_response_set(r, HTTP_HEADER_LOCATION, + CONST_STR_LEN("Location"), + BUF_PTR_LEN(p->conf.deny_url)); + r->http_status = 307; + } + else { + log_error(r->conf.errh, __FILE__, __LINE__, + "trigger-before-download.deny-url not configured"); + r->http_status = 500; + } + r->resp_body_finished = 1; + return HANDLER_FINISHED; +} + +URIHANDLER_FUNC(mod_trigger_b4_dl_uri_handler) { + plugin_data *p = p_d; + + if (NULL != r->handler_module) return HANDLER_GO_ON; + + mod_trigger_b4_dl_patch_config(r, p); + + if (!p->conf.trigger_regex || !p->conf.download_regex) return HANDLER_GO_ON; + +# if !defined(HAVE_GDBM_H) && !defined(USE_MEMCACHED) + return HANDLER_GO_ON; +# elif defined(HAVE_GDBM_H) && defined(USE_MEMCACHED) + if (!p->conf.db && !p->conf.memc) return HANDLER_GO_ON; + if (p->conf.db && p->conf.memc) { + /* can't decide which one */ + + return HANDLER_GO_ON; + } +# elif defined(HAVE_GDBM_H) + if (!p->conf.db) return HANDLER_GO_ON; +# else + if (!p->conf.memc) return HANDLER_GO_ON; +# endif + + /* X-Forwarded-For contains the ip behind the proxy */ + const buffer *remote_ip = + http_header_request_get(r, HTTP_HEADER_X_FORWARDED_FOR, + CONST_STR_LEN("X-Forwarded-For")); + if (NULL == remote_ip) { + remote_ip = &r->con->dst_addr_buf; + } + + if (p->conf.debug) { + log_error(r->conf.errh, __FILE__, __LINE__, "(debug) remote-ip: %s", remote_ip->ptr); + } + + const unix_time64_t cur_ts = log_epoch_secs; + + /* check if URL is a trigger -> insert IP into DB */ + if (mod_trigger_b4_dl_match(p->conf.trigger_regex, &r->uri.path)) { + /* the trigger matched */ +# if defined(HAVE_GDBM_H) + if (p->conf.db) { + datum key, val; + + *(const char **)&key.dptr = remote_ip->ptr; + key.dsize = buffer_clen(remote_ip); + + val.dptr = (char *)&cur_ts; + val.dsize = sizeof(cur_ts); + + if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) { + log_error(r->conf.errh, __FILE__, __LINE__, "insert failed"); + } + } +# endif +# if defined(USE_MEMCACHED) + if (p->conf.memc) { + buffer * const b = r->tmp_buf; + mod_trigger_b4_dl_memcached_key(b, p, remote_ip); + + if (p->conf.debug) { + log_error(r->conf.errh, __FILE__, __LINE__, "(debug) triggered IP: %s", b->ptr); + } + + if (MEMCACHED_SUCCESS != memcached_set(p->conf.memc, + BUF_PTR_LEN(b), + (const char *)&cur_ts, sizeof(cur_ts), + p->conf.trigger_timeout, 0)) { + log_error(r->conf.errh, __FILE__, __LINE__, "insert failed"); + } + } +# endif + } + + /* check if URL is a download -> check IP in DB, update timestamp */ + if (mod_trigger_b4_dl_match(p->conf.download_regex, &r->uri.path)) { + /* the download uri matched */ +# if defined(HAVE_GDBM_H) + if (p->conf.db) { + datum key, val; + unix_time64_t last_hit = 0; + + *(const char **)&key.dptr = remote_ip->ptr; + key.dsize = buffer_clen(remote_ip); + + val = gdbm_fetch(p->conf.db, key); + + if (val.dptr == NULL) { + /* not found, redirect */ + return mod_trigger_b4_dl_deny(r, p); + } + + if (val.dsize == sizeof(last_hit)) + memcpy(&last_hit, val.dptr, val.dsize); + else if (val.dsize == 4) { + int32_t t; + memcpy(&t, val.dptr, val.dsize); + last_hit = t; + } + + free(val.dptr); + + if (cur_ts - last_hit > p->conf.trigger_timeout) { + /* found, but timeout, redirect */ + + if (p->conf.db) { + if (0 != gdbm_delete(p->conf.db, key)) { + log_error(r->conf.errh, __FILE__, __LINE__, "delete failed"); + } + } + + return mod_trigger_b4_dl_deny(r, p); + } + + val.dptr = (char *)&cur_ts; + val.dsize = sizeof(cur_ts); + + if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) { + log_error(r->conf.errh, __FILE__, __LINE__, "insert failed"); + } + } +# endif +# if defined(USE_MEMCACHED) + if (p->conf.memc) { + buffer * const b = r->tmp_buf; + mod_trigger_b4_dl_memcached_key(b, p, remote_ip); + + if (p->conf.debug) { + log_error(r->conf.errh, __FILE__, __LINE__, "(debug) checking IP: %s", b->ptr); + } + + /** + * + * memcached is do expiration for us, as long as we can fetch it every thing is ok + * and the timestamp is updated + * + */ + if (MEMCACHED_SUCCESS != memcached_exist(p->conf.memc, BUF_PTR_LEN(b))) { + return mod_trigger_b4_dl_deny(r, p); + } + + /* set a new timeout */ + if (MEMCACHED_SUCCESS != memcached_set(p->conf.memc, + BUF_PTR_LEN(b), + (const char *)&cur_ts, sizeof(cur_ts), + p->conf.trigger_timeout, 0)) { + log_error(r->conf.errh, __FILE__, __LINE__, "insert failed"); + } + } +# endif + } + + return HANDLER_GO_ON; +} + +#if defined(HAVE_GDBM_H) +static void mod_trigger_b4_dl_trigger_gdbm(GDBM_FILE db, const unix_time64_t cur_ts, const int trigger_timeout) { + datum key, val, okey; + okey.dptr = NULL; + + /* according to the manual this loop + delete does delete all entries on its way + * + * we don't care as the next round will remove them. We don't have to perfect here. + */ + for (key = gdbm_firstkey(db); key.dptr; key = gdbm_nextkey(db, okey)) { + unix_time64_t last_hit = 0; + if (okey.dptr) { + free(okey.dptr); + okey.dptr = NULL; + } + + val = gdbm_fetch(db, key); + + if (val.dsize == sizeof(last_hit)) + memcpy(&last_hit, val.dptr, val.dsize); + else if (val.dsize == 4) { + int32_t t; + memcpy(&t, val.dptr, val.dsize); + last_hit = t; + } + + free(val.dptr); + + if (cur_ts - last_hit > trigger_timeout) { + gdbm_delete(db, key); + } + + okey = key; + } + if (okey.dptr) free(okey.dptr); + + /* reorg once a day */ + if ((cur_ts % (60 * 60 * 24) == 0)) gdbm_reorganize(db); +} + +TRIGGER_FUNC(mod_trigger_b4_dl_handle_trigger) { + /* check DB each minute */ + const unix_time64_t cur_ts = log_epoch_secs; + if (cur_ts % 60 != 0) return HANDLER_GO_ON; + UNUSED(srv); + + plugin_data * const p = p_d; + + /* (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + void *db = NULL; + int timeout = (int)p->defaults.trigger_timeout; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* trigger-before-download.gdbm-filename */ + if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) + db = cpv->v.v; + break; + case 4: /* trigger-before-download.trigger-timeout */ + timeout = (int)cpv->v.shrt; + break; + default: + break; + } + } + if (db) + mod_trigger_b4_dl_trigger_gdbm(db, cur_ts, timeout); + } + + return HANDLER_GO_ON; +} +#endif + + +#endif /* defined(HAVE_GDBM_H) || defined(USE_MEMCACHED) */ + + +int mod_trigger_b4_dl_plugin_init(plugin *p); +int mod_trigger_b4_dl_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = "trigger_b4_dl"; + +#if defined(HAVE_GDBM_H) || defined(USE_MEMCACHED) /* at least one required */ + + p->init = mod_trigger_b4_dl_init; + p->handle_uri_clean = mod_trigger_b4_dl_uri_handler; + p->set_defaults = mod_trigger_b4_dl_set_defaults; +#if defined(HAVE_GDBM_H) + p->handle_trigger = mod_trigger_b4_dl_handle_trigger; +#endif + p->cleanup = mod_trigger_b4_dl_free; + +#endif + + return 0; +} diff --git a/src/server.c b/src/server.c index 88b6e10d..52372c6a 100644 --- a/src/server.c +++ b/src/server.c @@ -919,6 +919,11 @@ static void show_features (void) { #else "\t- PAM support\n" #endif +#ifdef USE_MEMCACHED + "\t+ memcached support\n" +#else + "\t- memcached support\n" +#endif #if !defined(HAVE_SYS_INOTIFY_H) && !defined(HAVE_KQUEUE) #ifdef HAVE_FAM_H "\t+ FAM support\n" @@ -946,6 +951,11 @@ static void show_features (void) { "\t+ SQLite support\n" #else "\t- SQLite support\n" +#endif +#ifdef HAVE_GDBM_H + "\t+ GDBM support\n" +#else + "\t- GDBM support\n" #endif ; show_version(); -- 2.44.0