321 lines
6.7 KiB
Bash
Executable File
321 lines
6.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# dillo-gemini
|
|
# © 2020 Charles E. Lehner
|
|
# Copying and distribution of this file, with or without modification,
|
|
# are permitted in any medium without royalty provided the copyright
|
|
# notice and this notice are preserved. This file is offered as-is,
|
|
# without any warranty.
|
|
|
|
# Note: "read -d" is a Bashism
|
|
read -d '>' auth
|
|
read -d '>' cmd
|
|
case "$cmd" in
|
|
"<cmd='open_url' url='"*);;
|
|
*) echo $cmd; exit;;
|
|
esac
|
|
url=${cmd#"<cmd='open_url' url='"}
|
|
url=${url%"' '"}
|
|
|
|
serve_404() {
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
printf "Content-type: text/plain\r\n\r\n"
|
|
echo Not found
|
|
}
|
|
|
|
render_gemini() {
|
|
printf "Content-type: text/html\r\n\r\n"
|
|
if command -v ansi2html >/dev/null
|
|
then ansi2html
|
|
else cat
|
|
fi | awk '
|
|
function escape_html(str) {
|
|
gsub(/&/, "\\&", str)
|
|
gsub(/</, "\\<", str)
|
|
gsub(/>/, "\\>", str)
|
|
return str
|
|
}
|
|
BEGIN {
|
|
print "<!doctype html><body><style>"\
|
|
"h1, h2, h3 { margin: 0; }\n"\
|
|
"</style>"
|
|
}
|
|
{
|
|
sub(/\r$/, "")
|
|
}
|
|
/^```/ {
|
|
if (!in_literal) {
|
|
in_literal = 1
|
|
print "<pre>"
|
|
} else {
|
|
in_literal = 0
|
|
print "</pre>"
|
|
}
|
|
next
|
|
}
|
|
in_literal {
|
|
print escape_html($0)
|
|
next
|
|
}
|
|
in_list && !/^\*/ {
|
|
in_list = 0
|
|
print "</ul>"
|
|
}
|
|
/^\*/ {
|
|
if (!in_list) {
|
|
in_list = 1
|
|
printf "<ul>"
|
|
}
|
|
match($0, /^\*+[ \t]*/)
|
|
text = substr($0, RLENGTH+1)
|
|
printf "<li>%s</li>\n", escape_html(text)
|
|
next
|
|
}
|
|
/^#+/ {
|
|
match($0, /^#+/)
|
|
tag = "h" RLENGTH
|
|
match($0, /^(#+[ \t]*)/)
|
|
text = substr($0, RLENGTH+1)
|
|
html = escape_html(text)
|
|
printf "<%s style=\"font:sans-serif\">%s</%s>\n", tag, html, tag
|
|
next
|
|
}
|
|
/^>/ {
|
|
match($0, /^>+[ \t]*/)
|
|
text = substr($0, RLENGTH+1)
|
|
printf "<blockquote>%s</blockquote>\n", escape_html(text)
|
|
next
|
|
}
|
|
/^=>/ {
|
|
match($0, /^=>[ \t]*/)
|
|
href = substr($0, RLENGTH+1)
|
|
if (match(href, /^[^ \t]+/)) {
|
|
text = substr(href, RLENGTH+2)
|
|
href = substr(href, 0, RLENGTH)
|
|
}
|
|
match($0, /^=>[ \t]+/)
|
|
prefix = substr($0, 3, RLENGTH-3)
|
|
match(text, /^[ \t]+/)
|
|
text = substr(text, RLENGTH+1)
|
|
sub(/:1965/, "", href)
|
|
if (!text) {
|
|
text = href
|
|
}
|
|
if (match(href, /^gemini:\/\/[^/]+$/)) {
|
|
href = href "/"
|
|
}
|
|
sub(/^\t+/, "", prefix)
|
|
html = escape_html(text)
|
|
printf "<div>%s<a href=\"%s\">%s</a></div>\n", prefix, href, html
|
|
next
|
|
}
|
|
/^$/ {
|
|
print "<br>"
|
|
next
|
|
}
|
|
{
|
|
printf "<div>%s</div>\n", escape_html($0)
|
|
}
|
|
END {
|
|
print "</body>"
|
|
}
|
|
'
|
|
}
|
|
|
|
send_status_msg() {
|
|
printf "<cmd='send_status_message' msg='%s' '>" "$*"
|
|
}
|
|
|
|
serve_status_not_supported() {
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
printf "Content-type: text/plain\r\n\r\n"
|
|
echo Status not implemented: $1
|
|
echo $2
|
|
}
|
|
|
|
serve_missing_status() {
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
printf "Content-type: text/plain\r\n\r\n"
|
|
echo Empty status response. $2
|
|
}
|
|
|
|
serve_input() {
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
url=$1
|
|
prompt=$2
|
|
# TODO: html-escape prompt
|
|
printf "Content-type: text/html\r\n\r\n"
|
|
cat <<-EOF
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Input</title>
|
|
</head>
|
|
<body>
|
|
<form action="gemini:input:$url" method=get>
|
|
<h3>$prompt</h3>
|
|
<input name=q style='width:100%'>
|
|
<input type=submit>
|
|
</form>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
}
|
|
|
|
serve_success() {
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
type=$1
|
|
case "$type" in
|
|
text/gemini*) render_gemini;;
|
|
application/xml|application/*+xml) printf "Content-type: text/xml\r\n\r\n"; cat;;
|
|
*) printf "Content-type: %s\r\n\r\n" "$type"; cat;;
|
|
esac
|
|
}
|
|
|
|
serve_redirect() {
|
|
url=$1
|
|
send_status_msg "Redirected"
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
printf "Content-type: text/html\r\n\r\n"
|
|
# TODO: html-escape url
|
|
cat <<-EOF
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Redirect to $url</title>
|
|
</head>
|
|
<body>
|
|
<h3>Redirect to <a href="$url">$url</a></3>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
}
|
|
|
|
serve_real_redirect() {
|
|
send_status_msg "Redirected"
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
printf "Content-type: text/html\r\n\r\n"
|
|
# TODO: html-escape url
|
|
cat <<-EOF
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Redirecting to $url</title>
|
|
<meta http-equiv="Refresh" content="0; url=$url" />
|
|
</head>
|
|
<body>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
}
|
|
|
|
serve_error() {
|
|
status=$1
|
|
meta=$2
|
|
send_status_msg "Request failed"
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
printf "Content-type: text/html\r\n\r\n"
|
|
cat <<-EOF
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Request failed</title>
|
|
</head>
|
|
<body>
|
|
<h3>Request failed: $status</h3>
|
|
<code>$meta</code>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
}
|
|
|
|
serve_fail() {
|
|
meta="$1"
|
|
send_status_msg "Client certificate required"
|
|
printf "<cmd='start_send_page' url='' '>\n"
|
|
printf "Content-type: text/html\r\n\r\n"
|
|
cat <<-EOF
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Client certificate required</title>
|
|
</head>
|
|
<body>
|
|
<h3>Client certificate required</h3>
|
|
<p>Not implemented!</p>
|
|
<code>$meta</code>
|
|
</body>
|
|
</html>
|
|
EOF
|
|
}
|
|
|
|
serve_gemini_input() {
|
|
url=${1#gemini:input:}
|
|
|
|
# gemini:input://REQUEST?q=INPUT -> gemini://REQUEST?INPUT
|
|
url_no_query=${url%%?q=*}
|
|
if [ "$url_no_query" != "$url" ]; then
|
|
url="${url_no_query}?${url#*?q=}"
|
|
fi
|
|
|
|
url=$(printf "%s" "$url" | sed 's/\+/%20/g')
|
|
serve_real_redirect "$url"
|
|
}
|
|
|
|
blobs_dir=${ssb_path:-~/.${ssb_appname:-ssb}}/blobs
|
|
|
|
serve_gemini() {
|
|
url=${1%%#*}
|
|
url_noquery=${url%%\?*}
|
|
query=${url##*\?}
|
|
if [ "$query" = "$url_noquery" ]
|
|
then query=
|
|
else query="?$query"
|
|
fi
|
|
url_scheme_relative=${url_noquery#gemini://}
|
|
hostname=${url_scheme_relative%%/*}
|
|
path=${url_scheme_relative#*/}
|
|
if [ "$path" = "$hostname" ]; then path=; fi
|
|
host=${hostname%%:*}
|
|
port=${hostname##*:}
|
|
url="gemini://$hostname/$path$query"
|
|
|
|
if [ "$host" = "$port" ]; then port=1965; fi
|
|
send_status_msg "Sending request..."
|
|
printf "%s\r\n" "$url" | openssl s_client -quiet -connect "$host:$port" | serve_gemini_response "$url"
|
|
}
|
|
|
|
serve_gemini_response() {
|
|
url=$1
|
|
read status meta
|
|
send_status_msg "Status: $status"
|
|
meta=$(echo "$meta" | sed 's/\s*$//')
|
|
mkdir -p $blobs_dir/tmp ~/.dillo/gemini
|
|
tmp=$(mktemp $blobs_dir/tmp/XXXXXXXXXXXX)
|
|
tee "$tmp" | case "$status" in
|
|
1*) serve_input "$url" "$meta";;
|
|
2*) serve_success "$meta";;
|
|
3*) serve_redirect "$meta";;
|
|
4*) serve_error "$status" "$meta";;
|
|
5*) serve_error "$meta";;
|
|
#6*) serve_client_cert_required "$meta";;
|
|
'') serve_missing_status "$meta";;
|
|
*) serve_status_not_supported "$status" "$meta";;
|
|
esac
|
|
dest=$blobs_dir/sha256/$(sha256sum "$tmp" | awk '{print substr($1, 1, 2) "/" substr($1, 3)}')
|
|
if test -e "$dest" && cmp -s "$tmp" "$dest"
|
|
then
|
|
# unchanged resource
|
|
rm "$tmp"
|
|
else
|
|
mkdir -p "${dest%/*}"
|
|
mv "$tmp" "$dest"
|
|
fi
|
|
printf "%s %s %s %s\n" "$(date -u +%FT%TZ)" "$dest" "$url" "$status $meta" >>~/.dillo/gemini/history.txt
|
|
}
|
|
|
|
case "$url" in
|
|
gemini:input:*) serve_gemini_input "$url";;
|
|
gemini:*) serve_gemini "$url";;
|
|
*) serve_404;;
|
|
esac
|