First commit

This commit is contained in:
Björn Wärmedal 2021-08-06 14:57:05 +02:00
parent 930149a383
commit 0c6136348c
4 changed files with 278 additions and 1 deletions

View File

@ -1,2 +1,12 @@
# wobbly
# Wobbly, the Web Browser Based Gemini Browser
This is a very basic browser for the gemini network protocol, implemented as a web service.
The frontend is a webpage designed to look like a simple browser window. It has some javascript to make the experience smoother and reduce load on the backend. It also has a startpage in geminispace.
The backend is a CGI script that takes a URL and fetches it over gemini, parsing and transforming it into an HTML response which is then presented in the browser's main window.
## Dependencies
The backend depends on `gemcall` and `gemtextparser`, available near this repository.

24
public_gemini/index.gmi Normal file
View File

@ -0,0 +1,24 @@
# Welcome to Wobbly!
You might be wondering what this is. That's okay.
You know how website addresses start with http:// or https://? Those are network protocols, and they're not the only ones. The gemini network protocol was invented in 2019. It has some serious drawbacks that makes it a bad replacement for HTTP/HTTPS, but that's also its main selling point. Its real aim is a simplified experience, both as a developer, producer and consumer of content.
=> gemini://gemini.circumlunar.space/ You can read more about gemini here.
But a web browser doesn't understand the gemini network protocol, nor the gemtext markup language. It speaks HTTP/HTTPS and HTML/CSS/JavaScript. Therefore, I've made Wobbly.
Wobbly is a gemini browser that runs inside your web browser. It's very basic:
* It has a help button.
* Back, Up (in the directory tree), Forward.
* A URL address bar, and a button to Go!
Try opening this web page in full screen mode in your web browser! It'll probably look more familiar then.
Wanna see what's up in geminispace?
=> gemini://warmedal.se/~antenna/ Check out Antenna, which aggregates a lot of content published in the last week.
Cheers,
ew0k

109
public_html/browser.cgi Executable file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab
import gemcall
import gemtextparser
import urllib.parse
import sys
from os import getenv
urllib.parse.uses_relative.append("gemini")
urllib.parse.uses_netloc.append("gemini")
def clean(text):
return text.replace('<','&lt;').replace('>','&gt;')
print("Content-type: text/html\n\n")
if getenv('QUERY_STRING'):
try:
baseurl = getenv('QUERY_STRING').split('?')[0]
response = gemcall.request(baseurl)
if response.responsecode in [30, 31]:
newtarget = urllib.parse.urljoin(baseurl, response.meta)
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {response.meta}\"</h1><p>This address redirects to <a href='{newtarget}'>{newtarget}</a>.</p>")
elif response.responsecode == 20 and "text/" in response.meta:
responsetext = response.read(20*1024) # We don't support more than 20kb
if "text/gemini" in response.meta:
parser = gemtextparser.GemtextParser()
preform = False
listmode = False
blockquote = False
for line in parser.parseText(responsetext.decode()):
if line.linetype == gemtextparser.LineType.PREFORM:
if not preform:
print("<pre><code>")
preform = True
print(clean(line.text()))
continue
if not line.linetype == gemtextparser.LineType.PREFORM and preform:
print("</code></pre>")
preform = False
if line.linetype == gemtextparser.LineType.LISTITEM:
if not listmode:
print("<ul>")
listmode = True
print(f"<li>{clean(line.text())}</li>")
continue
if not line.linetype == gemtextparser.LineType.LISTITEM and listmode:
print("</ul>")
listmode = False
if line.linetype == gemtextparser.LineType.BLOCKQUOTE:
if not blockquote:
print("<blockquote>")
blockquote = True
print(clean(line.text()))
continue
if not line.linetype == gemtextparser.LineType.BLOCKQUOTE and blockquote:
print("</blockquote>")
blockquote = False
if line.linetype == gemtextparser.LineType.H1 and line.text():
print(f"<h1>{clean(line.text())}</h1>")
continue
if line.linetype == gemtextparser.LineType.H2 and line.text():
print(f"<h2>{clean(line.text())}</h2>")
continue
if line.linetype == gemtextparser.LineType.H3 and line.text():
print(f"<h3>{clean(line.text())}</h3>")
continue
if line.linetype == gemtextparser.LineType.LINK:
linkURL = urllib.parse.urljoin(baseurl, clean(line.linkURL().replace("'","")))
target = "" if linkURL.startswith("gemini://") else " target='_blank'"
if line.text():
print(f"<a href='{linkURL}'{target}>{clean(line.text())}</a><br/>")
else:
print(f"<a href='{linkURL}'{target}>{linkURL}</a><br/>")
continue
if line.linetype == gemtextparser.LineType.PLAIN:
print(f"{clean(line.text())}<br/>")
if listmode:
print("</ul>")
if blockquote:
print("</blockquote>")
if preform:
print("</code></pre>")
else:
print("<pre><code>")
print(clean(responsetext.decode()))
print("</code></pre>")
elif response.responsecode == 20:
print(f"<h1>RESPONSE: \"20 Success\", but...</h1><p>The Wobbly browser only supports text responses, and this response is '{clean(response.meta)}'.</p>")
elif response.responsecode in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59]:
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {clean(response.meta)}\"</h1><p>This is an error message from the server. Hopefully it makes sense to you.</p>")
elif response.responsecode in [60, 61, 62]:
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {clean(response.meta)}\"</h1><p>The Wobbly browser does not support client certificates. Please install a more full-featured browser to visit this page.</p>")
elif response.responsecode in [10,11]:
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {clean(response.meta)}\"</h1><p>The status codes 10 and 11 are requests for user input.</p><p>The only means of submitting user input to a server in gemini is via the so-called QUERY_STRING; the part of a URL that follows after a '?'. The Wobbly browser does not support this for privacy/security reasons. Please install a more full-featured browser to be able to submit data.</p>")
response.discard()
except:
print(f"<h1>Unknown Error</h1><p>Something went wrong when trying to fetch '{getenv('QUERY_STRING')}'. Could not recover.</p>")
else:
print("<h1>What?</h1><p>I'm sorry, but I don't know what you want me to do without a URL.</p>")

134
public_html/index.html Normal file
View File

@ -0,0 +1,134 @@
<html>
<head>
<title>Wobbly, the Web-based Gemini Browser</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel='icon' href='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>♊️</text></svg>'>
<style>
body { margin: 0; padding: 0 0 5em}
#wobbly-toolbar { background-color: #cccccc; width: 100%; position: fixed; top: 0; }
#wobbly-tools { display: flex; max-width: 60em; padding: 0.2em; margin: auto; }
#wobbly-tools input { flex-basis: 100%; }
#wobbly-help { display: none; background-color: #ffffcc; }
#wobbly-help-text { max-width: 45em; margin: auto; padding: 1.5em 1em 0.5em; }
#wobbly-window { max-width: 45em; margin: auto; padding: 2em 1em 5em; }
</style>
<script>
var geminiHistory = []
var geminiPresent = 0
function geminiInit(){
document.getElementById("wobbly-window").innerHTML = ""
var startpage = "gemini://warmedal.se/~wobbly/"
document.getElementById("wobbly-addressbar").value = startpage
geminiHistory.push({url:startpage})
geminiGo()
}
function geminiHelpToggle(){
if (document.getElementById("wobbly-help-button").innerHTML === 'Help') {
document.getElementById("wobbly-help").style.display = 'block'
document.getElementById("wobbly-help-button").innerHTML = "Unhelp"
} else {
document.getElementById("wobbly-help").style.display = 'none'
document.getElementById("wobbly-help-button").innerHTML = "Help"
}
}
function geminiGo(){
url = document.getElementById("wobbly-addressbar").value.trim().split("?")[0]
if (url.length == 0) {
return
}
if (! url.startsWith("gemini://")) {
url = "gemini://" + url
}
if (! (url === geminiHistory[geminiPresent].url)) {
geminiPresent++
geminiHistory[geminiPresent] = {url:url}
while (geminiHistory.length > geminiPresent +1) {
geminiHistory.pop()
}
}
var xmlHttp = new XMLHttpRequest()
xmlHttp.open( "GET", "browser.cgi?" + url )
xmlHttp.onload = function (e) {
var body = xmlHttp.responseText
document.getElementById("wobbly-window").innerHTML = body
geminiHistory[geminiPresent].body = body
}
xmlHttp.send(null)
}
function geminiBack(){
if (geminiPresent > 0) {
geminiPresent--
document.getElementById("wobbly-addressbar").value = geminiHistory[geminiPresent].url
document.getElementById("wobbly-window").innerHTML = geminiHistory[geminiPresent].body
}
}
// Big thanks to idiomdrottning for fixing my geminiUp function <3
function geminiUp(){
var components = geminiHistory[geminiPresent].url.match(/\/[^/]*/g)
if ("/" == components.pop()) components.pop()
if (components.length == 1) return
document.getElementById("wobbly-addressbar").value = "gemini:" + components.join("") + "/"
geminiGo()
}
function geminiForward(){
if (geminiPresent < geminiHistory.length - 1) {
geminiPresent++
document.getElementById("wobbly-addressbar").value = geminiHistory[geminiPresent].url
document.getElementById("wobbly-window").innerHTML = geminiHistory[geminiPresent].body
}
}
function geminiEnterListener(e) {
if(e.keyCode === 13) {
e.preventDefault()
geminiGo()
}
}
function geminiLinkClickListener(e) {
var e = window.e || e;
if (e.target.tagName === 'A' && e.target.href.startsWith("gemini://")) {
// We only care about clicked gemini:// links
e.preventDefault()
document.getElementById("wobbly-addressbar").value = e.target.href
geminiGo()
} else {
return;
}
}
if (document.addEventListener) {
document.addEventListener('click', geminiLinkClickListener, false);
} else {
document.attachEvent('onclick', geminiLinkClickListener);
}
</script>
</head>
<body onload="geminiInit()">
<div id="wobbly-toolbar">
<div id="wobbly-tools">
<button onclick="geminiHelpToggle()" id="wobbly-help-button">Help</button>
<button onclick="geminiBack()" id="wobbly-back">⬅️</button>
<button onclick="geminiUp()" id="wobbly-up">⬆️</button>
<button onclick="geminiForward()" id="wobbly-forward">➡️</button>
<input id="wobbly-addressbar" onkeypress="geminiEnterListener(event)"/>
<button onclick="geminiGo()" id="wobbly-go">Go!</button>
</div>
</div>
<div id="wobbly-help">
<div id="wobbly-help-text">
<h1>Wobbly Help Section</h1>
<ul>
<li>This browser is for casual reading. It only supports text documents, and only cares about any sort of layout in gemtext ones.</li>
<li>Javascript has no notion of sockets that aren't http/https. Therefore all traffic from here goes through a web server that fetches and serves the gemini pages translated to html. Input and client certificates are not supported, because they would be unsafe under these circumstances.</li>
<li>'Back' and 'Forward' buttons take you to cached versions of those pages. Pressing 'Go!' without typing in a new URL is a refresh.</li>
<li>'Up' means to go one level higher in the path on the current domain. If you are on 'gemini://warmedal.se/~antenna/' and press 'Up' you will visit 'gemini://warmedal.se/'.</li>
<li>Non-gemini links will open in a new tab in your browser as usual!</li>
</ul>
</div>
</div>
<div id="wobbly-window">
<h1>This Browser Requires Javascript</h1>
<p>Sorry, there's no meaningful way around that. It's not terribly much, however. Feel free to check the source code.</p>
</div>
</body>
</html>