#!/bin/sh -e # # # DASHT-SERVER-HTTP 1 2020-05-16 2.4.0 # # ## NAME # # dasht-server-http - simple search engine that powers dasht-server(1) # # ## SYNOPSIS # # `dasht-server-http` # # ### Examples # # printf 'GET / HTTP/1.1\r\n' | `dasht-server-http` # Shows topics (A-Z) from each installed [Dash] docset. # # printf 'GET /?docsets=*DOCSETS* HTTP/1.1\r\n' | `dasht-server-http` # Shows topics (A-Z) from installed [Dash] docsets matching *DOCSETS*. # # printf 'GET /?query=*PATTERN* HTTP/1.1\r\n' | `dasht-server-http` # Searches for *PATTERN* in all installed [Dash] docsets. # # printf 'GET /?query=*PATTERN*&docsets=*DOCSETS* HTTP/1.1\r\n' | `dasht-server-http` # Searches for *PATTERN* in installed [Dash] docsets matching *DOCSETS*. # # ## DESCRIPTION # # Reads a single HTTP request from stdin and writes a HTTP response to stdout. # Any `query=`*PATTERN* and `docsets=`*DOCSETS* parameters in the request URL # are passed to dasht-query-html(1) as its *PATTERN* and *DOCSETS* arguments. # # ## ENVIRONMENT # # `DASHT_DOCSETS_DIR` # Defines the filesystem location where your [Dash] docsets are installed. # If undefined, its value is assumed to be `$XDG_DATA_HOME/dasht/docsets/` # or, if `XDG_DATA_HOME` is undefined, `$HOME/.local/share/dasht/docsets/`. # # ## SEE ALSO # # dasht-query-html(1), dasht-server(1), dasht-docsets(1), dasht(1), [Dash] # # [Dash]: https://kapeli.com/dash # # ## AUTHOR # # Written in 2016 by Suraj N. Kurapati <https://github.com/sunaku/dasht> # Distributed under the terms of the ISC license (refer to README file). # Escapes XML "predefined entities" in given arguments. # See http://www.w3.org/TR/REC-xml/#sec-predefined-ent entities() { echo "$*" | sed -e 's/&/\&/g' \ -e 's/"/\"/g' \ -e "s/'/\'/g" \ -e 's/</\</g' \ -e 's/>/\>/g' } # Converts the given 2-xdigit string into a byte value. # See http://mywiki.wooledge.org/BashFAQ/071 hex2byte() { dec=$(( 0x$1 )) oct=$(printf '%03o' "$dec") printf "\\$oct" } # parse URL from request: only GET is supported url=$(awk ' /^GET [[:print:]]+ HTTP\/1.+\r$/ { print $2 } /^\r$/ { exit } # reached end of HTTP request ') # serve translated local file:// URLs over HTTP # (skip the / homepage and /? form submissions) if ! test "$url" = / -o -z "${url##/\?*}"; then file=${url%#*} # strip URI fragment, if any if test -f "$file"; then printf 'HTTP/1.0 200 OK\r\n' printf 'Content-Type: %s\r\n' "$(file -b --mime "$file" 2>&1)" printf '\r\n' cat "$file" 2>&1 || : else printf 'HTTP/1.0 400 Bad Request\r\n' printf '\r\n' fi exit fi # parse URL query parameters as shell variables eval "$( # split URL on query parameter delimiters (?&) IFS='?&' set -- $url shift # protocol, hostname, port number, path # convert segments into safely eval()able assignments for segment; do # split param=value param=${segment%%=*} value=${segment#*=} # only accept known parameters to prevent injection case "$param" in (query|docsets) # decode URL-encoded characters in parameter value value=$(echo "$value" | tr '+' ' ' | # + is space sed 's/%\([[:xdigit:]]\{2\}\)/$(hex2byte \1)/g') if test -n "$value"; then echo "$param=\"\$$param\${$param:+ }$value\"" echo "${param}_html=\$(entities \"\$$param\")" fi ;;esac done )" # emit response header printf 'HTTP/1.0 200 OK\r\n' printf 'Content-Type: text/html\r\n' printf '\r\n' # emit response body docsets_menu=$( { dasht-docsets # all installed docsets dasht-docsets $docsets # the current selection } | sort | uniq -c | awk '{ $1 = $1 > 1; print }' ) set -- $(echo "$docsets_menu" | awk ' BEGIN { matched = ignored = 0 } $1 { matched++ } !$1 { ignored++ } END { print matched, ignored, NR } ') cat <<HEADER <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>dasht $query_html $docsets_html</title> <style> body, input, select { font-family: monospace; } form { display: flex; } tr:hover { background-color: LightYellow; } a { text-decoration: none; } a:hover { text-decoration: underline; } b > i > u { text-decoration: none; font-style: normal; font-weight: normal; color: Black; background-color: Yellow; border: thin dotted Silver; } h1 > i { color: red; } </style> </head> <body> <form method="get" action="/"> <div> <label>Search for <input name="query" value="$query_html" type="search" placeholder="space is wildcard" autofocus> </label> <br> <label>in docsets <select name="docsets"> <option value="" selected>matched $1 out of $3</option> $(echo "$docsets_menu" | awk -v were_any_ignored=$2 '{ label = were_any_ignored && $1 ? "((( " $2 " )))" : $2 regex = $2; gsub("[[:punct:]]", "\\\\&", regex) value = "^" regex "$" print "<option value=\"" value "\">" label "</option>" }') </select> </label> <br> <label>with names <input name="docsets" value="$docsets_html" type="search" placeholder="chosen from above"> </label> <br> </div> <input type="submit"> </form> <br> HEADER if test $1 -eq 0; then # notify user when no docsets are installed so they can go install them echo "<h1><i>${docsets_html}</i> docsets not installed so <i>$query_html</i> not searched</h1>" elif results_html=$(dasht-query-html "$query" $docsets); then # translate local file:// URLs into HTTP ones so that we can serve them echo "$results_html" | sed 's,file://,,' else # notify user when no results are found so they can refine their search echo "<h1><i>$query_html</i> not found in $1 docsets matching <i>${docsets_html:-.*}</i></h1>" fi cat <<FOOTER </body> </html> FOOTER