diff options
-rwxr-xr-x | bollux | 207 |
1 files changed, 162 insertions, 45 deletions
diff --git a/bollux b/bollux index 46c381b..2de37ab 100755 --- a/bollux +++ b/bollux | |||
@@ -99,13 +99,6 @@ bollux_quit() { | |||
99 | # SIGINT is C-c, and I want to make sure bollux quits when it's typed. | 99 | # SIGINT is C-c, and I want to make sure bollux quits when it's typed. |
100 | trap bollux_quit SIGINT | 100 | trap bollux_quit SIGINT |
101 | 101 | ||
102 | # Bash built-in replacement for `sleep` | ||
103 | # | ||
104 | # [1]: #use-read-as-an-alternative-to-the-sleep-command | ||
105 | sleep() { # sleep SECONDS | ||
106 | read -rt "$1" <> <(:) || : | ||
107 | } | ||
108 | |||
109 | # Trim leading and trailing whitespace from a string. | 102 | # Trim leading and trailing whitespace from a string. |
110 | # | 103 | # |
111 | # [1]: #trim-leading-and-trailing-white-space-from-string | 104 | # [1]: #trim-leading-and-trailing-white-space-from-string |
@@ -177,6 +170,29 @@ prompt() { # prompt [-u] PROMPT [READ_ARGS...] | |||
177 | "${read_cmd[@]}" </dev/tty "$@" | 170 | "${read_cmd[@]}" </dev/tty "$@" |
178 | } | 171 | } |
179 | 172 | ||
173 | |||
174 | # Bash built-in replacement for `cat' | ||
175 | # | ||
176 | # One of the more pedantic bits of bollux (is 'pedantic' the right word?) -- | ||
177 | # `cat' is more than likely installed on any system with bash, so this function | ||
178 | # is really just here so I can say that bollux is written as purely in bash as | ||
179 | # possible. | ||
180 | passthru() { | ||
181 | while IFS= read -r; do | ||
182 | printf '%s\n' "$REPLY" | ||
183 | done | ||
184 | } | ||
185 | |||
186 | # Bash built-in replacement for `sleep' | ||
187 | # | ||
188 | # The commentary for `passthru' applies here as well, though I didn't write this | ||
189 | # function -- Dylan Araps did. | ||
190 | # | ||
191 | # [1]: #use-read-as-an-alternative-to-the-sleep-command | ||
192 | sleep() { # sleep SECONDS | ||
193 | read -rt "$1" <> <(:) || : | ||
194 | } | ||
195 | |||
180 | # MAIN BOLLUX DISPATCH FUNCTIONS ############################################### | 196 | # MAIN BOLLUX DISPATCH FUNCTIONS ############################################### |
181 | 197 | ||
182 | # Main entry point into `bollux'. | 198 | # Main entry point into `bollux'. |
@@ -772,71 +788,140 @@ gemini_response() { # gemini_response URL | |||
772 | run blastoff "$meta" # TODO: confirm redirect | 788 | run blastoff "$meta" # TODO: confirm redirect |
773 | ;; | 789 | ;; |
774 | (4*) # TEMPORARY ERROR | 790 | (4*) # TEMPORARY ERROR |
791 | # Since the 4* codes ([3], Appendix 1) are all server issues, | ||
792 | # bollux can treat them all basically the same. This is an area | ||
793 | # that could use some expansion. | ||
794 | local desc="Temporary error" | ||
795 | case "$code" in | ||
796 | (41) desc+=" (server unavailable)" ;; | ||
797 | (42) desc+=" (CGI error)" ;; | ||
798 | (43) desc+=" (proxy error)" ;; | ||
799 | (44) desc+=" (slow down)" ;; # could be particularly improved | ||
800 | esac | ||
775 | REDIRECTS=0 | 801 | REDIRECTS=0 |
776 | die "$((100 + code))" "Temporary error [$code]: $meta" | 802 | die "$((100 + code))" "$desc [$code]: $meta" |
777 | ;; | 803 | ;; |
778 | (5*) # PERMANENT ERROR | 804 | (5*) # PERMANENT ERROR |
805 | # The situation with the 5* codes is basically similar to the 4* | ||
806 | # codes. It could maybe use more thought as to what behavior to | ||
807 | # implement. Maybe adding the (bad) requests to history, | ||
808 | # subject to configuration? | ||
809 | local desc="Permanent failure" | ||
810 | case "$code" in | ||
811 | (51) desc+=" (not found)" ;; | ||
812 | (52) desc+=" (gone)" ;; | ||
813 | (53) desc+=" (proxy request refused)" ;; | ||
814 | # For some reason, codes 54--58 inclusive aren't used. | ||
815 | (59) desc+=" (bad request)" ;; | ||
816 | esac | ||
779 | REDIRECTS=0 | 817 | REDIRECTS=0 |
780 | die "$((100 + code))" "Permanent error [$code]: $meta" | 818 | die "$((100 + code))" "$desc [$code]: $meta" |
781 | ;; | 819 | ;; |
782 | (6*) # CERTIFICATE ERROR | 820 | (6*) # CERTIFICATE ERROR (TODO) |
821 | # Dealing with certificates is honestly the most important | ||
822 | # feature missing from bollux to get it to 1.0. Right now, | ||
823 | # bollux deals with 6* status codes identically to 4* and 5* | ||
824 | # codes. This is not ideal, in the slightest. | ||
825 | local desc="Client certificate required" | ||
826 | case "$code" in | ||
827 | (61) desc+=" (certificate not authorized)" ;; | ||
828 | (62) desc+=" (certificate not valid)" ;; | ||
829 | esac | ||
783 | REDIRECTS=0 | 830 | REDIRECTS=0 |
784 | log d "Not implemented: Client certificates" | 831 | log d "Not implemented: Client certificates" |
785 | die "$((100 + code))" "[$code] $meta" | 832 | die "$((100 + code))" "[$code] $meta" |
786 | ;; | 833 | ;; |
787 | (*) | 834 | (*) # UNKNOWN |
835 | # Just in case we get a weird, un-spec-compliant status code. | ||
788 | [[ -z "${code-}" ]] && die 100 "Empty response code." | 836 | [[ -z "${code-}" ]] && die 100 "Empty response code." |
789 | die "$((100 + code))" "Unknown response code: $code." | 837 | die "$((100 + code))" "Unknown response code: $code." |
790 | ;; | 838 | ;; |
791 | esac | 839 | esac |
792 | } | 840 | } |
793 | 841 | ||
794 | # GOPHER | 842 | # GOPHER ####################################################################### |
795 | # https://tools.ietf.org/html/rfc1436 protocol | 843 | # https://tools.ietf.org/html/rfc1436 protocol |
796 | # https://tools.ietf.org/html/rfc4266 url | 844 | # https://tools.ietf.org/html/rfc4266 url |
845 | # | ||
846 | # Gopher is the grand-daddy of gemini (or maybe just weird uncle? hm..), | ||
847 | # invented in 1991 as a fancier FTP. There's been a sort of resurgence in it as | ||
848 | # a consequence of the shittifying of the WWW, but it's shown its age (which is | ||
849 | # why Gemini was born). But why am I telling you this? You're reading the | ||
850 | # source code of a Gemini browser! You're a meganerd just like me. Welcome to | ||
851 | # the club, kid. | ||
852 | # | ||
853 | # Since gopher is so old, it actually has two RFCs: RFC 1436 [6] for the | ||
854 | # protocol itself, and RFC 4266 [7] for the URL format (gopher predates the | ||
855 | # URL!). However, requesting and handling responses is still fundamentally the | ||
856 | # same to gemini, so it was pretty easy to implement this. I don't think bollux | ||
857 | # handles all the possible item types, but it should get the main ones. | ||
858 | # | ||
859 | ################################################################################ | ||
860 | |||
861 | # Request a resource. | ||
797 | gopher_request() { # gopher_request URL | 862 | gopher_request() { # gopher_request URL |
798 | local url server port type path | 863 | local url="$1" |
799 | url="$1" | ||
800 | port=70 | ||
801 | 864 | ||
802 | # RFC 4266 | 865 | # [7] § 2.1 |
803 | [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] | 866 | [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] |
804 | server="${BASH_REMATCH[1]}" | 867 | local server="${BASH_REMATCH[1]}" \ |
805 | port="${BASH_REMATCH[3]:-70}" | 868 | port="${BASH_REMATCH[3]:-$BOLLUX_GOPHER_PORT}" \ |
806 | type="${BASH_REMATCH[6]:-1}" | 869 | type="${BASH_REMATCH[6]:-1}" \ |
807 | path="${BASH_REMATCH[7]}" | 870 | path="${BASH_REMATCH[7]}" |
808 | |||
809 | log d "URL='$url' SERVER='$server' TYPE='$type' PATH='$path'" | 871 | log d "URL='$url' SERVER='$server' TYPE='$type' PATH='$path'" |
810 | 872 | ||
873 | # Bash has this really neat feature where it can open a TCP socket | ||
874 | # directly. bollux uses that feature here to ask the server for the | ||
875 | # resource and then `passthru' it to the next thing. | ||
811 | exec 9<>"/dev/tcp/$server/$port" | 876 | exec 9<>"/dev/tcp/$server/$port" |
812 | printf '%s\r\n' "$path" >&9 | 877 | printf '%s\r\n' "$path" >&9 |
813 | passthru <&9 | 878 | passthru <&9 |
814 | } | 879 | } |
815 | 880 | ||
881 | # Handle a server response. | ||
816 | gopher_response() { # gopher_response URL | 882 | gopher_response() { # gopher_response URL |
817 | local url pre type cur_server | 883 | local url="$1" pre=false |
818 | pre=false | 884 | # [7] § 2.1 |
819 | url="$1" | 885 | # |
820 | # RFC 4266 | 886 | # Note that this duplicates the code in `gopher_request'. There might |
887 | # be a good way to thread this data through so that it's not computed | ||
888 | # twice. | ||
821 | [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] | 889 | [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] |
822 | cur_server="${BASH_REMATCH[1]}" | 890 | local cur_server="${BASH_REMATCH[1]}" |
823 | type="${BASH_REMATCH[6]:-1}" | 891 | local type="${BASH_REMATCH[6]:-1}" |
824 | 892 | ||
825 | run history_append "$url" "" # gopher doesn't really have titles, huh | 893 | run history_append "$url" "" # gopher doesn't really have titles, huh |
826 | 894 | ||
827 | log d "TYPE='$type'" | 895 | # Gopher has a concept of 'line types', or maybe 'item types' -- |
828 | 896 | # basically, each line in a gophermap starts with a character, its type, | |
897 | # and then is followed by a series of tab-separated fields describing | ||
898 | # where that type is and how to display it. The full list of original | ||
899 | # line types can be found in [6] § 3.8, though the types have also been | ||
900 | # extended over the years. Since bollux can only display types that are | ||
901 | # text-ish, it only concerns itself with those in this case statement. | ||
902 | # All the others are simply downloaded. | ||
829 | case "$type" in | 903 | case "$type" in |
830 | (0) # text | 904 | (0) # Item is a file |
905 | # Since gopher doesn't send MIME-type information in-band, we | ||
906 | # just assume it's text/plain, and try to convert it later to | ||
907 | # UTF-8 with `iconv'. | ||
831 | run display text/plain | 908 | run display text/plain |
832 | ;; | 909 | ;; |
833 | (1) # menu | 910 | (1) # Item is a directory [gophermap] |
911 | # Since I've already written all the code to typeset gemini | ||
912 | # well, it's easy to convert a gophermap to text/gemini and | ||
913 | # display it than to write a whole new gophermap typesetter. | ||
834 | run gopher_convert | run display text/gemini | 914 | run gopher_convert | run display text/gemini |
835 | ;; | 915 | ;; |
836 | (3) # failure | 916 | (3) # Error |
917 | # I don't know all the gopher error cases, and the spec is | ||
918 | # pretty quiet on them. So bollux just signals failure and | ||
919 | # bails. | ||
837 | die 203 "GOPHER: failed" | 920 | die 203 "GOPHER: failed" |
838 | ;; | 921 | ;; |
839 | (7) # search | 922 | (7) # Item is an Index-Search server |
923 | # Gopher search queries are separated from their resources by a | ||
924 | # TAB. It's wild. | ||
840 | if [[ "$url" =~ $'\t' ]]; then | 925 | if [[ "$url" =~ $'\t' ]]; then |
841 | run gopher_convert | run display text/gemini | 926 | run gopher_convert | run display text/gemini |
842 | else | 927 | else |
@@ -844,16 +929,42 @@ gopher_response() { # gopher_response URL | |||
844 | run blastoff "$url $REPLY" | 929 | run blastoff "$url $REPLY" |
845 | fi | 930 | fi |
846 | ;; | 931 | ;; |
847 | (*) # something else | 932 | (*) # Anything else |
933 | # The list at [6] § 3.8 includes the following (noted where it | ||
934 | # might be good to differently handle them in the future): | ||
935 | # | ||
936 | # 2. Item is a CSO phone-book server ***** | ||
937 | # 4. Item is a BinHexed Macintosh file | ||
938 | # 5. Item is DOS binary archive of some sort | ||
939 | # 6. Item is a UNIX uuencoded file | ||
940 | # 8. Item points to a text-based telnet session ***** | ||
941 | # 9. Item is a binary file! [exclamation point sic. -- ed.] | ||
942 | # +. Item is a redundant server ***** | ||
943 | # T. Item points to a text-based tn3270 session | ||
944 | # g. Item is a GIF format graphics file | ||
945 | # I. Item is some kind of image file | ||
946 | # | ||
947 | # As mentioned, there are other line types floating around as | ||
948 | # well. Since I don't browse gopher much, there's not much | ||
949 | # personal motivation to extend `gopher_response'; however pull | ||
950 | # requests are always welcome. | ||
848 | run download "$url" | 951 | run download "$url" |
849 | ;; | 952 | ;; |
850 | esac | 953 | esac |
851 | } | 954 | } |
852 | 955 | ||
853 | # convert gophermap to text/gemini (probably naive) | 956 | # Convert a gophermap naively to a gemini page. |
957 | # | ||
958 | # Based strongly on [8], but bash-ified. Due to the properties of link lines in | ||
959 | # gemini, many of the item types in `gemini_reponse' can be linked to the proper | ||
960 | # protocol handlers here -- so if a user is trying to reach a TCP link through | ||
961 | # gopher, bollux won't have to handle it, for example.* | ||
962 | # | ||
963 | # * Ideally -- right now, bollux simply errors out on all unknown protocols. | ||
964 | # More research needs to be done into how to farm out to `xdg-open' or a | ||
965 | # similar generic opener. | ||
854 | gopher_convert() { | 966 | gopher_convert() { |
855 | local type label path server port regex | 967 | local type label path server port regex |
856 | # [GOPHER_GEMINI] | ||
857 | while IFS= read -r; do | 968 | while IFS= read -r; do |
858 | printf -v regex '(.)([^\t]*)(\t([^\t]*)\t([^\t]*)\t([^\t]*))?' | 969 | printf -v regex '(.)([^\t]*)(\t([^\t]*)\t([^\t]*)\t([^\t]*))?' |
859 | if [[ "$REPLY" =~ $regex ]]; then | 970 | if [[ "$REPLY" =~ $regex ]]; then |
@@ -922,13 +1033,12 @@ gopher_convert() { | |||
922 | exec 9>&- | 1033 | exec 9>&- |
923 | } | 1034 | } |
924 | 1035 | ||
925 | 1036 | # HANDLING CONTENT ############################################################# | |
926 | # 'cat' but in pure bash | 1037 | # |
927 | passthru() { | 1038 | # After fetching the resource requested by the user, bollux needs to display or |
928 | while IFS= read -r; do | 1039 | # otherwise 'give' the resource to the user for consumption. |
929 | printf '%s\n' "$REPLY" | 1040 | # |
930 | done | 1041 | ################################################################################ |
931 | } | ||
932 | 1042 | ||
933 | # display the fetched content | 1043 | # display the fetched content |
934 | display() { # display METADATA [TITLE] | 1044 | display() { # display METADATA [TITLE] |
@@ -959,8 +1069,15 @@ display() { # display METADATA [TITLE] | |||
959 | case "$mime" in | 1069 | case "$mime" in |
960 | (text/*) | 1070 | (text/*) |
961 | set_title "$title${title:+ - }bollux" | 1071 | set_title "$title${title:+ - }bollux" |
962 | # render ANSI color escapes and don't wrap pre-formatted blocks | 1072 | # Build the `less' command |
963 | less_cmd=(less -RS) | 1073 | less_cmd=(less) |
1074 | # Render ANSI color escapes ONLY (as opposed to `-r', which | ||
1075 | # renders all escapes) | ||
1076 | less_cmd+=(-R) | ||
1077 | # Don't wrap text. `fold_line' takes care of wrapping normal | ||
1078 | # text, and pre-formatted text shouldn't wrap. | ||
1079 | less_cmd+=(-S) | ||
1080 | # Load the keybindings (see `lesskey'). | ||
964 | mklesskey && less_cmd+=(-k "$BOLLUX_LESSKEY") | 1081 | mklesskey && less_cmd+=(-k "$BOLLUX_LESSKEY") |
965 | local helpline="${KEY_OPEN}:open, " | 1082 | local helpline="${KEY_OPEN}:open, " |
966 | helpline+="${KEY_GOTO}/" | 1083 | helpline+="${KEY_GOTO}/" |