- Published on
Open links to local files in your browser
- Authors
- Name
- Bart Louwers
- @bartlwrs
I like to take notes with a browser-based application. Oftentimes it is useful to include links to local files in my notes, such as to a PDF of a paper or of an ebook. However, browsers these days have stringent security measures that prevent linking to local files directly. In this post I am going to share how I circumvented this. Be warned that it is a 'hands-on' approach that will require some hacking on your own to adapt it to your own environment.
First, we need to write a little server that will run locally in the background.
It will do the actual opening of files. And secondly, we need a browser
extension that intercepts file://
links and calls out to the local server.
The Server
I elected to use Deno for the server. It is a V8-based Javascript runtime (just like Node.js). It does not seem to be mature enough for anything serious, but it poised to become more relevant sooner or later; especially for scripts, since unlike Node.js, with Deno scripts are run in a sandbox by default, with the option of precisely defining laxer permissions.
The server simply listens on port 9090 and upon receiving a POST
request,
forwards the body to a platform-agnostic library for opening files (it uses
xdg-open
internally on Linux).
lfl-server.ts
import { createApp } from "https://deno.land/x/servest@v1.3.1/mod.ts";
import { open } from "https://deno.land/x/open/index.ts";
const app = createApp();
app.post("/", async (req) => {
const body = await req.text();
open(body);
await req.respond({
status: 200,
});
});
app.listen({
hostname: "localhost",
port: 9090,
});
We can install this script with
$ deno install -f --allow-net=localhost --allow-run --allow-read lfl-server.ts
Note the --allow-net=localhost
, if we for example forgot to set the host, the
library I am using would use 0.0.0.0
as a default hostname. Everyone on in the
local network would be able to open a file on our computer. However, since we
specify this permission, the server would not start since it doesn't have the
right permissions.
Now that the server is installed to ~/.deno/bin
, we want to automatically
start it. For Linux I did that by creating the
systemd user unit below.
~/.config/systemd/user/lfl-server.service
[Unit]
Description=Local File Links Server
[Service]
ExecStart=%h/.deno/bin/lfl-server
[Install]
WantedBy=default.target
And by enabling (and starting) it with:
$ systemctl enable --now --user lfl-server
The Browser Extension
Browser extensions are essentially just a scripts that run on some pages of interest.
We want to detect file://
links on a page, and change the click handler to
send out a request to the local server with the file path following file://
.
We can do that with the following:
extension.js
const fileLinks = document.querySelectorAll('a[href^="file:"]');
for (const fileLink of fileLinks) {
fileLink.addEventListener(
"click",
(e) => {
e.preventDefault();
const file = fileLink.attributes.href.value.substring("file://".length);
fetch("http://localhost:9999", {
method: "post",
body: file,
}).catch(console.error);
},
false,
);
}
One more file is needed to give the browser information about the extension: a manifest.json. A bare-bones version can be found below. I only want to intercept file links on my self-hosted Dokuwiki instance, where I do my note-taking, so I specify to only run the script when the domain matches that of my personal wiki. I also have to specify that I need permissions to make requests to localhost, since otherwise the request will be blocked thanks to CORS restrictions.
manifest.json
{
"manifest_version": 2,
"name": "Local File Links",
"version": "1.0",
"description": "Allows opening links to local files.",
"content_scripts": [
{
"matches": ["*://wiki.bartlouwers.nl/*"],
"js": ["extension.js"]
}
],
"permissions": ["http://localhost/"]
}
After creating a zip
-file containing those two files, the extension must be
changed to .xpi
in the case of Firefox, or to .crx
in the case of
Chromium-based browsers. Then, it can be installed. However, because of all the
havoc browser extensions have caused in the past, you need to have it signed
first. You can circumvent this if you install
Firefox Developer Edition and
temporarily disable xpinstall.signatures.required
on the about:config
page,
or on Chromium-based browsers by enabling developer mode on
chrome://extensions
.
And just like that, you can open local files with links in your browser!