diff options
-rwxr-xr-x | bollux | 370 |
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 | ||
7 | PRGN="${0##*/}" | 7 | set -euo pipefail |
8 | 8 | ||
9 | main() { | 9 | ### constants ### |
10 | if [[ -z "$1" ]]; then | 10 | PRGN="${0##*/}" # program name |
11 | echo "usage: $PRGN <URL>" | 11 | PROT="${BOLLUX_PROTO:-gemini}" # protocol |
12 | return 1 | 12 | PORT="${BOLLUX_PORT:-1965}" # port number |
13 | fi | 13 | LOGL="${BOLLUX_LOGLEVEL:-3}" # log level |
14 | MAXR="${BOLLUX_MAXREDIR:-5}" # max redirects | ||
15 | RDRS=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 | ||
27 | put() { 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")" | 31 | log() { # 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 | ||
24 | log() { | 51 | # halt and catch fire |
25 | printf '\e[34m%s:\t%s\e[0m\n' "$PRGN" "$*" >&2 | 52 | die() { # 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 | ||
28 | address() { | 65 | # fail if something isn't installed |
29 | local addr="$1" | 66 | require() { 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 | 69 | trim() { 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) | ||
72 | NOT_IMPLEMENTED() { die 200 "NOT IMPLEMENTED!!!"; } | ||
73 | NOT_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 | ||
36 | server() { | 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 | ||
45 | download() { | 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 | } | 98 | request() { # 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 | ||
49 | display() { | 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 | ||
57 | NOT_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[@]}" |
61 | NOT_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 | ||
65 | handle_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")" | 125 | handle() { # 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 | ||
191 | main "$@" | 203 | # display the page |
204 | display() { # 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 ### | ||
220 | bollux() { | ||
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 | |||
234 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then | ||
235 | # requirements here -- so they're only checked once | ||
236 | require openssl | ||
237 | |||
238 | bollux "$@" | ||
239 | fi | ||