Compare commits

...

11 Commits
master ... xr

  1. 3156
      examples/spasca-offline/engine/index.html
  2. 208
      index.js

File diff suppressed because it is too large Load Diff

@ -8,9 +8,10 @@ const express = require("express"); // could be good to replace with c
// (worked at least on RPi, Deck, Quest and x64 machines)
// Get port or default to 8082
const port = process.env.PORT || 8082;
const protocol = 'https'
const subclass = '192.168.4.'
const port = process.env.PORT || 8082; // assumes homogenity across peers
const protocol = process.env.PROTOCOL || 'https'
const subclass = process.env.SUBCLASS || '192.168.0.' // defaulting to IP used at home rather than RPi0. Could also change it there as it's not justified beside helping distinction.
// Object.values(require("os").networkInterfaces()).flat().filter(({ family, internal }) => family === "IPv4" && !internal).map(({ address }) => address)[0].split('.').slice(0,3).join('.')+'.'
const publicKeyPath = path.resolve(process.env.HOME,'.ssh','id_rsa_offlineoctopus.pub')
const publicKey = fs.readFileSync(publicKeyPath).toString().split(' ')[1]
@ -27,6 +28,12 @@ const minfileSaveFullPath = path.join(__dirname,'examples', minfilename)
const sshconfigpath = path.resolve(process.env.HOME,'.ssh','config')
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
let localServices = [ ]
const configFilePath = path.resolve(__dirname, "config.json")
@ -48,6 +55,17 @@ pmwiki http localhost 4000 /pmwiki.php
sshd see quest in ssh config, specific user and port
*/
const kwinmin = `
echo "const clients = workspace.clientList();
for (var i = 0; i < clients.length; i++) {
print(clients[i].caption);
clients[i].minimized = true;
}" > /tmp/kwinscriptdemo
num=$(dbus-send --print-reply --dest=org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript string:"/tmp/kwinscriptdemo" | awk 'END {print $2}' )
dbus-send --print-reply --dest=org.kde.KWin /$num org.kde.kwin.Script.run`
const utilsCmd = { // security risk but for now not accepting user input so safer
//'update' : { desc: 'note that will lose the state, e.g foundpeers', cmd: 'killall '+process.title+' && ' },
// should first download the new version and proceed only if new
@ -60,6 +78,19 @@ const utilsCmd = { // security risk but for now not accepting user input so safe
'listprototypes': { cmd: 'ls', context: {cwd: propath},
format: res => res.toString().split('\n')
},
'mousemove' : { cmd: 'xdotool mousemove ', optionsPattern: /\d+ \d+/},
'mouseup' : { cmd: 'xdotool mouseup 1'},
'mousedown' : { cmd: 'xdotool mousedown 1'},
// per device specific (until adjustable per user)
'minimizeall' : { cmd: kwinmin}, //KWin script
//'minimizeall' : { cmd: '/home/fabien/Prototypes/kwin-scripting/launch'}, //KWin script
'highresscreen' : { cmd: 'xrandr --output DP-4 --mode 3840x2160'},
'lowresscreen' : { cmd: 'xrandr --output DP-4 --mode 1920x1080'},
// 'anyresscreen' : { cmd: 'xrandr --output DP-4 --mode '}, // would require user input which is risky e.g here 'OxO; wget rootkit; bash rootkit;'
'availableRes' : { cmd: 'xrandr -q | grep 0x | grep -v primary | cut -f 4 -d " "',
format: res => res.toString().split('\n').filter( l => l!='' )
},
//'npmfind' : { desc: 'package manager finder', cmd: 'find . -wholename "*node_modules/acorn"' },
// security risk if relying on user provided name, e.g replacing acorn by user input
// example that could be generalized to other package managers e.g .deb or opkg
@ -67,6 +98,7 @@ const utilsCmd = { // security risk but for now not accepting user input so safe
// could be interesting to consider also recent containers and ~/.bashrc for services
const instructions = `
<pre>
/home/deck/.ssh/
trusted context, i.e on closed WiFi and over https with bearer authorization
/home/deck/.ssh/config
@ -88,6 +120,7 @@ ssh remarkable2 to get drawing preview
util functions
modify WiFi parameters, including AP if available
shutdown/reboot
</pre>
`
const auth_instructions = `generate md5 from pub offline-octopus then provide as bearer query param`
@ -105,6 +138,19 @@ app.use(function(req, res, next) {
app.get('/', (req, res) => {
res.send( instructions )
// see issue 20
})
app.get('/routes', (req, res) => {
let formattedRoutes = routes.map( r => `<li><a href='${r}'>${r}</a></li>` ).join('\n')
res.send( formattedRoutes )
})
app.get('/routes/json', (req, res) => {
// minimalist for JXR but could also be OpenAPI, cf https://git.benetou.fr/utopiah/offline-octopus/issues/16
// used in /webxr as
// fetch('/routes/json').then(r=>r.json()).then(r=>r.filter(p=>p.match(/resolution/)).map(p=>addNewNote('jxr fetch("https://192.168.0.129:8082'+p+'")')))
res.send( routes )
})
app.get('/authtestviaheader', (req, res) => {
@ -176,8 +222,8 @@ app.get('/exec', (req, res) => {
}
})
function execConfiguredCommand(cmdName){
let resultFromExecution = execSync(utilsCmd[cmdName].cmd, utilsCmd[cmdName].context)
function execConfiguredCommand(cmdName, options=''){
let resultFromExecution = execSync(utilsCmd[cmdName].cmd+options, utilsCmd[cmdName].context)
let formatter = utilsCmd[cmdName].format
if (formatter) resultFromExecution = formatter(resultFromExecution)
return resultFromExecution
@ -219,9 +265,11 @@ app.get('/recentfiles', (req, res) => {
res.json( {msg: 'not yet implemented'})
})
let dynURL = 'https://192.168.4.1/offline.html'
let dynURL = '/'
app.get('/hmdlink/set', (req, res) => { // could be a PUT instead
// e.g http://192.168.4.3:8082/hmdlink/set?url=http://192.168.4.3:8082/114df5f8-3921-42f0-81e7-48731b563571.thumbnails/f07120ba-0ca1-429d-869f-c704a52b7aa3.png
// should return a warning if req.query.url is undefined
if (!req.query.url) {res.json( {error: 'use /hmdlink/set?url=YOURURL'}); return}
dynURL = req.query.url
res.redirect( dynURL )
})
@ -231,7 +279,8 @@ app.get('/hmdlink', (req, res) => {
})
app.get('/webxr', (req, res) => {
res.redirect( '/local-metaverse-tooling/local-aframe-test.html' )
res.redirect( '/spasca-offline/engine/index.html')
//res.redirect( '/local-metaverse-tooling/local-aframe-test.html' )
})
// user for /scan to populate foundPeers
@ -251,7 +300,7 @@ app.get('/scan', (req, res) => {
function scanpeers(){
foundPeers = []
for (let i=1;i<25;i++){ // async so blasting, gives very quick result for positives
for (let i=1;i<254;i++){ // async so blasting, gives very quick result for positives
let url=protocol+'://'+subclass+i+':'+port+'/available'
let opt={rejectUnauthorized: false}
https.get(url, opt, res => {
@ -272,20 +321,57 @@ function scanpeers(){
app.get('/sshconfig', (req, res) => {
res.json( getSshConfig() )
// should filter on foundPeers to avoid offline peers
})
// note that stopping this process removes the mounts
app.get('/sshconfig/live', (req, res) => {
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(){
getSshConfig().map( l => {
// should scanpeers() first
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 + ':'
if (l.custom)
cs+= l.custom
else
cs+='/home/'+l.user
return cs + ' ' + path.resolve(__dirname, "sshfsmounts", l.name)
let targetPath = path.resolve(__dirname, sshfsmounts, l.name)
if (!fs.existsSync(targetPath)){ fs.mkdirSync(targetPath, { recursive: true }); }
mountPoints.push(l.name)
return cs + ' ' + targetPath
} )
.map( l => execSync(l))
//.map( l => console.log(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(){
@ -328,6 +414,17 @@ function getSshConfig(){
easier to revoke if need be
*/
app.get('/mouse/up', (req, res) => { res.json( execConfiguredCommand('mouseup') ) })
app.get('/mouse/down', (req, res) => { res.json( execConfiguredCommand('mousedown') ) })
app.get('/mouse/move', (req, res) => {
if( !req.query.options?.match(utilsCmd['mousemove']) ) { {res.json( {error: 'use /mouse/move?options=100 2000'}); return}}
res.json( execConfiguredCommand('mousemove',req.query.options) ) }
)
app.get('/minimizeall', (req, res) => { res.json( execConfiguredCommand('minimizeall') ) })
app.get('/resolution/high', (req, res) => { res.json( execConfiguredCommand('highresscreen') ) })
app.get('/resolution/low', (req, res) => { res.json( execConfiguredCommand('lowresscreen') ) })
app.get('/resolution/list', (req, res) => { res.json( execConfiguredCommand('availableRes') ) })
app.get('/localprototypes', (req, res) => {
// examples to disentangle own work for cloned existing repositories :
// find Prototypes/ -iwholename */.git/config | xargs grep git.benetou.fr
@ -370,6 +467,9 @@ app.get('/editor/save', (req, res) => {
}
})
let routes = app._router.stack.map( r => r.route?.path ).filter( r => typeof(r) == 'string' )
// it's a form of caching but so far routes are not dynamically generated so sufficient solution
const privateKey = fs.readFileSync("naf-key.pem", "utf8");
const certificate = fs.readFileSync("naf.pem", "utf8");
const credentials = { key: privateKey, cert: certificate };
@ -379,5 +479,85 @@ const webServer = https.createServer(credentials, app);
// const webServer = http.createServer(app);
// Listen on port
webServer.listen(port, () => {
console.log("listening on "+protocol+"://localhost:" + port);
console.log("listening on "+protocol+"://0.0.0.0:" + port)
getCommand()
});
// REPL testing
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer
});
// https://nodejs.org/api/readline.html#use-of-the-completer-function
function completer(line) {
const completions = help().split('\n');
const hits = completions.filter((c) => c.startsWith(line));
// Show all completions if none found
return [hits.length ? hits : completions, line];
}
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(){
return `
help()
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() {
console.log("\ndone");
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