From 35dba9bf0c4a5546452a6e2c841abeca83f1bbec Mon Sep 17 00:00:00 2001 From: Case Duckworth Date: Fri, 22 May 2020 15:53:35 -0500 Subject: It works! --- bollux | 370 +++++++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 209 insertions(+), 161 deletions(-) diff --git a/bollux b/bollux index 50246c9..8026179 100755 --- a/bollux +++ b/bollux @@ -1,191 +1,239 @@ -#!/bin/bash -# bollux: bash gemini client +#!/usr/bin/env bash +# bollux: a bash gemini client or whatever # Author: Case Duckworth # License: MIT -# Version: -1 +# Version: -0.8 -PRGN="${0##*/}" +set -euo pipefail -main() { - if [[ -z "$1" ]]; then - echo "usage: $PRGN " - return 1 - fi +### constants ### +PRGN="${0##*/}" # program name +PROT="${BOLLUX_PROTO:-gemini}" # protocol +PORT="${BOLLUX_PORT:-1965}" # port number +LOGL="${BOLLUX_LOGLEVEL:-3}" # log level +MAXR="${BOLLUX_MAXREDIR:-5}" # max redirects +RDRS=0 # redirects + +# LOGLEVELS: +# 0 - application fatal error +# 1 - application warning +# 2 - response error +# 3 - response logging +# 4 - application logging +# 5 - diagnostic + +### utility functions ### +# a better echo +put() { printf '%s\n' "$*"; } - log "$PRGN $*" - log address="$(address "$1"|tr '[:space:]' 'x')" - log server="$(server "$1")" +# conditionally log events to stderr +# lower = more important +log() { # log [LEVEL] [<] MESSAGE + case "$1" in + [0-5]) + lvl="$1" + shift + ;; + *) lvl=4 ;; + esac - address "$1" | - download "$(server "$1")" 2>/dev/null | - handle_status "$1" + output="$*" + if ((lvl < LOGL)); then + if (($# == 0)); then + while IFS= read -r line; do + output="$output${output:+$'\n'}$line" + done + fi + printf '\e[3%dm%s\e[0m:\t%s\n' "$((lvl + 1))" "$PRGN" "$output" >&2 + fi } -log() { - printf '\e[34m%s:\t%s\e[0m\n' "$PRGN" "$*" >&2 +# halt and catch fire +die() { # die [EXIT-CODE] MESSAGE + case "$1" in + [0-9]*) + ec="$1" + shift + ;; + *) ec=1 ;; + esac + + log 0 "$*" + exit "$ec" } -address() { - local addr="$1" - if [[ "$addr" != gemini://* ]]; then - addr="gemini://$addr" - fi - echo "$addr" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' +# fail if something isn't installed +require() { hash "$1" 2>/dev/null || die 127 "Requirement '$1' not found."; } + +# trim a string +trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; } + +# stubs for when things aren't implemented (fully) +NOT_IMPLEMENTED() { die 200 "NOT IMPLEMENTED!!!"; } +NOT_FULLY_IMPLEMENTED() { log 1 "NOT FULLY IMPLEMENTED!!!"; } + +### gemini ### +# normalize a gemini address +# example.com => gemini://example.com/ +_address() { # _address URL + addr="$1" + [[ "$addr" != *://* ]] && addr="$PROT://$addr" + trim <<<"$addr" } -server() { - local serv="${1#*://}" +# return only the server part from an address, with the port added +# gemini://example.com/path/to/file => example.com:1965 +_server() { + serv="$(_address "$1")" # normalize first + serv="${serv#*://}" serv="${serv%%/*}" - if [[ ! "$serv" = *:* ]]; then - serv="$serv:1965" + if [[ "$serv" != *:* ]]; then + serv="$serv:$PORT" fi - echo "$serv" + trim <<<"$serv" } -download() { - openssl s_client -crlf -ign_eof -quiet -connect "$1" -} +# request a gemini page +# by default, extract the server from the url +request() { # request [-s SERVER] URL + case "$1" in + -s) + serv="$(_server "$2")" + addr="$(_address "$3")" + ;; + *) + serv="$(_server "$1")" + addr="$(_address "$1")" + ;; + esac -display() { - addr="$(address "$1")" - printf ' 🚀 %s 🚀\n' "$addr" - echo - cat - printf ' 🚀 END: %s 🚀\n' "$addr" -} + log 5 "serv: $serv" + log 5 "addr: $addr" -NOT_IMPLEMENTED() { - log "NOT IMPLEMENTED!!!" >&2 - exit 127 -} -NOT_FULLY_IMPLEMENTED() { - log "NOT FULLY IMPLEMENTED!!!" >&2 + t="$(mktemp)" + + sslcmd=(openssl s_client -crlf -ign_eof -quiet -connect "$serv") + log "${sslcmd[@]}" + "${sslcmd[@]}" <<<"$addr" 2>"$t" + + ((LOGL > 4)) && cat "$t" + rm "$t" } -handle_status() { - # cf. https://gemini.circumlunar.space/docs/spec-spec.txt - addr="$(address "$1")" - IN="$(cat)" - head="$(head -n1 <<<"$IN")" - stat="$(awk '{print $1}' <<<"$head")" - msg="$(awk '{for(i=2;i<=NF;i++)printf "%s ", $i;printf "\n";}' <<<"$head")" - log "$stat $msg" - case "$stat" in - 10) # INPUT - # As per definition of single-digit code 1 in 1.3.2. - NOT_IMPLEMENTED - ;; - 20) # SUCCESS - # As per definition of single-digit code 2 in 1.3.2. - display "$addr" <<<"$IN" +# handle the response +# cf. gemini://gemini.circumlunar.space/docs/spec-spec.txt +handle() { # handle URL < RESPONSE + url="$(_address "$1")" + resp="$(cat)" + head="$(sed 1q <<<"$resp")" + body="$(sed 1d <<<"$resp")" + + code="$(awk '{print $1}' <<<"$head")" + meta="$(awk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$head")" + + log 5 "[$code] $meta" + + case "$code" in + 1*) # INPUT + log 3 "Input" + put "$meta" + read -rep "? " + bollux "$url?$REPLY" ;; - 21) # SUCCESS - END OF CLIENT CERTIFICATE SESSION - # The request was handled successfully and a response body will - # follow the response header. The line is a MIME media - # type which applies to the response body. In addition, the - # server is signalling the end of a transient client certificate - # session which was previously initiated with a status 61 - # response. The client should immediately and permanently - # delete the certificate and accompanying private key which was - # used in this request. - display "$addr" <<<"$IN" - NOT_FULLY_IMPLEMENTED + 2*) # SUCCESS + log 3 "Success" + case "$code" in + 20) log 5 "- OK" ;; + 21) log 5 "- End of client certificate session" ;; + *) log 2 "- Unknown response code: '$code'." ;; + esac + display <<<"$body" ;; - 30) # REDIRECT - TEMPORARY - # As per definition of single-digit code 3 in 1.3.2. - exec "$0" "$msg" + 3*) # REDIRECT + log 3 "Redirecting" + case "$code" in + 30) log 5 "- Temporary" ;; + 31) log 5 "- Permanent" ;; + *) log 2 "- Unknown response code: '$code'." ;; + esac + ((RDRS+=1)) + ((RDRS > MAXR)) && die "$code" "Too many redirects!" + bollux "$meta" ;; - 31) # REDIRECT - PERMANENT - # The requested resource should be consistently requested from - # the new URL provided in future. Tools like search engine - # indexers or content aggregators should update their - # configurations to avoid requesting the old URL, and end-user - # clients may automatically update bookmarks, etc. Note that - # clients which only pay attention to the initial digit of - # status codes will treat this as a temporary redirect. They - # will still end up at the right place, they just won't be able - # to make use of the knowledge that this redirect is permanent, - # so they'll pay a small performance penalty by having to follow - # the redirect each time. - exec "$0" "$msg" - NOT_FULLY_IMPLEMENTED + 4*) # TEMPORARY FAILURE + log 2 "Temporary failure" + case "$code" in + 41) log 5 "- Server unavailable" ;; + 42) log 5 "- CGI error" ;; + 43) log 5 "- Proxy error" ;; + 44) log 5 "- Rate limited" ;; + *) log 2 "- Unknown response code: '$code'." ;; + esac + exit "$code" ;; - 4*) # 40 - TEMPORARY FAILURE - # As per definition of single-digit code 4 in 1.3.2. - # 41 - SERVER UNAVAILABLE - # The server is unavailable due to overload or maintenance. - # (cf HTTP 503) - # 42 - CGI ERROR - # A CGI process, or similar system for generating dynamic - # content, died unexpectedly or timed out. - # 43 - PROXY ERROR - # A proxy request failed because the server was unable to - # successfully complete a transaction with the remote host. - # (cf HTTP 502, 504) - # 44 - SLOW DOWN - # Rate limiting is in effect. is an integer number of - # seconds which the client must wait before another request is - # made to this server. - # (cf HTTP 429) - printf 'OH SHIT!\n%s\t%s\n' "$stat" "$msg" | display "$addr" - NOT_FULLY_IMPLEMENTED + 5*) # PERMANENT FAILURE + log 2 "Permanent failure" + case "$code" in + 51) log 5 "- Not found" ;; + 52) log 5 "- No longer available" ;; + 53) log 5 "- Proxy request refused" ;; + 59) log 5 "- Bad request" ;; + *) log 2 "- Unknown response code: '$code'." ;; + esac + exit "$code" ;; - 5*) # 50 - PERMANENT FAILURE - # As per definition of single-digit code 5 in 1.3.2. - # 51 - NOT FOUND - # The requested resource could not be found but may be available - # in the future. - # (cf HTTP 404) - # (struggling to remember this important status code? Easy: - # you can't find things hidden at Area 51!) - # 52 - GONE - # The resource requested is no longer available and will not be - # available again. Search engines and similar tools should - # remove this resource from their indices. Content aggregators - # should stop requesting the resource and convey to their human - # users that the subscribed resource is gone. - # (cf HTTP 410) - # 53 - PROXY REQUEST REFUSED - # The request was for a resource at a domain not served by the - # server and the server does not accept proxy requests. - # 59 - BAD REQUEST - # The server was unable to parse the client's request, - # presumably due to a malformed request. - # (cf HTTP 400) - printf 'OH SHIT!\n%s\t%s\n' "$stat" "$msg" | display "$addr" - NOT_FULLY_IMPLEMENTED + 6*) # CLIENT CERT REQUIRED + log 2 "Client certificate required" + case "$code" in + 61) log 5 "- Transient cert requested" ;; + 62) log 5 "- Authorized cert required" ;; + 63) log 5 "- Cert not accepted" ;; + 64) log 5 "- Future cert rejected" ;; + 65) log 5 "- Expired cert rejected" ;; + *) log 2 "- Unknown response code: '$code'." ;; + esac + exit "$code" ;; - 6*) # 60 - CLIENT CERTIFICATE REQUIRED - # As per definition of single-digit code 6 in 1.3.2. - # 61 - TRANSIENT CERTIFICATE REQUESTED - # The server is requesting the initiation of a transient client - # certificate session, as described in 1.4.3. The client should - # ask the user if they want to accept this and, if so, generate - # a disposable key/cert pair and re-request the resource using it. - # The key/cert pair should be destroyed when the client quits, - # or some reasonable time after it was last used (24 hours? - # Less?) - # 62 - AUTHORISED CERTIFICATE REQUIRED - # This resource is protected and a client certificate which the - # server accepts as valid must be used - a disposable key/cert - # generated on the fly in response to this status is not - # appropriate as the server will do something like compare the - # certificate fingerprint against a white-list of allowed - # certificates. The client should ask the user if they want to - # use a pre-existing certificate from a stored "key chain". - # 63 - CERTIFICATE NOT ACCEPTED - # The supplied client certificate is not valid for accessing the - # requested resource. - # 64 - FUTURE CERTIFICATE REJECTED - # The supplied client certificate was not accepted because its - # validity start date is in the future. - # 65 - EXPIRED CERTIFICTE REJECTED - # The supplied client certificate was not accepted because its - # expiry date has passed. - printf 'OH SHIT!\n%s\t%s\n' "$stat" "$msg" | display "$addr" - NOT_FULLY_IMPLEMENTED + *) # ??? + die "$code" "Unknown response code: '$code'." ;; esac } -main "$@" +# display the page +display() { # display <<< STRING + cat + # TODO: use less with linking and stuff + # less -R -p'^=>' +g + # lesskey: + # l /=>\n # highlight links + # o pipe \n open_url # open the link on the top line + # u shell select_url % # shows a selection prompt for all urls (on screen? file?) + # Q exit 1 # for one of these, show a selection prompt for urls + # q exit 0 # for the other, just quit + ### + # also look into the prompt, the filename, and input preprocessor + # ($LESSOPEN, $LESSCLOSE) +} + +### main entry point ### +bollux() { + if (($# == 1)); then + url="$1" + else + read -rp "GO> " url + fi + + log 5 "url : $url" + log 5 "addr: $(_address "$url")" + log 5 "serv: $(_server "$url")" + + request "$url" | handle "$url" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + # requirements here -- so they're only checked once + require openssl + + bollux "$@" +fi -- cgit 1.4.1-21-gabe81