forked from acdw/bollux
It works!
This commit is contained in:
parent
6cce4bd5e7
commit
35dba9bf0c
372
bollux
372
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 <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
|
||||
|
|
Loading…
Reference in New Issue
Block a user