Fix colors
[bollux.git/.git] / bollux
1 #!/usr/bin/env bash
2 # bollux: a bash gemini client or whatever
3 # Author: Case Duckworth <acdw@acdw.net>
4 # License: MIT
5 # Version: -0.7
6
7 ### constants ###
8 PRGN="${0##*/}"                # program name
9 DLDR="${BOLLUX_DOWNDIR:-.}"    # where to download
10 LOGL="${BOLLUX_LOGLEVEL:-3}"   # log level
11 MAXR="${BOLLUX_MAXREDIR:-5}"   # max redirects
12 PORT="${BOLLUX_PORT:-1965}"    # port number
13 PROT="${BOLLUX_PROTO:-gemini}" # protocol
14 RDRS=0                         # redirects
15
16 # LOGLEVELS:
17 # 0 - application fatal error
18 # 1 - application warning
19 # 2 - response error
20 # 3 - response logging
21 # 4 - application logging
22 # 5 - diagnostic
23
24 ### utility functions ###
25 # a better echo
26 put() { printf '%s\n' "$*" >&3; }
27
28 # conditionally log events to stderr
29 # lower = more important
30 log() { # log [LEVEL] [<] MESSAGE
31         case "$1" in
32         -)
33                 lvl="-1"
34                 shift
35                 ;;
36         [0-5])
37                 lvl="$1"
38                 shift
39                 ;;
40         *) lvl=4 ;;
41         esac
42
43         output="$*"
44         if ((lvl < LOGL)); then
45                 if (($# == 0)); then
46                         while IFS= read -r line; do
47                                 output="$output${output:+$'\n'}$line"
48                         done
49                 fi
50                 printf '\e[34m%s\e[0m:\t%s\n' "$PRGN" "$output" >&2
51         fi
52 }
53
54 # halt and catch fire
55 die() { # die [EXIT-CODE] MESSAGE
56         case "$1" in
57         [0-9]*)
58                 ec="$1"
59                 shift
60                 ;;
61         *) ec=1 ;;
62         esac
63
64         log 0 "$*"
65         exit "$ec"
66 }
67
68 # ask the user for input
69 ask() { # ask PROMPT [READ_OPT...]
70         prompt="$1"
71         shift
72         read -e -r -u 3 -p "$prompt> " "$@"
73 }
74
75 # fail if something isn't installed
76 require() { hash "$1" 2>/dev/null || die 127 "Requirement '$1' not found."; }
77
78 # trim a string
79 trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; }
80
81 # stubs for when things aren't implemented (fully)
82 NOT_IMPLEMENTED() { die 200 "NOT IMPLEMENTED!!!"; }
83 NOT_FULLY_IMPLEMENTED() { log 1 "NOT FULLY IMPLEMENTED!!!"; }
84
85 ### gemini ###
86 # normalize a gemini address
87 # example.com => gemini://example.com/
88 _address() { # _address URL
89         addr="$1"
90
91         [[ "$addr" != *://* ]] && addr="$PROT://$addr"
92         trim <<<"$addr"
93 }
94
95 # return only the server part from an address, with the port added
96 # gemini://example.com/path/to/file => example.com:1965
97 _server() {
98         serv="$(_address "$1")" # normalize first
99         serv="${serv#*://}"
100         serv="${serv%%/*}"
101         if [[ "$serv" != *:* ]]; then
102                 serv="$serv:$PORT"
103         fi
104         trim <<<"$serv"
105 }
106
107 # request a gemini page
108 # by default, extract the server from the url
109 request() { # request [-s SERVER] URL
110         case "$1" in
111         -s)
112                 serv="$(_server "$2")"
113                 addr="$(_address "$3")"
114                 ;;
115         *)
116                 serv="$(_server "$1")"
117                 addr="$(_address "$1")"
118                 ;;
119         esac
120
121         log 5 "serv: $serv"
122         log 5 "addr: $addr"
123
124         sslcmd=(openssl s_client -crlf -ign_eof -quiet -connect "$serv")
125         log "${sslcmd[@]}"
126         "${sslcmd[@]}" <<<"$addr" 2>/dev/null
127 }
128
129 # handle the response
130 # cf. gemini://gemini.circumlunar.space/docs/spec-spec.txt
131 handle() { # handle URL < RESPONSE
132         URL="$1"
133         while read -r head; do
134                 break # wait to read the first line
135         done
136         code="$(awk '{print $1}' <<<"$head")"
137         meta="$(awk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$head")"
138
139         log 5 "[$code]  $meta"
140
141         case "$code" in
142         1*) # INPUT
143                 log 3 "Input"
144                 put "$meta"
145                 ask "?"
146                 bollux "$URL?$REPLY"
147                 ;;
148         2*) # SUCCESS
149                 log 3 "Success"
150                 case "$code" in
151                 20) log 5 "- OK" ;;
152                 21) log 5 "- End of client certificate session" ;;
153                 *) log 2 "- Unknown response code: '$code'." ;;
154                 esac
155                 display "$meta"
156                 ;;
157         3*) # REDIRECT
158                 log 3 "Redirecting"
159                 case "$code" in
160                 30) log 5 "- Temporary" ;;
161                 31) log 5 "- Permanent" ;;
162                 *) log 2 "- Unknown response code: '$code'." ;;
163                 esac
164                 ((RDRS += 1))
165                 ((RDRS > MAXR)) && die "$code" "Too many redirects!"
166                 bollux "$meta"
167                 ;;
168         4*) # TEMPORARY FAILURE
169                 log 2 "Temporary failure"
170                 case "$code" in
171                 41) log 5 "- Server unavailable" ;;
172                 42) log 5 "- CGI error" ;;
173                 43) log 5 "- Proxy error" ;;
174                 44) log 5 "- Rate limited" ;;
175                 *) log 2 "- Unknown response code: '$code'." ;;
176                 esac
177                 exit "$code"
178                 ;;
179         5*) # PERMANENT FAILURE
180                 log 2 "Permanent failure"
181                 case "$code" in
182                 51) log 5 "- Not found" ;;
183                 52) log 5 "- No longer available" ;;
184                 53) log 5 "- Proxy request refused" ;;
185                 59) log 5 "- Bad request" ;;
186                 *) log 2 "- Unknown response code: '$code'." ;;
187                 esac
188                 exit "$code"
189                 ;;
190         6*) # CLIENT CERT REQUIRED
191                 log 2 "Client certificate required"
192                 case "$code" in
193                 61) log 5 "- Transient cert requested" ;;
194                 62) log 5 "- Authorized cert required" ;;
195                 63) log 5 "- Cert not accepted" ;;
196                 64) log 5 "- Future cert rejected" ;;
197                 65) log 5 "- Expired cert rejected" ;;
198                 *) log 2 "- Unknown response code: '$code'." ;;
199                 esac
200                 exit "$code"
201                 ;;
202         *) # ???
203                 die "$code" "Unknown response code: '$code'."
204                 ;;
205         esac
206 }
207
208 # display the page
209 display() { # display MIMETYPE < DOCUMENT
210         mimetype="$1"
211         case "$mimetype" in
212         text/*)
213                 # normalize line endings to "\n"
214                 # awk 'BEGIN{RS=""}{gsub(/\r\n?/,"\n");print}'
215                 cat
216                 # TODO: use less with linking and stuff
217                 # less -R -p'^=>' +g
218                 # lesskey:
219                 # l /=>\n # highlight links
220                 # o pipe \n open_url # open the link on the top line
221                 # u shell select_url % # shows a selection prompt for all urls (on screen? file?)
222                 # Q exit 1  # for one of these, show a selection prompt for urls
223                 # q exit 0  # for the other, just quit
224                 ###
225                 # also look into the prompt, the filename, and input preprocessor
226                 # ($LESSOPEN, $LESSCLOSE)
227                 ;;
228         *)
229                 download "$URL"
230                 ;;
231         esac
232 }
233
234 download() { # download URL < FILE
235         tn="$(mktemp)"
236         dd status=progress >"$tn"
237         fn="$DLDR/${URL##*/}"
238         if [[ -f "$fn" ]]; then
239                 log - "Saved '$tn'."
240         else
241                 if mv "$tn" "$fn"; then
242                         log - "Saved '$fn'."
243                 else
244                         log 0 "Error saving '$fn'."
245                         log - "Saved '$tn'."
246                 fi
247         fi
248 }
249
250 ### main entry point ###
251 bollux() {
252         # use &3 for user input
253         exec 3<>/dev/tty
254
255         if (($# == 1)); then
256                 URL="$1"
257         else
258                 ask GO URL
259         fi
260
261         log 5 "URL : $URL"
262
263         request "$URL" | handle "$URL"
264 }
265
266 bollux_setup() {
267         mkfifo .resource
268         trap bollux_cleanup INT QUIT TERM EXIT
269 }
270
271 bollux_cleanup() {
272         echo
273         rm -f .resource
274 }
275
276 if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
277         set -euo pipefail # strict mode
278         # requirements here -- so they're only checked once
279         require awk
280         require dd
281         require mv
282         require openssl
283         require sed
284
285         bollux "$@"
286         echo
287 fi