#!/usr/bin/env bash # bollux: bash gemini client # Author: Case Duckworth # License: MIT # Version: -0.9 PRGN="${0##*/}" PORT=1965 LOG_LEVEL=3 # higher=more important. clean() { # '\e[?7h': re-enable line wrapping # '\e[2J': clear the screen # '\e[;r': reset the scroll area # '\e[?1049l': swap back to primary screen printf '\e[?7h\e[2J\e[;r\e[?1049l' exit } refresh() { # grab the terminal size shopt -s checkwinsize ( : : ) # '\e[?1049h': Swap to the alternate buffer. # '\e[?7l': Disable line wrapping. # '\e[2J': Clear the screen. # '\e[3;%sr': Set the scroll area. # '\e[999H': Move the cursor to the bottom. printf '\e[?1049h\e[?7l\e[2J\e[3;%sr\e[999H' "$((LINES - 1))" } resize() { refresh # '\e7': Save the cursor position. # '\e[?25l': Hide the cursor. # '\r': Move the cursor to column 0. # '\e[999B': Move the cursor to the bottom. # '\e[A': Move the cursor up a line. printf '\e7\e[?25l\r\e[999B\e[A' } bollux() { if [[ -n "$1" ]]; then loc="$1" else read -rp "GO> " loc fi log 2 "location: $loc" log 2 "address: $(address "$loc")" log 2 "server: $(server "$loc")" address "$loc" | download "$(server "$loc")" | handle } log() { case "$1" in [0-9]*) lvl="$1" shift ;; *) lvl=5 ;; esac if ((lvl >= LOG_LEVEL)); then if [[ "$2" == - ]]; then while IFS= read -r line; do printf '\e[33m%s\e[0m:\t%s\n' "$PRGN" "$line" >&2 done else printf '\e[34m%s\e[0m:\t%s\n' "bollux" "$*" >&2 fi fi } download() { # usage: # echo REQUEST | download SERVER # download SERVER REQUEST serv="$1" req= if (($# == 2)); then req="$2" else req="$(cat)" fi t="$(mktemp)" openssl s_client -crlf -ign_eof -quiet -connect "$serv" <<<"$req" 2>"$t" log 1 <"$t" rm "$t" } address() { addr="$1" if [[ "$addr" != gemini://* ]]; then addr="gemini://$addr" fi echo "$addr" | trim } server() { serv="${1#*://}" serv="${serv%%/*}" if [[ "$serv" != *:* ]]; then serv="$serv:$PORT" fi echo "$serv" | trim } trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } display() { echo cat echo } handle() { # cf. gemini://gemini.circumlunar.space/docs/spec-spec.txt resp="$(cat)" head="$(head -n1 <<<"$resp")" body="$(tail -n+2 <<<"$resp")" stat="$(awk '{print $1}' <<<"$head")" smsg="$( awk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n";}' <<<"$head" )" log "[$stat] $smsg" 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 <<<"$body" ;; 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 <<<"$body" NOT_FULLY_IMPLEMENTED ;; 30) # REDIRECT - TEMPORARY # As per definition of single-digit code 3 in 1.3.2. exec "$0" "$smsg" ;; 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" "$smsg" NOT_FULLY_IMPLEMENTED ;; 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" "$smsg" >&2 NOT_IMPLEMENTED ;; 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" "$smsg" >&2 NOT_IMPLEMENTED ;; 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" "$smsg" >&2 NOT_IMPLEMENTED ;; esac } NOT_IMPLEMENTED() { log "NOT IMPLEMENTED!!!" >&2 exit 127 } NOT_FULLY_IMPLEMENTED() { log "NOT FULLY IMPLEMENTED!!!" >&2 } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then bollux "$@" fi