It works!

This commit is contained in:
Case Duckworth 2020-05-22 15:53:35 -05:00
parent 6cce4bd5e7
commit 35dba9bf0c

372
bollux
View File

@ -1,191 +1,239 @@
#!/bin/bash
# bollux: bash gemini client
#!/usr/bin/env bash
# bollux: a bash gemini client or whatever
# Author: Case Duckworth <acdw@acdw.net>
# License: MIT
# Version: -1
# Version: -0.8
PRGN="${0##*/}"
set -euo pipefail
main() {
if [[ -z "$1" ]]; then
echo "usage: $PRGN <URL>"
return 1
### 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' "$*"; }
# conditionally log events to stderr
# lower = more important
log() { # log [LEVEL] [<] MESSAGE
case "$1" in
[0-5])
lvl="$1"
shift
;;
*) lvl=4 ;;
esac
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 "$PRGN $*"
log address="$(address "$1"|tr '[:space:]' 'x')"
log server="$(server "$1")"
address "$1" |
download "$(server "$1")" 2>/dev/null |
handle_status "$1"
}
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
log 5 "serv: $serv"
log 5 "addr: $addr"
t="$(mktemp)"
sslcmd=(openssl s_client -crlf -ign_eof -quiet -connect "$serv")
log "${sslcmd[@]}"
"${sslcmd[@]}" <<<"$addr" 2>"$t"
((LOGL > 4)) && cat "$t"
rm "$t"
}
display() {
addr="$(address "$1")"
printf ' 🚀 %s 🚀\n' "$addr"
echo
cat
printf ' 🚀 END: %s 🚀\n' "$addr"
}
# 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")"
NOT_IMPLEMENTED() {
log "NOT IMPLEMENTED!!!" >&2
exit 127
}
NOT_FULLY_IMPLEMENTED() {
log "NOT FULLY IMPLEMENTED!!!" >&2
}
code="$(awk '{print $1}' <<<"$head")"
meta="$(awk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$head")"
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
log 5 "[$code] $meta"
case "$code" in
1*) # INPUT
log 3 "Input"
put "$meta"
read -rep "? "
bollux "$url?$REPLY"
;;
20) # SUCCESS
# As per definition of single-digit code 2 in 1.3.2.
display "$addr" <<<"$IN"
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"
;;
21) # SUCCESS - END OF CLIENT CERTIFICATE SESSION
# The request was handled successfully and a response body will
# follow the response header. The <META> 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
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"
;;
30) # REDIRECT - TEMPORARY
# As per definition of single-digit code 3 in 1.3.2.
exec "$0" "$msg"
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"
;;
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
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"
;;
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. <META> 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
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"
;;
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*) # 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