about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbollux370
1 files changed, 209 insertions, 161 deletions
diff --git a/bollux b/bollux index 50246c9..8026179 100755 --- a/bollux +++ b/bollux
@@ -1,191 +1,239 @@
1#!/bin/bash 1#!/usr/bin/env bash
2# bollux: bash gemini client 2# bollux: a bash gemini client or whatever
3# Author: Case Duckworth <acdw@acdw.net> 3# Author: Case Duckworth <acdw@acdw.net>
4# License: MIT 4# License: MIT
5# Version: -1 5# Version: -0.8
6 6
7PRGN="${0##*/}" 7set -euo pipefail
8 8
9main() { 9### constants ###
10 if [[ -z "$1" ]]; then 10PRGN="${0##*/}" # program name
11 echo "usage: $PRGN <URL>" 11PROT="${BOLLUX_PROTO:-gemini}" # protocol
12 return 1 12PORT="${BOLLUX_PORT:-1965}" # port number
13 fi 13LOGL="${BOLLUX_LOGLEVEL:-3}" # log level
14MAXR="${BOLLUX_MAXREDIR:-5}" # max redirects
15RDRS=0 # redirects
16
17# LOGLEVELS:
18# 0 - application fatal error
19# 1 - application warning
20# 2 - response error
21# 3 - response logging
22# 4 - application logging
23# 5 - diagnostic
24
25### utility functions ###
26# a better echo
27put() { printf '%s\n' "$*"; }
14 28
15 log "$PRGN $*" 29# conditionally log events to stderr
16 log address="$(address "$1"|tr '[:space:]' 'x')" 30# lower = more important
17 log server="$(server "$1")" 31log() { # log [LEVEL] [<] MESSAGE
32 case "$1" in
33 [0-5])
34 lvl="$1"
35 shift
36 ;;
37 *) lvl=4 ;;
38 esac
18 39
19 address "$1" | 40 output="$*"
20 download "$(server "$1")" 2>/dev/null | 41 if ((lvl < LOGL)); then
21 handle_status "$1" 42 if (($# == 0)); then
43 while IFS= read -r line; do
44 output="$output${output:+$'\n'}$line"
45 done
46 fi
47 printf '\e[3%dm%s\e[0m:\t%s\n' "$((lvl + 1))" "$PRGN" "$output" >&2
48 fi
22} 49}
23 50
24log() { 51# halt and catch fire
25 printf '\e[34m%s:\t%s\e[0m\n' "$PRGN" "$*" >&2 52die() { # die [EXIT-CODE] MESSAGE
53 case "$1" in
54 [0-9]*)
55 ec="$1"
56 shift
57 ;;
58 *) ec=1 ;;
59 esac
60
61 log 0 "$*"
62 exit "$ec"
26} 63}
27 64
28address() { 65# fail if something isn't installed
29 local addr="$1" 66require() { hash "$1" 2>/dev/null || die 127 "Requirement '$1' not found."; }
30 if [[ "$addr" != gemini://* ]]; then 67
31 addr="gemini://$addr" 68# trim a string
32 fi 69trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; }
33 echo "$addr" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' 70
71# stubs for when things aren't implemented (fully)
72NOT_IMPLEMENTED() { die 200 "NOT IMPLEMENTED!!!"; }
73NOT_FULLY_IMPLEMENTED() { log 1 "NOT FULLY IMPLEMENTED!!!"; }
74
75### gemini ###
76# normalize a gemini address
77# example.com => gemini://example.com/
78_address() { # _address URL
79 addr="$1"
80 [[ "$addr" != *://* ]] && addr="$PROT://$addr"
81 trim <<<"$addr"
34} 82}
35 83
36server() { 84# return only the server part from an address, with the port added
37 local serv="${1#*://}" 85# gemini://example.com/path/to/file => example.com:1965
86_server() {
87 serv="$(_address "$1")" # normalize first
88 serv="${serv#*://}"
38 serv="${serv%%/*}" 89 serv="${serv%%/*}"
39 if [[ ! "$serv" = *:* ]]; then 90 if [[ "$serv" != *:* ]]; then
40 serv="$serv:1965" 91 serv="$serv:$PORT"
41 fi 92 fi
42 echo "$serv" 93 trim <<<"$serv"
43} 94}
44 95
45download() { 96# request a gemini page
46 openssl s_client -crlf -ign_eof -quiet -connect "$1" 97# by default, extract the server from the url
47} 98request() { # request [-s SERVER] URL
99 case "$1" in
100 -s)
101 serv="$(_server "$2")"
102 addr="$(_address "$3")"
103 ;;
104 *)
105 serv="$(_server "$1")"
106 addr="$(_address "$1")"
107 ;;
108 esac
48 109
49display() { 110 log 5 "serv: $serv"
50 addr="$(address "$1")" 111 log 5 "addr: $addr"
51 printf ' 🚀 %s 🚀\n' "$addr"
52 echo
53 cat
54 printf ' 🚀 END: %s 🚀\n' "$addr"
55}
56 112
57NOT_IMPLEMENTED() { 113 t="$(mktemp)"
58 log "NOT IMPLEMENTED!!!" >&2 114
59 exit 127 115 sslcmd=(openssl s_client -crlf -ign_eof -quiet -connect "$serv")
60} 116 log "${sslcmd[@]}"
61NOT_FULLY_IMPLEMENTED() { 117 "${sslcmd[@]}" <<<"$addr" 2>"$t"
62 log "NOT FULLY IMPLEMENTED!!!" >&2 118
119 ((LOGL > 4)) && cat "$t"
120 rm "$t"
63} 121}
64 122
65handle_status() { 123# handle the response
66 # cf. https://gemini.circumlunar.space/docs/spec-spec.txt 124# cf. gemini://gemini.circumlunar.space/docs/spec-spec.txt
67 addr="$(address "$1")" 125handle() { # handle URL < RESPONSE
68 IN="$(cat)" 126 url="$(_address "$1")"
69 head="$(head -n1 <<<"$IN")" 127 resp="$(cat)"
70 stat="$(awk '{print $1}' <<<"$head")" 128 head="$(sed 1q <<<"$resp")"
71 msg="$(awk '{for(i=2;i<=NF;i++)printf "%s ", $i;printf "\n";}' <<<"$head")" 129 body="$(sed 1d <<<"$resp")"
72 log "$stat $msg" 130
73 case "$stat" in 131 code="$(awk '{print $1}' <<<"$head")"
74 10) # INPUT 132 meta="$(awk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$head")"
75 # As per definition of single-digit code 1 in 1.3.2. 133
76 NOT_IMPLEMENTED 134 log 5 "[$code] $meta"
77 ;; 135
78 20) # SUCCESS 136 case "$code" in
79 # As per definition of single-digit code 2 in 1.3.2. 137 1*) # INPUT
80 display "$addr" <<<"$IN" 138 log 3 "Input"
139 put "$meta"
140 read -rep "? "
141 bollux "$url?$REPLY"
81 ;; 142 ;;
82 21) # SUCCESS - END OF CLIENT CERTIFICATE SESSION 143 2*) # SUCCESS
83 # The request was handled successfully and a response body will 144 log 3 "Success"
84 # follow the response header. The <META> line is a MIME media 145 case "$code" in
85 # type which applies to the response body. In addition, the 146 20) log 5 "- OK" ;;
86 # server is signalling the end of a transient client certificate 147 21) log 5 "- End of client certificate session" ;;
87 # session which was previously initiated with a status 61 148 *) log 2 "- Unknown response code: '$code'." ;;
88 # response. The client should immediately and permanently 149 esac
89 # delete the certificate and accompanying private key which was 150 display <<<"$body"
90 # used in this request.
91 display "$addr" <<<"$IN"
92 NOT_FULLY_IMPLEMENTED
93 ;; 151 ;;
94 30) # REDIRECT - TEMPORARY 152 3*) # REDIRECT
95 # As per definition of single-digit code 3 in 1.3.2. 153 log 3 "Redirecting"
96 exec "$0" "$msg" 154 case "$code" in
155 30) log 5 "- Temporary" ;;
156 31) log 5 "- Permanent" ;;
157 *) log 2 "- Unknown response code: '$code'." ;;
158 esac
159 ((RDRS+=1))
160 ((RDRS > MAXR)) && die "$code" "Too many redirects!"
161 bollux "$meta"
97 ;; 162 ;;
98 31) # REDIRECT - PERMANENT 163 4*) # TEMPORARY FAILURE
99 # The requested resource should be consistently requested from 164 log 2 "Temporary failure"
100 # the new URL provided in future. Tools like search engine 165 case "$code" in
101 # indexers or content aggregators should update their 166 41) log 5 "- Server unavailable" ;;
102 # configurations to avoid requesting the old URL, and end-user 167 42) log 5 "- CGI error" ;;
103 # clients may automatically update bookmarks, etc. Note that 168 43) log 5 "- Proxy error" ;;
104 # clients which only pay attention to the initial digit of 169 44) log 5 "- Rate limited" ;;
105 # status codes will treat this as a temporary redirect. They 170 *) log 2 "- Unknown response code: '$code'." ;;
106 # will still end up at the right place, they just won't be able 171 esac
107 # to make use of the knowledge that this redirect is permanent, 172 exit "$code"
108 # so they'll pay a small performance penalty by having to follow
109 # the redirect each time.
110 exec "$0" "$msg"
111 NOT_FULLY_IMPLEMENTED
112 ;; 173 ;;
113 4*) # 40 - TEMPORARY FAILURE 174 5*) # PERMANENT FAILURE
114 # As per definition of single-digit code 4 in 1.3.2. 175 log 2 "Permanent failure"
115 # 41 - SERVER UNAVAILABLE 176 case "$code" in
116 # The server is unavailable due to overload or maintenance. 177 51) log 5 "- Not found" ;;
117 # (cf HTTP 503) 178 52) log 5 "- No longer available" ;;
118 # 42 - CGI ERROR 179 53) log 5 "- Proxy request refused" ;;
119 # A CGI process, or similar system for generating dynamic 180 59) log 5 "- Bad request" ;;
120 # content, died unexpectedly or timed out. 181 *) log 2 "- Unknown response code: '$code'." ;;
121 # 43 - PROXY ERROR 182 esac
122 # A proxy request failed because the server was unable to 183 exit "$code"
123 # successfully complete a transaction with the remote host.
124 # (cf HTTP 502, 504)
125 # 44 - SLOW DOWN
126 # Rate limiting is in effect. <META> is an integer number of
127 # seconds which the client must wait before another request is
128 # made to this server.
129 # (cf HTTP 429)
130 printf 'OH SHIT!\n%s\t%s\n' "$stat" "$msg" | display "$addr"
131 NOT_FULLY_IMPLEMENTED
132 ;; 184 ;;
133 5*) # 50 - PERMANENT FAILURE 185 6*) # CLIENT CERT REQUIRED
134 # As per definition of single-digit code 5 in 1.3.2. 186 log 2 "Client certificate required"
135 # 51 - NOT FOUND 187 case "$code" in
136 # The requested resource could not be found but may be available 188 61) log 5 "- Transient cert requested" ;;
137 # in the future. 189 62) log 5 "- Authorized cert required" ;;
138 # (cf HTTP 404) 190 63) log 5 "- Cert not accepted" ;;
139 # (struggling to remember this important status code? Easy: 191 64) log 5 "- Future cert rejected" ;;
140 # you can't find things hidden at Area 51!) 192 65) log 5 "- Expired cert rejected" ;;
141 # 52 - GONE 193 *) log 2 "- Unknown response code: '$code'." ;;
142 # The resource requested is no longer available and will not be 194 esac
143 # available again. Search engines and similar tools should 195 exit "$code"
144 # remove this resource from their indices. Content aggregators
145 # should stop requesting the resource and convey to their human
146 # users that the subscribed resource is gone.
147 # (cf HTTP 410)
148 # 53 - PROXY REQUEST REFUSED
149 # The request was for a resource at a domain not served by the
150 # server and the server does not accept proxy requests.
151 # 59 - BAD REQUEST
152 # The server was unable to parse the client's request,
153 # presumably due to a malformed request.
154 # (cf HTTP 400)
155 printf 'OH SHIT!\n%s\t%s\n' "$stat" "$msg" | display "$addr"
156 NOT_FULLY_IMPLEMENTED
157 ;; 196 ;;
158 6*) # 60 - CLIENT CERTIFICATE REQUIRED 197 *) # ???
159 # As per definition of single-digit code 6 in 1.3.2. 198 die "$code" "Unknown response code: '$code'."
160 # 61 - TRANSIENT CERTIFICATE REQUESTED
161 # The server is requesting the initiation of a transient client
162 # certificate session, as described in 1.4.3. The client should
163 # ask the user if they want to accept this and, if so, generate
164 # a disposable key/cert pair and re-request the resource using it.
165 # The key/cert pair should be destroyed when the client quits,
166 # or some reasonable time after it was last used (24 hours?
167 # Less?)
168 # 62 - AUTHORISED CERTIFICATE REQUIRED
169 # This resource is protected and a client certificate which the
170 # server accepts as valid must be used - a disposable key/cert
171 # generated on the fly in response to this status is not
172 # appropriate as the server will do something like compare the
173 # certificate fingerprint against a white-list of allowed
174 # certificates. The client should ask the user if they want to
175 # use a pre-existing certificate from a stored "key chain".
176 # 63 - CERTIFICATE NOT ACCEPTED
177 # The supplied client certificate is not valid for accessing the
178 # requested resource.
179 # 64 - FUTURE CERTIFICATE REJECTED
180 # The supplied client certificate was not accepted because its
181 # validity start date is in the future.
182 # 65 - EXPIRED CERTIFICTE REJECTED
183 # The supplied client certificate was not accepted because its
184 # expiry date has passed.
185 printf 'OH SHIT!\n%s\t%s\n' "$stat" "$msg" | display "$addr"
186 NOT_FULLY_IMPLEMENTED
187 ;; 199 ;;
188 esac 200 esac
189} 201}
190 202
191main "$@" 203# display the page
204display() { # display <<< STRING
205 cat
206 # TODO: use less with linking and stuff
207 # less -R -p'^=>' +g
208 # lesskey:
209 # l /=>\n # highlight links
210 # o pipe \n open_url # open the link on the top line
211 # u shell select_url % # shows a selection prompt for all urls (on screen? file?)
212 # Q exit 1 # for one of these, show a selection prompt for urls
213 # q exit 0 # for the other, just quit
214 ###
215 # also look into the prompt, the filename, and input preprocessor
216 # ($LESSOPEN, $LESSCLOSE)
217}
218
219### main entry point ###
220bollux() {
221 if (($# == 1)); then
222 url="$1"
223 else
224 read -rp "GO> " url
225 fi
226
227 log 5 "url : $url"
228 log 5 "addr: $(_address "$url")"
229 log 5 "serv: $(_server "$url")"
230
231 request "$url" | handle "$url"
232}
233
234if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
235 # requirements here -- so they're only checked once
236 require openssl
237
238 bollux "$@"
239fi