Compare commits

..

9 Commits

  1. 43
      README.md
  2. 16
      cloudinit
  3. 29
      examples/events.html
  4. 162
      index.js

@ -0,0 +1,43 @@
# Offtopus, offline-octopus
## Goal
Provide a way to connect local machines to work and tinker with efficiently, building prototypes with resilience in mind.
## Demo
5min recorded demo video https://video.benetou.fr/w/sHE39WSZkQgfPNDdiypfog
Note that XR is optional. Original extract from https://youtu.be/BRjohy0ruAg?t=2061 for Future of Text demo day.
## How does it work
It provides a Web server with endpoints for different functions, including listing machines joining that network, make functions available to that new network of machines, sharing files, etc. Note that the point is to build on top of it, consequently functions as endpoints here are solely examples.
## How to install
1. clone repository
1. install dependancies (no package.json yet) but mostly express missing, i.e `npm i express`
1. run e.g `node .`
1. connect to it locally first, e.g https://localhost:8082 (assumes existing SSL certificates) or https://localhost:8082/routes
Note that the cloudinit file is an example on a brand new machine.
### tested on
- Linux machines, e.g Ubuntu on desktop, RPi Zero
- Android devices using Termux, e.g Quest 1
- iOS via iSH
Most likely partially works on Windows with Linux subsystem. System commands, e.g shutdown, xrandr, etc must be adapted.
### Developing and debugging
- See the console REPL, starting with help()
- see `/routes/json` to connect to all services
## Digging deeper
- Future of Text WebXR demo with shared filesystems https://video.benetou.fr/w/h291CfZiezenY1t46cQQo5 extract from https://www.youtube.com/results?search_query=demo+prototype+session
- hour long video discussion https://video.benetou.fr/w/aR81WVHg6H3E93GPG4jYUg
- self hosting AI notes https://fabien.benetou.fr/Content/SelfHostingArtificialIntelligence
- self hosting subreddit https://old.reddit.com/r/selfhosted/
- small Web https://ar.al/2020/08/07/what-is-the-small-web/
- DWeb https://en.wikipedia.org/wiki/Decentralized_web
### Inspiration
- Designing for serendipity: supporting end-user configuration of ubiquitous computing environments, Xerox PARC 2002
- Providing an Integrated User Experience of Networked Media, Devices, and Services through End-User Composition, 2008

@ -0,0 +1,16 @@
#!/bin/bash
cd /root
curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
source nodesource_setup.sh
apt install -y nodejs
apt update && apt install -y git
git clone https://git.benetou.fr/utopiah/offline-octopus
mkdir Prototypes
mv offline-octopus/ Prototypes/
cd Prototypes/offline-octopus/
npm i express
openssl req -nodes -new -x509 -keyout naf-key.pem -out naf.pem -subj "/C=BE/ST=Brussels/L=Brussels/O=Global Security/OU=IT Department/CN=offtopus.benetou.fr"
touch /root/.ssh/id_rsa_offlineoctopus.pub
touch /root/Prototypes/offline-octopus/.keyfrommd5
HOME=/root /usr/bin/node index.js &>> log.txt

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="https://glitch.com/favicon.ico" />
<title>offtopus events</title>
</head>
<body>
<div id="content"></div>
<hr>
<div id="events"></div>
<script>
const source = new EventSource('/events');
source.addEventListener('message', message => {
console.log('Got', message);
// Display the event data in the `content` div
document.querySelector('#events').innerHTML = event.data;
})
</script>
</body>
</html>

@ -9,8 +9,9 @@ const express = require("express"); // could be good to replace with c
// Get port or default to 8082 // Get port or default to 8082
const port = process.env.PORT || 8082; const port = process.env.PORT || 8082;
const protocol = process.env.PROTOCOL || 'https' const protocol = 'https'
const subclass = process.env.SUBCLASS || '192.168.4.' const subclass = '10.160.168.'
//const subclass = '192.168.4.'
const publicKeyPath = path.resolve(process.env.HOME,'.ssh','id_rsa_offlineoctopus.pub') const publicKeyPath = path.resolve(process.env.HOME,'.ssh','id_rsa_offlineoctopus.pub')
const publicKey = fs.readFileSync(publicKeyPath).toString().split(' ')[1] const publicKey = fs.readFileSync(publicKeyPath).toString().split(' ')[1]
@ -27,12 +28,6 @@ const minfileSaveFullPath = path.join(__dirname,'examples', minfilename)
const sshconfigpath = path.resolve(process.env.HOME,'.ssh','config') const sshconfigpath = path.resolve(process.env.HOME,'.ssh','config')
const propath = path.resolve(process.env.HOME,'Prototypes') const propath = path.resolve(process.env.HOME,'Prototypes')
const sshfsmounts = "sshfsmounts"
let workspaces = {}
let mountPoints = []
// note that stopping this process removes the mounts
// does not apply in a P2P fashion, must rely on local configuration here config.json // does not apply in a P2P fashion, must rely on local configuration here config.json
let localServices = [ ] let localServices = [ ]
const configFilePath = path.resolve(__dirname, "config.json") const configFilePath = path.resolve(__dirname, "config.json")
@ -237,8 +232,7 @@ app.get('/hmdlink', (req, res) => {
}) })
app.get('/webxr', (req, res) => { app.get('/webxr', (req, res) => {
res.redirect( '/spasca-offline/engine/index.html') res.redirect( '/local-metaverse-tooling/local-aframe-test.html' )
//res.redirect( '/local-metaverse-tooling/local-aframe-test.html' )
}) })
// user for /scan to populate foundPeers // user for /scan to populate foundPeers
@ -257,8 +251,9 @@ app.get('/scan', (req, res) => {
}) })
function scanpeers(){ function scanpeers(){
sendEventsToAll({'action':'scanning started'})
foundPeers = [] foundPeers = []
for (let i=1;i<254;i++){ // async so blasting, gives very quick result for positives for (let i=1;i<25;i++){ // async so blasting, gives very quick result for positives
let url=protocol+'://'+subclass+i+':'+port+'/available' let url=protocol+'://'+subclass+i+':'+port+'/available'
let opt={rejectUnauthorized: false} let opt={rejectUnauthorized: false}
https.get(url, opt, res => { https.get(url, opt, res => {
@ -279,57 +274,20 @@ function scanpeers(){
app.get('/sshconfig', (req, res) => { app.get('/sshconfig', (req, res) => {
res.json( getSshConfig() ) res.json( getSshConfig() )
// should filter on foundPeers to avoid offline peers
}) })
app.get('/sshconfig/live', (req, res) => { // note that stopping this process removes the mounts
res.json( getSshConfig().filter( i => foundPeers.map( e => subclass+e ).filter( x => i.connectionString.match(x) ).length ) )
})
app.get('/unifiedworkspaces', (req, res) => {
res.json( workspaces )
})
function getUnifiedworkspaces(){
mountPoints.map( mp => {
let dir = path.resolve(__dirname, sshfsmounts, mp)
workspaces[mp] = fs.readdirSync( dir )
console.log('reading', mp, workspaces.mp)
})
return workspaces
}
function mountAll(){ function mountAll(){
// should scanpeers() first getSshConfig().map( l => {
if (foundPeers.length==0) return
getSshConfig().filter( i => foundPeers.map( e => subclass+e ).filter( x => i.connectionString.match(x) ).length )
.map( l => {
let cs = 'sshfs ' + l.name + ':' let cs = 'sshfs ' + l.name + ':'
if (l.custom) if (l.custom)
cs+= l.custom cs+= l.custom
else else
cs+='/home/'+l.user cs+='/home/'+l.user
let targetPath = path.resolve(__dirname, sshfsmounts, l.name) return cs + ' ' + path.resolve(__dirname, "sshfsmounts", l.name)
if (!fs.existsSync(targetPath)){ fs.mkdirSync(targetPath, { recursive: true }); }
mountPoints.push(l.name)
return cs + ' ' + targetPath
} ) } )
//.map( l => console.log(l)) .map( l => execSync(l))
.map( l => { try { execSync(l) } catch (err) { console.log(err) } } )
}
function umountAll(){
mpath = path.resolve(__dirname, sshfsmounts)
fs.readdirSync( mpath )
// could rely on mountPoints instead
.map( f => {
try {
execSync('umount ' + path.resolve(__dirname, sshfsmounts, f) )
// should update mountPoints instead
} catch (err) {
console.log(err)
}
})
mountPoints = []
} }
function getSshConfig(){ function getSshConfig(){
@ -423,86 +381,30 @@ const webServer = https.createServer(credentials, app);
// const webServer = http.createServer(app); // const webServer = http.createServer(app);
// Listen on port // Listen on port
webServer.listen(port, () => { webServer.listen(port, () => {
console.log("listening on "+protocol+"://localhost:" + port) console.log("listening on "+protocol+"://localhost:" + port);
getCommand()
}); });
// REPL testing // SSE from https://www.digitalocean.com/community/tutorials/nodejs-server-sent-events-build-realtime-app
const readline = require("readline"); // adapted from jxr-permanence
const rl = readline.createInterface({ let clients = [];
input: process.stdin,
output: process.stdout, function eventsHandler(request, response, next) {
completer: completer const headers = {
}); 'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
// https://nodejs.org/api/readline.html#use-of-the-completer-function 'Cache-Control': 'no-cache'
function completer(line) { };
const completions = help().split('\n'); response.writeHead(200, headers);
const hits = completions.filter((c) => c.startsWith(line)); const clientId = Date.now();
// Show all completions if none found const newClient = { id: clientId, response };
return [hits.length ? hits : completions, line]; clients.push(newClient);
} request.on('close', () => { clients = clients.filter(client => client.id !== clientId); });
let command = ''
function getCommand(){
rl.question(process.title+" REPL: ", function(command) {
if (command == "close") {
rl.close();
} else {
try {
console.log(command, eval(command) )
} catch (err) {
console.log(err)
}
getCommand()
// somehow switch to node REPL proper after?!
}
});
} }
function help(){ function sendEventsToAll(data) {
return ` // function used to broadcast
help() clients.forEach(client => client.response.write(`data: ${JSON.stringify(data)}\n\n`))
execConfiguredCommand(cmdName)
getSshConfig()
mountAll()
umountAll()
scanpeers()
getUnifiedworkspaces()
foundPeers
port
protocol
subclass
publicKeyPath
publicKey
md5fromPub
process.title
filename
fileSaveFullPath
minfilename
minfileSaveFullPath
sshconfigpath
propath
localServices
configFilePath
utilsCmd
instructions
auth_instructions
process.title
sshfsmounts
mountPoints
workspaces
`
} }
rl.on("close", function() { app.get('/events', eventsHandler);
console.log("\ndone"); // for example /events.html shows when /scan begings (but not ends)
umountAll()
process.exit(0);
});
// Demo Day target :
// show files from ~/Prototypes as cubes from ssh mounted on a virtual workspace
// sshfs might not even be needed, see allpeers/exec instead
// wouldn't easily get content back though, just meta data

Loading…
Cancel
Save