├── .gitignore
├── DO_IT_LIVE.txt
├── EXAMPLE_onyx.json
├── Info.plist
├── LICENSE.txt
├── Mac Read Me.rtf
├── Makefile
├── Makefile.generic
├── Makefile.macos
├── Makefile.mxe
├── README.md
├── client.pl
├── ext
├── Makefile
├── agate.js
├── d
├── jasper.js
├── manifest.json
├── r
│ └── stop.svg
└── topaz.js
├── mac-onyx-inst
├── old
└── client.c
├── onyx.c
├── onyx.nsi
└── wintest.c
/.gitignore:
--------------------------------------------------------------------------------
1 | onyx
2 | *.exe
3 | *.xpi
4 | *.dmg
5 | mac/*
6 | *.dSYM
7 | *.dSYM/*
8 | .DS_Store
9 |
10 |
--------------------------------------------------------------------------------
/DO_IT_LIVE.txt:
--------------------------------------------------------------------------------
1 | ln -s /Users/spectre/src/nxo/onyx /Applications/onyx
2 | mkdir -p ~/Library/Application\ Support/Mozilla/NativeMessagingHosts
3 | ln -s /Users/spectre/src/nxo/onyx.json ~/Library/Application\ Support/Mozilla/NativeMessagingHosts/onyx.json
4 |
5 |
--------------------------------------------------------------------------------
/EXAMPLE_onyx.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "onyx",
3 | "description": "OverbiteNX Gopher system component",
4 | "path": "/home/linus/bin/onyx",
5 | "type": "stdio",
6 | "allowed_extensions": [ "overbitenx@floodgap.com" ]
7 | }
8 |
--------------------------------------------------------------------------------
/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | onyx-inst
9 | CFBundleIdentifier
10 | com.floodgap.onyx
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | Onyx
15 | CFBundleShortVersionString
16 | 0.9.2.1
17 | CFBundleVersion
18 | 902.1.0
19 | LSMinimumSystemVersion
20 | 10.9
21 | LSUIElement
22 |
23 | NSAppTransportSecurity
24 |
25 | NSAllowsArbitraryLoads
26 |
27 |
28 | NSHumanReadableCopyright
29 | © 2019 Cameron Kaiser
30 | NSMainNibFile
31 | MainMenu
32 | NSPrincipalClass
33 | NSApplication
34 |
35 |
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/classilla/overbitenx/61db0f01bd1643fff09477f45b4de9a0cc626c22/LICENSE.txt
--------------------------------------------------------------------------------
/Mac Read Me.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830
2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Monaco;}
3 | {\colortbl;\red255\green255\blue255;\red242\green242\blue242;\red0\green0\blue0;}
4 | {\*\expandedcolortbl;;\csgray\c95825;\csgray\c0\c85000;}
5 | \margl1440\margr1440\vieww10800\viewh8400\viewkind0
6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
7 |
8 | \f0\fs24 \cf0 Onyx is the native component that allows the OverbiteNX addon for Firefox to access Gopher sites. You must install Onyx on each system you use OverbiteNX with. Onyx does not require your administrator password or elevated permissions to run. It is supported on macOS 10.12 and higher, though it may work on earlier versions.\
9 | \
10 | To install on macOS:\
11 | \
12 | - Drag Onyx.app to
13 | \f1 \cf2 \cb3 \CocoaLigature0 /Applications
14 | \f0 \cf0 \cb1 \CocoaLigature1 . It will not work from any other location.\
15 | - Double-click Onyx to run its internal installer. It will install a JSON file to your home directory, located in
16 | \f1 \cf2 \cb3 \CocoaLigature0 ~/Library/Application Support/Mozilla/NativeMessagingHosts/onyx.json
17 | \f0 \cf0 \cb1 \CocoaLigature1 and then exit.\
18 | -
19 | \b Leave Onyx in your applications folder. Do not delete it.
20 | \b0 \
21 | - Return to or start Firefox (you don\'92t need to quit or restart Firefox if it was already running).\
22 | - If you already installed OverbiteNX, you can simply now browse to any Gopher URL. You do not need to restart Onyx in future sessions; Firefox now will handle that for you.\
23 | \
24 | Onyx is an unsigned application. By default, your Mac may not be allowed to run it directly. If your Mac says that Onyx is unsigned or not allowed to run, right-click on it and select Open, or temporarily change your Gatekeeper settings in System Preferences.\
25 | \
26 | If you have multiple users on your Mac, they should each double-click Onyx once to install that same JSON file.\
27 | \
28 | Periodically updates to Onyx may be made available. To install them,
29 | \b quit Firefox,
30 | \b0 drag the new Onyx to the applications folder (replacing the old version of Onyx), and restart Firefox. You do not need to start Onyx itself again for the update to take effect.\
31 | \
32 | To remove Onyx, remove the JSON file from your home folder (located at the path above) and Onyx.app
33 | \b when Firefox is not running.
34 | \b0 You should then uninstall OverbiteNX when you restart Firefox, as OverbiteNX will not be able to function without Onyx on your Mac.\
35 | \
36 | Copyright 2018, Cameron Kaiser.\
37 | All rights reserved.\
38 | Onyx and OverbiteNX are made available to you under the Floodgap Free Software License.}
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: default clean
2 |
3 | # This Makefile is intended for Macs which can build everything.
4 | # If you just want to build a subset, use the individual Makefile.
5 |
6 | default:
7 | $(MAKE) -f Makefile.generic
8 | $(MAKE) -f Makefile.macos
9 | $(MAKE) -f Makefile.mxe
10 | ( cd ext && $(MAKE) )
11 |
12 | clean:
13 | $(MAKE) -f Makefile.generic clean
14 | $(MAKE) -f Makefile.macos clean
15 | $(MAKE) -f Makefile.mxe clean
16 | ( cd ext && $(MAKE) clean )
17 |
--------------------------------------------------------------------------------
/Makefile.generic:
--------------------------------------------------------------------------------
1 | CC = /usr/bin/gcc
2 | CFLAGS ?= -O2
3 | #CFLAGS ?= -DDEBUG
4 | RM = /bin/rm
5 | default: onyx
6 | .PHONY: default clean
7 |
8 | onyx: onyx.c
9 | $(CC) $(CFLAGS) -g -o onyx onyx.c
10 |
11 | # Optional. client.pl is preferred for testing.
12 | client: client.c
13 | $(CC) $(CFLAGS) -g -o client client.c
14 |
15 | clean:
16 | $(RM) -rf onyx client onyx.dSYM client.dSYM
17 |
--------------------------------------------------------------------------------
/Makefile.macos:
--------------------------------------------------------------------------------
1 | MV=/bin/mv
2 | RM=/bin/rm
3 | MKDIR=/bin/mkdir
4 | DITTO=/usr/bin/ditto
5 | PLUTIL=/usr/bin/plutil
6 | HDIUTIL=/usr/bin/hdiutil
7 | PLATYPUS=/usr/local/bin/platypus
8 |
9 | WHERE=mac
10 | APP=$(WHERE)/Onyx.app
11 | APPCON=$(APP)/Contents
12 | APPOBJ=$(APPCON)/MacOS
13 |
14 | .PHONY: default clean
15 |
16 | # This Makefile requires Makefile.generic to have already run, since it
17 | # just packages that binary.
18 |
19 | Onyx.dmg: onyx clean
20 | $(MKDIR) -p $(WHERE)
21 | $(PLATYPUS) -a Onyx \
22 | -I com.floodgap.onyx \
23 | -i '' \
24 | -c mac-onyx-inst \
25 | -o "Progress Bar" \
26 | -p /bin/sh \
27 | -V 0.9.2.1 \
28 | -u "Cameron Kaiser" \
29 | -y $(APP)
30 | #
31 | # Manually fix the bundle so that everything is in the same
32 | # places as prior versions for backwards compatibility. This means
33 | # using a custom Info.plist and moving a few things around.
34 | #
35 | $(MV) $(APPOBJ)/Onyx $(APPOBJ)/onyx-inst
36 | $(DITTO) Info.plist $(APPCON)
37 | $(PLUTIL) -convert binary1 $(APPCON)/Info.plist
38 | $(DITTO) onyx $(APP)/Contents/MacOS
39 | #
40 | # Build disk image.
41 | #
42 | $(DITTO) "Mac Read Me.rtf" $(WHERE)
43 | $(HDIUTIL) create -sectors 6000 \
44 | -fs HFS+ -fsargs "-c c=64,a=16,e=16" \
45 | -volname "Overbite Onyx for macOS" \
46 | -srcfolder $(WHERE) \
47 | -format UDBZ \
48 | Onyx.dmg
49 |
50 | onyx:
51 | $(MAKE) -f Makefile.generic
52 |
53 | clean:
54 | $(RM) -rf $(WHERE) Onyx.dmg
55 |
--------------------------------------------------------------------------------
/Makefile.mxe:
--------------------------------------------------------------------------------
1 | MXE_ROOT=$(HOME)/mxe
2 | CC=$(MXE_ROOT)/usr/bin/i686-w64-mingw32.static-gcc
3 | CFLAGS ?= -O2
4 | #CFLAGS ?= -DDEBUG
5 | MAKENSIS=$(MXE_ROOT)/usr/i686-w64-mingw32.static/bin/makensis
6 | RM=/bin/rm
7 |
8 | onyxinst.exe: onyx.exe onyx.nsi wintest.exe
9 | $(MAKENSIS) onyx.nsi
10 |
11 | onyx.exe: onyx.c
12 | $(CC) $(CFLAGS) -g -o onyx.exe onyx.c -lws2_32
13 |
14 | wintest.exe: wintest.c
15 | $(CC) -g -o wintest.exe wintest.c -lws2_32
16 |
17 | clean:
18 | $(RM) -f onyx.exe onyxinst.exe wintest.exe
19 |
20 | .PHONY: clean
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OverbiteNX
2 |
3 | OverbiteNX is a Gopher client add-on for Firefox that allows Firefox to access sites over [the historical Gopher protocol](https://en.wikipedia.org/wiki/Gopher_(protocol)). It is a successor to OverbiteFF, which no longer functions under WebExtensions, intended to bridge users requiring direct connection to Gopher sites until WebExtensions implements more comprehensive network functionality.
4 |
5 | OverbiteNX comes in two pieces: OverbiteNX itself, which is a standard WebExtensions-compatible Firefox addon; and Onyx, a native component that OverbiteNX drives through Firefox to perform network access. Onyx is supported on macOS (10.12+ and probably earlier versions), Windows (7 and up, 32- or 64-bit) and Linux, and may work on other tier-3 platforms that can run Firefox. OverbiteNX is supported on Firefox 60 and up on the same platforms. Both OverbiteNX and Onyx must be installed for proper functionality.
6 |
7 | OverbiteNX and Onyx are provided to you under the [Floodgap Free Software License](https://www.floodgap.com/software/ffsl/). You use this software package at your own risk. It is otherwise unsupported, and no warranty is expressed or implied regarding merchantability or fitness for a particular purpose.
8 |
9 | OverbiteNX and Onyx are copyright (C) 2017-2019 Cameron Kaiser.
10 | All rights reserved.
11 |
12 | ## OverbiteNX is currently in beta testing
13 |
14 | OverbiteNX currently functions and runs, but has known bugs. If you want to help test or develop it, you will need to get your hands a little dirty.
15 |
16 | ## How to install the beta test
17 |
18 | 1. If you wish to have the source code for reference, clone or download this repo and put it somewhere convenient. If you don't have `git` on your computer, just [download this repo as a .zip file](https://github.com/classilla/overbitenx/archive/master.zip). However, as of the beta this is no longer required.
19 |
20 | 2. Install Onyx. You can get a pre-built binary from the [releases tab](https://github.com/classilla/overbitenx/releases) for Windows 7+ or macOS 10.12+, or see below on how to build from source (required for Linux/*BSD/etc.). The Windows version is distributed as an installer which can be run directly, and it can be uninstalled from the regular Add/Remove Programs control panel. The macOS version is distributed as a DMG; read the instructions inside the disk image for how to install and uninstall.
21 |
22 | 3. *You have a choice:* if you would like to use an officially signed extension, you can [download it from AMO](https://addons.mozilla.org/en-US/firefox/addon/overbitenx/). As the extension is updated, it will be automatically pushed to you. Alternatively, you can load the extension directly from the source code. In Firefox, go to `about:debugging` and add a "Temporary Add-on." Browse to where you put the repo, enter the `ext` directory, and select `manifest.json`. The disadvantage of loading it directly, however, is that you will need to repeat this step every time Firefox starts up.
23 |
24 | 4. Type or navigate to any URL starting with `gopher://`. Firefox will ask if you want to use OverbiteNX; you do (check the box if you want to remember that choice, which is strongly advised or you will see that requester box a lot). Assuming everything is correctly installed, the browser will download and display the requested resource.
25 |
26 | If you notice any untoward behaviour, the current beta test generates copious debugging output to the Browser Console. Please include a transcript of this output in any issue you file.
27 |
28 | ## How to build from source
29 |
30 | 1. Onyx is written in portable C that should compile on nearly any POSIX-compliant system. There are some Win32-specific sections due to irregularities with Winsock. `gcc` and `clang` are both supported compilers.
31 |
32 | 2. If you have a Mac, and both Xcode and [MXE with NSIS](http://mxe.cc/) are installed, then you can just type `make` and the macOS DMG and Windows installer (and .xpi for Firefox, eventually) will be automatically built. If the Windows build blows up, make sure the path in `Makefile.mxe` is correctly pointing to your MXE binaries, and that you installed NSIS (which includes `makensis`). Note that the Mac application is unsigned. If you just want to build the Mac version by itself, do `make -f Makefile.macos` instead. The Mac version requires both a recent version of Xcode and [Platypus](https://sveinbjorn.org/platypus) to build.
33 |
34 | 3. If you are building on Linux/*BSD/etc. or a Mozilla tier-3 system, make sure you have both `make` and a C compiler installed (either `gcc` or `clang` is acceptable, though you may need to symlink your `clang` to `gcc` depending on your system's configuration), and build Onyx with `make -f Makefile.generic`. Once this is done, copy the resulting `onyx` binary to your desired location. Copy `EXAMPLE_onyx.json` to `onyx.json` and change the path in that file to the location of your new `onyx` binary, then copy `onyx.json` to [where the native manifest should be on your system](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_manifests#Manifest_location). If you have MXE installed, you should also be able to build the Windows version (again, verify the path to your MXE binaries is correct) with `make -f Makefile.mxe`.
35 |
36 | 4. Building on Windows itself is not yet supported, but should work with [MinGW](http://www.mingw.org/). If you devise a working `Makefile` for this environment, please file a pull request.
37 |
38 | 5. To build the `.xpi` for Firefox, enter the `ext` directory and type `make`. The `.xpi` thus generated is unsigned. However, making and installing the `.xpi` is not necessary to test OverbiteNX (see step 3 above).
39 |
40 | ## Theory of operation
41 |
42 | OverbiteNX is intended to be highly modular, both for purposes of maintenance and as an educational example of using native messaging to get around WebExtensions' sometimes ridiculous limitations. The basic notion is shown graphically:
43 |
44 | ````
45 | +------+
46 | | Onyx |-----> Gopherspace
47 | +------+
48 | ^
49 | native code |
50 | ----------------------|-------------------------
51 | add-on background |
52 | | +---------+
53 | | | Agate |
54 | +---------+ +---------+
55 | +---------->| Topaz |<----------+
56 | | +---------+ |
57 | | ^ |
58 | | | |
59 | -----|----------------|----------------|--------
60 | tabs | | |
61 | +--------+ +--------+ +--------+
62 | | Jasper | | Jasper | | Jasper | etc.
63 | +--------+ +--------+ +--------+
64 | ````
65 |
66 | Onyx runs as a native application separately, though as a subprocess connected by pipes, of the browser. Within the browser, the OverbiteNX add-on has two background scripts, Topaz and Agate. Agate handles rewriting history and bookmarks to match the canonical URL (avoiding the user's bookmarks being polluted by `moz-extension:` URLs that may no longer be valid). Topaz acts as the gateway to Onyx, accepting and queueing requests from the Jasper client in each browser tab with a Gopher URL, and then passing the requests to Onyx and proxying the response back to the tab that made it. (This also includes some basic tab management, since knowing when a tab is closed or navigating away from a Gopher URL is necessary to properly maintain the work queue.)
67 |
68 | The Jasper front-end functions essentially as a local AJAX web application. Once OverbiteNX's dispatch scaffold page is loaded into a tab by the browser's protocol handler, as far as the browser is concerned, the page is considered "loaded." However, the page then loads Jasper. The new instance of Jasper takes the encoded URL and makes it into a request to Topaz. Topaz queues the request, and when Onyx is idle, sends it to Onyx. Onyx makes the connection and returns data to the requesting instance of Jasper via Topaz, which is then used to asynchronously display the resource. This is very different than OverbiteFF, which implemented a low-level network channel, enabling it to be a "first-class protocol" in the browser. Because of this difference, Jasper implements its own progress bar and other UI elements (though Topaz provides the stop button as a page action because this also manipulates the work queue), since the browser is otherwise unaware of what is actually occurring. This difference also explains the basis of some particular limitations with OverbiteNX.
69 |
70 | ## Implementation notes
71 |
72 | Onyx is written in C. Although C is not a safe language (please, Rusties, don't send me E-mail, I don't want to hear it), it is the most portable option right now and allows Onyx to be built with a minimal toolchain. Onyx also has no dependencies on any external libraries, and to further reduce its attack surface is written to do only the bare minimum amount of work necessary to connect to and pass data from a remote gopher site, shunting the remainder off to Topaz, Agate and Jasper.
73 |
74 | Onyx transactions use a small subset of JSON, just objects with one single-letter key and a string value (which, for network requests and data, is a hex-encoded payload). The protocol is documented within Onyx's source code. Using this very small subset means Onyx doesn't need to be built with an entire JSON library which could have its own bugs, and also means malicious servers can't fuzz Onyx (or, for that matter, Topaz and Jasper) by causing malformed JSON to be generated.
75 |
76 | Jasper uses a minimal UI so that the display "just works" on any configuration, and little localization work is required. Menu icons are actually emoji, and the font is always monospace.
77 |
78 | Onyx only accepts requests to port numbers on its internal whitelist, which was observationally based off an extract of past and present servers in Veronica-2. Other port numbers are rejected.
79 |
80 | ## Irregularities and current limitations
81 |
82 | Onyx is interruptable, but not currently multi-threaded. Topaz queues requests as they arrive from Jasper instances and sends the next request to Onyx when the prior request terminates. If the request is cancelled (such as the user navigating away, closing the tab, clicking the stop page action, etc.), Topaz will interrupt Onyx and then send the next request in queue. Until their request is serviced, however, Jasper instances with requests in the work queue must wait their turn. In practice this is only of interest if you have multiple Gopher tabs running, or if you have a particularly long and slow download. Making Onyx (and, thus, Topaz) multi-threaded is a future goal, but wasn't necessary for the MVP.
83 |
84 | To stop the current transaction, you must either click the "stop sign" page action that appears during data transfer, or close the tab or navigate away. The browser does not know that an Onyx network transfer is in progress, so it does not enable the regular browser stop button. There is no known API to enable this in WebExtensions currently.
85 |
86 | There is also no support in WebExtensions' `downloads` component for streaming data to a download session. Images and downloads must instead be pulled into browser RAM as a JavaScript `Blob` and then a blob URL generated to display the image or download the file. For this reason Topaz will cut off a transaction at 16MB to prevent a large file or a malicious server from causing you to run out of memory (especially on a 32-bit system), so if you intend to download your DVD .isos over Gopher, you probably want a dedicated client. However, it also requires Jasper to leak the blob URL so that you can continue to interact with the image and/or download when completed; if the blob URL is revoked immediately after the request finishes, then the browser acts as it does not exist, which is fairly inconvenient and causes unexpected behaviour.
87 |
88 | HTML and XML files are currently displayed as plain text. Because Jasper looks like an AJAX web app to Firefox, there are security concerns about loading and displaying arbitary HTML inside the page scaffold. There may be a way to sanitize this in a future version, but because Jasper isn't a low-level channel, links to Gopher-hosted inline content (images, style sheets, JavaScript) within an HTML document will probably not work even then without implementing some sort of HTML renderer (yo dawg).
89 |
90 | The address bar shows the address of the scaffold page within OverbiteNX, not the actual canonical URL. There appears to be no way to change this from the add-on itself. As a result, when you bookmark a Gopher URL, the star doesn't actually show up in the address bar (even though the Gopher URL is truly bookmarked) because Agate immediately rewrites it and thus it no longer matches the current address.
91 |
92 | Related to this phenomenon is that both the OverbiteNX `moz-extension://` URL and the canonical `gopher://` URL appear in your history for any given Gopher site. Transparently deleting the `moz-extension://` URL wrecks the history of the current tab and there is no provision in WebExtensions for rewriting individual history entries, so Agate simply adds an entry instead.
93 |
94 | Proxies are not yet supported by Onyx.
95 |
96 | The inline view feature of OverbiteFF is not yet implemented in OverbiteNX.
97 |
98 | CAPS support (for features such as path breadcrumbs, etc.) is not yet implemented in OverbiteNX.
99 |
100 | If you have an idea how to fix or improve these issues, please file a pull request or issue.
101 |
--------------------------------------------------------------------------------
/client.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 |
3 | # Copyright 2017-8 Cameron Kaiser.
4 | # All rights reserved.
5 | #
6 | # This script emits a NativeMessaging request over stdout which can be
7 | # piped to Onyx for testing.
8 | #
9 | # For Win32, see wintest.c.
10 |
11 | sub post {
12 | $string = "{\"a\":\"" .
13 | sprintf("%04x", $port) .
14 | unpack("H2", $itype) .
15 | sprintf("%04x", length($host)) .
16 | unpack("H*", $host) .
17 | unpack("H*", $sel). "0d0a" .
18 | "\"}";
19 |
20 | print STDOUT pack("L", length($string)) . $string;
21 | }
22 |
23 | select(STDOUT); $|++;
24 | ($host, $port, $itype, $sel) = (@ARGV);
25 | die("usage: $0 host port itype sel | onyx\n")
26 | if (!length($itype));
27 | &post;
28 |
29 | sleep 11;
30 |
31 |
--------------------------------------------------------------------------------
/ext/Makefile:
--------------------------------------------------------------------------------
1 | RM=/bin/rm
2 |
3 | # This builds the XPI for Firefox.
4 |
5 | .PHONY: default clean
6 |
7 | default: clean
8 | zip -r ../overbitenx.xpi *.js manifest.json d r
9 |
10 | clean:
11 | $(RM) -f ../overbitenx.xpi
12 |
--------------------------------------------------------------------------------
/ext/agate.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Agate module: URL rewrite support (background)
3 | *
4 | * Copyright 2018, 2022 Cameron Kaiser.
5 | * All rights reserved.
6 | */
7 |
8 |
9 | // If we change the URL for the browser scaffold, it needs to change here too.
10 | var me = browser.runtime.getURL("d");
11 |
12 | function munge(url) {
13 | if (url.indexOf(me) != 0) return null;
14 |
15 | let nurl = decodeURIComponent(url.substring(url.indexOf("?")+1));
16 | return nurl;
17 | }
18 |
19 | function canonicalizeBookmark(id, item) {
20 | let nurl = munge(item.url);
21 | if (nurl) {
22 | // Change the URL to what the user "expects" as bookmarks
23 | // are created. We don't care if this succeeds or fails.
24 | browser.bookmarks.update(id, { url : nurl });
25 | }
26 | }
27 |
28 | function canonicalizeHistory(item) {
29 | let nurl = munge(item.url);
30 | if (nurl) {
31 | // Change the history entry to the URLs the user "expects."
32 | // Deleting the prior history items has a tendency to muck
33 | // up the tab's history, though it's more elegant, so we
34 | // leave the old history entries alone and just add new ones
35 | // so that the omnibar will suggest them (hopefully with
36 | // higher affinity). We don't care if this succeeds or fails.
37 | //
38 | // XXX: If Mozilla ever adds the ability to look at an
39 | // individual tab's history easily, then we could add a tab
40 | // closer handler here to filter the history when the tab is
41 | // closed and clean this up a little better.
42 |
43 | browser.history.addUrl({
44 | url : nurl,
45 | title : item.title,
46 | transition : item.transition,
47 | visitTime : item.visitTime
48 | });
49 | }
50 | }
51 |
52 | browser.bookmarks.onCreated.addListener(canonicalizeBookmark);
53 | browser.history.onVisited.addListener(canonicalizeHistory);
54 |
--------------------------------------------------------------------------------
/ext/d:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
60 |
61 |
62 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/ext/jasper.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Jasper module: front end
3 | *
4 | * Copyright 2017-2022 Cameron Kaiser.
5 | * All rights reserved.
6 | */
7 |
8 | var itype = null;
9 | var sel = null;
10 | var host = null;
11 | var port = -1;
12 |
13 | var prog = 0;
14 | var buf = [];
15 | var mbuf = "";
16 | var fnum = 0;
17 |
18 | var url = null;
19 | var url_s = "";
20 | var url_p = "";
21 | var hopo_s = "";
22 |
23 | /*
24 | * Translate hex-encoded data from Topaz into ready-to-use formats.
25 | */
26 |
27 | function hex2a(hexx) {
28 | // Hex to string, including any necessary UTF-8 conversion.
29 | let ta = hex2ta(hexx);
30 |
31 | // Do this in pieces to avoid overflowing Function.prototype.apply
32 | // if we ever get big chunks.
33 | let str = "";
34 | let i = 0;
35 | let len = ta.length;
36 | for (i=0; i\n"); }
65 | function clean(x) {
66 | // Clean an arbitrary string for display in an HTML page.
67 | x = x.replace(/\&/g, "&");
68 | x = x.replace(/\/g, ">");
70 |
71 | // whitespace is a beyotch!
72 | x = x.replace(/ /g, " ");
73 | x = x.replace(/ \ /g, " ");
74 | x = x.replace(/\ /g, " ");
75 | x = x.replace(/^ /, " ");
76 |
77 | return x;
78 | }
79 |
80 | /*
81 | * UI
82 | */
83 |
84 | function progress(i) {
85 | // Control the front-end client bar.
86 |
87 | if ((prog + i) > 99) {
88 | prog = 100;
89 | di("prog_b").value = 100;
90 | setTimeout(function() {
91 | di("prog").style.display = "none";
92 | }, 100);
93 | return;
94 | }
95 |
96 | if ((prog + i) > 80) {
97 | prog = 80;
98 | di("prog").removeAttribute("value");
99 | return;
100 | }
101 | prog += i;
102 | di("prog_b").value = prog;
103 | }
104 |
105 | function clicker(e) {
106 | // Global document click handler (but just for local URLs). This
107 | // serves two purposes: first, it speeds up navigation considerably,
108 | // and second, it avoids a glitch with history munging when we put
109 | // protocol-handler-handled URLs back-to-back-to-back. This is
110 | // (expectantly) installed when a menu type is loaded.
111 |
112 | // Uh.
113 | if (!e.target || !e.target.tagName)
114 | return true;
115 |
116 | // Ignore clicks on things that aren't links or form buttons.
117 | if (e.target.tagName != "BUTTON" && e.target.tagName != "A")
118 | return true;
119 |
120 | // Ignore anything but left clicks.
121 | if (!e.which || e.which != 1)
122 | return true;
123 |
124 | // Button clicks handled here. These are for anything with an
125 | // embedded form, such as itype 7.
126 | if (e.target.tagName == "BUTTON") {
127 | e.preventDefault();
128 |
129 | // Recover the ID number.
130 | let i = e.target.id.substr(3);
131 | // Redirect to the search server encoded in the page.
132 | let k = "?" +
133 | di("_fl" + i).value + // already encoded
134 | "%09" +
135 | encodeURIComponent(di("_ft" + i).value);
136 | console.log(k);
137 | location.href = k;
138 | return false;
139 | }
140 |
141 | // Ignore clicks on links that are likely to be handled directly
142 | // by the browser. Right now this is HTTP(S) and FTP.
143 | if (!e.target.href || e.target.href.indexOf("http") == 0 ||
144 | e.target.href.indexOf("ftp://") == 0)
145 | return true;
146 |
147 | // The link is either ours, or likely to be run by a protocol
148 | // handler. Don't ignore it to avoid the tab munging glitch.
149 | e.preventDefault();
150 |
151 | if (e.target.href.indexOf(url_p) == 0)
152 | // Ours. This is faster, and doesn't munge the tab history.
153 | // The URL is already sanitized; the browser will encode it.
154 | location.href = "?" + e.target.href;
155 | else
156 | // Not ours. Open a new tab with the new protocol handler.
157 | browser.tabs.create({ url : e.target.href });
158 | return false;
159 | }
160 |
161 | function uierror(w, x) {
162 | // Display a UI error message.
163 | progress(100);
164 |
165 | di("pre").style.display = "none";
166 | di("title").insertAdjacentHTML('afterbegin', w);
167 | di("data").insertAdjacentHTML('afterbegin',
168 | '
' +
169 | '' + x + '
');
170 | di("title").style.display = "";
171 | di("data").style.display = "";
172 | }
173 |
174 | const _EMOJI_UNSUPPORTED = "⛔"; // not supported
175 | function icontype(i) {
176 | // Returns an emoji icon for a supported item type, or the
177 | // universal unsupported item type icon.
178 | return (
179 | (i == "i") ? " " : // exception
180 |
181 | (i == "0") ? "📄" : // document
182 | (i == "1") ? "📁" : // menu
183 | // XXX: itype 2 not yet supported
184 | (i == "3") ? "⚠" : // error icon
185 | (i == "4") ? "💾" : // BinHex
186 | (i == "5") ? "💾" : // zip
187 | (i == "6") ? "💾" : // uucode
188 | (i == "7") ? "🔍" : // search
189 | (i == "8") ? "📞" : // tel-net (get it?)
190 | (i == "9") ? "💾" : // generic binary
191 | (i == "d") ? "📑" : // PDF document
192 | (i == "g") ? "📷" : // GIF
193 | (i == "h") ? "📄" : // HTML document
194 | (i == "p") ? "📷" : // PNG
195 | (i == "s") ? "🔊" : // audio file
196 | (i == "x") ? "📄" : // XML document
197 | (i == "I") ? "📷" : // JPEG or other images
198 | (i == "T") ? "📞" : // tel-net 3270 (get it?)
199 | (i == ";") ? "🎥" : // movie file
200 | _EMOJI_UNSUPPORTED);
201 | }
202 |
203 | /*
204 | * Process menu items
205 | */
206 |
207 | function next(x, p) {
208 | // Process a tab-delimited line into a head and tail.
209 | let y = x.indexOf("\t");
210 | if (y < 0) { eout(p); return [ null, null, p ]; }
211 |
212 | let k = x.substr(0,y);
213 | let l = x.substr(y+1);
214 | // k (the head) can be 0-length, but not l (the tail).
215 | if (!l || !l.length) { eout(p); return [ null, null, p ]; }
216 |
217 | return [ k, l, null ];
218 | }
219 |
220 | function pushMenuData(s) {
221 | // Receive and process menu data as it comes.
222 | mbuf += s;
223 |
224 | for(;;) {
225 | if (mbuf.indexOf("\n") < 0) {
226 | // We don't have a full item to process.
227 | return;
228 | }
229 | let ds = null;
230 | let p = -1;
231 | let h = null;
232 | let s = null;
233 | let e = 0;
234 | let i = null;
235 | let l = null;
236 |
237 | let m = mbuf.substr(0, mbuf.indexOf("\n"));
238 | mbuf = mbuf.substr(mbuf.indexOf("\n") + 1);
239 |
240 | [ ds, m, e ] = next(m, "no selector"); if (e) continue;
241 | if (ds.length < 2) {
242 | // It is possible, and the RFC does not forbid,
243 | // to have a null display string. As a practical
244 | // matter, however, this is only viable for i
245 | // itemtype. Note that doing it this way may cause
246 | // us to accept otherwise non-RFC-adherent lines.
247 | if (ds == "i") {
248 | out(
249 | '
' +
250 | '
' + "\n"
251 | );
252 | continue;
253 | }
254 | eout("invalid display string");
255 | continue;
256 | }
257 |
258 | // Separate the itype and clean the display string to be
259 | // HTML-safe.
260 | i = ds.substr(0, 1);
261 | ds = clean(ds.substr(1));
262 |
263 | [ s, m, e ] = next(m, "no host"); if (e) continue;
264 | [ h, m, e ] = next(m, "no port"); if (e) continue;
265 |
266 | // Don't allow hostnames with naughty characters.
267 | // Other characters won't resolve, but shouldn't result in
268 | // exploitable HTML, at least.
269 | if (h.indexOf("\\") > -1 || h.indexOf('"') > -1 ||
270 | h.indexOf(">") > -1 || h.indexOf("<") > -1) {
271 | eout("invalid character in hostname");
272 | continue;
273 | }
274 |
275 | // Validate the item type and assign an emoji icon.
276 | let ee = icontype(i);
277 |
278 | // We can stop and process hURLs plus itypes i and 3 now
279 | // since these do not need to be otherwise validated.
280 | // This code also handles item types we don't support.
281 | if (i == "i" || i == "3" || ee == _EMOJI_UNSUPPORTED) {
282 | out(
283 | '' + ee + '
' +
284 | '' + ds + "
\n"
285 | );
286 | continue;
287 | }
288 | if (i == "h" &&
289 | // Other h's fall through.
290 | ((s.indexOf("URL:") == 0 && s.length > 4)
291 | ||
292 | (s.indexOf("/URL:") == 0 && s.length > 5))
293 | ) {
294 | if (s.charAt(0) == "/") s = s.substr(1);
295 | s = s.substr(4);
296 |
297 | if (s.indexOf("\\") > -1 || s.indexOf('"') > -1 ||
298 | s.indexOf(">") > -1 || s.indexOf("<") > -1) {
299 | eout("invalid character in hURL");
300 | continue;
301 | }
302 |
303 | // Only whitelisted schemes are allowed.
304 | if (
305 | s.indexOf("news:") != 0 &&
306 | s.indexOf("cso://") != 0 && // Lynx, not RFC
307 | s.indexOf("ftp://") != 0 &&
308 | s.indexOf("git://") != 0 &&
309 | s.indexOf("ssh://") != 0 &&
310 | s.indexOf("http://") != 0 &&
311 | s.indexOf("nntp://") != 0 &&
312 | s.indexOf("wais://") != 0 &&
313 | s.indexOf("mailto:") != 0 &&
314 | s.indexOf("https://") != 0 &&
315 | s.indexOf("whois://") != 0 &&
316 | s.indexOf("gopher://") != 0 && // how meta
317 | s.indexOf("rlogin://") != 0 && // Lynx, not RFC
318 | s.indexOf("telnet://") != 0 &&
319 | s.indexOf("tn3270://") != 0 &&
320 | 1) {
321 | eout("hURL scheme not on whitelist");
322 | continue;
323 | }
324 |
325 | out(
326 | '' +
328 | '\n"
330 | );
331 | continue;
332 | }
333 |
334 | // Interactable types.
335 | if (m.indexOf("\t") > 0) {
336 | // It is possible to have trailing fields after the
337 | // declared port number, and we should support that
338 | // (as much for eventual Gopher+ support as well as
339 | // future extensions to the protocol).
340 | [ p, m, e ] = next(m, "syntax error");
341 | if (e) continue;
342 | } else {
343 | p = m;
344 | }
345 | p = parseInt(p);
346 | if (p < 1 || p > 65535) {
347 | eout("preposterous port number "+p);
348 | continue;
349 | }
350 |
351 | // Process legacy GET links (we need the port number for
352 | // them). We only support port 80; the future is hURLs,
353 | // and people should be using those in new installations.
354 | // Intentionally allow malformed GETs to fallthru and
355 | // generate malformed menu entries to punish lazy admins.
356 | if (i == "h" && p == 80 && s.indexOf("GET /") == 0) {
357 | s = s.substr(4);
358 | if (s.indexOf("\\") > -1 || s.indexOf('"') > -1 ||
359 | s.indexOf(">") > -1 || s.indexOf("<") > -1) {
360 | eout("invalid character in GET URL");
361 | continue;
362 | }
363 | s = "http://" + h + s;
364 | out(
365 | '' +
367 | '\n"
369 | );
370 | continue;
371 | }
372 |
373 | // Process itype 8 and itype T.
374 | if (i == "T" || i == "8") {
375 | let sc = (i == "8") ? "telnet" : "tn3270";
376 |
377 | // The selector may or may not be relevant and is
378 | // mostly informational. We'll allow it under the
379 | // same rules as hURLs.
380 | if (s.indexOf("\\") > -1 || s.indexOf('"') > -1 ||
381 | s.indexOf(">") > -1 || s.indexOf("<") > -1) {
382 | eout("invalid character in telnet selector");
383 | continue;
384 | }
385 |
386 | s = sc + "://" + h + ((p == 23) ? "" : ":"+p) + "/" + s;
387 | out(
388 | '' +
390 | '\n"
392 | );
393 | continue;
394 | }
395 |
396 | // All other types follow.
397 | // The selector needs to be URL-safe. However, we undo
398 | // slash conversion to maintain a reasonably visually
399 | // parseable path given that most hosts are now POSIX.
400 | s = encodeURIComponent(s);
401 | s = s.replace(/\%2[fF]/g, "/");
402 |
403 | // Compute the new URL.
404 | l = url_p + "://" + h + ((p == 70) ? "" : ":"+p) +
405 | "/" + i + s;
406 |
407 | // Process itype 7 (put up a form) -- eventually itype 2.
408 | // See clicker() for how this works.
409 | if (i == "7" /* || i == "2" */) {
410 | out(
411 | '' + ee + '
' +
412 | '\n"
419 | );
420 |
421 | fnum++;
422 | continue;
423 | }
424 |
425 | // The remainder are document types.
426 | // Emit the completed menu entry.
427 | out(
428 | '' +
430 | '\n"
432 | );
433 | }
434 | }
435 |
436 | /*
437 | * Handle messages from Topaz.
438 | *
439 | * Most Topaz messages originate in Onyx ultimately and are proxied to the
440 | * client being serviced. These messages fall into several types:
441 | *
442 | * E-response: error occurred prior to data, terminal
443 | * S-response: status message, non-terminal
444 | * D-response: data packet, non-terminal
445 | * F-response: fin(al) packet, terminal, success or failure
446 | */
447 |
448 | function handleMessage(response, sender, sendResponse) {
449 | if (response.e) {
450 |
451 | /*
452 | * E-responses are terminal and include an ASCII keyword
453 | * explanation. All but one originate from Onyx.
454 | *
455 | * This is generated by Topaz when Onyx is not present:
456 | * no_onyx
457 | *
458 | * These relate to me being incompetent:
459 | * no_length_header: we screwed up. "Shouldn't happen"
460 | * bad_length_header: we screwed up. "Shouldn't happen"
461 | * syntax_error: we screwed up. "Shouldn't happen"
462 | *
463 | * These (probably) don't relate to me being incompetent:
464 | * port_not_allowed: port not in Onyx whitelist. The port
465 | * number is appended in case I actually was incompetent.
466 | * resolve: host not found
467 | * timeout: timeout on connect (timeout on data is an
468 | * F-response)
469 | * socket: connection failure. A system-dependent error
470 | * code is appended.
471 | * write_failed: failure between connect and sending request
472 | *
473 | * For the purposes of the current request, these are
474 | * unrecoverable, but the user may be able to try it again.
475 | */
476 |
477 | let k = "";
478 | progress(100);
479 |
480 | if (response.e == "no_onyx") {
481 | uierror("Onyx is not installed",
482 | "OverbiteNX requires the Onyx native component to access Gopher sites." +
483 | '
' +
484 | "Install Onyx for your operating system from "+
485 | 'Github .' +
486 | "Reload this page." +
487 | " ");
488 | return;
489 | } else if (response.e.substr(0,17) == "port_not_allowed:") {
490 | k = "🚫 "; // prohibited
491 | document.title = "Port not allowed: "+hopo_s;
492 | } else if (response.e == "resolve") {
493 | k = "❌ "; // red X
494 | document.title = "Host not found: "+host;
495 | di("title").insertAdjacentHTML('afterbegin', k+host);
496 | return;
497 | } else if (response.e == "timeout") {
498 | k = "⌛ "; // empty hourglass
499 | document.title = "Timeout on connect: "+hopo_s;
500 | } else if (response.e == "write_failed" ||
501 | response.e.substr(0,7) == "socket:") {
502 | k = "🚫 "; // prohibited
503 | document.title = "Connection failure: "+hopo_s;
504 | } else {
505 | console.log("unexpected E-response "+response.e);
506 | return;
507 | }
508 |
509 | di("title").insertAdjacentHTML('afterbegin', k+hopo_s);
510 | return;
511 |
512 | } else if (response.s) {
513 |
514 | /*
515 | * S-responses include an ASCII keyword explanation.
516 | *
517 | * connecting: host resolved
518 | * connected: host connected
519 | * data: request sent, data imminent
520 | */
521 |
522 | if (response.s == "connecting") {
523 | // Just set the document title to the URL. This,
524 | // in turn, looks "correct" in the history dropdowns.
525 | document.title = url_s;
526 |
527 | if (itype == "0" || itype == "2" /* eventually */ ||
528 | itype == "x" || itype == "h" /* stopgap */) {
529 | // Text type
530 | // We don't need to clear the fields, just
531 | // disable the ones we don't want showing.
532 | // This only gets triggered once per load.
533 | di("data").style.display = "none";
534 | di("title").style.display = "none";
535 | } else if (itype == "1" || itype == "7") {
536 | // Menu type
537 | di("pre").style.display = "none";
538 |
539 | // Install the document click handler (but
540 | // see clicker() for why).
541 | document.addEventListener("click", clicker);
542 | } else {
543 | // Binary type
544 | di("pre").style.display = "none";
545 | di("title").style.display = "none";
546 | }
547 | buf = [];
548 | mbuf = "";
549 | prog = 0;
550 | fnum = 0;
551 | } else if (response.s == "connected") {
552 | progress(10);
553 | } else if (response.s == "data") {
554 | progress(20);
555 | if (itype == "1" || itype == "7") {
556 | let s = encodeURIComponent(sel);
557 | s = s.replace(/\%2[fF]/g, "/");
558 | s = itype + s;
559 | if (s == "1/" || s == "1" || s == "/1/") s = "";
560 | di("title").insertAdjacentHTML(
561 | 'afterbegin',
562 | url_p + "://" +
563 | '' + hopo_s + ' /' + s);
565 | }
566 | } else {
567 | console.log("unexpected state from Onyx: "+response.s);
568 | }
569 | return;
570 |
571 | } else if (response.f) {
572 |
573 | /*
574 | * F-responses are terminal.
575 | * If the first character is 1, a failure has occurred.
576 | * If the first character is 0, the transfer succeeded.
577 | */
578 |
579 | progress(100);
580 |
581 | if (response.f.charAt(0) == "1") {
582 | // Failure. Stop. The data is not valid.
583 | // The F-response includes additional information
584 | // which we don't process in this version.
585 | return;
586 | }
587 |
588 | /* A successful transfer; the data is valid. However, we
589 | may need to do additional translation at this point for
590 | binary data.
591 |
592 | For blob types, unfortunately we can't revoke the URL
593 | because the user may interact with it after the load
594 | (such as saving images, download manager, etc.). The
595 | best solution right now is just to leak the URL and
596 | hope for the best. */
597 |
598 | if (itype == "4" || itype == "5" || itype == "6"
599 | || itype == "d" || itype == "s" || itype == ";"
600 | || itype == "9") {
601 | let blob = new Blob(buf, { type: "octet/stream" });
602 | let url = window.URL.createObjectURL(blob);
603 | browser.downloads.download({
604 | url: url,
605 | filename: url_s.split('\\').pop().split('/').pop(),
606 | saveAs: true // stop drive-by downloads
607 | });
608 | // XXX: downloads.onErased could allow us to free the
609 | // blob. Have to think about how that would work.
610 | } else if (itype == "I" || itype == "g" || itype == "p") {
611 | let blob = new Blob(buf, { type : (
612 | (itype == "g") ? "image/gif" :
613 | (itype == "p") ? "image/png" :
614 | "image/jpeg"
615 | )});
616 | let img = document.createElement("img");
617 | img.src = window.URL.createObjectURL(blob);
618 | document.body.appendChild(img);
619 | }
620 | return;
621 |
622 | } else if (response.d) {
623 |
624 | /*
625 | * D-response. A hex-encoded data packet is attached.
626 | */
627 |
628 | progress(1);
629 |
630 | if (itype == "4" || itype == "5" || itype == "6" ||
631 | itype == "9" ||
632 | itype == "s" || itype == "d" || itype == ";" ||
633 | itype == "I" || itype == "g" || itype == "p") {
634 | // Binary type
635 | buf.push(hex2ta(response.d));
636 | } else if (itype == "0" || itype == "2" /* eventually */ ||
637 | itype == "x" || itype == "h" /* stopgap */) {
638 | // Text type
639 | let k = hex2a(response.d);
640 | k = k.replace(/\&/g, "&");
641 | k = k.replace(/\/g, ">");
643 |
644 | di("pre").insertAdjacentHTML('beforeend', k);
645 | } else // Menu type
646 | pushMenuData(hex2a(response.d));
647 | return;
648 |
649 | }
650 | console.log("Unexpected message type "+ JSON.stringify(response));
651 | }
652 |
653 | /*
654 | * Send the URL to the Topaz backend
655 | */
656 |
657 | // The user might try to save the page to disk. If so, the URL should not
658 | // be sent: it's no longer valid, and we're not in a WebExtensions context.
659 | // Handle this situation immediately.
660 | if (typeof browser !== "undefined") {
661 | // Now within WebExtension context in the browser. Send the message.
662 |
663 | browser.runtime.onMessage.addListener(handleMessage); try{ (function(){
664 |
665 | // Validate the URI, but the built-in URI object only allows HTTP, so
666 | // make it think it's got one. If this trick fails, we wouldn't have
667 | // properly parsed it anyway, so no harm done. However, if we got this
668 | // URI from a form submission, it will have an embedded tab we will
669 | // need to preserve or it will be swept away as meaningless whitespace.
670 |
671 | let fq = "";
672 | url_s = decodeURIComponent(document.location.search.substr(1));
673 | if (url_s.indexOf("\t") > 0) { // invalid otherwise
674 | fq = url_s.substr(url_s.indexOf("\t")); // include the tab
675 | url_s = url_s.substr(0, url_s.indexOf("\t"));
676 | }
677 | url_p = url_s.substr(0, url_s.indexOf("://"));
678 |
679 | url = new URL("https" + url_s.substr(url_s.indexOf("://")));
680 | if (url && url.hostname && url.pathname) {
681 | // If pathname is '' or '/' then itype == 1.
682 | // Otherwise, pathname must be at least two characters,
683 | // and itype is its second character.
684 | let pathn = decodeURIComponent(url.pathname);
685 |
686 | if (pathn == "" || pathn == "/") {
687 | itype = "1";
688 | sel = pathn;
689 | } else if (pathn.length > 1) {
690 | itype = pathn.substr(1, 1);
691 | sel = pathn.substr(2);
692 | } else {
693 | throw {
694 | w : "Couldn't understand URL", x :
695 | "The URL you typed could not be processed into a valid Gopher request."
696 | };
697 | return;
698 | }
699 |
700 | // Reject bad itypes.
701 | let ee = icontype(itype);
702 | if (ee == " " || ee == _EMOJI_UNSUPPORTED ||
703 | // These are allowed in menus, but not directly.
704 | itype == "8" || itype == "T") {
705 | let ii = clean(itype); // don't be naughty
706 | throw {
707 | w : "Unsupported item type "+ii, x :
708 | "OverbiteNX does not currently support accessing resources of this type."
709 | };
710 | return;
711 | }
712 |
713 | // Proceed to document load.
714 | // Add back any query parameters.
715 | if (url.search)
716 | sel += url.search;
717 | sel += fq;
718 | host = url.hostname;
719 | port = parseInt(url.port);
720 | port = (port > 0) ? port : 70;
721 | hopo_s = host + ((port != 70) ? (":"+port) : "");
722 |
723 | browser.runtime.sendMessage({
724 | host: host,
725 | itype: itype,
726 | port: port,
727 | sel: sel
728 | });
729 | } else {
730 | throw {
731 | w : "Couldn't understand URL", x :
732 | "The URL you typed could not be processed into a valid Gopher request."
733 | };
734 | } }());
735 | } catch(e) {
736 | // Error. However, the page hasn't loaded yet, so we do
737 | // this instead:
738 | window.addEventListener("load", function() {
739 | if (e.w) {
740 | uierror(e.w, e.x);
741 | } else {
742 | uierror("Unexpected error", e);
743 | }
744 | });
745 | }
746 | }
747 |
--------------------------------------------------------------------------------
/ext/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "manifest_version": 2,
4 | "name": "OverbiteNX",
5 | "version": "0.2",
6 | "description": "Enables the Gopher protocol in Firefox.",
7 | "author": "The Overbite Project",
8 | "homepage_url" : "https://gopher.floodgap.com/overbite/",
9 |
10 | "applications": {
11 | "gecko": {
12 | "id": "overbitenx@floodgap.com",
13 | "strict_min_version": "59.0"
14 | }
15 | },
16 | "permissions": [
17 | "bookmarks",
18 | "downloads",
19 | "history",
20 | "nativeMessaging",
21 | "tabs"
22 | ],
23 | "background": {
24 | "scripts": [
25 | "topaz.js",
26 | "agate.js"
27 | ]
28 | },
29 | "page_action": {
30 | "browser_style" : true,
31 | "default_icon" : "r/stop.svg",
32 | "default_title" : "Stop Gopher transfer"
33 | },
34 | "protocol_handlers": [
35 | {
36 | "name" : "OverbiteNX",
37 | "protocol" : "gopher",
38 | "uriTemplate" : "d?%s"
39 | }
40 | ]
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/ext/r/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ext/topaz.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Topaz module: back end and native component interface (background)
3 | *
4 | * All requests from Jasper instances in browser tabs must go through the
5 | * single Topaz browser back end to the single Onyx native component back end.
6 | * Topaz also includes tab management to facilitate directly manipulating the
7 | * queue of tasks for Onyx.
8 | *
9 | * Copyright 2017-8 Cameron Kaiser.
10 | * All rights reserved.
11 | */
12 |
13 | var requestQueue = [];
14 | var currentTab = null;
15 | var port = null;
16 | var backendIdle = false;
17 | var filtering = false;
18 | var totalData = 0;
19 |
20 | /*
21 | * Convert to Onyx hex-encoding for forming requests.
22 | */
23 |
24 | function a2hex(str) {
25 | let hexx = '';
26 | let hex = '';
27 | for (var i = 0, l = str.length; i < l; i ++) {
28 | hex = Number(str.charCodeAt(i)).toString(16);
29 | if (hex.length < 2) hexx += "0";
30 | hexx += hex;
31 | }
32 | return hexx;
33 | }
34 | function shorttohex(short) {
35 | let hexx = "0000" + short.toString(16);
36 | return hexx.substring(hexx.length - 4);
37 | }
38 |
39 | /*
40 | * Read and process the next Jasper client request in the queue.
41 | */
42 |
43 | function nextInOnyxQueue() {
44 | console.log("popqueue ("+requestQueue.length+")");
45 |
46 | // This shouldn't ever happen, but busy-wait if we hit this when
47 | // a tab was just closed.
48 | while(filtering) { /* */ }
49 |
50 | if (!requestQueue || requestQueue.length < 1) {
51 | currentTab = null;
52 | return;
53 | }
54 |
55 | backendIdle = false;
56 |
57 | // This is atomic, so we don't need to use the filtering mutex.
58 | let next = requestQueue.shift();
59 | currentTab = next.tab;
60 | console.log("popqueue now serving "+currentTab);
61 |
62 | let str = '';
63 | str += shorttohex(next.request.port);
64 | str += a2hex(next.request.itype);
65 | str += shorttohex(next.request.host.length);
66 | str += a2hex(next.request.host);
67 | str += a2hex(next.request.sel);
68 | str += "0d0a"; // \r\n
69 |
70 | console.log(str);
71 |
72 | try {
73 | port.postMessage({ "a" : str });
74 | } catch(e) { console.log("postMessage: "+e); }
75 | }
76 |
77 | /*
78 | * Cancel the current transaction (if any).
79 | */
80 |
81 | function cancelTransaction() {
82 | console.log("explicit cancel");
83 | // Don't set backendIdle to false here: wait for Onyx to
84 | // acknowledge and our message handler below will set it
85 | // when it does.
86 |
87 | port.postMessage({ "a" : "000000" });
88 | // NB: anything after port 0 is ignored; I'm just paranoid.
89 | currentTab = null;
90 | }
91 |
92 | /*
93 | * Cancel any arbitrary tab's transaction.
94 | */
95 |
96 | function cancelByTab(id) {
97 | while(filtering) { /* */ }
98 |
99 | // Lock the queue and remove all entries matching that tab (if
100 | // there are entries in the queue).
101 | if (requestQueue && requestQueue.length) {
102 | filtering = true;
103 | requestQueue = requestQueue.filter((value) => {
104 | return value.tab != id;
105 | });
106 | filtering = false;
107 | }
108 |
109 | // If we cancelled the transaction for the tab currently being
110 | // serviced, explicitly cancel so that the next one can be handled.
111 | // Always check this since we could be handling a request with an
112 | // empty queue.
113 | if (currentTab == id)
114 | cancelTransaction();
115 | }
116 |
117 | /*
118 | * Page action for cancellation. This is set and handled in the background,
119 | * so it's better to do it here since we have access to the queue.
120 | */
121 |
122 | browser.pageAction.onClicked.addListener((tab) => {
123 | browser.pageAction.hide(tab.id);
124 | cancelByTab(tab.id);
125 | browser.tabs.sendMessage(tab.id, { "f" : "1:terminated" });
126 | });
127 |
128 | /*
129 | * Handlers for message events from instances of Jasper and from Onyx.
130 | */
131 |
132 | // Get URLs from Jasper content script instances so we can push data back.
133 | // Since multiple Jaspers within multiple tabs could be asking, we queue the
134 | // requests since Onyx v1 does not multiplex connections.
135 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
136 | // If the port to Onyx is not connected, connect the port.
137 | if (!port) {
138 | try {
139 | port = browser.runtime.connectNative("onyx");
140 | port.onDisconnect.addListener((p) => {
141 | // This is called if the connection fails,
142 | // such as improper installation or Onyx not
143 | // being present on this system. We don't
144 | // care about "orderly" disconnects since Onyx
145 | // does its own cleanup.
146 | if (!p.error) return;
147 | console.log("onDisconnect called: "+p.error);
148 |
149 | // If Onyx unexpectedly quits, we should tell
150 | // the current tab. If it never starts, there
151 | // won't be a current tab, so we should tell
152 | // the sender.
153 | let q = (currentTab) ? currentTab :
154 | sender.tab.id;
155 | if (!q) return; // ?!
156 | browser.pageAction.hide(q);
157 | browser.tabs.sendMessage(q,
158 | { "e" : "no_onyx" });
159 | port = null; // force a retry
160 | });
161 | } catch(e) {
162 | console.log("While connecting to Onyx: "+e);
163 | port = null;
164 | }
165 | if (!port) {
166 | console.log("Failed to initialize Onyx connection");
167 | browser.tabs.sendMessage(sender.tab.id,
168 | { "e" : "no_onyx" });
169 | return;
170 | }
171 |
172 | // Master function for handling responses from Onyx.
173 | port.onMessage.addListener((response) => {
174 | // The init message is handled here.
175 | if (response.i) {
176 | console.log(browser.runtime.getManifest().version
177 | + " Onyx init: "+response.i);
178 | backendIdle = true;
179 | nextInOnyxQueue();
180 | return;
181 | }
182 |
183 | // Other messages are forwarded to the current tab,
184 | // but ignore them if the current tab has gone away.
185 | if (!currentTab) return;
186 |
187 | console.log("message to "+currentTab);
188 | if (response.e) {
189 | console.log("Onyx error: "+response.e);
190 | browser.pageAction.hide(currentTab);
191 | browser.tabs.sendMessage(currentTab, response);
192 | backendIdle = true;
193 | nextInOnyxQueue();
194 | } else if (response.s) {
195 | console.log("Onyx status: "+response.s);
196 | browser.tabs.sendMessage(currentTab, response);
197 | totalData = 0;
198 | } else if (response.d) {
199 | console.log("Onyx data");
200 |
201 | // To avoid a malicious server sending us more
202 | // data than we can fit in memory, limit the
203 | // total response per request to 16MB. If you
204 | // need to download ISOs over Gopherspace, I
205 | // strongly advise a dedicated client (or
206 | // some way to avoid having to load binary
207 | // data into Blobs).
208 | totalData += response.d.length;
209 | if (totalData > 2 * 16 * 1024 * 1024) {
210 |
211 | console.log("Gopher transaction exceeds 16MB, terminated.");
212 |
213 | browser.tabs.sendMessage(currentTab,
214 | { "f" : "1:data_limit" });
215 | cancelByTab(currentTab);
216 | return;
217 | }
218 | browser.tabs.sendMessage(currentTab, response);
219 | } else if (response.f) {
220 | console.log("Onyx fin: "+response.f);
221 | browser.pageAction.hide(currentTab);
222 | browser.tabs.sendMessage(currentTab, response);
223 | } else
224 | console.log("Onyx WTF "+JSON.stringify(response));
225 | });
226 | }
227 |
228 | // Client request.
229 | // Remove all other queued requests from this tab and cancel as
230 | // needed, since a tab can have only one request pending.
231 | // If this was the same tab being serviced, mark the backend idle.
232 | // We do it this way because cancelByTab can change what we think
233 | // is the tab currently being serviced.
234 | if (currentTab == sender.tab.id) {
235 | cancelByTab(sender.tab.id);
236 | backendIdle = true; // we know this must be the case!
237 | } else
238 | cancelByTab(sender.tab.id);
239 |
240 | // Validation occurs on the client side, since it has to know
241 | // what the itype is (so it would have to have parsed it).
242 | requestQueue.push({
243 | tab : sender.tab.id,
244 | request : request
245 | });
246 |
247 | // The user can cancel while the request is queued (in fact, we
248 | // encourage it, since it means less useless traffic through Onyx).
249 | browser.pageAction.show(sender.tab.id);
250 |
251 | if (backendIdle)
252 | nextInOnyxQueue();
253 | });
254 |
255 | // Filter tabs as we close so that leftover requests they may have made do not
256 | // get uselessly sent to Onyx.
257 | browser.tabs.onRemoved.addListener((id, info) => { cancelByTab(id); });
258 |
259 | // If the user navigates to a different URL while the gopher resource is
260 | // loading, and the tab they're using is the one being serviced, cancel it.
261 | browser.tabs.onUpdated.addListener((id, info, tab) => {
262 | if (currentTab == id && info.url && info.url.length) {
263 | cancelByTab(id);
264 | backendIdle = true; // we know this must be the case!
265 | }
266 | });
267 |
268 |
--------------------------------------------------------------------------------
/mac-onyx-inst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Copyright 2019 Cameron Kaiser.
4 | # All rights reserved.
5 | #
6 | # This script was originally written in Perl, but now is written in sh to
7 | # prepare for the new sucky world of Catalina where scripting languages
8 | # are deprecated and Tim Cook smiles his mysterious venomous smile. In
9 | # particular, prepare for zsh instead of bash actually being /bin/sh.
10 | if [ -n "$ZSH_VERSION" ]; then emulate -L ksh; fi
11 |
12 | JSON_DIR="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts"
13 | echo "Onyx Installer Tool (C)2019 Cameron Kaiser"
14 | echo "Installing to: $JSON_DIR"
15 | echo " "
16 |
17 | if [ ! -x "/Applications/Onyx.app/Contents/MacOS/onyx" ]; then
18 | echo "Onyx.app must be in /Applications."
19 | exit 255
20 | fi
21 | if [ -e "$JSON_DIR/onyx.json" ]; then
22 | echo "Onyx appears to be already installed for this user."
23 | exit 254
24 | fi
25 |
26 | echo "Creating destination directory."
27 | /bin/mkdir -p "$JSON_DIR"
28 | if [ ! -d "$JSON_DIR" ]; then
29 | echo "Unable to create Onyx connection file."
30 | exit 253
31 | fi
32 | echo "Creating connector file."
33 | cat <"$JSON_DIR/onyx.json"
34 | {
35 | "name": "onyx",
36 | "description": "OverbiteNX Gopher system component",
37 | "path": "/Applications/Onyx.app/Contents/MacOS/onyx",
38 | "type": "stdio",
39 | "allowed_extensions": [ "overbitenx@floodgap.com" ]
40 | }
41 | EOF
42 |
43 | if [ -e "$JSON_DIR/onyx.json" ]; then
44 | echo "Onyx was successfully installed for this user."
45 | exit 0
46 | fi
47 |
48 | echo "Onyx was unable to install its connector to Firefox."
49 | exit 128
50 |
--------------------------------------------------------------------------------
/old/client.c:
--------------------------------------------------------------------------------
1 | /* Copyright 2018 Cameron Kaiser.
2 | All rights reserved.
3 |
4 | XXX: This isn't used right now. client.pl is better on Unix things.
5 | For Win32, see wintest.c. */
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | int main(int argc, char **argv) {
14 | char strig[1024];
15 | uint32_t l;
16 | int c, i, j, k, p;
17 |
18 | if (argc != 5) {
19 | fprintf(stderr, "usage: %s host port itype sel\n",
20 | argv[0]);
21 | exit(255);
22 | }
23 | p = atoi(argv[2]);
24 | if (p < 1 || p > 65535) {
25 | fprintf(stderr, "nonsense port: %d\n", p);
26 | exit(255);
27 | }
28 | sprintf(strig, "{\"a\":\"%04x%02x%04x",
29 | p,
30 | (unsigned char)argv[3][0],
31 | (unsigned int)strlen(argv[1]));
32 | l = strlen(strig);
33 | c = 1; /* host */
34 | for(;;) {
35 | unsigned char ln, hn;
36 |
37 | for(i=0; i> 4;
40 | strig[l++] = (hn > 9) ? hn + 87 : hn + 48;
41 | strig[l++] = (ln > 9) ? ln + 87 : ln + 48;
42 | }
43 | if (c == 4)
44 | break;
45 | c = 4; /* sel */
46 | }
47 | // add 0d0a
48 | strig[l++] = '0';
49 | strig[l++] = 'd';
50 | strig[l++] = '0';
51 | strig[l++] = 'a';
52 | strig[l++] = '"';
53 | strig[l++] = '}';
54 | strig[l] = '\0';
55 |
56 | fwrite(&l, sizeof(uint32_t), 1, stdout);
57 | fprintf(stdout, "%s", strig);
58 | fflush(stdout);
59 | sleep(3);
60 | return 0;
61 | }
62 |
--------------------------------------------------------------------------------
/onyx.c:
--------------------------------------------------------------------------------
1 | /* Copyright 2017-8 Cameron Kaiser.
2 | All rights reserved.
3 | Released under the Floodgap Free Software License.
4 |
5 | The Onyx native component is the heart of OverbiteNX. It accepts hex-encoded
6 | JSON requests over standard input in compliance with the WebExtensions
7 | Native Messaging protocol and emits hex-encoded responses.
8 | It supports POSIX and Win32.
9 |
10 | */
11 |
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 |
22 | #ifdef _WIN32
23 | #include "winsock2.h"
24 | #define SHUT_WR SD_SEND
25 | #define SHUT_RD SD_RECEIVE
26 | #else
27 | #include
28 | #include
29 | #include
30 | #endif
31 |
32 | /* Protocol version, not file version. */
33 | #define VERSION "1"
34 |
35 | // Size per network read (max). This seems pretty good.
36 | #define BUFFER_SIZE 4096
37 |
38 | void json_out(char *msg, char type) {
39 | // Emit a formatted JSON message to stdout.
40 | uint32_t olength;
41 |
42 | olength = 9 + strlen(msg);
43 | fwrite(&olength, sizeof(uint32_t), 1, stdout);
44 | fprintf(stdout, "{\"%c\":\"%s\"}\n", type, msg);
45 |
46 | // Firefox expects this data promptly, so we must flush.
47 | fflush(stdout);
48 | }
49 |
50 | void json_init(char *msg) { json_out(msg, 'i'); }
51 | void json_error(char *msg) { json_out(msg, 'e'); }
52 | void json_state(char *msg) { json_out(msg, 's'); }
53 | void json_fin(char *msg) { json_out(msg, 'f'); }
54 |
55 | char *unhex(char *msg, uint32_t *out, size_t max) {
56 | // Convert big-endian hex to an integer.
57 | // Only max of 2, 4 and 8 hex characters are accepted.
58 | // Return pointer to the character that follows.
59 | size_t c;
60 | unsigned char w;
61 |
62 | *out = 0;
63 | assert(max == 2 || max == 4 || max == 8);
64 |
65 | for(c=0; c= '0' && w <= '9') ||
69 | (w >= 'a' && w <= 'f') ||
70 | (w >= 'A' && w <= 'F')
71 | );
72 |
73 | *out <<= 4;
74 | if (w > 96) w = w - 87;
75 | else
76 | if (w > 64) w = w - 55;
77 | else
78 | w = w - 48;
79 | *out |= w;
80 | }
81 | return (msg + max);
82 | }
83 |
84 | int main(int argc, char **argv) {
85 | char emsg[256];
86 | char buf[BUFFER_SIZE];
87 | char ebuf[BUFFER_SIZE + BUFFER_SIZE + 1];
88 | char *in, *val, *host, *sel;
89 | uint32_t plength, bread, port, seq, itype, hlength, i, j, ln, hn;
90 | int32_t k;
91 | int sockfd, sent, sent_b;
92 | struct sockaddr_in addr;
93 | struct hostent *server;
94 | fd_set fdset, fdrset;
95 | struct timeval tv;
96 | #ifdef _WIN32
97 | unsigned char so_error = 0, timeouts = 0;
98 | size_t so_error_len = sizeof(so_error);
99 | unsigned long socket_mode = 1;
100 | WSADATA wsaData;
101 | DWORD length = 0;
102 |
103 | // Ask for Winsock2 v2.2 (Win98+).
104 | assert(WSAStartup(MAKEWORD(2,2), &wsaData) != SOCKET_ERROR);
105 |
106 | setmode(fileno(stdin), O_BINARY);
107 | setmode(fileno(stdout), O_BINARY);
108 | #else
109 | int so_error = 0;
110 | socklen_t so_error_len = sizeof(so_error);
111 | #endif
112 |
113 | json_init("ready v" VERSION);
114 | for(;;) {
115 |
116 | // Wait for data on stdin.
117 | #ifdef _WIN32
118 | // select() in Windows only works on sockets, so use Win32 API.
119 | if (WaitForSingleObject(GetStdHandle(STD_INPUT_HANDLE),
120 | INFINITE))
121 | continue;
122 | #else
123 | FD_ZERO(&fdset);
124 | FD_SET(STDIN_FILENO, &fdset);
125 |
126 | if (select(STDIN_FILENO + 1, &fdset, NULL, NULL, NULL) < 1)
127 | continue;
128 | #endif
129 |
130 | // The Native Messaging protocol emits a 32-bit message length
131 | // in native byte order.
132 | if (!fread(&plength, sizeof(uint32_t), 1, stdin)) {
133 | // EOF won't be triggered until we actually read.
134 | if (feof(stdin)) exit(0);
135 |
136 | // Not EOF, just a malformed packet.
137 | json_error("no_length_header");
138 | continue;
139 | }
140 |
141 | #if DEBUG
142 | fprintf(stderr, "%i bytes to follow\n", plength);
143 | #endif
144 | in = malloc(plength + 1);
145 | bread = fread(in, sizeof(char), plength, stdin);
146 | if (bread != plength) {
147 | if (sprintf(emsg, "bad_length_header:%i:%i",
148 | plength, bread))
149 | json_error(emsg);
150 | free(in);
151 | continue;
152 | }
153 | in[plength] = '\0';
154 | #if DEBUG
155 | fprintf(stderr, "received packet >>%s<<\n", in);
156 | #endif
157 |
158 | // Process trivial JSON of this format:
159 | // {
160 | // "a" : "hex bytes"
161 | // }
162 | //
163 | // Length and offset below are based on the original bytes, so *2
164 | // for the hex-encoded bytes. Values are big-endian.
165 | //
166 | // Offset Length Description
167 | // ------ ------ -----------
168 | // 00 02 uint16 Port # or (if 0) cancel transmission.
169 | // If port is non-zero, then
170 | // 02 01 uint8 item type
171 | // 03 02 uint16 length of host name, unencoded
172 | // 05 -- host name followed by selector
173 | // End of bytes.
174 |
175 | // Read until we get a colon, then a quotation mark. This is
176 | // the start of the encoded packet within the JSON wrapper.
177 | val = in;
178 | for(;;) {
179 | if (val[0] == ':') {
180 | val++;
181 | break;
182 | }
183 | val++;
184 | assert(val < (in+plength));
185 | }
186 | for(;;) {
187 | if (val[0] == '"') {
188 | val++;
189 | break;
190 | }
191 | val++;
192 | assert(val < (in+plength));
193 | }
194 |
195 | val = (char *)unhex(val, &port, 4);
196 | if (!port) {
197 | #if DEBUG
198 | fprintf(stderr, "port = 0, treated as cancel\n");
199 | #endif
200 | free(in);
201 | continue;
202 | }
203 | assert(port);
204 |
205 | // Port whitelist.
206 | // These were observationally derived from prior and
207 | // current extracts of Veronica-2.
208 | if (!(0 ||
209 | port == 13 ||
210 | port == 43 || /* whois */
211 | port == 70 || /* main port and variant ports */
212 | port == 71 ||
213 | port == 72 ||
214 | port == 79 || /* finger */
215 | port == 80 || /* some servers speak both */
216 | port == 105 || /* CSO */
217 | port == 1070 ||
218 | port == 2347 || /* Veronica default */
219 | port == 3000 ||
220 | port == 3070 ||
221 | port == 3099 ||
222 | port == 4323 ||
223 | port == 7055 ||
224 | port == 7070 ||
225 | port == 7071 ||
226 | port == 7072 ||
227 | port == 7077 ||
228 | port == 7080 ||
229 | port == 7777 ||
230 | port == 27070 ||
231 | 0)) {
232 | if (sprintf(emsg, "port_not_allowed:%i", port))
233 | json_error(emsg);
234 | free(in);
235 | continue;
236 | }
237 |
238 | val = (char *)unhex(val, &itype, 2);
239 | val = (char *)unhex(val, &hlength, 4);
240 | assert((hlength + hlength) <= plength);
241 |
242 | host = malloc(hlength + 1);
243 | for(i=0; i= (in+plength)) {
250 | json_error("syntax_error");
251 | free(host);
252 | free(in);
253 | continue;
254 | }
255 |
256 | sel = malloc(plength - hlength); // XXX: overly cautious
257 | for(i=0; val<(in + plength); i++) {
258 | if (val[0] == 34) {
259 | sel[i] = '\0';
260 | break;
261 | }
262 | val = (char *)unhex(val, &j, 2);
263 | sel[i] = (char)j;
264 | }
265 | if (val == (in + plength)) {
266 | json_error("syntax_error");
267 | free(sel);
268 | free(host);
269 | free(in);
270 | continue;
271 | }
272 |
273 | #if DEBUG
274 | fprintf(stderr, "\"%s\" %i %c \"%s\"\n", host, port, itype, sel);
275 | #endif
276 | // We now have the host and selector, so we can jettison
277 | // the input buffer.
278 | free(in);
279 |
280 | // Attempt to connect.
281 | json_state("connecting");
282 |
283 | sockfd = socket(AF_INET, SOCK_STREAM, 0);
284 | server = gethostbyname(host);
285 | if (sockfd < 0 || server == NULL) {
286 | json_error("resolve");
287 | free(sel);
288 | free(host);
289 | continue;
290 | }
291 |
292 | // Use a 10-second connect timeout.
293 | #ifdef _WIN32
294 | ioctlsocket(sockfd, FIONBIO, &socket_mode);
295 | #else
296 | fcntl(sockfd, F_SETFL, O_NONBLOCK);
297 | #endif
298 | memset((char *)&addr, 0, sizeof(addr));
299 | addr.sin_family = AF_INET;
300 | memcpy(
301 | (char *)&addr.sin_addr.s_addr,
302 | (char *)server->h_addr,
303 | server->h_length
304 | );
305 | addr.sin_port = htons(port);
306 | (void)connect(sockfd, (const struct sockaddr *)&addr,
307 | sizeof(addr));
308 |
309 | // The connect is interruptable by activity on stdin.
310 | #ifdef _WIN32
311 | // This convoluted mess is required because Win32's
312 | // WaitForMultipleObjects etc. family will always return
313 | // true on our standard input pipe. We ping-pong between
314 | // half-second waits on the socket and checking stdin
315 | // because Winsock select() won't work on input pipes either.
316 | // Once we get to 10 seconds, or there is stdin, abort.
317 | // Another such loop is in the data phase later on.
318 |
319 | timeouts = 0;
320 | for(;;) {
321 | HANDLE sockh;
322 |
323 | length = 0;
324 |
325 | // Check if there is actually any data on stdin.
326 | // We don't really care if this fails.
327 | (void)PeekNamedPipe(GetStdHandle(STD_INPUT_HANDLE),
328 | NULL, 0, NULL, &length, NULL);
329 | if (length > 0)
330 | break;
331 |
332 | // No. Check the socket.
333 | sockh = WSACreateEvent();
334 | WSAEventSelect(sockfd, sockh, FD_WRITE);
335 | if(WaitForSingleObject(sockh, 500)) {
336 | // Not ready, or we timed out.
337 | timeouts++;
338 | #if DEBUG
339 | fprintf(stderr, "timeout, %i counted\n",
340 | timeouts);
341 | #endif
342 | if (timeouts == 20)
343 | break;
344 | continue;
345 | }
346 |
347 | break;
348 | }
349 |
350 | if (length > 0) {
351 | // Data on stdin; abort. The main loop will get it.
352 | free(sel);
353 | free(host);
354 | close(sockfd);
355 | continue;
356 | }
357 | if (timeouts == 20) {
358 | json_error("timeout");
359 | free(sel);
360 | free(host);
361 | close(sockfd);
362 | continue;
363 | }
364 |
365 | timeouts = 0;
366 | #else
367 | FD_ZERO(&fdset);
368 | FD_ZERO(&fdrset);
369 | FD_SET(sockfd, &fdset);
370 | FD_SET(STDIN_FILENO, &fdrset);
371 | memset((char *)&tv, 0, sizeof(tv));
372 | tv.tv_sec = 10;
373 | tv.tv_usec = 0;
374 |
375 | if (select(sockfd + 1, &fdrset, &fdset, NULL, &tv) < 1) {
376 | // I guess an errant signal could trigger this,
377 | // but I really don't care to make it reentrant.
378 | json_error("timeout");
379 | free(sel);
380 | free(host);
381 | close(sockfd);
382 | continue;
383 | }
384 | if (FD_ISSET(STDIN_FILENO, &fdrset)) {
385 | // Data on stdin; abort. The main loop will get it.
386 | free(sel);
387 | free(host);
388 | close(sockfd);
389 | continue;
390 | }
391 | #endif
392 | // We have shot our wad at the host, so release that.
393 | free(host);
394 |
395 | // No data on stdin; must be socket activity.
396 | // Check if we actually got a connection.
397 | getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error,
398 | &so_error_len);
399 | if (so_error) {
400 | if (sprintf(emsg, "socket:%i", (unsigned int)so_error))
401 | json_error(emsg);
402 | #if DEBUG
403 | perror("socket error");
404 | #endif
405 | free(sel);
406 | close(sockfd);
407 | continue;
408 | }
409 |
410 | // Connection was successful.
411 | json_state("connected");
412 |
413 | // Send the selector.
414 | // Because Winsock doesn't know how to read()/write() on
415 | // a socket, use send()/recv(), which work everywhere.
416 | //
417 | // Note that we are treating our non-blocking socket as if it
418 | // could atomically write, but we already know the socket is
419 | // writable, so this invariably "just works" as if it were a
420 | // blocking socket. It's still wrong, mind you, but it works.
421 | if (send(sockfd, sel, strlen(sel), 0) < 0) {
422 | // Failed to send.
423 | json_error("write_failed");
424 | free(sel);
425 | close(sockfd);
426 | continue;
427 | }
428 | json_state("data");
429 |
430 | // Receive data until the socket closes or times out.
431 | // Any activity on stdin cancels the transmission.
432 | // We can free everything now, we don't need it anymore.
433 | free(sel);
434 |
435 | for(;;) {
436 | #ifdef _WIN32
437 | // Another convoluted mess. Here, we ping-pong with a
438 | // 10 timeout limit. Kludgey, but seems reliable.
439 |
440 | HANDLE sockh;
441 | DWORD state=0;
442 |
443 | length = 0;
444 |
445 | // Check if there is actually any data on stdin.
446 | // We don't really care if this fails.
447 | (void)PeekNamedPipe(GetStdHandle(STD_INPUT_HANDLE),
448 | NULL, 0, NULL, &length, NULL);
449 | if (length > 0) {
450 | // Yes. Terminate.
451 | json_fin("1:terminated");
452 | // The loop will pick up the packet shortly.
453 | break;
454 | }
455 |
456 | // No. Check the socket.
457 | sockh = WSACreateEvent();
458 | WSAEventSelect(sockfd, sockh,
459 | FD_READ | FD_CLOSE);
460 | if(WaitForSingleObject(sockh, 500)) {
461 | // No data, or timed out.
462 | timeouts++;
463 | #if DEBUG
464 | fprintf(stderr, "timeout, %i counted\n",
465 | timeouts);
466 | #endif
467 | if (timeouts == 10) {
468 | json_fin("1:timeout");
469 | break;
470 | }
471 | continue;
472 | }
473 |
474 | // Fall through to recv().
475 | timeouts = 0;
476 | #else
477 | FD_ZERO(&fdset);
478 | FD_SET(sockfd, &fdset);
479 | FD_SET(STDIN_FILENO, &fdset);
480 | memset((char *)&tv, 0, sizeof(tv));
481 | tv.tv_sec = 5;
482 | tv.tv_usec = 0;
483 |
484 | // Much better!
485 | if (select(sockfd + 1, &fdset, NULL, NULL, &tv) < 1) {
486 | json_fin("1:timeout");
487 | break;
488 | }
489 | if (FD_ISSET(STDIN_FILENO, &fdset)) {
490 | json_fin("1:terminated");
491 | // The loop will pick up the packet shortly.
492 | break;
493 | }
494 | if (!FD_ISSET(sockfd, &fdset)) {
495 | // Huh.
496 | continue;
497 | }
498 | #endif
499 |
500 | // Must have data on the socket, or EOF.
501 | k = recv(sockfd, buf, BUFFER_SIZE, 0);
502 | if (k < 1) {
503 | json_fin("0:ok");
504 | break;
505 | }
506 |
507 | // Emit the data packet.
508 | // { " d " : " (6 bytes)
509 | // k * 2 bytes
510 | // " } \n (3 bytes)
511 | #if DEBUG
512 | fprintf(stderr, "data, %i bytes received\n", k);
513 | #endif
514 |
515 | j=0;
516 | for (i=0; i> 4) & 0x0f;
519 | ebuf[j++] = (hn > 9) ? hn + 87 : hn + 48;
520 | ebuf[j++] = (ln > 9) ? ln + 87 : ln + 48;
521 | }
522 | ebuf[j] = '\0';
523 | j += 9;
524 | fwrite(&j, sizeof(uint32_t), 1, stdout);
525 | fprintf(stdout, "{\"d\":\"%s\"}\n", ebuf);
526 | fflush(stdout);
527 | }
528 |
529 | close(sockfd);
530 | #if DEBUG
531 | fprintf(stderr, "completed transaction\n");
532 | #endif
533 | json_init("ready v" VERSION);
534 | }
535 |
536 | return 0;
537 | }
538 |
--------------------------------------------------------------------------------
/onyx.nsi:
--------------------------------------------------------------------------------
1 | RequestExecutionLevel user
2 | SetCompressor zlib
3 | Name "OverbiteNX Onyx Component"
4 | OutFile "onyxinst.exe"
5 | InstallDir "$LOCALAPPDATA\OverbiteNX"
6 |
7 | VIProductVersion "0.9.2.0"
8 | VIAddVersionKey "FileVersion" "0.9.2.0"
9 | VIAddVersionKey "ProductName" "OverbiteNX Onyx Component Installer"
10 | VIAddVersionKey "CompanyName" "The Overbite Project"
11 | VIAddVersionKey "LegalCopyright" "© 2018 Cameron Kaiser"
12 | VIAddVersionKey "FileDescription" "The native component for the OverbiteNX Gopher client."
13 |
14 | function StrReplace
15 | Exch $0
16 | Exch
17 | Exch $1
18 | Exch
19 | Exch 2
20 | Exch $2
21 | Push $3
22 | Push $4
23 | Push $5
24 | Push $6
25 | Push $7
26 | Push $R0
27 | Push $R1
28 | Push $R2
29 | StrCpy $3 "-1"
30 | StrCpy $5 ""
31 | StrLen $6 $1
32 | StrLen $7 $0
33 | Loop:
34 | IntOp $3 $3 + 1
35 | Loop_noinc:
36 | StrCpy $4 $2 $6 $3
37 | StrCmp $4 "" ExitLoop
38 | StrCmp $4 $1 Replace
39 | Goto Loop
40 |
41 | Replace:
42 | StrCpy $R0 $2 $3
43 | IntOp $R2 $3 + $6
44 | StrCpy $R1 $2 "" $R2
45 | StrCpy $2 $R0$0$R1
46 | IntOp $3 $3 + $7
47 | Goto Loop_noinc
48 | ExitLoop:
49 |
50 | StrCpy $0 $2
51 | Pop $R2
52 | Pop $R1
53 | Pop $R0
54 | Pop $7
55 | Pop $6
56 | Pop $5
57 | Pop $4
58 | Pop $3
59 | Pop $2
60 | Pop $1
61 | Exch $0
62 | FunctionEnd
63 |
64 | Page directory
65 | Page instfiles
66 | UninstPage uninstConfirm
67 | UninstPage instfiles
68 |
69 | Section
70 |
71 | SetOutPath $INSTDIR
72 | File "onyx.exe"
73 |
74 | FileOpen $0 "$INSTDIR\onyx.json" w
75 | FileWrite $0 "{"
76 | FileWriteByte $0 "13"
77 | FileWriteByte $0 "10"
78 | FileWrite $0 ' "name" : "onyx",'
79 | FileWriteByte $0 "13"
80 | FileWriteByte $0 "10"
81 | FileWrite $0 ' "description": "OverbiteNX Gopher system component",'
82 | FileWriteByte $0 "13"
83 | FileWriteByte $0 "10"
84 | FileWrite $0 ' "path": "'
85 | ; Double-backslash the path for JSON purposes.
86 | Push "$INSTDIR\onyx.exe"
87 | Push "\"
88 | Push "BACKSLASH_SEQUENCE"
89 | Call StrReplace
90 | Push "BACKSLASH_SEQUENCE"
91 | Push "\\"
92 | Call StrReplace
93 | Pop $1
94 | FileWrite $0 $1
95 | FileWrite $0 '",'
96 | FileWriteByte $0 "13"
97 | FileWriteByte $0 "10"
98 | FileWrite $0 ' "type": "stdio",'
99 | FileWriteByte $0 "13"
100 | FileWriteByte $0 "10"
101 | FileWrite $0 ' "allowed_extensions": [ "overbitenx@floodgap.com" ]'
102 | FileWriteByte $0 "13"
103 | FileWriteByte $0 "10"
104 | FileWrite $0 "}"
105 | FileWriteByte $0 "13"
106 | FileWriteByte $0 "10"
107 | FileClose $0
108 |
109 | WriteUninstaller "$INSTDIR\Uninstall Onyx.exe"
110 |
111 | SetRegView 64
112 | WriteRegStr HKCU "SOFTWARE\Mozilla\NativeMessagingHosts\onyx" "" "$INSTDIR\onyx.json"
113 |
114 | WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "DisplayName" "OverbiteNX Onyx Component"
115 | WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "Comments" "This is the native component that enables Firefox to connect to Gopher servers using OverbiteNX."
116 | WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "Publisher" "The Overbite Project"
117 | WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "HelpLink" "https://gopher.floodgap.com/overbite/"
118 | WriteRegStr HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "UninstallString" "$\"$INSTDIR\Uninstall Onyx.exe$\""
119 | WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "EstimatedSize" "130"
120 | WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "NoModify" "1"
121 | WriteRegDWORD HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX" "NoRepair" "1"
122 |
123 | SectionEnd
124 |
125 | Section "Uninstall"
126 |
127 | MessageBox MB_OKCANCEL "Make sure Firefox is closed before continuing with uninstallation." IDOK next IDCANCEL quit
128 |
129 | quit:
130 | Abort "Restart the Uninstaller after quitting Firefox."
131 |
132 | next:
133 | SetRegView 64
134 | DeleteRegKey HKCU "SOFTWARE\Mozilla\NativeMessagingHosts\onyx"
135 | DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\OverbiteNX"
136 |
137 | Delete "$INSTDIR\Uninstall Onyx.exe"
138 | Delete "$INSTDIR\onyx.json"
139 | Delete "$INSTDIR\onyx.exe"
140 |
141 | RMDir "$INSTDIR"
142 |
143 | SectionEnd
144 |
145 |
--------------------------------------------------------------------------------
/wintest.c:
--------------------------------------------------------------------------------
1 | /* Copyright 2018 Cameron Kaiser.
2 | All rights reserved.
3 |
4 | Test harness for Onyx on Win32. This launches it as a subprocess
5 | and sends it a request as the browser would. The subprocess is needed
6 | to make sure the pipe stays open long enough (or else Onyx will self
7 | terminate).
8 |
9 | For non-Windows with Perl, see client.pl. */
10 |
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 |
20 | int main(int argc, char **argv) {
21 | PROCESS_INFORMATION pi;
22 | SECURITY_ATTRIBUTES sa;
23 | STARTUPINFO si;
24 | HANDLE hsir, hsiw;
25 | BOOL rv;
26 | DWORD dw, dr;
27 | char strig[1024];
28 | uint32_t *phony, l;
29 | int c, i, j, k, p;
30 |
31 | phony = (uint32_t *)strig;
32 | if (argc != 5) {
33 | fprintf(stderr, "usage: %s host port itype sel\n",
34 | argv[0]);
35 | exit(255);
36 | }
37 | p = atoi(argv[2]);
38 | if (p < 1 || p > 65535) {
39 | fprintf(stderr, "nonsense port: %d\n", p);
40 | exit(255);
41 | }
42 | assert(sizeof(uint32_t) == 4);
43 | // Leave room at the beginning for the 32-bit length.
44 | sprintf((char *)(strig + 4), "{\"a\":\"%04x%02x%04x%04x",
45 | p, // port
46 | (unsigned char)argv[3][0], // item type
47 | (unsigned int)strlen(argv[1])); // host length
48 | // Start inserting after this header.
49 | l = 20;
50 | c = 1; /* host */
51 | for(;;) {
52 | unsigned char ln, hn;
53 |
54 | for(i=0; i> 4;
57 | strig[l++] = (hn > 9) ? hn + 87 : hn + 48;
58 | strig[l++] = (ln > 9) ? ln + 87 : ln + 48;
59 | }
60 | if (c == 4)
61 | break;
62 | c = 4; /* sel */
63 | }
64 | // add 0d0a
65 | strig[l++] = '0';
66 | strig[l++] = 'd';
67 | strig[l++] = '0';
68 | strig[l++] = 'a';
69 | strig[l++] = '"';
70 | strig[l++] = '}';
71 | strig[l] = '\0';
72 |
73 | phony[0] = l - 4; // evil way to set length. Don't include length word!
74 |
75 | sa.nLength = sizeof(SECURITY_ATTRIBUTES);
76 | sa.bInheritHandle = TRUE;
77 | sa.lpSecurityDescriptor = NULL;
78 |
79 | // Create child stdin pipe.
80 | if (!CreatePipe(&hsir, &hsiw, &sa, 0)) {
81 | DWORD er = GetLastError();
82 | fprintf(stderr, "error (CreatePipe 2) %i\n", er);
83 | exit(255);
84 | }
85 | // Ensure it is not inherited.
86 | if (!SetHandleInformation(hsiw, HANDLE_FLAG_INHERIT, 0)) {
87 | DWORD er = GetLastError();
88 | fprintf(stderr, "error (SetHandleInformation 2) %i\n", er);
89 | exit(255);
90 | }
91 |
92 | // Since we are not trying to capture stdout/stderr, we don't
93 | // need to do the same steps for that (hsor/hsow, hsor). Instead,
94 | // just create the child process now.
95 | ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
96 | ZeroMemory(&si, sizeof(STARTUPINFO));
97 | si.cb = sizeof(STARTUPINFO);
98 | si.hStdError = GetStdHandle(STD_OUTPUT_HANDLE); // "hsow"
99 | si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); // "hsow"
100 | si.hStdInput = hsir;
101 | si.dwFlags |= STARTF_USESTDHANDLES;
102 |
103 | rv = CreateProcess(TEXT("onyx.exe"), TEXT("onyx.exe"),
104 | NULL, // security attributes
105 | NULL, // primary thread SA
106 | TRUE, // inherit handles
107 | 0, // flags
108 | NULL, // use parent env
109 | NULL, // use parent cwd
110 | &si,
111 | &pi);
112 |
113 | if (!rv) {
114 | DWORD er = GetLastError();
115 | fprintf(stderr, "error (CreateProcess) %i\n", er);
116 | exit(255);
117 | }
118 |
119 | CloseHandle(pi.hProcess);
120 | CloseHandle(pi.hThread);
121 |
122 | WriteFile(hsiw, strig, (DWORD)l, NULL, NULL);
123 |
124 | // Onyx self-terminates when it determines there is no more data
125 | // on stdin and/or the stdin pipe has terminated. Thus, we don't need
126 | // to set JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. Just wait around to
127 | // make sure all data is received.
128 |
129 | Sleep(11000);
130 | return 0;
131 | }
132 |
133 |
--------------------------------------------------------------------------------