├── .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 |
63 | 64 |
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 | '
' + 327 | '🔗
' + 328 | '
' + 329 | '' + ds + "
\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 | '
' + 366 | '🔗
' + 367 | '
' + 368 | '' + ds + "
\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 | '
' + 389 | '' + ee + '
' + 390 | '
' + 391 | '' + ds + "
\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 | '
' + 413 | '
' + ds + '
' + 414 | '' + 416 | '' + 417 | '' + 418 | "
\n" 419 | ); 420 | 421 | fnum++; 422 | continue; 423 | } 424 | 425 | // The remainder are document types. 426 | // Emit the completed menu entry. 427 | out( 428 | '
' + 429 | '' + ee + '
' + 430 | '
' + 431 | '' + ds + "
\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 | "
  1. Install Onyx for your operating system from "+ 485 | 'Github.' + 486 | "
  2. 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 | --------------------------------------------------------------------------------