@ -7,22 +7,49 @@ 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 = 'https'
const subclass = '192.168.4.'
// Setup and configure Express http server.
// Setup and configure Express http server.
const app = express ( ) ;
const app = express ( ) ;
app . use ( express . static ( path . resolve ( _ _dirname , "." , "examples" ) ) ) ;
app . use ( express . static ( path . resolve ( _ _dirname , "." , "examples" ) ) ) ;
process . title = 'offtopus' // offline-octopus seems too long
const filename = 'editor.html'
const fileSaveFullPath = path . join ( _ _dirname , 'examples' , filename )
const minfilename = 'editor.html.minimal'
const minfileSaveFullPath = path . join ( _ _dirname , 'examples' , minfilename )
const sshconfigpath = path . resolve ( process . env . HOME , '.ssh' , 'config' )
const propath = path . resolve ( process . env . HOME , 'Prototypes' )
// does not apply in a P2P fashion, must rely on local configuration here config.json
let localServices = [ ]
const configFilePath = path . resolve ( _ _dirname , "config.json" )
if ( fs . existsSync ( configFilePath ) ) {
const configurationFromFile = JSON . parse ( fs . readFileSync ( configFilePath ) . toString ( ) )
localServices = configurationFromFile
}
console . log ( localServices )
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
// ideally all handles within node
'shutdown' : { cmd : 'shutdown -h now' } , // not available everywhere, e.g unrooted Quest
'listprototypes' : { cmd : 'ls' , context : { cwd : propath } ,
format : res => res . toString ( ) . split ( '\n' )
} ,
}
const instructions = `
const instructions = `
/ h o m e / d e c k / . s s h /
/ h o m e / d e c k / . s s h /
trusted context , i . e on closed WiFi and over https , still . could check as bearer
trusted context , i . e on closed WiFi and over https , still . could check as bearer
/ h o m e / d e c k / . s s h / c o n f i g
/ h o m e / d e c k / . s s h / c o n f i g
could limit to known IP range or class e . g cat . ssh / config | grep 192.168 . 4. - C 3
could limit to known IP range or class e . g cat . ssh / config | grep 192.168 . 4. - C 3
could also re - add new entries rather than extend the format
could also re - add new entries rather than extend the format
/ h o m e / d e c k / . s s h / a u t h o r i z e d _ k e y s
/ h o m e / d e c k / . s s h / k n o w n _ h o s t s
/ h o m e / d e c k / . s s h / i d _ r s a _ s t e a m d e c k
/ h o m e / d e c k / . s s h / i d _ r s a _ s t e a m d e c k . p u b
/ h o m e / d e c k / s e r v e r . l o c a t e d b
/ h o m e / d e c k / s e r v e r . l o c a t e d b
seems to be plain text with metadata
seems to be plain text with metadata
/ h o m e / d e c k / d e s k t o p . p l o c a t e . d b
/ h o m e / d e c k / d e s k t o p . p l o c a t e . d b
@ -31,6 +58,7 @@ const instructions = `
ssh remarkable2 to get drawing preview
ssh remarkable2 to get drawing preview
conversion should be done , if necessary , on device
conversion should be done , if necessary , on device
not feasible right now without toltec to get opkg to get node
for typed text
for typed text
cat 43 * . rm | sed "s/[^a-zA-z0-9 ]//g" | sed "s/[OT,EC]//g"
cat 43 * . rm | sed "s/[^a-zA-z0-9 ]//g" | sed "s/[OT,EC]//g"
@ -38,6 +66,20 @@ util functions
modify WiFi parameters , including AP if available
modify WiFi parameters , including AP if available
shutdown / reboot
shutdown / reboot
`
`
const auth _instructions = ` generate md5 from pub offline-octopus then provide as bearer query param `
app . get ( '/' , ( req , res ) => {
res . send ( instructions )
} )
app . get ( '/authtest' , ( req , res ) => {
if ( req . query ? . bearer != md5fromPub ) { res . send ( auth _instructions ) ; return ; }
// relying on this line for each specific route
// this is NOT authentification proper, even less secure
// this is done ONLY to avoid mistakes on a secure LAN
res . json ( { msg : "success" } )
} )
app . get ( '/pwa' , ( req , res ) => {
app . get ( '/pwa' , ( req , res ) => {
// for offline use on mobile or VR headset
// for offline use on mobile or VR headset
// should try to sync back when devices connect back
// should try to sync back when devices connect back
@ -45,54 +87,91 @@ app.get('/pwa', (req, res) => {
// can be cascading or federatede or properly P2P
// can be cascading or federatede or properly P2P
// responsive programming also, not "just" design
// responsive programming also, not "just" design
res . redirect ( 'pwa/index.html' )
res . redirect ( 'pwa/index.html' )
// see also /editor
} )
} )
const utilsCmd = {
'shutdown' : { cmd : 'shutdown -h now' } ,
let resultFromLastGlobalCmd = { }
'listprototypes' : { cmd : 'ls' , context : { cwd : '/home/deck/Prototypes' } ,
// more reliably path.join(__dirname,'')
// should check on e.g publicKey or md5fromPub
format : res => res . toString ( ) . split ( '\n' )
app . get ( '/allpeers/exec' , ( req , res ) => {
} ,
if ( req . query ? . bearer != md5fromPub ) { res . send ( auth _instructions ) ; return ; }
}
app . get ( '/exec' , ( req , res ) => {
if ( ! req . query . cmdName ) {
if ( ! req . query . cmdName ) {
res . json ( utilsCmd )
res . json ( utilsCmd )
} else {
} else {
foundPeers . map ( i => {
let url = protocol + '://' + subclass + i + ':' + port
+ '/exec?cmdName=' + req . query . cmdName
+ '&bearer=' + req . query . bearer
let opt = { rejectUnauthorized : false }
https . get ( url , opt , res => {
let data = '' ;
res . on ( 'data' , chunk => {
data += chunk ;
} ) ;
res . on ( 'end' , ( ) => {
console . log ( 'data' , url , data )
data = JSON . parse ( data ) ;
resultFromLastGlobalCmd [ url ] = data
} )
} ) . on ( 'error' , err => {
console . log ( err . message ) ;
} ) . end ( )
} )
res . json ( { msg : 'started' } ) // could redirect('/all/result') too after a timeout
}
} )
app . get ( '/allpeers/result' , ( req , res ) => {
res . json ( resultFromLastGlobalCmd )
} )
let resultFromExecution = execSync ( utilsCmd [ req . query . cmdName ] . cmd , utilsCmd [ req . query . cmdName ] . context )
app . get ( '/exec' , ( req , res ) => {
let formatter = utilsCmd [ req . query . cmdName ] . format
if ( req . query ? . bearer != md5fromPub ) { res . send ( auth _instructions ) ; return ; }
if ( formatter ) resultFromExecution = formatter ( resultFromExecution )
if ( ! req . query . cmdName ) {
res . json ( resultFromExecution )
res . json ( utilsCmd )
} else {
res . json ( execConfiguredCommand ( req . query . cmdName ) )
}
}
} )
} )
function execConfiguredCommand ( cmdName ) {
let resultFromExecution = execSync ( utilsCmd [ cmdName ] . cmd , utilsCmd [ cmdName ] . context )
let formatter = utilsCmd [ cmdName ] . format
if ( formatter ) resultFromExecution = formatter ( resultFromExecution )
return resultFromExecution
}
// app.get('/interface/unregister', (req, res) => {
// app.get('/interface/unregister', (req, res) => {
// app.get('/interface/register', (req, res) => {
// app.get('/interface/register', (req, res) => {
// could be use for e.g reMarkable2, Quest2, etc with specifically accepted or prefered formats
// could be use for e.g reMarkable2, Quest2, etc with specifically accepted or prefered formats
app . get ( '/services/unregister' , ( req , res ) => {
app . get ( '/services/unregister' , ( req , res ) => {
res . json ( { msg : 'not yet implemented' } )
res . json ( { msg : 'not yet implemented' } )
} )
} )
app . get ( '/services/register' , ( req , res ) => {
app . get ( '/services/register' , ( req , res ) => {
res . json ( { msg : 'not yet implemented' } )
res . json ( { msg : 'not yet implemented' } )
// example {name:'hmdlink', desc:'share URL between local devices', protocol:'http', port:8082, path: '/hmdlink', url:'http://192.168.4.3:8082/hmdlink'},
// example {name:'hmdlink', desc:'share URL between local devices', protocol:'http', port:8082, path: '/hmdlink', url:'http://192.168.4.3:8082/hmdlink'},
} )
} )
app . get ( '/updates' , ( req , res ) => {
app . get ( '/updates' , ( req , res ) => {
// could rely on a git reachable by all peers
// could rely on a git reachable by all peers
// this might be feasible from this very https server as read-only
// this might be feasible from this very https server as read-only
// surely feasible via ssh
// could killall offtopus first then pull then restart detached
res . json ( { msg : 'not yet implemented' } )
res . json ( { msg : 'not yet implemented' } )
} )
} )
app . get ( '/recentfiles' , ( req , res ) => {
app . get ( '/recentfiles' , ( req , res ) => {
// e.g lsof | grep home | grep vim | grep -v firefox
// e.g lsof | grep home | grep vim | grep -v firefox
// or history | grep vi
// or history | grep vi
// should be available after (ideally local) conversion if needed, e.g rm -> .svg on reMarkable
// should be available after (ideally local) conversion if needed, e.g rm -> .svg on reMarkable
res . json ( { msg : 'not yet implemented' } )
res . json ( { msg : 'not yet implemented' } )
} )
} )
app . get ( '/services' , ( req , res ) => {
app . get ( '/services' , ( req , res ) => {
// could be probbed first to check for availability
// could be probbed first to check for availability
// should be updated also via register/unregister
// should be updated also via register/unregister
res . json ( [
res . json ( localServices )
{ name : 'kiwix' , desc : 'offline Wikipedia, Stackoverflow, etc' , protocol : 'http' , port : 8080 , url : 'http://192.168.4.3:8080' } ,
{ name : 'hmdlink' , desc : 'share URL between local devices' , protocol : 'http' , port : 8082 , path : '/hmdlink' , url : 'http://192.168.4.3:8082/hmdlink' } ,
] )
} )
} )
let dynURL = 'https://192.168.4.1/offline.html'
let dynURL = 'https://192.168.4.1/offline.html'
@ -101,25 +180,34 @@ app.get('/hmdlink/set', (req, res) => { // could be a PUT instead
dynURL = req . query . url
dynURL = req . query . url
res . redirect ( dynURL )
res . redirect ( dynURL )
} )
} )
app . get ( '/webxr' , ( req , res ) => {
res . redirect ( '/local-metaverse-tooling/local-aframe-test.html' )
} )
app . get ( '/hmdlink' , ( req , res ) => {
app . get ( '/hmdlink' , ( req , res ) => {
res . redirect ( dynURL )
res . redirect ( dynURL )
} )
} )
app . get ( '/json' , ( req , res ) => {
res . json ( instructions . split ( '\n' ) )
app . get ( '/webxr' , ( req , res ) => {
res . redirect ( '/local-metaverse-tooling/local-aframe-test.html' )
} )
} )
// user for /scan to populate foundPeers
app . get ( '/available' , ( req , res ) => {
app . get ( '/available' , ( req , res ) => {
res . json ( true )
res . json ( true )
} )
} )
app . get ( '/foundpeers' , ( req , res ) => {
app . get ( '/foundpeers' , ( req , res ) => {
res . json ( foundPeers )
res . json ( foundPeers )
} )
} )
let foundPeers = [ ]
let foundPeers = [ ]
app . get ( '/scan' , ( req , res ) => {
app . get ( '/scan' , ( req , res ) => {
scanpeers ( )
res . json ( { msg : 'started' } ) // could redirect('/foundpeers') too after a timeout
} )
function scanpeers ( ) {
foundPeers = [ ]
for ( let i = 1 ; i < 25 ; 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 = 'https://192.168.4.' + i + ':8082/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 => {
let data = '' ;
let data = '' ;
@ -135,20 +223,60 @@ app.get('/scan', (req, res) => {
//console.log(err.message); usually ECONNREFUSED or EHOSTUNREACH
//console.log(err.message); usually ECONNREFUSED or EHOSTUNREACH
} ) . end ( )
} ) . end ( )
}
}
res . json ( { msg : 'started' } ) // could redirect('/foundpeers') too
}
} )
app . get ( '/' , ( req , res ) => {
app . get ( '/sshconfig' , ( req , res ) => {
res . send ( instructions )
res . json ( getSshConfig ( ) )
// should filter on foundPeers to avoid offline peers
} )
} )
// returns connections string for e.g sshfs
function getSshConfig ( ) {
// here assume user deck
let txt = fs . readFileSync ( sshconfigpath ) . toString ( )
return txt . split ( 'Host ' )
. filter ( m => m . match ( subclass ) )
. map ( c => {
p = c . replaceAll ( ' ' , '' )
. split ( '\n' )
. filter ( i => i . match ( /^[a-zA-Z]/ ) ) ;
return p . filter ( pm => pm . match ( 'User ' ) ) [ 0 ] . replace ( 'User ' , '' ) +
p . filter ( pm => pm . match ( 'HostName' ) ) [ 0 ] . replace ( 'HostName ' , '@' )
} )
}
/ *
e . g AFTER mounting
f . readdirSync ( './sshfsmounts/' ) . map ( d => f . readdirSync ( './sshfsmounts/' + d ) )
location : / h o m e / d e c k / P r o t o t y p e s / o f f l i n e - o c t o p u s / s s h f s m o u n t s
sshfs remarkable2 : / h o m e / r o o t / x o c h i t l - d a t a / r e m a r k a b l e 2 /
works if available
sshfs fabien @ 192.168 . 4.1 : / h o m e / f a b i e n / r p i 0 /
still prompts for password , need manual login
ls rpi0 /
ls remarkable2 /
must be done after passwordless login , i . e after ssh - copy - id
made a dedicated key : / h o m e / d e c k / . s s h / i d _ r s a _ o f f l i n e o c t o p u s
easier to revoke if need be
* /
const publicKeyPath = path . resolve ( process . env . HOME , '.ssh' , 'id_rsa_offlineoctopus.pub' )
const publicKey = fs . readFileSync ( publicKeyPath ) . toString ( ) . split ( ' ' ) [ 1 ]
const md5fromPub = fs . readFileSync ( path . resolve ( _ _dirname , ".keyfrommd5" ) ) . toString ( ) . replace ( '\n' , '' )
app . get ( '/localprototypes' , ( req , res ) => {
app . get ( '/localprototypes' , ( req , res ) => {
// res.json( spawn('find Prototypes/ -iwholename */.git/config | xargs grep git.benetou.fr') )
// res.json( spawn('find Prototypes/ -iwholename */.git/config | xargs grep git.benetou.fr') )
// should use execSync now
// should use execSync now
} )
} )
app . get ( '/editor/recover' , ( req , res ) => {
app . get ( '/editor/recover' , ( req , res ) => {
// could move the previous file with time stamp
// could move the previous file with time stamp
fs . copyFileSync ( minfileSaveFullPath , fileSaveFullPath )
fs . copyFileSync ( minfileSaveFullPath , fileSaveFullPath )
res . json ( { msg : 'copied' } )
res . json ( { msg : 'copied' } )
} )
} )
/ *
/ *
* example of finding past local solution , here shiki for syntax highlighting
* example of finding past local solution , here shiki for syntax highlighting
*
*
@ -160,14 +288,13 @@ onig.wasm
see syntax - highlighting branch in SpaSca git repository
see syntax - highlighting branch in SpaSca git repository
in / h o m e / d e c k / s e r v e r h o m e / f a b i e n / w e b / f u t u r e _ o f _ t e x t _ d e m o / e n g i n e /
in / h o m e / d e c k / s e r v e r h o m e / f a b i e n / w e b / f u t u r e _ o f _ t e x t _ d e m o / e n g i n e /
* /
* /
let filename = 'editor.html'
let fileSaveFullPath = path . join ( _ _dirname , 'examples' , filename )
let minfilename = 'editor.html.minimal'
let minfileSaveFullPath = path . join ( _ _dirname , 'examples' , minfilename )
app . get ( '/editor/read' , ( req , res ) => {
app . get ( '/editor/read' , ( req , res ) => {
content = fs . readFileSync ( fileSaveFullPath ) . toString ( )
content = fs . readFileSync ( fileSaveFullPath ) . toString ( )
res . json ( { msg : content } )
res . json ( { msg : content } )
} )
} )
app . get ( '/editor/save' , ( req , res ) => {
app . get ( '/editor/save' , ( req , res ) => {
let content = req . query . content // does not escape, loses newlines
let content = req . query . content // does not escape, loses newlines
if ( ! content ) {
if ( ! content ) {
@ -188,32 +315,6 @@ 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 http ://localhost:" + port ) ;
console . log ( "listening on " + protocol + " ://localhost:" + port ) ;
} ) ;
} ) ;
// returns connections string for e.g sshfs
function loadSshConfig ( ) {
// here assume user deck
let txt = f . readFileSync ( '/home/deck/.ssh/config' ) . toString ( )
return txt . split ( 'Host ' )
. filter ( m => m . match ( '192.168.4.' ) )
. map ( c => {
p = c . replaceAll ( ' ' , '' )
. split ( '\n' )
. filter ( i => i . match ( /^[a-zA-Z]/ ) ) ;
return p . filter ( pm => pm . match ( 'User ' ) ) [ 0 ] . replace ( 'User ' , '' ) +
p . filter ( pm => pm . match ( 'HostName' ) ) [ 0 ] . replace ( 'HostName ' , '@' )
} )
}
/ *
e . g AFTER mounting
f . readdirSync ( './sshfsmounts/' ) . map ( d => f . readdirSync ( './sshfsmounts/' + d ) )
location : / h o m e / d e c k / P r o t o t y p e s / o f f l i n e - o c t o p u s / s s h f s m o u n t s
sshfs remarkable2 : / h o m e / r o o t / x o c h i t l - d a t a / r e m a r k a b l e 2 /
works if available
sshfs fabien @ 192.168 . 4.1 : / h o m e / f a b i e n / r p i 0 /
still prompts for password , need manual login
ls rpi0 /
ls remarkable2 /
* /