Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9414807070 | |||
| c6e72023fc | |||
| 78229f9b8f | |||
| 8c89994e8d | |||
| 4af9a219c3 | |||
| ff1454d061 | |||
| 6cb7a8c8c8 | |||
| 9892f29a1d | |||
| 386416094b | |||
| 28c5b31e2d | |||
| fa3ee1fc2a |
3156
examples/spasca-offline/engine/index.html
Normal file
3156
examples/spasca-offline/engine/index.html
Normal file
File diff suppressed because it is too large
Load Diff
208
index.js
208
index.js
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user