diff --git a/.gitignore b/.gitignore index 16a6e5f..420398a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1 @@ -Makefile -autom4te.cache -bashlib -config.log -config.status -configure +bashlib-*.tar.gz diff --git a/INSTALL b/INSTALL index 403bece..6ea8e81 100644 --- a/INSTALL +++ b/INSTALL @@ -1,11 +1,19 @@ -bashlib 0.5 +bashlib 4 -Installation of bashlib is relatively straightforward: +bashlib is a bash library: there is nothing to configure and nothing +to build, the checked-out file is the finished library (it calls no +external tools). To run the test suite: + + # make check + +To install (the default prefix is /usr/local): - # gunzip -c bashlib-0.5.tar.gz | tar xf - - # cd bashlib-0.5 - # ./configure # make install + # make install PREFIX=/usr # or wherever + +Packagers can use the usual DESTDIR redirection: + + # make install DESTDIR=$RPM_BUILD_ROOT Using bashlib in your CGI scripts is as simple as beginning your scripts with: @@ -38,7 +46,6 @@ needs to be done. Patches and feature requests are accepted. TODO * Better documentation, probably a man page - * Autoconf-based install script * More functions * Functions to generate HTML * Functions to set cookies diff --git a/Makefile.in b/Makefile similarity index 56% rename from Makefile.in rename to Makefile index be4c7ac..d1063b9 100644 --- a/Makefile.in +++ b/Makefile @@ -1,9 +1,6 @@ # Makefile for bashlib # ---------------------------------------------------------------------- -# $Id$ -# @configure_input@ -# ---------------------------------------------------------------------- -# bashlib +# bashlib # Copyright (C) 2002-2005 darren chamberlain # # This program is free software; you can redistribute it and/or modify @@ -20,35 +17,30 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA # ---------------------------------------------------------------------- -PREFIX = @prefix@ -VERSION = @bashlib_version@ +# There is nothing to configure or build: bashlib calls no external +# tools, so the checked-out file is the finished library. +PREFIX ?= /usr/local +DESTDIR ?= +VERSION = 4 all: - ./configure + @echo "nothing to build; try 'make check' or 'make install'" install: - @INSTALL@ bashlib $(PREFIX)/bin + mkdir -p $(DESTDIR)$(PREFIX)/bin + install -m 755 bashlib $(DESTDIR)$(PREFIX)/bin + +check: + ./run_tests.sh dist: - @MKDIR@ bashlib-$(VERSION) - @CP@ configure bashlib-$(VERSION)/ - @CP@ bashlib.in bashlib-$(VERSION)/ - @CP@ Makefile.in bashlib-$(VERSION)/ - @CP@ INSTALL bashlib-$(VERSION)/ - @CP@ COPYING bashlib-$(VERSION)/ + mkdir bashlib-$(VERSION) + cp bashlib Makefile INSTALL COPYING run_tests.sh bashlib-$(VERSION)/ + cp -r examples bashlib-$(VERSION)/ cd bashlib-$(VERSION); ln -s INSTALL README - @TAR@ cf bashlib-$(VERSION).tar bashlib-$(VERSION) - @GZIP@ --best bashlib-$(VERSION).tar - @RM@ -rf bashlib-$(VERSION) + tar cf bashlib-$(VERSION).tar bashlib-$(VERSION) + gzip --best bashlib-$(VERSION).tar + rm -rf bashlib-$(VERSION) clean: - @RM@ -f Makefile bashlib config.cache config.log config.status - @RM@ -fr ./autom4te.cache/ - -cvs-clean: - $(MAKE) clean - @RM@ -f configure - @RM@ -f bashlib-$(VERSION).tar.gz - -distclean: - $(MAKE) cvs-clean + rm -f bashlib-$(VERSION).tar bashlib-$(VERSION).tar.gz diff --git a/bashlib b/bashlib new file mode 100755 index 0000000..ce700b7 --- /dev/null +++ b/bashlib @@ -0,0 +1,287 @@ +#!/bin/bash + +# Author: darren chamberlain +# Co-Author: Paul Bournival +# Co-Author: Mikhail Novosyolov +# + +# bashlib is used by sourcing it at the beginning of scripts that +# needs its functionality (by using the . or source commands). +# +# The library targets bash: it relies on bash-only features such as +# ${!var} indirection, printf -v, ${!PREFIX@} listings, ${var//pat/rep} +# substitution and read -d. All hot paths are fork-free (builtins only): +# param()/cookie() used to spawn env|grep|sed|cut on every call, and the +# URL decoding used to fork once per %XX escape. + +# +# Set version number +# Must be an integer because bash cannot compare float numbers +# +VERSION="4" + +# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# Initialization stuff begins here. These things run immediately, and +# do the parameter/cookie parsing. +# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +# Characters removed by safe_param(), as one glob character class. +# The class is built by concatenation because a literal ' cannot +# appear inside a '...' string: '[$`<>"%;)(&+' + "'" + ']'. +SAFE_STRIP='[$`<>"%;)(&+'"'"']' + +# capture stdin for POST methods. POST requests don't always come in +# with a newline attached, so read everything up to EOF (the empty -d +# delimiter means NUL, which never occurs in CGI input). A plain +# $(cat) would fork; read is a builtin. +STDIN= +IFS= read -r -d '' STDIN || true +if [ -n "${STDIN}" ]; then + QUERY_STRING="${STDIN}&${QUERY_STRING}" +fi + +# Handle GET and POST requests... (the QUERY_STRING will be set) +if [ -n "${QUERY_STRING}" ]; then + # name=value params, separated by either '&' or ';' + case ${QUERY_STRING} in + *=*) + # '&' and ';' become spaces, so the unquoted expansion below splits + # QUERY_STRING into one word per parameter: + # "a=1&b=2;c=3" -> "a=1 b=2 c=3" -> words "a=1" "b=2" "c=3" + for Q in ${QUERY_STRING//[;&]/ } ; do + # + # Clear our local variables + # + unset name + value= + + # + # Decode the name of the key so that it can never break the eval + # below: turn %XX into \xHH for printf, decode '+' as a space, + # then strip '.', '-', '$' and '`'. + # + # cut at the first '=': "user%2Ename=admin" -> "user%2Ename" + name=${Q%%=*} + # every % becomes \x: "user%2Ename" -> "user\x2Ename" + name=${name//%/\\x} + # '+' is an encoded space: "a+b+c" -> "a b c" + name=${name//+/ } + # delete '.' and '-' (literal ones only; encoded %2E/%2D survive + # here and decode back below): "user.name-x" -> "usernamex" + name=${name//[.-]/} + # decode \xHH hex: "user\x2Enamex" -> "user.namex" + printf -v name '%b' "${name}" + # delete every '$' + name=${name//$/} + # delete every '`' + name=${name//\`/} + + # + # Decode the value: turn %XX into \xHH and let a single printf + # do the whole job. printf reads at most two hex digits per + # \xHH, so no separators between escapes are needed. + # + # cut at the first '=': "q=hello%21" -> "hello%21" + value=${Q#*=} + # decode %XX: "hello%21" -> "hello!" + printf -v value '%b' "${value//%/\\x}" + + eval "export FORM_${name}='${value}'" + done + ;; + *) # keywords: foo.cgi?a+b+c + # "alpha+beta+gamma" -> "alpha beta gamma" + eval "export KEYWORDS='${QUERY_STRING//+/ }'" + ;; + esac +fi + +# +# this section works identically to the query string parsing code, +# with the (obvious) exception that variables are stuck into the +# environment with the prefix COOKIE_ rather than FORM_. This is to +# help distinguish them from the other variables that get set +# automatically. +# +if [ -n "${HTTP_COOKIE}" ]; then + for Q in ${HTTP_COOKIE}; do + # + # Clear our local variables + # + name= + value= + + # + # drop one trailing ';': "session=abc123;" -> "session=abc123" + # + Q=${Q%;} + + # + # Decode the name of the key; see the parameter section above. + # + # cut at the first '=': "session=abc123" -> "session" + name=${Q%%=*} + # every % becomes \x: "a%73b" -> "a\x73b" + name=${name//%/\\x} + # '+' is an encoded space: "a+b" -> "a b" + name=${name//+/ } + # delete '.' and '-': "user.name" -> "username" + name=${name//[.-]/} + # decode \xHH hex: "a\x73b" -> "asb" + printf -v name '%b' "${name}" + + # + # Decode the cookie value; see the parameter section above. + # + # cut at the first '=': "q=a%20b" -> "a%20b" + value=${Q#*=} + # decode %XX: "a%20b" -> "a b" + printf -v value '%b' "${value//%/\\x}" + + # + # Export COOKIE_${name} into the environment + # + eval "export COOKIE_${name}='${value}'" + done +fi + +# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# functions and all that groovy stuff +# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# +# Shameless plug, advertises verion. +version() { + echo "bashlib, version ${VERSION}" +} + +version_html() { + echo -n "bashlib," + echo "version ${VERSION}" +} + +# +# Internal helper: store the raw stored value of parameter $1 in REPLY. +# Anything that is not a valid variable name yields an empty value. +# +_param_value() { + # "foo" and "FORM_foo" both give "FORM_foo" + local name="FORM_${1#FORM_}" + case ${name} in + # empty name or not a valid variable name -> empty value + FORM_|FORM_*[!A-Za-z0-9_]*) REPLY= ;; + # ${!name} is the value of the variable whose name is in $name + *) REPLY="${!name}" ;; + esac +} + +# +# Parameter function. +# * When called with no arguments, returns a list of parameters that +# were passed in. +# * When called with one argument, returns the value of that parameter +# (if any) +# * When called with more than one argument, assumes that the first is a +# paramter name and the rest are values to be assigned to a paramter of +# that name. +# +param() { + local name value + if [ $# -eq 1 ]; then + _param_value "$1" + # '+' is an encoded space: "a+b" -> "a b" + value="${REPLY//+/ }" + elif [ $# -gt 1 ]; then + name=$1 + shift + eval "export 'FORM_${name}=$*'" + return + else + # ${!FORM_@} expands to every existing variable named FORM_* + for name in ${!FORM_@}; do + # "FORM_foo" -> "foo" + echo "${name#FORM_}" + done + return + fi + echo "${value}" + unset name + unset value +} + +# shell invocation and X-site scripting prevention +safe_param() { + local value + if [ $# -eq 1 ]; then + _param_value "$1" + # '+' is an encoded space: "a+b" -> "a b" + value="${REPLY//+/ }" + # delete the SAFE_STRIP characters: 'a$b"c' -> 'abc' + echo "${value//${SAFE_STRIP}/}" + else + param "$@" + fi +} + +# cookie function. Same explanation as param +cookie() { + local name value + if [ $# -eq 1 ]; then + # "foo" and "COOKIE_foo" both give "COOKIE_foo" + name="COOKIE_${1#COOKIE_}" + case ${name} in + # empty name or not a valid variable name -> empty value + COOKIE_|COOKIE_*[!A-Za-z0-9_]*) value= ;; + # ${!name} is the value of the variable whose name is in $name + *) value="${!name}" ;; + esac + elif [ $# -gt 1 ]; then + name=$1 + shift + eval "export 'COOKIE_${name}=$*'" + return + else + # ${!COOKIE_@} expands to every existing variable named COOKIE_* + for name in ${!COOKIE_@}; do + # "COOKIE_foo" -> "foo" + echo "${name#COOKIE_}" + done + return + fi + echo "${value}" + unset name + unset value +} + +# keywords returns a list of keywords. This is only set when the script is +# called with an ISINDEX form (these are pretty rare nowadays). +keywords() { + echo "${KEYWORDS}" +} + +set_cookie() { + local name=$1 + shift + local value=$* + # "" -> "; a=1" -> "; a=1; b=2" + bashlib_cookies="${bashlib_cookies}; ${name}=${value}" + + # drop the leading ';' only (the space after it stays): "; a=1" -> " a=1" + bashlib_cookies=${bashlib_cookies#;} + + cookie "$name" "$value" +} + +# +# send_redirect takes a URI and redirects the browser to that uri, exiting +# the script along the way. +# +send_redirect() { + local uri + if [ $# -eq 1 ]; then + uri=$1 + else + uri="http://${SERVER_NAME}/${SCRIPT_NAME}" + fi + echo "Location: ${uri}" + echo "" +} diff --git a/bashlib.in b/bashlib.in deleted file mode 100644 index bcdae1c..0000000 --- a/bashlib.in +++ /dev/null @@ -1,250 +0,0 @@ -#!/bin/bash - -# Author: darren chamberlain -# Co-Author: Paul Bournival -# - -####### -# Updated Oct 15 2004 by Tony Clayton -# * add safe_param() function with XSS and shell-invocation prevention -# * add extra "| tr -d '$`'" sanity check to name decoding to prevent shell -# invocation of param names. -# * ported function defs to be bash/ash compatible -####### - -# bashlib is used by sourcing it at the beginning of scripts that -# needs its functionality (by using the . or source commands). - -PATH=/bin:/usr/bin - -# -# Set version number -# -VERSION="0.05" - -# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Initialization stuff begins here. These things run immediately, and -# do the parameter/cookie parsing. -# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -# Global debug flag. Set to 0 to disable debugging throughout the lib -DEBUG=0 - -# capture stdin for POST methods. POST requests don't always come in -# with a newline attached, so we use cat to grab stdin and append a newline. -# This is a wonderful hack, and thanks to paulb. -STDIN=$(@CAT@) -if [ -n "${STDIN}" ]; then - QUERY_STRING="${STDIN}&${QUERY_STRING}" -fi - -# Handle GET and POST requests... (the QUERY_STRING will be set) -if [ -n "${QUERY_STRING}" ]; then - # name=value params, separated by either '&' or ';' - if echo ${QUERY_STRING} | grep '=' >/dev/null ; then - for Q in $(@ECHO@ ${QUERY_STRING} | @TR@ ";&" "\012") ; do - # - # Clear our local variables - # - unset name - unset value - unset tmpvalue - - # - # get the name of the key, and decode it - # - name=${Q%%=*} - name=$(@ECHO@ ${name} | \ - @SED@ -e 's/%\(\)/\\\x/g' | \ - @TR@ "+" " ") - name=$(@ECHO@ ${name} | \ - @TR@ -d ".-") - name=$(@PRINTF@ ${name} | @TR@ -d '$`') - - # - # get the value and decode it. This is tricky... printf chokes on - # hex values in the form \xNN when there is another hex-ish value - # (i.e., a-fA-F) immediately after the first two. My (horrible) - # solution is to put a space aftet the \xNN, give the value to - # printf, and then remove it. - # - tmpvalue=${Q#*=} - tmpvalue=$(@ECHO@ ${tmpvalue} | \ - @SED@ -e 's/%\(..\)/\\\x\1 /g') - #echo "Intermediate \$value: ${tmpvalue}" 1>&2 - - # - # Iterate through tmpvalue and printf each string, and append it to - # value - # - for i in ${tmpvalue}; do - g=$(@PRINTF@ ${i}) - value="${value}${g}" - done - #value=$(echo ${value}) - - eval "export FORM_${name}='${value}'" - done - else # keywords: foo.cgi?a+b+c - Q=$(echo ${QUERY_STRING} | tr '+' ' ') - eval "export KEYWORDS='${Q}'" - fi -fi - -# -# this section works identically to the query string parsing code, -# with the (obvious) exception that variables are stuck into the -# environment with the prefix COOKIE_ rather than FORM_. This is to -# help distinguish them from the other variables that get set -# automatically. -# -if [ -n "${HTTP_COOKIE}" ]; then - for Q in ${HTTP_COOKIE}; do - # - # Clear our local variables - # - name= - value= - tmpvalue= - - # - # Strip trailing ; off the value - # - Q=${Q%;} - - # - # get the name of the key, and decode it - # - name=${Q%%=*} - name=$(@ECHO@ ${name} | \ - @SED@ -e 's/%\(\)/\\\x/g' | \ - @TR@ "+" " ") - name=$(@ECHO@ ${name} | \ - @TR@ -d ".-") - name=$(@PRINTF@ ${name}) - - # Decode the cookie value. See the parameter section above for - # an explanation of what this is doing. - tmpvalue=${Q#*=} - tmpvalue=$(@ECHO@ ${tmpvalue} | \ - @SED@ -e 's/%\(..\)/\\\x\1 /g') - #echo "Intermediate \$value: ${tmpvalue}" 1>&2 - - # - # Iterate through tmpvalue and printf each string, and append it to - # value - # - for i in ${tmpvalue}; do - g=$(@PRINTF@ ${i}) - value="${value}${g}" - done - #value=$(echo ${value}) - - # - # Export COOKIE_${name} into the environment - # - #echo "exporting COOKIE_${name}=${value}" 1>&2 - eval "export COOKIE_${name}='${value}'" - done -fi - -# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# functions and all that groovy stuff -# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# -# Shameless plug, advertises verion. -version { - echo "bashlib, version ${VERSION}" -} - -version_html { - echo -n "bashlib," - echo "version ${VERSION}" -} - -# -# Parameter function. -# * When called with no arguments, returns a list of parameters that -# were passed in. -# * When called with one argument, returns the value of that parameter -# (if any) -# * When called with more than one argument, assumes that the first is a -# paramter name and the rest are values to be assigned to a paramter of -# that name. -# -param() { - local name - local value - if [ $# -eq 1 ]; then - name=$1 - name=$(echo ${name} | @SED@ -e 's/FORM_//') - value=$(@ENV@ | @GREP@ "^FORM_${name}" | @SED@ -e 's/FORM_//' | @CUT@ -d= -f2-) - elif [ $# -gt 1 ]; then - name=$1 - shift - eval "export 'FORM_${name}=$*'" - else - value=$(@ENV@ | @GREP@ '^FORM_' | @SED@ -e 's/FORM_//' | @CUT@ -d= -f1) - fi - echo ${value} - unset name - unset value -} - -# shell invocation and X-site scripting prevention -safe_param() { - param $* | @TR@ -d '$`<>"%;)(&+'"'" -} - -# cookie function. Same explanation as param -cookie() { - local name - local value - if [ $# -eq 1 ]; then - name=$1 - name=$(echo ${name} | @SED@ -e 's/COOKIE_//') - value=$(@ENV@ | @GREP@ "^COOKIE_${name}" | @SED@ -e 's/COOKIE_//' | @CUT@ -d= -f2-) - elif [ $# -gt 1 ]; then - name=$1 - shift - eval "export 'COOKIE_${name}=$*'" - else - value=$(@ENV@ | @GREP@ '^COOKIE_' | @SED@ -e 's/COOKIE_//' | @CUT@ -d= -f1) - fi - echo ${value} - unset name - unset value -} - -# keywords returns a list of keywords. This is only set when the script is -# called with an ISINDEX form (these are pretty rare nowadays). -keywords() { - echo ${KEYWORDS} -} - -set_cookie() { - local name=$1 - shift - local value=$* - bashlib_cookies="${bashlib_cookies}; ${name}=${value}" - - bashlib_cookies=${bashlib_cookies#;} - - cookie $name $value -} - -# -# send_redirect takes a URI and redirects the browser to that uri, exiting -# the script along the way. -# -send_redirect() { - local uri - if [ $# -eq 1 ]; then - uri=$1 - else - uri="http://${SERVER_NAME}/${SCRIPT_NAME}" - fi - echo "Location: ${uri}" - echo "" -} - diff --git a/configure.in b/configure.in deleted file mode 100644 index a0ce2f1..0000000 --- a/configure.in +++ /dev/null @@ -1,47 +0,0 @@ -dnl configure.in for bashlib -dnl ---------------------------------------------------------------------- -dnl $Id$ -dnl ---------------------------------------------------------------------- -dnl bashlib -dnl Copyright (C) 2002-2005 darren chamberlain -dnl -dnl This program is free software; you can redistribute it and/or modify -dnl it under the terms of the GNU General Public License as published by -dnl the Free Software Foundation; version 2. -dnl -dnl This program is distributed in the hope that it will be useful, but -dnl WITHOUT ANY WARRANTY; without even the implied warranty of -dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -dnl General Public License for more details. -dnl -dnl You should have received a copy of the GNU General Public License -dnl along with this program; if not, write to the Free Software -dnl Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -dnl USA -dnl ---------------------------------------------------------------------- - -AC_REVISION($Revision$) - -dnl bashlib version -AC_SUBST(bashlib_version)dnl -bashlib_version=0.5 - -AC_INIT(bashlib.in) - -AC_PATH_PROG(AWK, awk, "") -AC_PATH_PROG(CAT, cat, "") -AC_PATH_PROG(CUT, cut, "") -AC_PATH_PROG(CP, cp, "") -AC_PATH_PROG(ECHO, echo, "") -AC_PATH_PROG(ENV, env, "") -AC_PATH_PROG(GREP, grep, "") -AC_PATH_PROG(GZIP, gzip, "") -AC_PATH_PROG(INSTALL, install, "") -AC_PATH_PROG(MKDIR, mkdir, "") -AC_PATH_PROG(PRINTF, printf, "") -AC_PATH_PROG(RM, rm, "") -AC_PATH_PROG(SED, sed, "") -AC_PATH_PROG(TAR, tar, "") -AC_PATH_PROG(TR, tr, "") - -AC_OUTPUT(Makefile bashlib) diff --git a/examples/bashlib.sourceforge.net_backup.html b/examples/bashlib.sourceforge.net_backup.html new file mode 100644 index 0000000..8642fd7 --- /dev/null +++ b/examples/bashlib.sourceforge.net_backup.html @@ -0,0 +1,111 @@ + + + bashlib - CGI programming with the bash shell + + +
+

bashlib - CGI programming with the bash shell

+ + [ Project Page + | Why? + | bashlib news + | How it came about + | Using bashlib ]
+ + [ CVs eXtender + | Something you don't like? + | Getting bashlib + | Other Resources + | Comments? ] +
+
+

Why?

+

bashlib is a shell script that makes CGI programming in the bash shell easier, or at least more tolerable. It contains a few functions that get called automatically and place form elements (from POSTs and GETs) and cookies in your environment. It also contains complete documentation on how to use these variables and how to set cookies manually.

+

Recent bashlib News

+

The most recent version of bashlib will always be available from http://bashlib.sourceforge.net/src/bashlib-current.tar.gz and will + be browsable at http://bashlib.sourceforge.net/src/bashlib-current/.

+

Version 0.4 (download or browse) of bashlib, released March 12, 2002, is fully autoconfiscated. To install it, run ./configure as usual. If you check out the CVS version, you'll need autoconf installed to create the configure script from configure.in.

+

I released version 0.3 (download or browse)of bashlib on February 21, 2001. The only difference between 0.3 and 0.2 is that the tarball works. I've been testing it for a while, and it definitely seems stable enough for real use. The interface has changed a little; the sample below reflects these changes.

+

How this came about, and what it is useful for

+

Things such as this are born of necessity. My ISP, while being incredibly useful in most ways, doesn't have a recent version of Perl available for their casual users (e.g., dial-up accounts, with web pages in http://www.isp.net/~mylogin/) -- Perl is a "value-added" service. This was a great source of frustration for me, since I am a Perl programmer by day. So, I began rooting (not literally) around for other methods of writing CGI scripts, and realized that the shell, which I spent most of my time using, was not only a great generaly purpose scripting environment, but also a good one for CGI scripts. This naturally led me to forms and cookies, and here we are today.

+

No, I really don't have this much free time. The current release is the result of about a days worth of work (on and off throughout the day), with some help. Well, that's not quite true -- 9 years of using Unix went into it as well.

+

Using bashlib

+

Using bashlib is pretty straight-forward. More important, however, is knowing what to do with the variables once they come into your script and knowing how to write CGI scripts. (This script is not running here, for obvious reasons.)

+
+#!/bin/bash
+
+# this sources bashlib into your current environment
+. /usr/local/lib/bashlib
+
+echo "Content-type: text/html"
+echo ""
+
+# OK, so we've sent the header... now send some content
+echo "<html><title>Crack This Server</title><body>"
+
+# print a "hello" if the username is filled out
+username=`param username`
+if [ -n "x$username" != "x" ] ; then
+    echo "<h1>Hello, $username</h1>
+fi
+
+echo "<h2>Users on `/bin/hostname`</h2>"
+echo "<ul>"
+
+# for each user in the passwd file, print their login and full name
+# bold them if they are the current user
+for user in $(cat /etc/passwd | awk -F: '{print $1 "\t" $5}') ; do
+    echo "<li>"
+    if [ "$username" = "$user" ] ; then
+        echo "<strong>$user</strong>"
+    else
+        echo "$user"
+    fi
+    echo "</li>"
+done
+echo "</ul>"
+echo "</body></html>"
+  
+ + + +

Other Resoruces

+
    +
  • I recommend checking out bashish if you use bash often; it is dedicated to the configuration of bash. Although it isn't helpful for CGI programming in bash, it makes day to day usage of bash quite nice. To quote:
  • +
    +Bashish is a theme engine for the console.
    +It lets you customize title, prompt, background, foreground, colors, font and a lot of other things.
    +Bashish is also very configurable, you can turn on and off nearly all features.
    +
    +
  • ^txt2regex$ is a Regular Expression "wizard", all written with bash2 builtins, that converts human sentences to RegExs. with a simple interface, you just answer to questions and build your own RegEx for a large variety of programs, like awk, ed, emacs, grep, perl, php, procmail, python, sed and vim. there are more than 20 supported programs. it's bash so download and run, no compilation needed.
  • +
  • For folks who do a lot of command-line-based web work, surfaw might be useful:
  • +
    + Surfraw (Shell Users' Revolutionary Front Rage Against the Web) provides a Unix command line interface to a variety of popular Web search engines and sites, including Google, Altavista, Babelfish, Raging, DejaNews, Research Index, Yahoo!, WeatherNews, Slashdot, freshmeat, and many others. +
    +
  • The BASH Programming - Introduction HOW-TO and Advanced Bash-Scripting HOWTO: A guide to shell scripting, using Bash are very, very useful.
  • +
  • I just came across Prentice Hall's 1996 book Portable Shell Programming, and it is wonderful. All the examples are pure Bourne shell, so they are (naturally) portable, and also very informative.
  • +
  • And, finally, while it's not free, David Tansley's Linux & Unix Shell Programming is a wonderful resource (get it from fatbrain).
  • +
+ + + +

Something you don't like?

+

OK, so there's probably something in here you think should be different, could be better, etc. Well, drop me a line (email me at dlc@users.sourceforge.net) and let me know. You can write it out in words ("bashlib should uuencode GIFs on the fly"), provide a patch (via diff -u), or rewrite the whole thing (try to keep it in shell, though). All reasonable emails will be read, and probably answered as well.

+ + + +

Getting bashlib

+

bashlib is a pretty short script (as libraries go), and all versions can be browsed here. It is available for download from Sourceforge at http.

+

bashlib is also available from anonymous CVS. The CVS repository can be checked out through anonymous (pserver) CVS. When prompted for a password for anonymous, simply press the Enter key.

+
+cvs -d:pserver:anonymous@cvs.bashlib.sourceforge.net:/cvsroot/bashlib login 
+cvs -z3 -d:pserver:anonymous@cvs.bashlib.sourceforge.net:/cvsroot/bashlib co bashlib
+
+
+ SourceForge Logo +
+ diff --git a/examples/promo-codes.sh b/examples/promo-codes.sh new file mode 100644 index 0000000..781c5bd --- /dev/null +++ b/examples/promo-codes.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# CGI script +# Does not work with FastCGI (fcgi) because it requires additional communication, see https://unix.stackexchange.com/a/241694 +set -e +set -f +#set -u # BAH01215: ./bashlib: line 82: value: unbound variable + +# https://stackoverflow.com/questions/3919755/how-to-parse-query-string-from-a-bash-cgi-script + +# source bashlib https://github.com/mikhailnov/bashlib +. ./bashlib 2>/dev/null || . bashlib || ( echo "Failed to source bashlib!" ; exit 1 ) + +# get file with promo codes +for file in './promo_codes_1.txt' '/var/www/domain.tld/promo_codes_1.txt' +do + [ -f "$file" ] && file_codes="$file" && break +done + +do_redirect_back(){ +code_error_type="${code_error_type:-неверный}" +echo -n " + +
+Ошибка: вы ввели ${code_error_type} промо-код! +
+
+
" +} + +do_redirect_forward(){ +redir_URL='https://domain.tld/page2' +echo -n " + + + + + +Вы будете перенаправлены на страницу записи. Если она не открылась, перейдите по ссылке. +" +} + +write_used_code(){ + # used codes will be converted to empty lines + # TODO: check if this file is writable and redirect back in case of error + sed -e "s/${code}//g" -i "$file_codes" +} + +code="$(param code)" +if grep -q "$code" "$file_codes" + then write_used_code && do_redirect_forward # don't redirect if failed to write_used_code + else do_redirect_back +fi diff --git a/examples/write-to-file.sh b/examples/write-to-file.sh new file mode 100755 index 0000000..d76ebf0 --- /dev/null +++ b/examples/write-to-file.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Example request: +# http://localhost/cgi-bin/r8168.cgi?rev=0x00&probe=b0cdd5070e + +# https://github.com/mikhailnov/bashlib, https://abf.io/import/bashlib +. bashlib ||{ echo "Failed to source bashlib!" ; exit 1 ;} + +readonly file="/var/www/r8168.list" +readonly lock="/var/www/r8168.lock" +readonly rev="$(safe_param rev)" +readonly probe="$(safe_param probe)" + +# possible values: 0x15, 0x09 +if ! [[ "$rev" =~ ^0x..$ ]]; then + echo "Status: 400 Bad request" + echo "Content-Type: text/plain; charset=utf-8" + echo "" + echo "Incorrect value of rev" + exit 1 +fi + +if [ ${#probe} -gt 20 ] || ! [[ "$probe" =~ ^[a-zA-Z0-9]+$ ]]; then + echo "Status: 400 Bad request" + echo "Content-Type: text/plain; charset=utf-8" + echo "" + echo "Incorrect value of probe" + exit 1 +fi + +set -e +echo "$rev;$probe" | flock "$lock" tee -a "$file" >/dev/null +echo "Status: 200 OK" +echo "Content-Type: text/plain; charset=utf-8" +echo "" +echo "OK" diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..5f52d09 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,248 @@ +#!/bin/sh +# +# run_tests.sh -- self-contained test suite for bashlib functions. +# +# bashlib parses QUERY_STRING, HTTP_COOKIE and stdin at source time, so +# every case sources the library in a pristine environment (env -i) with +# a controlled CGI environment. stdout of each case is compared +# byte-for-byte with the expected output (trailing newline included). +# +# Usage: tests/run_tests.sh (or: make check from the top directory) + +PASS=0 +FAIL=0 +TOTAL=0 + +here=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) || exit 1 +LIB=$here/bashlib + +if [ ! -f "$LIB" ]; then + echo "bashlib not found at $LIB -- broken checkout?" >&2 + exit 1 +fi + +BASH_PROG=$(command -v "${BASH:-bash}" 2>/dev/null) || BASH_PROG= +if [ -z "$BASH_PROG" ] || [ ! -x "$BASH_PROG" ]; then + echo "bash interpreter not found" >&2 + exit 1 +fi + +TMP=${TMPDIR:-/tmp}/bashlib-tests.$$ +(umask 077 && mkdir "$TMP") || { echo "cannot create $TMP" >&2; exit 1; } +trap 'rm -rf "$TMP"' EXIT +trap 'rm -rf "$TMP"; exit 1' INT TERM + +# make_case -- write a snippet that sources bashlib then runs +make_case() { + printf '. "%s"\n%s\n' "$LIB" "$1" > "$TMP/case.sh" +} + +# bl [ENV=VAL ...] -- run a case with empty stdin (plain GET request) +bl() { + code=$1; shift + make_case "$code" + env -i "$@" "$BASH_PROG" --norc "$TMP/case.sh" >"$TMP/got" 2>"$TMP/err" [ENV=VAL ...] -- run a case with on stdin (POST) +bl_post() { + data=$1; code=$2; shift 2 + make_case "$code" + printf '%s' "$data" | env -i "$@" "$BASH_PROG" --norc "$TMP/case.sh" >"$TMP/got" 2>"$TMP/err" +} + +# show -- render file bytes with visible line ends +show() { + awk '{ printf "%s\\n", $0 } END { if (NR == 0) print "(empty)" }' "$1" +} + +# ok -- compare stdout of the last bl*() run with +# , a printf %b string ("line1\nline2\n"). +ok() { + desc=$1; exp=$2 + TOTAL=$((TOTAL + 1)) + printf '%b' "$exp" > "$TMP/want" + if cmp -s "$TMP/want" "$TMP/got"; then + PASS=$((PASS + 1)) + printf 'ok %2d - %s\n' "$TOTAL" "$desc" + else + FAIL=$((FAIL + 1)) + printf 'not ok %2d - %s\n' "$TOTAL" "$desc" + printf ' expected: %s\n' "$(show "$TMP/want")" + printf ' actual: %s\n' "$(show "$TMP/got")" + if [ -s "$TMP/err" ]; then + printf ' stderr:\n' + sed 's/^/ /' "$TMP/err" + fi + fi +} + +echo "bashlib test suite" +echo " library: $LIB" +echo + +# --- version ------------------------------------------------------------ + +bl 'version' +ok 'version prints name and release' 'bashlib, version 4\n' + +bl 'version_html' +ok 'version_html prints html link and version' \ + 'bashlib,version 4\n' + +# --- GET parameter parsing ---------------------------------------------- + +bl 'param name' 'QUERY_STRING=name=value' +ok 'GET: single name=value parameter' 'value\n' + +bl 'param a; param b; param c' 'QUERY_STRING=a=1&b=2;c=3' +ok 'GET: parameters separated by & and ;' '1\n2\n3\n' + +bl 'param q' 'QUERY_STRING=q=hello+world' +ok 'GET: + decodes to space' 'hello world\n' + +bl 'param q' 'QUERY_STRING=q=one+two+three+four' +ok 'GET: every + decodes to space, not only the first one' 'one two three four\n' + +bl 'param q' 'QUERY_STRING=q=a++b' +ok 'GET: consecutive + decode to consecutive spaces' 'a b\n' + +bl 'param w' 'QUERY_STRING=w=hello%20world' +ok 'GET: %XX hex escapes decode' 'hello world\n' + +bl 'param p' 'QUERY_STRING=p=b%2Bc%2Bd' +ok 'GET: every encoded %2B is returned as space' 'b c d\n' + +bl 'param s' 'QUERY_STRING=s=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82' +ok 'GET: multi-byte utf-8 %XX sequences decode' 'привет\n' + +bl 'param org' 'QUERY_STRING=org=%D0%90%D0%9E+%D0%90%D0%BB%D1%8C%D1%84%D0%B0-%D0%91%D0%B0%D0%BD%D0%BA+%D0%A1%D1%83%D0%BF%D0%B5%D1%80' +ok 'GET: literal - before %XX does not break decoding' 'АО Альфа-Банк Супер\n' + +bl 'param q' 'QUERY_STRING=q=-%D0%B0y' +ok 'GET: %-escape glued after a literal - decodes fully' '-аy\n' + +bl 'param q' 'QUERY_STRING=q=-start' +ok 'GET: value starting with - is not eaten' '-start\n' + +bl 'param x' 'QUERY_STRING=x=50%25+off' +ok 'GET: decoded % char survives round trip' '50% off\n' + +bl 'param usernamex' 'QUERY_STRING=user.name-x=1' +ok 'GET: dots and dashes are stripped from names' '1\n' + +bl 'param asb' 'QUERY_STRING=a%73b=1' +ok 'GET: %XX escapes in names decode' '1\n' + +bl 'param a' 'QUERY_STRING=a=' +ok 'GET: empty value yields empty string' '\n' + +bl 'param | grep -c .' +ok 'no CGI input: param lists nothing' '0\n' + +# --- param() ------------------------------------------------------------ + +bl 'param | sort' 'QUERY_STRING=b=2&a=1&c=3' +ok 'param: no arguments lists parameter names' 'a\nb\nc\n' + +bl 'param FORM_a' 'QUERY_STRING=a=1' +ok 'param: FORM_ prefix is stripped from the argument' '1\n' + +bl 'param foo bar baz +param foo' +ok 'param: set value silently and read it back' 'bar baz\n' + +# --- safe_param() ------------------------------------------------------- + +bl 'safe_param s' 'QUERY_STRING=s=%24%60%3C%3E%22%25%3B%29%28%26%2B' +ok 'safe_param: shell/html metacharacters are removed' ' \n' + +bl 'safe_param x' 'QUERY_STRING=x=%3Cscript%3Ealert%281%29%3C%2Fscript%3E' +ok 'safe_param: XSS payload is neutralised' 'scriptalert1/script\n' + +bl 'safe_param msg' 'QUERY_STRING=msg=hello+world' +ok 'safe_param: benign text and spaces survive' 'hello world\n' + +bl 'safe_param msg' 'QUERY_STRING=msg=hello+world+123' +ok 'safe_param: spaces are not lost after the first +' \ + 'hello world 123\n' + +bl 'safe_param "foo bar" >/dev/null +param | grep -c .' +ok 'safe_param: a name with spaces does not silently set a param' '0\n' + +# --- keywords() --------------------------------------------------------- + +bl 'keywords' 'QUERY_STRING=alpha+beta+gamma' +ok 'keywords: isindex-style query becomes a keyword list' 'alpha beta gamma\n' + +bl 'keywords' 'QUERY_STRING=alpha++beta' +ok 'keywords: consecutive + keep consecutive spaces' 'alpha beta\n' + +# --- POST via stdin ----------------------------------------------------- + +bl_post 'a=1&b=2' 'param a; param b' +ok 'POST: stdin is parsed as form data' '1\n2\n' + +bl_post 'a=1' 'param a; param b' 'QUERY_STRING=b=2' +ok 'POST: stdin params are merged with QUERY_STRING' '1\n2\n' + +# --- cookie() ----------------------------------------------------------- + +bl 'cookie session; cookie theme' 'HTTP_COOKIE=session=abc123; theme=dark' +ok 'cookies: HTTP_COOKIE is parsed' 'abc123\ndark\n' + +bl 'cookie | sort' 'HTTP_COOKIE=session=abc123; theme=dark' +ok 'cookie: no arguments lists cookie names, one per line' 'session\ntheme\n' + +bl 'cookie org' 'HTTP_COOKIE=org=%D0%90%D0%BB%D1%8C%D1%84%D0%B0-%D0%91%D0%B0%D0%BD%D0%BA' +ok 'cookies: literal - before %XX does not break decoding' 'Альфа-Банк\n' + +bl 'cookie foo bar qux +cookie foo' +ok 'cookie: set value silently and read it back' 'bar qux\n' + +bl 'cookie foo "a b" +cookie foo' +ok 'cookie: consecutive spaces in a value survive' 'a b\n' + +# --- set_cookie() ------------------------------------------------------- + +# $bashlib_cookies must stay literal: it is expanded by the inner bash of +# the test case, not by the runner. +# shellcheck disable=SC2016 +bl 'set_cookie theme light +set_cookie lang en +echo "[$bashlib_cookies]" +cookie theme +cookie lang' +ok 'set_cookie: accumulates pairs silently and exports them (leading space is current behaviour)' \ + '[ theme=light; lang=en]\nlight\nen\n' + +# Same as above: expanded inside the case's inner bash. +# shellcheck disable=SC2016 +bl 'set_cookie m "a b" +echo "[$bashlib_cookies]" +cookie m' +ok 'set_cookie: preserves consecutive spaces in values' '[ m=a b]\na b\n' + +# --- send_redirect() ---------------------------------------------------- + +bl 'send_redirect http://example.org/x' +ok 'send_redirect: emits Location header and a blank line' \ + 'Location: http://example.org/x\n\n' + +bl 'send_redirect' 'SERVER_NAME=www.example.org' 'SCRIPT_NAME=cgi-bin/app.cgi' +# Description text; $SERVER_NAME is literal. +# shellcheck disable=SC2016 +ok 'send_redirect: defaults to http://$SERVER_NAME/$SCRIPT_NAME' \ + 'Location: http://www.example.org/cgi-bin/app.cgi\n\n' + +# --- summary ------------------------------------------------------------ + +echo +echo "$PASS of $TOTAL tests passed" +if [ "$FAIL" -ne 0 ]; then + echo "$FAIL test(s) failed" >&2 + exit 1 +fi