├── .dockerignore ├── .gitignore ├── .gitlab-ci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── Build.js ├── Config-Production.js ├── Config.js ├── ConfigBackup.js ├── DDNS.js ├── DNS.js ├── Database.js ├── Disks.js ├── Filesystem.js ├── HTTP.js ├── Human.js ├── Images.js ├── MDNS.js ├── MinkeApp.js ├── MinkeSetup.js ├── Monitor.js ├── Network.js ├── Pull.js ├── Skeletons.js ├── System.js ├── UPNP.js ├── Updater.js ├── index.js ├── native │ ├── binding.gyp │ ├── iface-socket.cc │ └── native.js ├── package-lock.json ├── package.json ├── pages │ ├── Applications.js │ ├── Configure.js │ ├── Console.js │ ├── HB.js │ ├── Log.js │ ├── Main.js │ ├── css │ │ ├── apps.css │ │ ├── colors-dark.css │ │ ├── colors-light.css │ │ ├── configure.css │ │ ├── console.css │ │ └── main.css │ ├── html │ │ ├── Applications.html │ │ ├── Configure.html │ │ ├── Console.html │ │ ├── Log.html │ │ ├── Main.html │ │ ├── ProxyFail.html │ │ └── partials │ │ │ ├── App.html │ │ │ ├── AppStatus.html │ │ │ ├── BackupAndRestore.html │ │ │ ├── Disks.html │ │ │ ├── DownloadFile.html │ │ │ ├── EditShares.html │ │ │ ├── EditTable.html │ │ │ ├── Networks.html │ │ │ ├── SelectBackups.html │ │ │ ├── SelectDirectory.html │ │ │ ├── SelectShares.html │ │ │ ├── SelectWebsites.html │ │ │ ├── ShowTable.html │ │ │ └── Tags.html │ ├── img │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── manifest.json │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png │ ├── pages.js │ └── script │ │ ├── qrcode.js │ │ └── script.js ├── skeletons │ ├── builtin │ │ ├── apache.skeleton │ │ ├── bitwarden.skeleton │ │ ├── dhcpserver.skeleton │ │ ├── discourse.skeleton │ │ ├── dnscrypt.skeleton │ │ ├── dnshosts.skeleton │ │ ├── dohblock.skeleton │ │ ├── domainproxy.skeleton │ │ ├── duplicati.skeleton │ │ ├── filebot.skeleton │ │ ├── folders.skeleton │ │ ├── ghost.skeleton │ │ ├── homebridge.skeleton │ │ ├── homebridge_alexabridge.skeleton │ │ ├── homebridge_broadlink.skeleton │ │ ├── homebridge_fglair.skeleton │ │ ├── homebridge_neato.skeleton │ │ ├── homebridge_nest.skeleton │ │ ├── homebridge_netatmo.skeleton │ │ ├── homebridge_notifications.skeleton │ │ ├── homebridge_purpleair.skeleton │ │ ├── homebridge_raspberryshake.skeleton │ │ ├── homebridge_roborock.skeleton │ │ ├── homebridge_shelly.skeleton │ │ ├── homebridge_tplink.skeleton │ │ ├── jellyfin.skeleton │ │ ├── lancache.skeleton │ │ ├── mastodon.skeleton │ │ ├── minecraftserver.skeleton │ │ ├── minio.skeleton │ │ ├── minke.skeleton │ │ ├── netproxy.skeleton │ │ ├── networkshares.skeleton │ │ ├── openvpnclient.skeleton │ │ ├── openvpnserver.skeleton │ │ ├── pihole.skeleton │ │ ├── plex.skeleton │ │ ├── pptpclient.skeleton │ │ ├── radarr.skeleton │ │ ├── reverseproxy.skeleton │ │ ├── samba.skeleton │ │ ├── sftp.skeleton │ │ ├── shadowsocks.skeleton │ │ ├── sonarr.skeleton │ │ ├── speedtest.skeleton │ │ ├── syncthing.skeleton │ │ ├── timemachine.skeleton │ │ ├── torrelay.skeleton │ │ ├── torservices.skeleton │ │ ├── torsocks.skeleton │ │ ├── upnpmonitor.skeleton │ │ ├── wiki.skeleton │ │ ├── wireguardclient.skeleton │ │ ├── wireguardserver.skeleton │ │ └── wordpress.skeleton │ └── disabled │ │ ├── ntopng.skeleton │ │ └── privatenetwork.skeleton ├── test │ ├── dns.js │ ├── environment.js │ ├── expandstring.js │ ├── filesystem.js │ ├── fixture │ │ ├── dns.fixture.js │ │ ├── filesystem.fixture.js │ │ ├── minkeapp.fixture.js │ │ ├── minkesetup.fixture.js │ │ ├── skeletons.fixture.js │ │ └── system.fixture.js │ ├── images.js │ ├── restart.js │ ├── skeletons.js │ └── variables.js └── utils │ ├── Barrier.js │ ├── Events.js │ └── Flatten.js ├── assets ├── MinkeBoxBigLong.png ├── MinkeBoxLogo.png ├── MinkeBoxLogoBorderless.png ├── MinkeBoxLong.png ├── cross-icon.png ├── install │ ├── AboutToCreate.png │ ├── AllDone.png │ ├── Applications.png │ ├── AtomPC.jpeg │ ├── ClickAway.png │ ├── Disk.png │ ├── DiskImageAdded.png │ ├── DiskImageSelection.png │ ├── Disks After.png │ ├── Disks Before.png │ ├── Disks During.png │ ├── DownloadInstall.png │ ├── EnableEFI.png │ ├── Etcher.png │ ├── FirstBoot.png │ ├── FixNetwork.png │ ├── InitialVM.png │ ├── InstallApps.png │ ├── InternetSpeedTest.png │ ├── MainConfig.png │ ├── Memory.png │ ├── NewVM.png │ ├── Screen Shot 2019-04-03 at 4.14.48 PM.png │ ├── Select.png │ ├── SpeedTestEnter.png │ ├── SpeedTestOpened.png │ ├── SpeedTestStart.png │ ├── StartUp.png │ ├── Win10Find.png │ ├── iu 6.47.28 PM.png │ └── overview.png └── overview.jpg ├── extras ├── docker-compose.yml └── minkebox └── startup.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | app/node_modules/* 2 | app/native/build/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/node_modules 2 | .DS_Store 3 | app/native/build 4 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project: 'ericvh/gitlab-ci-arm-template' 3 | file: '/.gitlab-ci.yml' 4 | 5 | variables: 6 | CI_BUILDX_ARCHS: "linux/arm64,linux/amd64" 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.11 2 | 3 | EXPOSE 53/tcp 53/udp 80/tcp 4 | VOLUME /minke 5 | 6 | LABEL net.minkebox.system="true" 7 | 8 | ENTRYPOINT ["/startup.sh"] 9 | 10 | COPY startup.sh /startup.sh 11 | COPY app/package.json /app/package.json 12 | COPY app/native /app/native 13 | RUN apk add nodejs npm \ 14 | tzdata openntpd e2fsprogs parted \ 15 | iproute2 \ 16 | make gcc g++ python ;\ 17 | cd /app ; npm install --unsafe-perm --production ; apk del npm make gcc g++ python 18 | 19 | COPY app/ /app 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019-2020, MinkeBox 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MinkeBox 2 | 3 | MinkeBox is an Open Source project designed to make it simple to run Dockerized applications at home. Docker might have already revolutionized how companies run massively parallel applications, but it also has the potential to simplify running all those useful little applications at home. Need a VPN into your home or office? Need a Wiki or a Webserver? Backups? Cache all those huge Steam downloads for you friends or roommates? Maybe even run a Minecraft server for you and your friends? These things already exists as Docker apps, and MinkeBox aims to make it easy to get them running. 4 | 5 | ![overview](assets/overview.jpg) 6 | -------------------------------------------------------------------------------- /app/Build.js: -------------------------------------------------------------------------------- 1 | module.exports = "05/31/20 18:00:09" 2 | -------------------------------------------------------------------------------- /app/Config-Production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ROOT: '/minke', 3 | CONFIG_NAME: 'Production', 4 | WEB_PORT: 80, 5 | REGISTRY_HOST: 'registry.minkebox.net', 6 | REGISTRY_DEFAULT_TAG: 'latest', 7 | DDNS_UPDATE: 'https://ddns.minkebox.net/update', 8 | CAPTCH_QUESTION: 'https://captcha.minkebox.net/captcha', 9 | CAPTCH_VERIFY: 'https://captcha.minkebox.net/verify', 10 | GLOBALDOMAIN: '.minkebox.net', 11 | DEFAULT_FALLBACK_RESOLVER: '1.1.1.1' 12 | }; 13 | -------------------------------------------------------------------------------- /app/Config.js: -------------------------------------------------------------------------------- 1 | try { 2 | module.exports = require('./Config-Development'); 3 | } 4 | catch (_) { 5 | module.exports = require('./Config-Production'); 6 | } 7 | -------------------------------------------------------------------------------- /app/ConfigBackup.js: -------------------------------------------------------------------------------- 1 | const Config = require('./Config'); 2 | const FS = require('fs'); 3 | const Database = require('./Database'); 4 | const Images = require('./Images'); 5 | let MinkeApp; 6 | 7 | const VERSION = 1; 8 | const BACKUP_PATH = `${Config.ROOT}/minkebox.config`; 9 | 10 | const ConfigBackup = { 11 | 12 | backup: async function() { 13 | MinkeApp = MinkeApp || require('./MinkeApp'); 14 | const backup = { 15 | version: VERSION, 16 | config: await Database.getConfig('minke'), 17 | applications: MinkeApp.getApps().reduce((acc, app) => { 18 | if (app._image !== Images.MINKE) { 19 | acc.push(app.toJSON()); 20 | } 21 | return acc; 22 | }, []) 23 | }; 24 | return backup; 25 | }, 26 | 27 | restore: async function(backup) { 28 | if (backup.version !== VERSION) { 29 | throw new Error('Backup version not supported'); 30 | } 31 | 32 | // Shutdown the system - we're about to erase 33 | await MinkeApp.shutdown({}); 34 | 35 | // Erase all the apps and system configuration 36 | await Database.reset(); 37 | 38 | // Restore the system configuration 39 | await Database.saveConfig(backup.config); 40 | 41 | // Restore the apps 42 | await Promise.all(backup.applications.map(app => { 43 | return Database.saveApp(app); 44 | })); 45 | 46 | // Force an immediate restart of Minke to load the new setup 47 | MinkeApp.getAppById('minke').systemRestart('restore'); 48 | } 49 | } 50 | 51 | module.exports = { 52 | 53 | save: async function() { 54 | FS.writeFileSync(BACKUP_PATH, JSON.stringify(await ConfigBackup.backup())); 55 | }, 56 | 57 | restore: async function(backup) { 58 | ConfigBackup.restore(JSON.parse(backup)); 59 | }, 60 | 61 | HTML: async function(ctx) { 62 | ctx.type = 'text/plain'; 63 | ctx.body = JSON.stringify(await ConfigBackup.backup()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/DDNS.js: -------------------------------------------------------------------------------- 1 | const HTTP = require('http'); 2 | const HTTPS = require('https'); 3 | const Config = require('./Config'); 4 | const UPNP = require('./UPNP'); 5 | 6 | const FALLBACK_GETIP = 'http://api.ipify.org'; 7 | const DDNS_URL = `${Config.DDNS_UPDATE}`; 8 | const TICKS = 10 * 60 * 1000; // 10 minutes 9 | const FORCE_TICKS = 24 * 60 * 60 * 1000; // 1 day 10 | const RETRY = 60 * 1000; // 1 minute 11 | const DELAY = 10 * 1000; // 10 seconds 12 | 13 | const DDNS = { 14 | 15 | _gids: {}, 16 | _pending: null, 17 | _key: '', 18 | _tick: null, 19 | 20 | start: function(key) { 21 | this._key = key; 22 | let ticks = 0; 23 | if (this._tick) { 24 | clearInterval(this._tick); 25 | } 26 | this._tick = setInterval(() => { 27 | this._update(ticks <= 0); 28 | ticks--; 29 | if (ticks < 0) { 30 | ticks = Math.floor(FORCE_TICKS / TICKS); 31 | } 32 | }, TICKS); 33 | 34 | this._humanVerified = (evt) => { 35 | if (evt.human === 'yes') { 36 | this._update(true); 37 | } 38 | }; 39 | Root.on('human.verified', this._humanVerified); 40 | }, 41 | 42 | stop: function() { 43 | Root.off('human.verified', this._humanVerified); 44 | }, 45 | 46 | register: function(app) { 47 | //console.log('register', app._globalId); 48 | this._gids[app._globalId] = { 49 | app: app, 50 | lastIP: null, 51 | lastIP6: null 52 | }; 53 | this._update(true); 54 | Root.emit('human.verify', {}); 55 | }, 56 | 57 | unregister: function(app) { 58 | //console.log('unregister', app._globalId); 59 | delete this._gids[app._globalId]; 60 | }, 61 | 62 | _update: function(force) { 63 | if (Object.keys(this._gids).length) { // Dont store keys - may change after we've got the IP address 64 | if (force) { 65 | Object.values(this._gids).forEach(entry => { 66 | entry.lastIP = null; 67 | entry.lastIP6 = null; 68 | }); 69 | } 70 | clearTimeout(this._pending); 71 | this._pending = setTimeout(() => { 72 | this._getExternalIP().then(eip => { 73 | if (!eip) { 74 | setTimeout(() => this._update(true), RETRY); 75 | } 76 | else { 77 | Object.keys(this._gids).forEach(gid => { 78 | const entry = this._gids[gid]; 79 | const ip = entry.app._remoteIP || eip; 80 | const ip6 = entry.app.getNATIP6() ? entry.app.getSLAACAddress() : null; 81 | if (ip != entry.lastIP || ip6 != entry.lastIP6) { 82 | if (ip) { 83 | if (!ip6) { 84 | //console.log(`${DDNS_URL}?key=${this._key}&host=${gid}&ip=${ip}`); 85 | HTTPS.get(`${DDNS_URL}?key=${this._key}&host=${gid}&ip=${ip}`, () => {}); 86 | } 87 | else { 88 | //console.log(`${DDNS_URL}?key=${this._key}&host=${gid}&ip=${ip}&ip6=${ip6}`); 89 | HTTPS.get(`${DDNS_URL}?key=${this._key}&host=${gid}&ip=${ip}&ip6=${ip6}`, () => {}); 90 | } 91 | } 92 | entry.lastIP = ip; 93 | entry.lastIP6 = ip6; 94 | } 95 | }); 96 | } 97 | }); 98 | }, DELAY); 99 | } 100 | }, 101 | 102 | _getExternalIP: async function() { 103 | return new Promise(resolve => { 104 | //console.log('_getExternalIP'); 105 | UPNP.getExternalIP().then((ip) => { 106 | //console.log('_gotExternaIP', ip); 107 | if (ip) { 108 | resolve(ip); 109 | } 110 | else if (FALLBACK_GETIP) { 111 | // Fallback 112 | HTTP.get(FALLBACK_GETIP, (res) => { 113 | res.on('data', (data) => { 114 | //console.log('_gotExternaIP fallback', data.toString('utf8')); 115 | resolve(data.toString('utf8')); 116 | }); 117 | }); 118 | } 119 | else { 120 | resolve(null); 121 | } 122 | }); 123 | }); 124 | } 125 | 126 | } 127 | 128 | module.exports = DDNS; 129 | -------------------------------------------------------------------------------- /app/Database.js: -------------------------------------------------------------------------------- 1 | const Config = require('./Config'); 2 | const DB = require('nedb'); 3 | const FS = require('fs'); 4 | 5 | const DB_PATH = `${Config.ROOT}/db`; 6 | 7 | function _wrap(fn) { 8 | return async function(db, ...args) { 9 | return new Promise((resolve, reject) => { 10 | args.push((err, val) => { 11 | if (err) { 12 | reject(err); 13 | } 14 | else { 15 | resolve(val); 16 | } 17 | }); 18 | fn.apply(db, args); 19 | }); 20 | } 21 | } 22 | 23 | const Database = { 24 | 25 | init: async function() { 26 | const DB_APPS = `${DB_PATH}/apps.db`; 27 | const DB_CONFIG = `${DB_PATH}/config.db`; 28 | const DB_COMPACT_SEC = 60 * 60 * 24; // Every day 29 | 30 | FS.mkdirSync(DB_PATH, { recursive: true }); 31 | 32 | Database._apps = new DB({ filename: DB_APPS, autoload: true }); 33 | Database._apps.persistence.setAutocompactionInterval(DB_COMPACT_SEC * 1000); 34 | Database._config = new DB({ filename: DB_CONFIG, autoload: true }); 35 | Database._config.persistence.setAutocompactionInterval(DB_COMPACT_SEC * 1000); 36 | }, 37 | 38 | reset: async function() { 39 | await this._remove(Database._config, {}, { multi: true }); 40 | await this._remove(Database._apps, {}, { multi: true }); 41 | }, 42 | 43 | getConfig: async function(id) { 44 | return await this._findOne(Database._config, { _id: id }); 45 | }, 46 | 47 | saveConfig: async function(configJson) { 48 | await this._update(Database._config, { _id: configJson._id }, configJson, { upsert: true }); 49 | }, 50 | 51 | getApps: async function() { 52 | return await this._find(Database._apps, {}); 53 | }, 54 | 55 | saveApp: async function(appJson) { 56 | await this._update(Database._apps, { _id: appJson._id }, appJson, { upsert: true }); 57 | }, 58 | 59 | removeApp: async function(id) { 60 | await this._remove(Database._apps, { _id: id }); 61 | }, 62 | 63 | newAppId: function() { 64 | return Database._apps.createNewId(); 65 | }, 66 | 67 | _find: _wrap(DB.prototype.find), 68 | _findOne: _wrap(DB.prototype.findOne), 69 | _update: _wrap(DB.prototype.update), 70 | _remove: _wrap(DB.prototype.remove) 71 | }; 72 | 73 | module.exports = Database; 74 | -------------------------------------------------------------------------------- /app/HTTP.js: -------------------------------------------------------------------------------- 1 | const FS = require('fs'); 2 | const Koa = require('koa'); 3 | const KoaRouter = require('koa-router'); 4 | const KoaProxy = require('koa-proxy'); 5 | 6 | const TEMPORARY_REDIRECT = 307; 7 | 8 | function makeProxy(to) { 9 | const app = new Koa(); 10 | app.use(async (ctx, next) => { 11 | try { 12 | // We transform location redirects ourselves because the proxy doesn't handle this. The proxy will 13 | // do redirect internally but if we let that happen then redirects from /aaa to /aaa/ aren't seen 14 | // by the browser which can break some apps (e.g. PiHole). 15 | const requestOrigin = ctx.request.origin; 16 | await next(); 17 | const location = ctx.response.get('location'); 18 | if (location && location.indexOf(to) === 0) { 19 | ctx.response.set('Location', `${requestOrigin}${location.substring(to.length)}`); 20 | } 21 | } 22 | catch (e) { 23 | //console.log(e); 24 | ctx.type = 'text/html'; 25 | ctx.body = FS.readFileSync(`${__dirname}/pages/html/ProxyFail.html`); 26 | } 27 | }); 28 | app.use(KoaProxy({ 29 | host: to, 30 | jar: true, // Send cookies 31 | followRedirect: false, // Handle redirects by hand (see above)) 32 | suppressRequestHeaders: [ 33 | 'origin' 34 | ], 35 | overrideResponseHeaders: { 36 | 'X-MinkeBox-Proxy': 'true' 37 | }, 38 | suppressResponseHeaders: [ 39 | 'content-security-policy', 40 | 'x-frame-options' 41 | ] 42 | })); 43 | const server = app.listen(); 44 | return new Promise(resolve => { 45 | server.on('listening', () => { 46 | resolve({ 47 | port: server.address().port, 48 | close: () => { 49 | server.close(); 50 | } 51 | }); 52 | }); 53 | }); 54 | } 55 | 56 | function Proxy(app, from, to) { 57 | this._router = KoaRouter({ 58 | prefix: from 59 | }); 60 | this._router.all('(.*)', async (ctx) => { 61 | if (!app._webProxy) { 62 | app._webProxy = await makeProxy(to); 63 | } 64 | ctx.redirect(`http://${ctx.request.header.host}:${app._webProxy.port}${ctx.params[0] || ''}`); 65 | ctx.status = TEMPORARY_REDIRECT; 66 | }); 67 | } 68 | 69 | function Redirect(from, url) { 70 | this._router = KoaRouter({ 71 | prefix: from 72 | }); 73 | this._router.all('/', async (ctx) => { 74 | ctx.redirect(url); 75 | ctx.status = TEMPORARY_REDIRECT; 76 | }); 77 | } 78 | 79 | const HTTP = { 80 | 81 | createProxy: function(app, from, path, to) { 82 | const f = new Proxy(app, from, to); 83 | return { 84 | url: `${from}${path || ''}`.replace(/\/\//g,'/'), 85 | http: f._router.middleware() 86 | }; 87 | }, 88 | 89 | createNewTab: function(app, from, path, to) { 90 | const f = new Redirect(from, `${to}${path || ''}`); 91 | return { 92 | url: from, 93 | target: '_blank', 94 | http: f._router.middleware() 95 | }; 96 | }, 97 | 98 | createNewTabProxy: function(app, from, path, to) { 99 | const f = new Proxy(app, from, to); 100 | return { 101 | url: `${from}${path || ''}`.replace(/\/\//g,'/'), 102 | target: '_blank', 103 | http: f._router.middleware() 104 | }; 105 | }, 106 | 107 | createUrl: function(url) { 108 | return { 109 | url: url 110 | }; 111 | } 112 | 113 | } 114 | 115 | module.exports = HTTP; 116 | -------------------------------------------------------------------------------- /app/Human.js: -------------------------------------------------------------------------------- 1 | const HTTPS = require('https'); 2 | const Config = require('./Config'); 3 | 4 | const VERIFY_URL = `${Config.CAPTCH_VERIFY}`; 5 | 6 | 7 | const Human = { 8 | 9 | start: function(globalId, humanRef) { 10 | this._id = globalId; 11 | this._ref = humanRef; 12 | Root.on('human.verify', this._verify); 13 | Root.on('system.captcha.token', this._update); 14 | }, 15 | 16 | _update: function(evt) { 17 | if (evt.token) { 18 | switch (evt.token) { 19 | case 'cancel': 20 | Human._ref.value = 'no'; 21 | break; 22 | case 'maybe': 23 | Human._ref.value = 'maybe'; 24 | break; 25 | default: 26 | HTTPS.get(`${VERIFY_URL}?key=${Human._id}&token=${evt.token}`, res => { 27 | if (res.statusCode === 200) { 28 | Human._ref.value = 'yes'; 29 | } 30 | else { 31 | Human._ref.value = 'no'; 32 | } 33 | Root.emit('human.verified', { human: Human._ref.value }); 34 | }); 35 | break; 36 | } 37 | } 38 | }, 39 | 40 | stop: function() { 41 | Root.off('system.captcha.token', this._update); 42 | Root.off('human.verify', this._verify); 43 | }, 44 | 45 | _verify: function(evt) { 46 | if (evt.force || Human._ref.value === 'unknown') { 47 | Root.emit('system.captcha'); 48 | } 49 | }, 50 | 51 | isVerified: function() { 52 | return this._ref.value === 'yes'; 53 | } 54 | 55 | }; 56 | 57 | module.exports = Human; 58 | -------------------------------------------------------------------------------- /app/Images.js: -------------------------------------------------------------------------------- 1 | const Config = require('./Config'); 2 | 3 | module.exports = { 4 | 5 | MINKE: `${Config.REGISTRY_HOST}/minkebox/minke`, 6 | MINKE_HELPER: `${Config.REGISTRY_HOST}/minkebox/minke-helper`, 7 | MINKE_UPDATER: `${Config.REGISTRY_HOST}/minkebox/minke-updater`, 8 | 9 | _overrides: Config.REGISTRY_TAG_OVERRIDES || {}, 10 | 11 | withTag: function (name) { 12 | if (name.indexOf(':') !== -1) { 13 | return name; 14 | } 15 | else { 16 | return `${name}:${this._overrides[name] || Config.REGISTRY_DEFAULT_TAG}`; 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /app/Monitor.js: -------------------------------------------------------------------------------- 1 | const TIMEOUT = 2000; // 2 seconds 2 | 3 | async function runCmd(container, cmd) { 4 | const exec = await container.exec({ 5 | AttachStdin: false, 6 | AttachStdout: true, 7 | AttachStderr: false, 8 | Tty: false, 9 | Cmd: [ 'sh', '-c', cmd ] 10 | }); 11 | const stream = await exec.start(); 12 | let buffer = ''; 13 | docker.modem.demuxStream(stream.output, { 14 | write: (data) => { 15 | buffer += data.toString('utf8'); 16 | } 17 | }, null); 18 | return new Promise(resolve => { 19 | let timeout = setTimeout(() => { 20 | if (!timeout) { 21 | timeout = null; 22 | try { 23 | stream.output.destroy(); 24 | } 25 | catch (_) { 26 | } 27 | resolve(''); 28 | } 29 | }, TIMEOUT); 30 | stream.output.on('close', () => { 31 | if (!timeout) { 32 | clearTimeout(timeout); 33 | timeout = null; 34 | } 35 | resolve(buffer); 36 | }); 37 | }); 38 | } 39 | 40 | const Monitor = { 41 | 42 | create: function(args) { 43 | const container = args.target === 'helper' ? args.app._helperContainer : args.app._container; 44 | const cmd = args.cmd; 45 | return { 46 | init: args.init.replace(/{{ID}}/g, args.app._id), 47 | update: async () => { 48 | return await runCmd(container, cmd); 49 | } 50 | } 51 | } 52 | 53 | }; 54 | 55 | module.exports = Monitor; 56 | -------------------------------------------------------------------------------- /app/System.js: -------------------------------------------------------------------------------- 1 | const OS = require('os'); 2 | const FS = require('fs'); 3 | 4 | const MEMINFO = '/proc/meminfo'; 5 | const TICK = 10 * 1000; // 10 seconds 6 | const EVENT_NAME = 'system.stats'; 7 | 8 | const System = { 9 | 10 | _lastIdle: 0, 11 | _lastActive: 0, 12 | _cpuLoad: 0, 13 | _memoryUsed: 0, 14 | _timer: null, 15 | 16 | _updateMemory: function() { 17 | const lines = FS.readFileSync(MEMINFO, { encoding: 'utf8' }).split('\n'); 18 | let count = 0; 19 | let memtotal = 1; 20 | let memavailable = 1; 21 | for (let i = 0; i < lines.length && count < 2; i++) { 22 | const line = lines[i]; 23 | if (line.indexOf('MemTotal:') === 0) { 24 | memtotal = parseInt(line.substring(9)); 25 | count++; 26 | } 27 | else if (line.indexOf('MemAvailable:') === 0) { 28 | memavailable = parseInt(line.substring(13)); 29 | count++; 30 | } 31 | } 32 | if (count === 2) { 33 | this._memoryUsed = Math.floor(100 - memavailable / memtotal * 100); 34 | } 35 | }, 36 | 37 | _updateCPU: function() { 38 | const cpus = OS.cpus(); 39 | const idle = cpus.reduce((total, cpu) => total + cpu.times.idle, 0); 40 | const active = cpus.reduce((total, cpu) => total + cpu.times.user + cpu.times.sys + cpu.times.nice + cpu.times.irq, 0); 41 | const iDiff = idle - this._lastIdle; 42 | const aDiff = active - this._lastActive; 43 | if (aDiff + iDiff > 0) { 44 | this._cpuLoad = Math.floor(100 * aDiff / (aDiff + iDiff)); 45 | this._lastIdle = idle; 46 | this._lastActive = active; 47 | } 48 | }, 49 | 50 | start: function() { 51 | Root.on(`${EVENT_NAME}.start`, () => { 52 | const update = () => { 53 | this._updateMemory(); 54 | this._updateCPU(); 55 | Root.emit(EVENT_NAME, { cpuLoad: this._cpuLoad, memoryUsed: this._memoryUsed }); 56 | }; 57 | if (this._timer) { 58 | clearInterval(this._timer); 59 | } 60 | this._timer = setInterval(update, TICK); 61 | update(); 62 | }); 63 | Root.on(`${EVENT_NAME}.stop`, () => { 64 | clearInterval(this._timer); 65 | this._timer = null; 66 | }); 67 | } 68 | 69 | }; 70 | 71 | module.exports = System; 72 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/node 2 | 3 | global.DEBUG = !!process.env.DEBUG; 4 | global.SYSTEM = !!process.env.SYSTEM; 5 | 6 | const Koa = require('koa'); 7 | const Router = require('koa-router'); 8 | const Websockify = require('koa-websocket'); 9 | const CacheControl = require('koa-cache-control'); 10 | const Docker = require('dockerode'); 11 | const EventEmitter = require('events'); 12 | const Config = require('./Config'); 13 | const Events = require('./utils/Events'); 14 | 15 | // More listeners 16 | EventEmitter.defaultMaxListeners = 50; 17 | 18 | global.Root = new Events(); // System events 19 | 20 | const Pages = require('./pages/pages'); 21 | const MinkeApp = require('./MinkeApp'); 22 | const MinkeSetup = require('./MinkeSetup'); 23 | const UPNP = require('./UPNP'); 24 | 25 | const PORT = Config.WEB_PORT; 26 | 27 | const App = Websockify(new Koa()); 28 | global.docker = new Docker({socketPath: '/var/run/docker.sock'}); 29 | 30 | App.on('error', (err) => { 31 | console.error(err); 32 | }); 33 | 34 | App.use(async (ctx, next) => { 35 | const start = Date.now(); 36 | await next(); 37 | const ms = Date.now() - start; 38 | const domainname = MinkeApp.getLocalDomainName(); 39 | const domainsrc = domainname ? `http://*.${domainname} http://*.${domainname}:* https://*.${domainname}` : ''; 40 | ctx.set('Content-Security-Policy', 41 | "default-src 'self';" + 42 | "script-src 'self' 'unsafe-inline' 'unsafe-eval';" + 43 | "style-src 'self' 'unsafe-inline';" + 44 | "img-src 'self' data:;" + 45 | `frame-src ${domainsrc} http://${ctx.request.header.host}:* https://*.minkebox.net;` + 46 | `connect-src 'self' ws://${ctx.headers.host};` + 47 | "font-src 'none';" + 48 | "object-src 'none';" + 49 | "media-src 'none';" 50 | ); 51 | ctx.set('X-Response-Time', `${ms}ms`); 52 | }); 53 | 54 | App.use(CacheControl({ noCache: true })); 55 | 56 | const root = Router(); 57 | const wsroot = Router(); 58 | 59 | Pages.register(root, wsroot); 60 | UPNP.register(root, wsroot); 61 | 62 | App.use(root.middleware()); 63 | App.ws.use(wsroot.middleware()); 64 | App.ws.use(async (ctx, next) => { 65 | await next(ctx); 66 | if (ctx.websocket.listenerCount('message') === 0) { 67 | ctx.websocket.close(); 68 | } 69 | }); 70 | 71 | // MIGRATION - RESTART_REASON Remove May 26, 2020 72 | const restart = process.env.RESTART_REASON || MinkeSetup.restartReason('exit'); 73 | MinkeApp.startApps(App, { inherit: restart === 'restart' || restart === 'update', port: PORT }); 74 | 75 | process.on('uncaughtException', (e) => { 76 | console.error(e) 77 | }); 78 | process.on('SIGINT', async () => { 79 | await MinkeApp.getAppById('minke').systemRestart('halt'); 80 | }); 81 | process.on('SIGTERM', async () => { 82 | await MinkeApp.getAppById('minke').systemRestart('halt'); 83 | }); 84 | process.on('SIGUSR1', async() => { 85 | await MinkeApp.getAppById('minke').systemRestart('restart'); 86 | }); 87 | -------------------------------------------------------------------------------- /app/native/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "native", 5 | "cflags!": [ "-fno-exceptions" ], 6 | "cflags_cc!": [ "-fno-exceptions" ], 7 | "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ], 8 | "include_dirs": [ " 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | Napi::Number BindUDPIFace(const Napi::CallbackInfo& info) { 8 | int fd = socket(AF_INET, SOCK_DGRAM, 0); 9 | std::string iface = info[0].As().Utf8Value(); 10 | int rc = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.c_str(), iface.length()); 11 | if (rc < 0) { 12 | printf("bindtodevice %d %s error: %d\n", fd, iface.c_str(), errno); 13 | } 14 | std::string address = info[1].As().Utf8Value(); 15 | int port = info[2].As().Int32Value(); 16 | struct sockaddr_in addr; 17 | addr.sin_family = AF_INET; 18 | addr.sin_port = htons(port); 19 | rc = inet_pton(AF_INET, address.c_str(), &addr.sin_addr.s_addr); 20 | if (rc < 0) { 21 | printf("inet_pton %d %s %s %d error: %d\n", fd, iface.c_str(), address.c_str(), port, errno); 22 | } 23 | rc = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); 24 | if (rc < 0) { 25 | printf("bind %d %s %s %d error: %d\n", fd, iface.c_str(), address.c_str(), port, errno); 26 | } 27 | //printf("udp %d %s %s %d\n", fd, iface.c_str(), address.c_str(), port); 28 | return Napi::Number::New(info.Env(), fd); 29 | } 30 | 31 | Napi::Number BindTCPIFace(const Napi::CallbackInfo& info) { 32 | int fd = socket(AF_INET, SOCK_STREAM, 0); 33 | std::string iface = info[0].As().Utf8Value(); 34 | int rc = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.c_str(), iface.length()); 35 | if (rc < 0) { 36 | printf("bindtodevice %d %s error: %d\n", fd, iface.c_str(), errno); 37 | } 38 | std::string address = info[1].As().Utf8Value(); 39 | int port = info[2].As().Int32Value(); 40 | struct sockaddr_in addr; 41 | addr.sin_family = AF_INET; 42 | addr.sin_port = htons(port); 43 | rc = inet_pton(AF_INET, address.c_str(), &addr.sin_addr.s_addr); 44 | if (rc < 0) { 45 | printf("inet_pton %d %s %s %d error: %d\n", fd, iface.c_str(), address.c_str(), port, errno); 46 | } 47 | rc = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); 48 | if (rc < 0) { 49 | printf("bind %d %s %s %d error: %d\n", fd, iface.c_str(), address.c_str(), port, errno); 50 | } 51 | //printf("tcp %d %s %s %d\n", fd, iface.c_str(), address.c_str(), port); 52 | return Napi::Number::New(info.Env(), fd); 53 | } 54 | 55 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 56 | exports.Set(Napi::String::New(env, "BindUDPIFace"), Napi::Function::New(env, BindUDPIFace)); 57 | exports.Set(Napi::String::New(env, "BindTCPIFace"), Napi::Function::New(env, BindTCPIFace)); 58 | return exports; 59 | } 60 | 61 | NODE_API_MODULE(native, Init) 62 | -------------------------------------------------------------------------------- /app/native/native.js: -------------------------------------------------------------------------------- 1 | const DGram = require('dgram'); 2 | const Net = require('net'); 3 | const _native = require('./build/Release/native'); 4 | 5 | module.exports = { 6 | 7 | getTCPSocketOnInterface: function(interfaceName, address, port) { 8 | const fd = _native.BindTCPIFace(interfaceName, address, port); 9 | return new Net.Socket({ fd: fd }); 10 | }, 11 | 12 | getUDPSocketOnInterface: function(interfaceName, address, port) { 13 | const udp = DGram.createSocket('udp4'); 14 | const fd = _native.BindUDPIFace(interfaceName, address, port); 15 | setImmediate(() => { 16 | udp.bind({ fd: fd }); 17 | }); 18 | return udp; 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minkebox", 3 | "description": "MinkeBox", 4 | "license": "BSD-3-Clause", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://gitlab.com/minkebox/minke.git" 8 | }, 9 | "scripts": { 10 | "install": "cd native; node-gyp rebuild", 11 | "test": "./node_modules/mocha/bin/mocha" 12 | }, 13 | "dependencies": { 14 | "@sindresorhus/df": "^3.1.1", 15 | "ace-builds": "^1.4.12", 16 | "base64url": "^3.0.1", 17 | "chart.js": "^2.9.3", 18 | "debounce": "^1.2.0", 19 | "detect-rpi": "^1.4.0", 20 | "dns-packet": "^5.2.1", 21 | "dockerode": "^2.5.8", 22 | "fast-glob": "^2.2.7", 23 | "handlebars": "^4.7.6", 24 | "ip-address": "^6.3.0", 25 | "js-interpreter": "^2.2.0", 26 | "js-yaml": "^3.14.0", 27 | "koa": "^2.13.0", 28 | "koa-cache-control": "^2.0.0", 29 | "koa-proxy": "^1.0.0-alpha.3", 30 | "koa-router": "^7.4.0", 31 | "koa-websocket": "^5.0.1", 32 | "moment-timezone": "^0.5.31", 33 | "nedb": "^1.8.0", 34 | "netmask": "^1.0.6", 35 | "network": "^0.4.1", 36 | "node-addon-api": "^3.0.0", 37 | "node-fetch": "^2.6.0", 38 | "node-ssdp": "^4.0.0", 39 | "purecss": "^1.0.1", 40 | "sortablejs": "^1.10.2", 41 | "tar-stream": "^2.1.3", 42 | "uuid": "^3.4.0", 43 | "ws": "^6.2.1", 44 | "xml-js": "^1.6.11", 45 | "xterm": "^4.8.1", 46 | "xterm-addon-fit": "^0.4.0" 47 | }, 48 | "devDependencies": { 49 | "mocha": "^7.2.0", 50 | "mock-require": "^3.0.3", 51 | "sinon": "^9.0.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/pages/Console.js: -------------------------------------------------------------------------------- 1 | const FS = require('fs'); 2 | const Config = require('../Config'); 3 | const Handlebars = require('./HB'); 4 | const MinkeApp = require('../MinkeApp'); 5 | 6 | const consoleTemplate = Handlebars.compile(FS.readFileSync(`${__dirname}/html/Console.html`, { encoding: 'utf8' })); 7 | 8 | async function PageHTML(ctx) { 9 | const app = MinkeApp.getAppById(ctx.params.id); 10 | const tab = [{ name: 'Main', cid: '', selected: (ctx.query.c || 'm') === 'm' }]; 11 | if (app._networks.primary.name !== 'host') { 12 | tab.push({ name: 'Helper', cid: 'h', selected: (ctx.query.c === 'h' ) }); 13 | } 14 | app._secondary.forEach((_, idx) => tab.push({ name: `#${idx}`, cid: `${idx}`, selected: ctx.query.c == idx })); 15 | ctx.type = 'text/html'; 16 | ctx.body = consoleTemplate({ 17 | id: app._id, 18 | name: app._name, 19 | tab: tab, 20 | DarkMode: MinkeApp.getDarkMode() 21 | }); 22 | } 23 | 24 | async function PageWS(ctx) { 25 | const app = MinkeApp.getAppById(ctx.params.id); 26 | if (!app) { 27 | console.log(`Missing app ${ctx.params.id}`); 28 | return; 29 | } 30 | let container = null; 31 | switch (ctx.query.c || 'm') { 32 | case 'm': 33 | container = app._container; 34 | break; 35 | case 'h': 36 | container = app._helperContainer; 37 | break; 38 | default: 39 | if (app._secondaryContainers) { 40 | container = app._secondaryContainers[ctx.query.c]; 41 | } 42 | break; 43 | } 44 | if (!container) { 45 | console.log(`Missing container ${ctx.query.c || 'm'} for ${app._name}`); 46 | return; 47 | } 48 | const exec = await container.exec({ 49 | AttachStdin: true, 50 | AttachStdout: true, 51 | AttachStderr: true, 52 | Tty: true, 53 | Cmd: [ 'sh', '-c', 'test -x /bin/bash && exec /bin/bash; exec sh' ] 54 | }); 55 | const stream = await exec.start({ 56 | stdin: true 57 | }); 58 | 59 | ctx.websocket.on('message', msg => { 60 | try { 61 | msg = JSON.parse(msg); 62 | switch (msg.type) { 63 | case 'console.from': 64 | stream.output.write(msg.value); 65 | break; 66 | default: 67 | break; 68 | } 69 | } 70 | catch (e) { 71 | console.log(e); 72 | } 73 | }); 74 | ctx.websocket.on('error', () => { 75 | ctx.websocket.close(); 76 | stream.output.destroy(); 77 | }); 78 | ctx.websocket.on('close', () => { 79 | stream.output.destroy(); 80 | }); 81 | 82 | function write(data) { 83 | try { 84 | ctx.websocket.send(JSON.stringify({ type: 'console.to', data: data.toString('utf8') })); 85 | } 86 | catch (_) { 87 | stream.output.destroy(); 88 | } 89 | } 90 | docker.modem.demuxStream(stream.output, { write: write }, { write: write }); 91 | 92 | stream.output.on('end', () => { 93 | try { 94 | ctx.websocket.send(JSON.stringify({ type: 'console.close' })); 95 | } 96 | catch (_) { 97 | } 98 | ctx.websocket.close(); 99 | }); 100 | } 101 | 102 | module.exports = { 103 | HTML: PageHTML, 104 | WS: PageWS 105 | }; 106 | -------------------------------------------------------------------------------- /app/pages/HB.js: -------------------------------------------------------------------------------- 1 | const Handlebars = require('handlebars'); 2 | 3 | Handlebars.registerHelper({ 4 | eq: function (v1, v2) { 5 | return v1 == v2; 6 | }, 7 | ne: function (v1, v2) { 8 | return v1 != v2; 9 | }, 10 | lt: function (v1, v2) { 11 | return v1 < v2; 12 | }, 13 | gt: function (v1, v2) { 14 | return v1 > v2; 15 | }, 16 | lte: function (v1, v2) { 17 | return v1 <= v2; 18 | }, 19 | gte: function (v1, v2) { 20 | return v1 >= v2; 21 | }, 22 | and: function () { 23 | return Array.prototype.slice.call(arguments).every(Boolean); 24 | }, 25 | or: function () { 26 | return Array.prototype.slice.call(arguments, 0, -1).some(Boolean); 27 | }, 28 | nsp: function(v) { 29 | return v.replace(/ /g, ''); 30 | }, 31 | mod: function(v1, v2) { 32 | return v1 % v2; 33 | } 34 | }); 35 | 36 | Handlebars.registerHelper('index', (context) => { 37 | return 'index' in context.data ? context.data.index : context.data.root.index; 38 | }); 39 | 40 | module.exports = Handlebars; 41 | -------------------------------------------------------------------------------- /app/pages/Log.js: -------------------------------------------------------------------------------- 1 | const FS = require('fs'); 2 | const Config = require('../Config'); 3 | const Handlebars = require('./HB'); 4 | const MinkeApp = require('../MinkeApp'); 5 | const Filesystem = require('../Filesystem'); 6 | 7 | const logTemplate = Handlebars.compile(FS.readFileSync(`${__dirname}/html/Log.html`, { encoding: 'utf8' })); 8 | 9 | async function PageHTML(ctx) { 10 | const app = MinkeApp.getAppById(ctx.params.id); 11 | const tab = [{ name: 'Main', cid: '', selected: (ctx.query.c || 'm') === 'm' }]; 12 | if (app._networks.primary.name !== 'host') { 13 | tab.push({ name: 'Helper', cid: 'h', selected: (ctx.query.c === 'h' ) }); 14 | } 15 | app._secondary.forEach((_, idx) => tab.push({ name: `#${idx}`, cid: `${idx}`, selected: ctx.query.c == idx })); 16 | ctx.type = 'text/html'; 17 | ctx.body = logTemplate({ 18 | id: app._id, 19 | name: `Logs for ${app._name}`, 20 | tab: tab 21 | }); 22 | } 23 | 24 | async function PageWS(ctx) { 25 | const app = MinkeApp.getAppById(ctx.params.id); 26 | if (!app) { 27 | console.log(`Missing app ${ctx.params.id}`); 28 | return; 29 | } 30 | 31 | let logs = { destroy: function() {} }; 32 | 33 | ctx.websocket.on('error', () => { 34 | ctx.websocket.close(); 35 | logs.destroy(); 36 | }); 37 | ctx.websocket.on('close', () => { 38 | logs.destroy(); 39 | }); 40 | 41 | function write(prefix, data) { 42 | try { 43 | ctx.websocket.send(JSON.stringify({ type: 'console.to', data: `${prefix}${data.toString('utf8')}` })); 44 | } 45 | catch (_) { 46 | } 47 | } 48 | 49 | if (app.isRunning()) { 50 | let container = null; 51 | switch (ctx.query.c || 'm') { 52 | case 'm': 53 | container = app._container; 54 | break; 55 | case 'h': 56 | container = app._helperContainer; 57 | break; 58 | default: 59 | if (app._secondaryContainers) { 60 | container = app._secondaryContainers[ctx.query.c]; 61 | } 62 | break; 63 | } 64 | if (!container) { 65 | console.log(`Missing container ${ctx.query.c || 'm'} for ${app._name}`); 66 | return; 67 | } 68 | 69 | logs = await container.logs({ 70 | follow: true, 71 | stdout: true, 72 | stderr: true 73 | }); 74 | logs.on('close', () => { 75 | ctx.websocket.close(); 76 | }); 77 | docker.modem.demuxStream(logs, { write: data => write('\033[37m', data) }, { write: data => write('\033[33m', data) }); 78 | } 79 | else { 80 | const fs = Filesystem.create(app); 81 | let ext = ''; 82 | switch (ctx.query.c || 'm') { 83 | case 'm': 84 | break; 85 | case 'h': 86 | ext = '_helper'; 87 | break; 88 | default: 89 | ext = `_${ctx.query.c}`; 90 | break; 91 | } 92 | const logs = fs.getLogs(ext); 93 | write('\033[37m', '[STDOUT]\n'); 94 | write('\033[37m', logs.stdout); 95 | write('\033[33m', '\n[STDERR]\n'); 96 | write('\033[33m', logs.stderr); 97 | write('\033[31m', '\n[TERMINATED]\n'); 98 | } 99 | } 100 | 101 | module.exports = { 102 | HTML: PageHTML, 103 | WS: PageWS 104 | }; 105 | -------------------------------------------------------------------------------- /app/pages/css/colors-dark.css: -------------------------------------------------------------------------------- 1 | :root.darkmode-true, :root.darkmode-auto { 2 | --default-text-color: #e0e0e0; 3 | --alt-text-color: #999; 4 | 5 | --nav-text-color: white; 6 | --nav-background-color: #2e2e2e; 7 | 8 | --app-list-border-color: #505050; 9 | --app-list-background-color: #252525; 10 | --app-background-color: #252525; 11 | --app-border-color: #505050; 12 | --app-border-size: 6px; 13 | --app-background-hover-color: #3e3e3e; 14 | --app-background-hover-color2: rgba(255,255,255,0.07); 15 | 16 | --widget-page-background-color: #1e1e1e; 17 | --widget-background-color: #2e2e2e; 18 | --widget-border-size: 1px; 19 | --widget-background-hover-color: #3e3e3e; 20 | --widget-border-color: transparent; 21 | --widget-shadow-color: transparent; 22 | --primary-text-color: #e0e0e0; 23 | --secondary-text-color: #c0c0c0; 24 | 25 | --config-background-color: #1e1e1e; 26 | --config-text-color: #e0e0e0; 27 | --config-input-background-color: #404040; 28 | --config-placeholder-text-color: #a0a0a0; 29 | --config-input-disabled-background-color: #2e2e2e; 30 | --config-input-disabled-text-color: #606060; 31 | --config-input-shadow-color: transparent; 32 | --config-sidebar-background-color: #2e2e2e; 33 | --config-sidebar-text-color: #e0e0e0; 34 | 35 | --help-background-color: #202040; 36 | 37 | --newapp-icon-shadow-color: transparent; 38 | --newapp-icon-background-color: #101010; 39 | --newapp-icon-border-size: 3px; 40 | 41 | --graph-grid-color: #666666; 42 | 43 | --popbox-overlay-color: rgba(0,0,0,0.7); 44 | } 45 | -------------------------------------------------------------------------------- /app/pages/css/colors-light.css: -------------------------------------------------------------------------------- 1 | :root.darkmode-false, :root.darkmode-auto { 2 | --default-text-color: #333; 3 | --alt-text-color: #999; 4 | 5 | --nav-text-color: white; 6 | --nav-background-color: rgb(37, 42, 58); 7 | 8 | --app-list-border-color: #ddd; 9 | --app-list-background-color: #fff; 10 | --app-background-color: #fff; 11 | --app-border-color: #ddd; 12 | --app-border-size: 12px; 13 | --app-background-hover-color: #eee; 14 | --app-background-hover-color2: rgba(0,0,0,0.07); 15 | 16 | --widget-page-background-color: #f8f8f8; 17 | --widget-background-color: white; 18 | --widget-background-hover-color: #eee; 19 | --widget-border-color: #ccc; 20 | --widget-border-size: 2px; 21 | --widget-shadow-color: #bbb; 22 | --primary-text-color: black; 23 | --secondary-text-color: grey; 24 | 25 | --config-background-color: #f8f8f8; 26 | --config-text-color: #777; 27 | --config-input-background-color: white; 28 | --config-placeholder-text-color: #ccc; 29 | --config-input-disabled-background-color: #eaeded; 30 | --config-input-disabled-text-color: #cad2d3; 31 | --config-input-shadow-color: #ddd; 32 | --config-sidebar-background-color: rgb(61, 79, 93); 33 | --config-sidebar-text-color: #fff; 34 | 35 | --help-background-color: #dbeef5; 36 | 37 | --newapp-icon-shadow-color: #999; 38 | --newapp-icon-background-color: #fcfcfc; 39 | --newapp-icon-border-size: 5px; 40 | 41 | --graph-grid-color: #f2f2f2; 42 | 43 | --popbox-overlay-color: rgba(60, 52, 66, 0.7); 44 | } 45 | -------------------------------------------------------------------------------- /app/pages/css/console.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0px; 3 | height: 100%; 4 | } 5 | body { 6 | overflow: hidden; 7 | overscroll-behavior: contain; 8 | } 9 | html.darkmode-true body { 10 | background-color: black; 11 | } 12 | @media (prefers-color-scheme: dark) { 13 | html.darkmode-auto body { 14 | background-color: black; 15 | } 16 | } 17 | #tabs { 18 | position: fixed; 19 | top: 0px; 20 | width: 100%; 21 | background: rgb(61, 79, 93); 22 | color: #cccccc; 23 | padding: 10px 5px; 24 | margin-bottom: 5px; 25 | font-family: sans-serif; 26 | z-index: 1; 27 | } 28 | #tabs a { 29 | display: inline-block; 30 | padding: 0px 7px; 31 | cursor: pointer; 32 | text-decoration: none; 33 | color: inherit; 34 | } 35 | #tabs a:hover { 36 | color: rgb(61, 146, 201); 37 | } 38 | #tabs a.selected { 39 | color: #ffffff; 40 | cursor: default; 41 | } 42 | #log, #console { 43 | position: absolute; 44 | width: 100%; 45 | top: 0px; 46 | bottom: 0px; 47 | padding-top: 43px; 48 | } 49 | -------------------------------------------------------------------------------- /app/pages/html/Console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | {{#each tab}} 13 | {{name}} 14 | {{/each}} 15 |
16 |
17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/pages/html/Log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | {{#each tab}} 13 | {{name}} 14 | {{/each}} 15 |
16 |
17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/pages/html/ProxyFail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 22 | 23 | 24 |
25 |
Application is currently not available.
26 |
If the application was recently started, it may take a little while to become available.
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /app/pages/html/partials/App.html: -------------------------------------------------------------------------------- 1 |
2 | {{#if link}} 3 |
4 |
{{status}}
5 |

{{name}}

6 |
{{ip}}
7 | 8 | 9 |
10 | {{else}} 11 |
12 |
{{status}}
13 |

{{name}}

14 |
{{ip}}
15 |
16 | {{/if}} 17 |
18 | -------------------------------------------------------------------------------- /app/pages/html/partials/AppStatus.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /app/pages/html/partials/BackupAndRestore.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Backup 5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 |
Restore Configuration
15 |
16 | Are you sure you want to restore? EVERYTHING will be REMOVED! 17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | 44 | -------------------------------------------------------------------------------- /app/pages/html/partials/Disks.html: -------------------------------------------------------------------------------- 1 | {{#each disks}} 2 | 8 |
9 | {{#if (eq status "ready")}} 10 |
{{percentage}}% Used
11 | {{else if (eq status "formatting")}} 12 |
Formatting ...
13 | {{else}} 14 |
Unused
15 | {{/if}} 16 |
17 |
18 | {{/each}} 19 |
20 |
21 |
Format Disk
22 |
23 | Are you sure you want to format the disk? EVERYTHING on your disk will be REMOVED! 24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 | 46 | -------------------------------------------------------------------------------- /app/pages/html/partials/DownloadFile.html: -------------------------------------------------------------------------------- 1 | 2 | {{#if value}} 3 | Download 4 | 9 | {{else}} 10 | 11 | 16 | {{/if}} 17 | 18 | -------------------------------------------------------------------------------- /app/pages/html/partials/EditShares.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{#each shareables}} 11 | 12 | 13 | 14 | 15 | {{/each}} 16 | 17 |
Share name+
{{name}}{{#if empty}}{{else}}{{/if}}
18 | -------------------------------------------------------------------------------- /app/pages/html/partials/EditTable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{#each headers}} 5 | 6 | {{/each}} 7 | {{#if controls}}{{/if}} 8 | 9 | 10 | 11 | {{#each value}} 12 | 13 | {{#each this}} 14 | 27 | {{/each}} 28 | {{#if ../controls}}{{/if}} 29 | 30 | {{/each}} 31 | 32 |
{{name}}+
15 | 26 |
33 | -------------------------------------------------------------------------------- /app/pages/html/partials/Networks.html: -------------------------------------------------------------------------------- 1 | {{#each networks}} 2 | 5 | {{/each}} 6 | -------------------------------------------------------------------------------- /app/pages/html/partials/SelectBackups.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{#each backups}} 9 | 10 | 13 | 16 | 17 | {{/each}} 18 | 19 |
ApplicationBackup
11 | {{name}} 12 | 14 | 15 |
20 | -------------------------------------------------------------------------------- /app/pages/html/partials/SelectDirectory.html: -------------------------------------------------------------------------------- 1 | 2 | {{#each shareables}} 3 | {{#if shares}} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{#each shares}} 11 | 12 | 15 | 18 | 21 | 22 | {{/each}} 23 | 24 | {{/if}} 25 | {{/each}} 26 |
{{app._name}}DescriptionSelect
13 | {{name}} 14 | 16 | {{description}} 17 | 19 | 20 |
27 | -------------------------------------------------------------------------------- /app/pages/html/partials/SelectShares.html: -------------------------------------------------------------------------------- 1 | 2 | {{#each shareables}} 3 | {{#if shares}} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{#each shares}} 11 | 12 | 15 | 18 | 21 | 22 | {{/each}} 23 | 24 | {{/if}} 25 | {{/each}} 26 |
{{app._name}}DescriptionShare
13 | 14 | 16 | {{description}} 17 | 19 | 20 |
27 | -------------------------------------------------------------------------------- /app/pages/html/partials/SelectWebsites.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Name 5 | 6 | Port 7 | Published Name 8 | Publish 9 | 10 | 11 | 12 | 13 | 14 | {{#each websites}} 15 | 16 | {{appid}} 17 | {{name}} 18 | {{hostname}} 19 | {{port}} 20 | 21 | 22 | {{ip}} 23 | {{ip6}} 24 | 25 | {{/each}} 26 | 27 | -------------------------------------------------------------------------------- /app/pages/html/partials/ShowTable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{#each headers}} 5 | 6 | {{/each}} 7 | 8 | 9 | 10 | {{#each value}} 11 | 12 | {{#each this}} 13 | 16 | {{/each}} 17 | 18 | {{/each}} 19 | 20 |
{{name}}
14 | {{this}} 15 |
21 | -------------------------------------------------------------------------------- /app/pages/html/partials/Tags.html: -------------------------------------------------------------------------------- 1 | {{#each tags}} 2 | 5 | {{/each}} 6 | -------------------------------------------------------------------------------- /app/pages/img/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/android-icon-144x144.png -------------------------------------------------------------------------------- /app/pages/img/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/android-icon-192x192.png -------------------------------------------------------------------------------- /app/pages/img/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/android-icon-36x36.png -------------------------------------------------------------------------------- /app/pages/img/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/android-icon-48x48.png -------------------------------------------------------------------------------- /app/pages/img/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/android-icon-72x72.png -------------------------------------------------------------------------------- /app/pages/img/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/android-icon-96x96.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-114x114.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-120x120.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-144x144.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-152x152.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-180x180.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-57x57.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-60x60.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-72x72.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-76x76.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon-precomposed.png -------------------------------------------------------------------------------- /app/pages/img/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/apple-icon.png -------------------------------------------------------------------------------- /app/pages/img/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /app/pages/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/favicon-16x16.png -------------------------------------------------------------------------------- /app/pages/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/favicon-32x32.png -------------------------------------------------------------------------------- /app/pages/img/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/favicon-96x96.png -------------------------------------------------------------------------------- /app/pages/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/favicon.ico -------------------------------------------------------------------------------- /app/pages/img/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /app/pages/img/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/ms-icon-144x144.png -------------------------------------------------------------------------------- /app/pages/img/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/ms-icon-150x150.png -------------------------------------------------------------------------------- /app/pages/img/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/ms-icon-310x310.png -------------------------------------------------------------------------------- /app/pages/img/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/app/pages/img/ms-icon-70x70.png -------------------------------------------------------------------------------- /app/pages/pages.js: -------------------------------------------------------------------------------- 1 | const FS = require('fs'); 2 | const Path = require('path'); 3 | const Config = require('../Config'); 4 | 5 | const CACHE_MAXAGE = 24 * 60 * 60; // 24 hours 6 | const debug = (Config.CONFIG_NAME !== 'Production'); 7 | 8 | const Pages = { 9 | '/': require('./Main'), 10 | '/new/application': require('./Applications'), 11 | '/configure/:id': require('./Configure'), 12 | '/minkebox.config': require('../ConfigBackup'), 13 | '/console/:id': require('./Console'), 14 | '/log/:id': require('./Log') 15 | }; 16 | 17 | const JSPages = { 18 | '/js/ace.js': `${__dirname}/../node_modules/ace-builds/${debug ? 'src' : 'src-min'}/ace.js`, 19 | '/js/theme-twilight.js': `${__dirname}/../node_modules/ace-builds/${debug ? 'src' : 'src-min'}/theme-twilight.js`, 20 | '/js/chart.js': `${__dirname}/../node_modules/chart.js/dist/${debug ? 'Chart.js' : 'Chart.min.js'}`, 21 | '/js/sortable.js': `${__dirname}/../node_modules/sortablejs/dist/sortable.umd.js`, 22 | '/js/xterm.js': `${__dirname}/../node_modules/xterm/lib/xterm.js`, 23 | '/js/xterm.js.map': `${__dirname}/../node_modules/xterm/lib/xterm.js.map`, 24 | '/js/xterm-addon-fit.js': `${__dirname}/../node_modules/xterm-addon-fit/lib/xterm-addon-fit.js`, 25 | '/js/xterm-addon-fit.js.map': `${__dirname}/../node_modules/xterm-addon-fit/lib/xterm-addon-fit.js.map` 26 | }; 27 | 28 | function pages(root, wsroot) { 29 | 30 | for (let key in JSPages) { 31 | const body = FS.readFileSync(JSPages[key], { encoding: 'utf8' }); 32 | root.get(key, async (ctx) => { 33 | ctx.body = body; 34 | ctx.type = 'text/javascript'; 35 | ctx.cacheControl = { maxAge: CACHE_MAXAGE }; 36 | }); 37 | } 38 | 39 | root.get('/js/:script', async (ctx) => { 40 | const script = ctx.params.script; 41 | if (Path.dirname(script) === '.') { 42 | ctx.body = FS.readFileSync(`${__dirname}/script/${script}`, { encoding: 'utf8' }); 43 | } 44 | ctx.type = 'text/javascript'; 45 | if (!debug) { 46 | ctx.cacheControl = { maxAge: CACHE_MAXAGE }; 47 | } 48 | }); 49 | root.get('/css/pure.css', async (ctx) => { 50 | ctx.body = FS.readFileSync(`${__dirname}/../node_modules/purecss/build/pure-min.css`, { encoding: 'utf8' }); 51 | ctx.type = 'text/css'; 52 | ctx.cacheControl = { maxAge: CACHE_MAXAGE }; 53 | }); 54 | root.get('/css/xterm.css', async (ctx) => { 55 | ctx.body = FS.readFileSync(`${__dirname}/../node_modules/xterm/css/xterm.css`, { encoding: 'utf8' }); 56 | ctx.type = 'text/css'; 57 | ctx.cacheControl = { maxAge: CACHE_MAXAGE }; 58 | }); 59 | root.get('/css/:style', async (ctx) => { 60 | const style = ctx.params.style; 61 | if (Path.dirname(style) === '.') { 62 | ctx.body = FS.readFileSync(`${__dirname}/css/${style}`, { encoding: 'utf8' }); 63 | } 64 | ctx.type = 'text/css'; 65 | if (!debug) { 66 | ctx.cacheControl = { maxAge: CACHE_MAXAGE }; 67 | } 68 | }); 69 | root.get('/img/:img', async (ctx) => { 70 | const img = ctx.params.img; 71 | if (Path.dirname(img) === '.') { 72 | ctx.body = FS.readFileSync(`${__dirname}/img/${img}`); 73 | } 74 | ctx.type = 'image/png'; 75 | ctx.cacheControl = { maxAge: CACHE_MAXAGE }; 76 | }); 77 | 78 | for (let key in Pages) { 79 | if (Pages[key].HTML) { 80 | root.get(key, Pages[key].HTML); 81 | } 82 | if (Pages[key].WS) { 83 | wsroot.get(Path.normalize(`${key}/ws`), Pages[key].WS); 84 | } 85 | } 86 | } 87 | 88 | module.exports = { 89 | register: pages 90 | }; 91 | -------------------------------------------------------------------------------- /app/skeletons/builtin/apache.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Website`, 3 | description: `Simple, static, website based on the Apache web server`, 4 | image: `registry.minkebox.net/minkebox/apache`, 5 | uuid: `9C2BC9F1-A4BE-4B1B-A206-84A19479F1C3`, 6 | tags: [ 'Web' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Web pages` 11 | }, 12 | { 13 | type: `Text`, 14 | text: `Configure this website by selecting a folder containing your website pages. This can find the website logs in the "File Share" application.` 15 | }, 16 | { 17 | type: `SelectDirectory`, 18 | name: `/usr/local/apache2/htdocs`, 19 | description: `Website's pages` 20 | }, 21 | { 22 | type: `Header`, 23 | title: `Network` 24 | }, 25 | { 26 | type: `Text`, 27 | text: `Select which network this application will use. You probably want home unless this application is being used on a private network.` 28 | }, 29 | { 30 | type: `SelectNetwork`, 31 | name: `primary`, 32 | description: `Select network` 33 | } 34 | ], 35 | properties: [ 36 | { 37 | type: `Directory`, 38 | name: `/usr/local/apache2/htdocs`, 39 | style: `store` 40 | }, 41 | { 42 | type: `Directory`, 43 | name: `/usr/local/apache2/log`, 44 | style: `store`, 45 | shares: [ 46 | { 47 | name: `/`, 48 | description: `Access logs` 49 | } 50 | ], 51 | backup: true 52 | }, 53 | { 54 | type: `Port`, 55 | name: `80/tcp`, 56 | port: 80, 57 | protocol: `TCP`, 58 | web: { 59 | type: `newtab`, 60 | path: `` 61 | }, 62 | mdns: { 63 | type: `_http._tcp` 64 | } 65 | }, 66 | { 67 | type: `Network`, 68 | name: `primary`, 69 | value: `home` 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /app/skeletons/builtin/bitwarden.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Bitwarden`, 3 | description: `Bitwarden password manager`, 4 | image: `bitwardenrs/server`, 5 | uuid: `AFD02CE2-AE46-4B1F-8164-7A18986D2609`, 6 | tags: [ 7 | `Security`, 8 | `Web` 9 | ], 10 | actions: [ 11 | { 12 | type: `NavButton`, 13 | name: `Admin`, 14 | url: `http://{{__HOMEIP}}/admin` 15 | }, 16 | { 17 | type: `Text`, 18 | text: `Bitwarden is a self-hosted password manager with applications for all major operating system, mobiles and browsers.` 19 | }, 20 | { 21 | type: `Header`, 22 | title: `Administator` 23 | }, 24 | { 25 | type: `Text`, 26 | text: `Bitwarden has a single administator account which is protected by a long, random, unguessable admin token.` 27 | }, 28 | { 29 | type: `EditEnvironment`, 30 | description: `Admin token`, 31 | name: `ADMIN_TOKEN`, 32 | initValue: `{{__RANDOMHEX(64)}}` 33 | } 34 | ], 35 | properties: [ 36 | { 37 | type: `Directory`, 38 | style: `store`, 39 | name: `/data`, 40 | backup: true 41 | }, 42 | { 43 | type: `Environment`, 44 | name: `ROCKET_ENV`, 45 | value: `staging` 46 | }, 47 | { 48 | type: `Environment`, 49 | name: `ROCKET_PORT`, 50 | value: `80` 51 | }, 52 | { 53 | type: `Environment`, 54 | name: `ADMIN_TOKEN` 55 | }, 56 | { 57 | type: `Environment`, 58 | name: `ROCKET_WORKERS`, 59 | value: `10` 60 | }, 61 | { 62 | type: `Port`, 63 | name: `3012/tcp`, 64 | port: 3012, 65 | protocol: `TCP` 66 | }, 67 | { 68 | type: `Port`, 69 | name: `80/tcp`, 70 | port: 80, 71 | protocol: `TCP`, 72 | web: { 73 | tab: `inline`, 74 | path: `/` 75 | } 76 | }, 77 | { 78 | type: `Network`, 79 | name: `primary`, 80 | value: `home` 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /app/skeletons/builtin/dnscrypt.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `DNSCrypt`, 3 | description: `Encrypt and authenticate DNS traffic`, 4 | image: `registry.minkebox.net/minkebox/dnscrypt`, 5 | uuid: `189E9948-07D3-40B3-87B2-B0F53E8F2DCF`, 6 | tags: [ 7 | `Dns`, 8 | `Security`, 9 | `Networking` 10 | ], 11 | actions: [ 12 | { 13 | type: `Text`, 14 | text: `DNSCrypt is a protocol that authenticates communications between a MinkeBox and a global DNS resolver. It prevents DNS spoofing. It uses cryptographic signatures to verify that responses originate from the chosen DNS resolver and haven’t been tampered with.` 15 | }, 16 | { 17 | type: `Header`, 18 | title: `Configure` 19 | }, 20 | { 21 | type: `Text`, 22 | text: `Provide a primary and optional secondary secure DNS server.

23 | Servers can be specified using either sdns:// URLs or https:// URLs.

24 | https:// is used for DNS-over-HTTPS. A hostname in these URLs can contain only a single period (.) otherwise an IP address should be used.` 25 | }, 26 | { 27 | type: `EditEnvironment`, 28 | name: `SERVER1`, 29 | description: `Primary URL`, 30 | validate: `^(sdns://[a-zA-Z0-0+/]+|https://([a-zA-Z][a-zA-Z0-9]*.[a-zA-Z][a-zA-z0-9]*|[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)(:[0-9]+|)/.*)$`, 31 | initValue: `https://cloudflare-dns.com/dns-query` 32 | }, 33 | { 34 | type: `EditEnvironment`, 35 | name: `SERVER2`, 36 | description: `Secondary URL`, 37 | validate: `^(sdns://[a-zA-Z0-0+/]+|https://([a-zA-Z][a-zA-Z0-9]*.[a-zA-Z][a-zA-z0-9]*|[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)(:[0-9]+|)/.*)$` 38 | } 39 | ], 40 | properties: [ 41 | { 42 | type: `Environment`, 43 | name: `IP6`, 44 | value: `{{!!__HOMEIP6}}` 45 | }, 46 | { 47 | type: `Environment`, 48 | name: `SERVER1` 49 | }, 50 | { 51 | type: `Environment`, 52 | name: `SERVER2` 53 | }, 54 | { 55 | type: `Port`, 56 | name: `53/tcp`, 57 | port: 53, 58 | protocol: `TCP` 59 | }, 60 | { 61 | type: `Port`, 62 | name: `53/udp`, 63 | port: 53, 64 | protocol: `UDP`, 65 | dns: true 66 | }, 67 | { 68 | type: `Network`, 69 | name: `primary`, 70 | value: `home` 71 | }, 72 | { 73 | type: `Network`, 74 | name: `secondary`, 75 | value: `dns` 76 | } 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /app/skeletons/builtin/dnshosts.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Hostnames`, 3 | description: `Create simple mappings between fully qualified hostnames and IP addresses`, 4 | image: `registry.minkebox.net/minkebox/dnshosts`, 5 | uuid: `4ACEAEFE-78DD-447D-8209-0659A54EF7B1`, 6 | tags: [ 'Networking', `Dns` ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Hostnames` 11 | }, 12 | { 13 | type: `Text`, 14 | text: `Enter fully qualified hostnames (e.g. minkebox.com) and associated IP addresses.` 15 | }, 16 | { 17 | type: `EditFileAsTable`, 18 | name: `/etc/dnshosts.d/hosts.conf`, 19 | description: `Hostnames`, 20 | headers: [ 21 | { 22 | name: `Hostname` 23 | }, 24 | { 25 | name: `IP Address` 26 | } 27 | ], 28 | pattern: `{{V[1]}} {{V[0]}} 29 | `, 30 | join: `` 31 | } 32 | ], 33 | properties: [ 34 | { 35 | type: `File`, 36 | name: `/etc/dnshosts.d/hosts.conf` 37 | }, 38 | { 39 | type: `Port`, 40 | name: `53/tcp`, 41 | port: 53, 42 | protocol: `TCP` 43 | }, 44 | { 45 | type: `Port`, 46 | name: `53/udp`, 47 | port: 53, 48 | protocol: `UDP`, 49 | dns: true 50 | }, 51 | { 52 | type: `Network`, 53 | name: `primary`, 54 | value: `home` 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /app/skeletons/builtin/dohblock.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `DoH Block`, 3 | description: `Block Firefox from bypassing your DNS`, 4 | image: `registry.minkebox.net/minkebox/dohblock`, 5 | uuid: `BE5ABE19-AD1A-48CE-A89B-259891A253BB`, 6 | tags: [ 'Networking', `Dns` ], 7 | actions: [ 8 | ], 9 | properties: [ 10 | { 11 | type: `Port`, 12 | name: `53/tcp`, 13 | port: 53, 14 | protocol: 'TCP' 15 | }, 16 | { 17 | type: `Port`, 18 | name: `53/udp`, 19 | port: 53, 20 | protocol: 'UDP', 21 | dns: true 22 | }, 23 | { 24 | type: `Network`, 25 | name: `primary`, 26 | value: `home` 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /app/skeletons/builtin/domainproxy.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Domain Proxy`, 3 | description: `Send web requests for specific domains via a proxy`, 4 | image: `registry.minkebox.net/minkebox/domainproxy`, 5 | uuid: `EDBA640E-0E00-4CC3-AE04-5B5247A631A3`, 6 | tags: [ 'Proxy', `Dns` ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Networking` 11 | }, 12 | { 13 | type: 'Text', 14 | text: 'Traffic targetting the domains listed below, will be routed to the proxy IP address.' 15 | }, 16 | { 17 | type: `EditEnvironment`, 18 | name: `PROXYIP`, 19 | description: `Proxy IP Address`, 20 | placeholder: '0.0.0.0' 21 | }, 22 | { 23 | type: `Text`, 24 | text: `Select source network for traffic. You probably want home unless this application is being used on a private network.` 25 | }, 26 | { 27 | type: `SelectNetwork`, 28 | name: `secondary`, 29 | description: `Select source network` 30 | }, 31 | { 32 | type: `Header`, 33 | title: `Proxied Domains` 34 | }, 35 | { 36 | type: `EditFileAsTable`, 37 | name: `/etc/dnsmasq.d/proxies.preconf`, 38 | description: `Add the domains (which will automatically include any sub-domains) here. All traffic to these domains will be proxied via the proxy address.`, 39 | headers: [ 40 | { name: `Domain name` } 41 | ], 42 | pattern: `{{V[0]}}` 43 | } 44 | ], 45 | properties: [ 46 | { 47 | type: `File`, 48 | style: 'boot', 49 | name: `/etc/dnsmasq.d/proxies.preconf` 50 | }, 51 | { 52 | type: `Environment`, 53 | name: `PROXYIP` 54 | }, 55 | { 56 | type: `Port`, 57 | name: `53/tcp`, 58 | port: 53, 59 | protocol: 'TCP' 60 | }, 61 | { 62 | type: `Port`, 63 | name: `53/udp`, 64 | port: 53, 65 | protocol: 'UDP', 66 | dns: true 67 | }, 68 | { 69 | type: `Network`, 70 | name: `primary`, 71 | value: `none` 72 | }, 73 | { 74 | type: `Network`, 75 | name: `secondary`, 76 | value: `home` 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /app/skeletons/builtin/duplicati.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Backups`, 3 | description: `Multi-protocol, offsite backups using Duplicati`, 4 | image: `linuxserver/duplicati`, 5 | uuid: `1D93B03F-819A-475C-8A96-5CCCBFA58019`, 6 | tags: [ 'Backups' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Backups` 11 | }, 12 | { 13 | type: `Text`, 14 | text: `Select which folders you want to backup. The backup schedules are created in Duplicati itself.` 15 | }, 16 | { 17 | type: `SelectShares`, 18 | name: `/source`, 19 | description: `Select folders` 20 | }, 21 | { 22 | type: `Text`, 23 | text: `Select which applications you want to backup.` 24 | }, 25 | { 26 | type: `SelectBackups`, 27 | name: `/applications`, 28 | description: `Select folders` 29 | } 30 | ], 31 | properties: [ 32 | { 33 | type: `Directory`, 34 | name: `/backups`, 35 | style: `store` 36 | }, 37 | { 38 | type: `Directory`, 39 | name: `/config`, 40 | style: `boot` 41 | }, 42 | { 43 | type: `Directory`, 44 | name: `/source`, 45 | style: `temp` 46 | }, 47 | { 48 | type: `Directory`, 49 | name: `/applications`, 50 | style: `temp` 51 | }, 52 | { 53 | type: `Port`, 54 | name: `8200/tcp`, 55 | port: 8200, 56 | protocol: `TCP`, 57 | web: { 58 | type: `newtab`, 59 | path: `` 60 | }, 61 | mdns: { 62 | type: `_http._tcp` 63 | } 64 | }, 65 | { 66 | type: `Network`, 67 | name: `primary`, 68 | value: `home` 69 | } 70 | ], 71 | monitor: { 72 | cmd: `curl -s -c /tmp/cj http://localhost:8200 -q -o /dev/null; echo '[['; curl http://localhost:8200/api/v1/serverstate?X-XSRF-Token=$(tail -n1 /tmp/cj | cut -f7); echo '],['; curl http://localhost:8200/api/v1/progressstate?X-XSRF-Token=$(tail -n1 /tmp/cj | cut -f7); echo '],['; curl http://localhost:8200/api/v1/backups?X-XSRF-Token=$(tail -n1 /tmp/cj | cut -f7); echo ']]'`, 73 | polling: 60, 74 | parser: ` 75 | const j = JSON.parse(input.replace(//g, '')); 76 | output.status = j[0][0].SuggestedStatusIcon == 'Active' ? 'Running' : 'Idle'; 77 | output.nrbackups = j[2][0].length; 78 | output.scheduled = j[0][0].ProposedSchedule.length; 79 | output.next = output.scheduled ? j[2][0].find(b => b.Backup.ID == j[0][0].ProposedSchedule[0].Item1).Backup.Name : null; 80 | output.active = j[0][0].ActiveTask ? j[2][0].find(b => b.Backup.ID == j[0][0].ActiveTask.Item2).Backup.Name : null; 81 | output.last = output.active || j[1][0].Error ? null : j[2][0].find(b => b.Backup.ID == j[1][0].BackupID).Backup.Name; 82 | output.current = output.status == 'Idle' ? 0 : j[1][0].ProcessedFileSize; 83 | output.total = output.status == 'Idle' ? 0 : j[1][0].TotalFileSize; 84 | `, 85 | header: ` 86 | 91 | `, 92 | template: ` 93 |

94 |
{{status}}
Current Status
95 |
{{nrbackups}}
Backups
96 |
{{scheduled}}
Scheduled
97 | {{#if active}} 98 |
{{active}}
Active
99 | {{else if next}} 100 |
{{next}}
Next
101 | {{else if last}} 102 |
{{last}}
Last
103 | {{/if}} 104 |
105 | ` 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/skeletons/builtin/filebot.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Filebot`, 3 | description: `The ultimate tool for organizing and renaming your movies, tv shows or anime, and music well as downloading subtitles and artwork`, 4 | image: `coppit/filebot`, 5 | uuid: `1EBAD7F2-5C5B-4732-86DE-ACA05ADD91F1`, 6 | tags: [ 7 | `Media`, 8 | `Storage` 9 | ], 10 | actions: [ 11 | { 12 | type: `Header`, 13 | title: `Storage` 14 | }, 15 | { 16 | type: `SelectDirectory`, 17 | name: `/media`, 18 | description: `Select storage to organize` 19 | } 20 | ], 21 | properties: [ 22 | { 23 | type: `Feature`, 24 | name: `localtime` 25 | }, 26 | { 27 | type: `Directory`, 28 | name: `/config`, 29 | style: `boot` 30 | }, 31 | { 32 | type: `Directory`, 33 | name: `/media`, 34 | style: `store` 35 | }, 36 | { 37 | type: `File`, 38 | name: `/config/filebot.conf`, 39 | value: `SETTLE_DURATION=10 40 | MAX_WAIT_TIME=01:00 41 | MIN_PERIOD=05:00 42 | DEBUG=0 43 | OPENSUBTITLES_USER="" 44 | OPENSUBTITLES_PASSWORD="" 45 | SUBTITLE_LANG="" 46 | ALLOW_REPROCESSING=yes 47 | RUN_UI=yes 48 | ` 49 | }, 50 | { 51 | type: `Port`, 52 | name: `8080/tcp`, 53 | port: 8080, 54 | protocol: `TCP`, 55 | web: { 56 | tab: `newtab`, 57 | path: `/` 58 | } 59 | }, 60 | { 61 | type: `Network`, 62 | name: `primary`, 63 | value: `home` 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /app/skeletons/builtin/folders.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Files and Folders`, 3 | description: `Create folders which can be shared with other applications`, 4 | image: `registry.minkebox.net/minkebox/folders`, 5 | uuid: `FE8D1F85-0F18-4FFB-BA7F-FD91D2354CFE`, 6 | tags: [ `Storage` ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Files and Folders` 11 | }, 12 | { 13 | type: `EditShares`, 14 | name: `/folders`, 15 | description: `Create a list of shares to store file and folders. These can be share with other apps.` 16 | }, 17 | { 18 | type: `Text` 19 | }, 20 | { 21 | type: `Text`, 22 | text: `You can rename a share by editing its name, but you cannot remove it unless it is empty. All shares and their data are erased if you remove this application.` 23 | } 24 | ], 25 | properties: [ 26 | { 27 | type: `Feature`, 28 | name: `localtime` 29 | }, 30 | { 31 | type: `Directory`, 32 | name: `/folders`, 33 | style: `store`, 34 | backup: true 35 | }, 36 | { 37 | type: `Port`, 38 | name: `80/tcp`, 39 | port: 80, 40 | protocol: 'TCP', 41 | web: { 42 | tab: `inline`, 43 | widget: `inline`, 44 | path: ``, 45 | private: true 46 | } 47 | }, 48 | { 49 | type: `Network`, 50 | name: `primary`, 51 | value: `home` 52 | } 53 | ], 54 | monitor: { 55 | cmd: `cd /folders;du -d0 *`, 56 | init: ` 57 |
58 | 59 |
60 | 107 | ` 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/skeletons/builtin/ghost.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Blog`, 3 | description: `Blogging using the Ghost publishing platform`, 4 | images: { 5 | x64: `ghost:alpine` 6 | }, 7 | uuid: `F301B560-3262-4B4D-B09D-75CBB86B3BB1`, 8 | tags: [ 9 | `Blog`, 10 | `Web` 11 | ], 12 | actions: [ 13 | { 14 | type: `Header`, 15 | title: `Configuration` 16 | }, 17 | { 18 | type: `Environment`, 19 | name: `url`, 20 | description: `Base URL of this blog` 21 | } 22 | ], 23 | properties: [ 24 | { 25 | type: `Environment`, 26 | name: `url` 27 | }, 28 | { 29 | type: `Directory`, 30 | name: `/var/lib/ghost/content`, 31 | style: `store`, 32 | shares: [ 33 | { 34 | name: `/`, 35 | description: `Blog` 36 | } 37 | ] 38 | }, 39 | { 40 | type: `Port`, 41 | name: `2368/tcp`, 42 | port: 2368, 43 | protocol: `TCP`, 44 | web: { 45 | type: `newtab`, 46 | url: `{{url}}` 47 | } 48 | }, 49 | { 50 | type: `Network`, 51 | name: `primary`, 52 | value: `home` 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `HomeBridge`, 3 | description: `Add HomeKit devices using HomeBridge`, 4 | image: `registry.minkebox.net/minkebox/homebridge`, 5 | uuid: `DB46A1EB-63B6-4BAC-94B3-B897807D3312`, 6 | tags: [ 7 | `HomeKit` 8 | ], 9 | actions: [ 10 | { 11 | type: `Header`, 12 | title: `Configure HomeKit` 13 | }, 14 | { 15 | type: `EditEnvironment`, 16 | description: `Pin used to add this device to your network using the Home app`, 17 | name: `BRIDGE_PIN`, 18 | initValue: `111-11-111`, 19 | id: 'pin' 20 | }, 21 | { 22 | type: `EditEnvironment`, 23 | name: `BRIDGE_SETUPID`, 24 | initValue: `{{__RANDOMHEX(4)}}`, 25 | id: 'setupid', 26 | visible: false 27 | }, 28 | { 29 | type: `Header`, 30 | title: `Configure HomeBridge` 31 | }, 32 | { 33 | type: `EditEnvironment`, 34 | name: `PACKAGES`, 35 | description: `Plugins` 36 | }, 37 | { 38 | type: `EditFile`, 39 | name: `/app/accessories-config.json`, 40 | description: `Accessories`, 41 | initValue: `[ 42 | ]` 43 | }, 44 | { 45 | type: `EditFile`, 46 | name: `/app/platforms-config.json`, 47 | description: `Platforms`, 48 | initValue: `[ 49 | ]` 50 | }, 51 | { 52 | type: `EditEnvironmentAsCheckbox`, 53 | name: `INSECURE`, 54 | description: `Support insecure operations` 55 | }, 56 | { 57 | type: `Header`, 58 | title: `Homekit Code` 59 | }, 60 | { 61 | type: `Text`, 62 | text: ``, 63 | id: `qrcode` 64 | }, 65 | { 66 | type: `Script`, 67 | include: `qrcode` 68 | }, 69 | { 70 | type: `Script`, 71 | script: ` 72 | const rqr = document.querySelector('#qrcode'); 73 | const rpin = document.querySelector('#pin .value'); 74 | const rsetupid = document.querySelector('#setupid .value'); 75 | function uri(pin, setupid) { 76 | const CATEGORY_BRIDGE = 2; 77 | const SUPPORTS_IP = 1 << 28; 78 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 79 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 80 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 81 | return 'X-HM://' + payload + setupid; 82 | } 83 | function qr() { 84 | const content = uri(rpin.value, rsetupid.value); 85 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 86 | } 87 | rpin.addEventListener('input', qr); 88 | qr(); 89 | ` 90 | } 91 | ], 92 | properties: [ 93 | { 94 | type: `Port`, 95 | name: `51826/tcp`, 96 | port: 51826, 97 | protocol: `TCP` 98 | }, 99 | { 100 | type: `Environment`, 101 | name: `BRIDGE_USERNAME`, 102 | value: `{{__MACADDRESS}}` 103 | }, 104 | { 105 | type: `Environment`, 106 | name: `BRIDGE_PIN` 107 | }, 108 | { 109 | type: `Environment`, 110 | name: `BRIDGE_SETUPID` 111 | }, 112 | { 113 | type: `Environment`, 114 | name: `PACKAGES` 115 | }, 116 | { 117 | type: `Environment`, 118 | name: `INSECURE` 119 | }, 120 | { 121 | type: `File`, 122 | name: `/app/accessories-config.json`, 123 | style: `boot` 124 | }, 125 | { 126 | type: `File`, 127 | name: `/app/platforms-config.json`, 128 | style: `boot` 129 | }, 130 | { 131 | type: `Directory`, 132 | name: `/app/homebridge`, 133 | style: `boot` 134 | }, 135 | { 136 | type: `Network`, 137 | name: `primary`, 138 | value: `home` 139 | }, 140 | ] 141 | } 142 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_broadlink.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Broadlink`, 3 | description: `Control IR and RF devices in HomeKit using a Broadlink Univeral Remote`, 4 | image: `registry.minkebox.net/minkebox/homebridge_broadlink`, 5 | uuid: `82F67F61-4F02-43CC-8FF4-55BE45A83D72`, 6 | tags: [ 'HomeKit' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure HomeKit` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | description: `Pin used to add this device to your network using the Home app`, 15 | name: `BRIDGE_PIN`, 16 | initValue: `111-11-111`, 17 | id: 'pin' 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `BRIDGE_SETUPID`, 22 | initValue: `{{__RANDOMHEX(4)}}`, 23 | id: 'setupid', 24 | visible: false 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Configure Broadlink` 29 | }, 30 | { 31 | type: `Text`, 32 | text: `Configure your accessories below (see here for more information).` 33 | }, 34 | { 35 | type: `EditFile`, 36 | name: `/app/accessories-config.json`, 37 | description: `Accessories`, 38 | initValue: `[ 39 | ]` 40 | }, 41 | { 42 | type: `Header`, 43 | title: `Homekit Code` 44 | }, 45 | { 46 | type: `Text`, 47 | text: ``, 48 | id: `qrcode` 49 | }, 50 | { 51 | type: `Script`, 52 | include: `qrcode` 53 | }, 54 | { 55 | type: `Script`, 56 | script: ` 57 | const rqr = document.querySelector('#qrcode'); 58 | const rpin = document.querySelector('#pin .value'); 59 | const rsetupid = document.querySelector('#setupid .value'); 60 | function uri(pin, setupid) { 61 | const CATEGORY_BRIDGE = 2; 62 | const SUPPORTS_IP = 1 << 28; 63 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 64 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 65 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 66 | return 'X-HM://' + payload + setupid; 67 | } 68 | function qr() { 69 | const content = uri(rpin.value, rsetupid.value); 70 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 71 | } 72 | rpin.addEventListener('input', qr); 73 | qr(); 74 | ` 75 | } 76 | ], 77 | properties: [ 78 | { 79 | type: `Feature`, 80 | name: `localtime` 81 | }, 82 | { 83 | type: `Port`, 84 | name: `51826/tcp`, 85 | port: 51826, 86 | protocol: 'TCP' 87 | }, 88 | { 89 | type: `Network`, 90 | name: `primary`, 91 | value: `home` 92 | }, 93 | { 94 | type: `Environment`, 95 | name: `BRIDGE_USERNAME`, 96 | value: `{{__MACADDRESS}}` 97 | }, 98 | { 99 | type: `Environment`, 100 | name: `BRIDGE_PIN` 101 | }, 102 | { 103 | type: `Environment`, 104 | name: `BRIDGE_SETUPID` 105 | }, 106 | { 107 | type: `Directory`, 108 | name: `/app/homebridge`, 109 | style: 'store' 110 | }, 111 | { 112 | type: `File`, 113 | name: `/app/accessories-config.json`, 114 | style: `boot` 115 | } 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_neato.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Neato`, 3 | description: `Add Neato robotic vacuum cleaner to your HomeKit network using HomeBridge`, 4 | image: `registry.minkebox.net/minkebox/homebridge_neato`, 5 | uuid: `0096B537-96D7-4C81-8F11-60E392567756`, 6 | tags: [ 'HomeKit' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure HomeKit` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | description: `Pin used to add this device to your network using the Home app`, 15 | name: `BRIDGE_PIN`, 16 | initValue: `111-11-111`, 17 | id: 'pin' 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `BRIDGE_SETUPID`, 22 | initValue: `{{__RANDOMHEX(4)}}`, 23 | id: 'setupid', 24 | visible: false 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Configure Neato` 29 | }, 30 | { 31 | type: `EditEnvironment`, 32 | description: `Your Neato email`, 33 | name: `USERNAME` 34 | }, 35 | { 36 | type: `EditEnvironment`, 37 | description: `Your Neato password`, 38 | name: `PASSWORD` 39 | }, 40 | { 41 | type: `Header`, 42 | title: `Homekit Code` 43 | }, 44 | { 45 | type: `Text`, 46 | text: ``, 47 | id: `qrcode` 48 | }, 49 | { 50 | type: `Script`, 51 | include: `qrcode` 52 | }, 53 | { 54 | type: `Script`, 55 | script: ` 56 | const rqr = document.querySelector('#qrcode'); 57 | const rpin = document.querySelector('#pin .value'); 58 | const rsetupid = document.querySelector('#setupid .value'); 59 | function uri(pin, setupid) { 60 | const CATEGORY_BRIDGE = 2; 61 | const SUPPORTS_IP = 1 << 28; 62 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 63 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 64 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 65 | return 'X-HM://' + payload + setupid; 66 | } 67 | function qr() { 68 | const content = uri(rpin.value, rsetupid.value); 69 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 70 | } 71 | rpin.addEventListener('input', qr); 72 | qr(); 73 | ` 74 | } 75 | ], 76 | properties: [ 77 | { 78 | type: `Port`, 79 | name: `51826/tcp`, 80 | port: 51826, 81 | protocol: 'TCP' 82 | }, 83 | { 84 | type: `Network`, 85 | name: `primary`, 86 | value: `home` 87 | }, 88 | { 89 | type: `Environment`, 90 | name: `BRIDGE_USERNAME`, 91 | value: `{{__MACADDRESS}}` 92 | }, 93 | { 94 | type: `Environment`, 95 | name: `BRIDGE_PIN` 96 | }, 97 | { 98 | type: `Environment`, 99 | name: `BRIDGE_SETUPID` 100 | }, 101 | { 102 | type: `Environment`, 103 | name: `USERNAME` 104 | }, 105 | { 106 | type: `Environment`, 107 | name: `PASSWORD` 108 | }, 109 | { 110 | type: `Directory`, 111 | name: `/app/homebridge`, 112 | style: 'store' 113 | } 114 | ] 115 | } 116 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_nest.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Nest`, 3 | description: `Add Google Nest devices to your HomeKit network using HomeBridge`, 4 | image: `registry.minkebox.net/minkebox/homebridge_nest`, 5 | uuid: `2B7C89E3-F3F3-4A31-8503-ADFDDEEF34FE`, 6 | tags: [ 'HomeKit' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure HomeKit` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | description: `Pin used to add this device to your network using the Home app`, 15 | name: `BRIDGE_PIN`, 16 | initValue: `111-11-111`, 17 | id: 'pin' 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `BRIDGE_SETUPID`, 22 | initValue: `{{__RANDOMHEX(4)}}`, 23 | id: 'setupid', 24 | visible: false 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Configure Nest` 29 | }, 30 | { 31 | type: `Text`, 32 | text: `To bridge your Nest devices onto the HomeKit network, you need to provides various pieces of security information. Instructions for gettings these can be found at here.` 33 | }, 34 | { 35 | type: `EditEnvironment`, 36 | description: `Your issueToken`, 37 | name: `ISSUE_TOKEN` 38 | }, 39 | { 40 | type: `EditEnvironment`, 41 | description: `Your cookies`, 42 | name: `COOKIES` 43 | }, 44 | { 45 | type: `EditEnvironment`, 46 | description: `Your apiKey`, 47 | name: `API_KEY` 48 | }, 49 | { 50 | type: `Header`, 51 | title: `Homekit Code` 52 | }, 53 | { 54 | type: `Text`, 55 | text: ``, 56 | id: `qrcode` 57 | }, 58 | { 59 | type: `Script`, 60 | include: `qrcode` 61 | }, 62 | { 63 | type: `Script`, 64 | script: ` 65 | const rqr = document.querySelector('#qrcode'); 66 | const rpin = document.querySelector('#pin .value'); 67 | const rsetupid = document.querySelector('#setupid .value'); 68 | function uri(pin, setupid) { 69 | const CATEGORY_BRIDGE = 2; 70 | const SUPPORTS_IP = 1 << 28; 71 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 72 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 73 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 74 | return 'X-HM://' + payload + setupid; 75 | } 76 | function qr() { 77 | const content = uri(rpin.value, rsetupid.value); 78 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 79 | } 80 | rpin.addEventListener('input', qr); 81 | qr(); 82 | ` 83 | } 84 | ], 85 | properties: [ 86 | { 87 | type: `Port`, 88 | name: `51826/tcp`, 89 | port: 51826, 90 | protocol: 'TCP' 91 | }, 92 | { 93 | type: `Network`, 94 | name: `primary`, 95 | value: `home` 96 | }, 97 | { 98 | type: `Environment`, 99 | name: `BRIDGE_USERNAME`, 100 | value: `{{__MACADDRESS}}` 101 | }, 102 | { 103 | type: `Environment`, 104 | name: `BRIDGE_PIN` 105 | }, 106 | { 107 | type: `Environment`, 108 | name: `BRIDGE_SETUPID` 109 | }, 110 | { 111 | type: `Environment`, 112 | name: `ISSUE_TOKEN` 113 | }, 114 | { 115 | type: `Environment`, 116 | name: `COOKIES` 117 | }, 118 | { 119 | type: `Environment`, 120 | name: `API_KEY` 121 | }, 122 | { 123 | type: `Directory`, 124 | name: `/app/homebridge`, 125 | style: 'store' 126 | } 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_netatmo.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Netatmo`, 3 | description: `Add Netatmo devices to your HomeKit network using HomeBridge`, 4 | image: `registry.minkebox.net/minkebox/homebridge_netatmo`, 5 | uuid: `0B85CC4F-5FE9-4FC5-89B4-EE6C83C2A2CE`, 6 | tags: [ 'HomeKit' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure HomeKit` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | description: `Pin used to add this device to your network using the Home app`, 15 | name: `BRIDGE_PIN`, 16 | initValue: `111-11-111`, 17 | id: 'pin' 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `BRIDGE_SETUPID`, 22 | initValue: `{{__RANDOMHEX(4)}}`, 23 | id: 'setupid', 24 | visible: false 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Configure Netatmo` 29 | }, 30 | { 31 | type: `Text`, 32 | text: `To bridge your Netatmo devices onto the HomeKit network, you need to provides various pieces of security information. These can be found by logging into your Netatmo account at https://dev.netatmo.com.` 33 | }, 34 | { 35 | type: `EditEnvironment`, 36 | description: `Your Netatmo username`, 37 | name: `USERNAME` 38 | }, 39 | { 40 | type: `EditEnvironment`, 41 | description: `Your Netatmo password`, 42 | name: `PASSWORD` 43 | }, 44 | { 45 | type: `EditEnvironment`, 46 | description: `The client ID of the Netatmo app created in your development account`, 47 | name: `CLIENT_ID` 48 | }, 49 | { 50 | type: `EditEnvironment`, 51 | description: `The associated client secret`, 52 | name: `CLIENT_SECRET` 53 | }, 54 | { 55 | type: `Header`, 56 | title: `Homekit Code` 57 | }, 58 | { 59 | type: `Text`, 60 | text: ``, 61 | id: `qrcode` 62 | }, 63 | { 64 | type: `Script`, 65 | include: `qrcode` 66 | }, 67 | { 68 | type: `Script`, 69 | script: ` 70 | const rqr = document.querySelector('#qrcode'); 71 | const rpin = document.querySelector('#pin .value'); 72 | const rsetupid = document.querySelector('#setupid .value'); 73 | function uri(pin, setupid) { 74 | const CATEGORY_BRIDGE = 2; 75 | const SUPPORTS_IP = 1 << 28; 76 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 77 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 78 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 79 | return 'X-HM://' + payload + setupid; 80 | } 81 | function qr() { 82 | const content = uri(rpin.value, rsetupid.value); 83 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 84 | } 85 | rpin.addEventListener('input', qr); 86 | qr(); 87 | ` 88 | } 89 | ], 90 | properties: [ 91 | { 92 | type: `Port`, 93 | name: `51826/tcp`, 94 | port: 51826, 95 | protocol: 'TCP' 96 | }, 97 | { 98 | type: `Network`, 99 | name: `primary`, 100 | value: `home` 101 | }, 102 | { 103 | type: `Environment`, 104 | name: `BRIDGE_USERNAME`, 105 | value: `{{__MACADDRESS}}` 106 | }, 107 | { 108 | type: `Environment`, 109 | name: `BRIDGE_PIN` 110 | }, 111 | { 112 | type: `Environment`, 113 | name: `BRIDGE_SETUPID` 114 | }, 115 | { 116 | type: `Environment`, 117 | name: `CLIENT_ID` 118 | }, 119 | { 120 | type: `Environment`, 121 | name: `CLIENT_SECRET` 122 | }, 123 | { 124 | type: `Environment`, 125 | name: `USERNAME` 126 | }, 127 | { 128 | type: `Environment`, 129 | name: `PASSWORD` 130 | }, 131 | { 132 | type: `Directory`, 133 | name: `/app/homebridge`, 134 | style: 'store' 135 | } 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_raspberryshake.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Raspberry Shake`, 3 | description: `Integrate Raspberry Pi Seismograph as a HomeKit motion sensor`, 4 | image: `registry.minkebox.net/minkebox/homebridge_raspberryshake`, 5 | uuid: `ECFCD7FF-FE34-4CB1-B513-10FC05FB127E`, 6 | tags: [ 'HomeKit' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure HomeKit` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | name: `BRIDGE_SETUPID`, 15 | initValue: `{{__RANDOMHEX(4)}}`, 16 | id: `setupid`, 17 | visible: false 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | description: `Pin used to add this device to your network using the Home app`, 22 | name: `BRIDGE_PIN`, 23 | initValue: `111-11-111`, 24 | id: `pin` 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Configure Seismograph` 29 | }, 30 | { 31 | type: `Text`, 32 | text: `Instructions on how to change the configuration can be found here` 33 | }, 34 | { 35 | type: `EditEnvironment`, 36 | description: `Station ID`, 37 | name: `STATION` 38 | }, 39 | { 40 | type: `EditEnvironment`, 41 | description: `Lowpass filter`, 42 | name: `LOW`, 43 | defaultValue: `9` 44 | }, 45 | { 46 | type: `EditEnvironment`, 47 | description: `Highpass filter`, 48 | name: `HIGH`, 49 | defaultValue: `0.8` 50 | }, 51 | { 52 | type: `EditEnvironment`, 53 | description: `STA`, 54 | name: `STA`, 55 | defaultValue: `6` 56 | }, 57 | { 58 | type: `EditEnvironment`, 59 | description: `LTA`, 60 | name: `LTA`, 61 | defaultValue: `30` 62 | }, 63 | { 64 | type: `EditEnvironment`, 65 | description: `Threshold`, 66 | name: `THRESHOLD`, 67 | defaultValue: `4.5` 68 | }, 69 | { 70 | type: `EditEnvironment`, 71 | description: `Reset`, 72 | name: `RESET`, 73 | defaultValue: `0.5` 74 | }, 75 | { 76 | type: `Header`, 77 | title: `Homekit Code` 78 | }, 79 | { 80 | type: `Text`, 81 | text: ``, 82 | id: `qrcode` 83 | }, 84 | { 85 | type: `Script`, 86 | include: `qrcode` 87 | }, 88 | { 89 | type: `Script`, 90 | script: ` 91 | const rqr = document.querySelector('#qrcode'); 92 | const rpin = document.querySelector('#pin .value'); 93 | const rsetupid = document.querySelector('#setupid .value'); 94 | function uri(pin, setupid) { 95 | const CATEGORY_BRIDGE = 2; 96 | const SUPPORTS_IP = 1 << 28; 97 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 98 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 99 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 100 | return 'X-HM://' + payload + setupid; 101 | } 102 | function qr() { 103 | const content = uri(rpin.value, rsetupid.value); 104 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 105 | } 106 | rpin.addEventListener('input', qr); 107 | qr(); 108 | ` 109 | } 110 | ], 111 | properties: [ 112 | { 113 | type: `Feature`, 114 | name: `localtime` 115 | }, 116 | { 117 | type: `Port`, 118 | name: `51826/tcp`, 119 | port: 51826, 120 | protocol: `TCP` 121 | }, 122 | { 123 | type: `Network`, 124 | name: `primary`, 125 | value: `home` 126 | }, 127 | { 128 | type: `Environment`, 129 | name: `BRIDGE_USERNAME`, 130 | value: `{{__MACADDRESS}}` 131 | }, 132 | { 133 | type: `Environment`, 134 | name: `BRIDGE_PIN` 135 | }, 136 | { 137 | type: `Environment`, 138 | name: `BRIDGE_SETUPID` 139 | }, 140 | { 141 | type: `Environment`, 142 | name: `STATION` 143 | }, 144 | { 145 | type: `Environment`, 146 | name: `LOW` 147 | }, 148 | { 149 | type: `Environment`, 150 | name: `HIGH` 151 | }, 152 | { 153 | type: `Environment`, 154 | name: `STA` 155 | }, 156 | { 157 | type: `Environment`, 158 | name: `LTA` 159 | }, 160 | { 161 | type: `Environment`, 162 | name: `THRESHOLD` 163 | }, 164 | { 165 | type: `Environment`, 166 | name: `RESET` 167 | }, 168 | { 169 | type: `Directory`, 170 | name: `/root/homebridge`, 171 | style: `boot` 172 | }, 173 | { 174 | type: `Directory`, 175 | name: `/root/rsudp/screenshots`, 176 | style: `store`, 177 | shares: [ 178 | { 179 | name: `/`, 180 | description: `Screenshots` 181 | } 182 | ] 183 | } 184 | ] 185 | } 186 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_roborock.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Roborock`, 3 | description: `Add a Roborock vacuum to your HomeKit network using HomeBridge`, 4 | images: { 5 | x64: `registry.minkebox.net/minkebox/homebridge_roborock` 6 | }, 7 | uuid: `B2AD4A7B-9843-4EA4-902C-CDFAE00E123A`, 8 | tags: [ 'HomeKit' ], 9 | actions: [ 10 | { 11 | type: `Header`, 12 | title: `Configure HomeKit` 13 | }, 14 | { 15 | type: `EditEnvironment`, 16 | description: `Pin used to add this device to your network using the Home app`, 17 | name: `BRIDGE_PIN`, 18 | initValue: `111-11-111`, 19 | id: 'pin' 20 | }, 21 | { 22 | type: `EditEnvironment`, 23 | name: `BRIDGE_SETUPID`, 24 | initValue: `{{__RANDOMHEX(4)}}`, 25 | id: 'setupid', 26 | visible: false 27 | }, 28 | { 29 | type: `Header`, 30 | title: `Configure Roborock` 31 | }, 32 | { 33 | type: `Text`, 34 | text: `You need to provide the IP address of your vacuum as well as the security token. The security token is tricky to extract, but there are good instructions here` 35 | }, 36 | { 37 | type: `EditEnvironment`, 38 | description: `The IP address of your vacuum`, 39 | name: `ROBOROCK_IP` 40 | }, 41 | { 42 | type: `EditEnvironment`, 43 | description: `The security token of your vacuum`, 44 | name: `ROBOROCK_TOKEN` 45 | }, 46 | { 47 | type: `Header`, 48 | title: `Homekit Code` 49 | }, 50 | { 51 | type: `Text`, 52 | text: ``, 53 | id: `qrcode` 54 | }, 55 | { 56 | type: `Script`, 57 | include: `qrcode` 58 | }, 59 | { 60 | type: `Script`, 61 | script: ` 62 | const rqr = document.querySelector('#qrcode'); 63 | const rpin = document.querySelector('#pin .value'); 64 | const rsetupid = document.querySelector('#setupid .value'); 65 | function uri(pin, setupid) { 66 | const CATEGORY_BRIDGE = 2; 67 | const SUPPORTS_IP = 1 << 28; 68 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 69 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 70 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 71 | return 'X-HM://' + payload + setupid; 72 | } 73 | function qr() { 74 | const content = uri(rpin.value, rsetupid.value); 75 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 76 | } 77 | rpin.addEventListener('input', qr); 78 | qr(); 79 | ` 80 | } 81 | ], 82 | properties: [ 83 | { 84 | type: `Port`, 85 | name: `51826/tcp`, 86 | port: 51826, 87 | protocol: 'TCP' 88 | }, 89 | { 90 | type: `Network`, 91 | name: `primary`, 92 | value: `home` 93 | }, 94 | { 95 | type: `Environment`, 96 | name: `BRIDGE_USERNAME`, 97 | value: `{{__MACADDRESS}}` 98 | }, 99 | { 100 | type: `Environment`, 101 | name: `BRIDGE_PIN` 102 | }, 103 | { 104 | type: `Environment`, 105 | name: `BRIDGE_SETUPID` 106 | }, 107 | { 108 | type: `Environment`, 109 | name: `ROBOROCK_IP` 110 | }, 111 | { 112 | type: `Environment`, 113 | name: `ROBOROCK_TOKEN` 114 | }, 115 | { 116 | type: `Directory`, 117 | name: `/app/homebridge`, 118 | style: 'store' 119 | } 120 | ] 121 | } 122 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_shelly.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Shelly`, 3 | description: `Add Shelly devices to your HomeKit network using HomeBridge`, 4 | image: `registry.minkebox.net/minkebox/homebridge_shelly`, 5 | uuid: `A482FC93-05EC-41CF-BAD4-10712C0B715F`, 6 | tags: [ 'HomeKit' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure HomeKit` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | description: `Pin used to add this device to your network using the Home app`, 15 | name: `BRIDGE_PIN`, 16 | initValue: `111-11-111`, 17 | id: 'pin' 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `BRIDGE_SETUPID`, 22 | initValue: `{{__RANDOMHEX(4)}}`, 23 | id: 'setupid', 24 | visible: false 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Configure Shelly` 29 | }, 30 | { 31 | type: `Text`, 32 | text: `The Shelly devices on your HomeKit network will be found automatically.` 33 | }, 34 | { 35 | type: `Header`, 36 | title: `Homekit Code` 37 | }, 38 | { 39 | type: `Text`, 40 | text: ``, 41 | id: `qrcode` 42 | }, 43 | { 44 | type: `Script`, 45 | include: `qrcode` 46 | }, 47 | { 48 | type: `Script`, 49 | script: ` 50 | const rqr = document.querySelector('#qrcode'); 51 | const rpin = document.querySelector('#pin .value'); 52 | const rsetupid = document.querySelector('#setupid .value'); 53 | function uri(pin, setupid) { 54 | const CATEGORY_BRIDGE = 2; 55 | const SUPPORTS_IP = 1 << 28; 56 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 57 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 58 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 59 | return 'X-HM://' + payload + setupid; 60 | } 61 | function qr() { 62 | const content = uri(rpin.value, rsetupid.value); 63 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 64 | } 65 | rpin.addEventListener('input', qr); 66 | qr(); 67 | ` 68 | } 69 | ], 70 | properties: [ 71 | { 72 | type: `Port`, 73 | name: `51826/tcp`, 74 | port: 51826, 75 | protocol: 'TCP' 76 | }, 77 | { 78 | type: `Port`, 79 | name: `80/tcp`, 80 | port: 80, 81 | protocol: `TCP`, 82 | web: { 83 | tab: `inline`, 84 | path: `` 85 | }, 86 | mdns: { 87 | type: `_http._tcp` 88 | } 89 | }, 90 | { 91 | type: `Network`, 92 | name: `primary`, 93 | value: `home` 94 | }, 95 | { 96 | type: `Environment`, 97 | name: `BRIDGE_USERNAME`, 98 | value: `{{__MACADDRESS}}` 99 | }, 100 | { 101 | type: `Environment`, 102 | name: `BRIDGE_PIN` 103 | }, 104 | { 105 | type: `Environment`, 106 | name: `BRIDGE_SETUPID` 107 | }, 108 | { 109 | type: `Directory`, 110 | name: `/app/homebridge`, 111 | style: 'store' 112 | } 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /app/skeletons/builtin/homebridge_tplink.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Kasa TP-Link`, 3 | description: `Add Kasa TP-Link bulbs and plugs to your HomeKit network using HomeBridge`, 4 | image: `registry.minkebox.net/minkebox/homebridge_tplink`, 5 | uuid: `F1E6AC67-B66B-4515-B4B3-7C53B7FB8288`, 6 | tags: [ 'HomeKit' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure HomeKit` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | description: `Pin used to add this device to your network using the Home app`, 15 | name: `BRIDGE_PIN`, 16 | initValue: `111-11-111`, 17 | id: 'pin' 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `BRIDGE_SETUPID`, 22 | initValue: `{{__RANDOMHEX(4)}}`, 23 | id: 'setupid', 24 | visible: false 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Configure Kasa TP-Link` 29 | }, 30 | { 31 | type: `Text`, 32 | text: `The Kasa TP-Link devices on your HomeKit network will be found automatically.` 33 | }, 34 | { 35 | type: `Header`, 36 | title: `Homekit Code` 37 | }, 38 | { 39 | type: `Text`, 40 | text: ``, 41 | id: `qrcode` 42 | }, 43 | { 44 | type: `Script`, 45 | include: `qrcode` 46 | }, 47 | { 48 | type: `Script`, 49 | script: ` 50 | const rqr = document.querySelector('#qrcode'); 51 | const rpin = document.querySelector('#pin .value'); 52 | const rsetupid = document.querySelector('#setupid .value'); 53 | function uri(pin, setupid) { 54 | const CATEGORY_BRIDGE = 2; 55 | const SUPPORTS_IP = 1 << 28; 56 | const lval = BigInt(SUPPORTS_IP | parseInt(pin.replace(/-/g, '')) | ((CATEGORY_BRIDGE & 1) << 31)); 57 | const hval = BigInt(CATEGORY_BRIDGE >> 1); 58 | const payload = ('000000000' + ((hval << BigInt(32)) + lval).toString(36).toUpperCase()).substr(-9); 59 | return 'X-HM://' + payload + setupid; 60 | } 61 | function qr() { 62 | const content = uri(rpin.value, rsetupid.value); 63 | rqr.innerHTML = '
' + new QRCode({ join: true, content: content }).svg() + '
'; 64 | } 65 | rpin.addEventListener('input', qr); 66 | qr(); 67 | ` 68 | } 69 | ], 70 | properties: [ 71 | { 72 | type: `Port`, 73 | name: `51826/tcp`, 74 | port: 51826, 75 | protocol: 'TCP' 76 | }, 77 | { 78 | type: `Network`, 79 | name: `primary`, 80 | value: `home` 81 | }, 82 | { 83 | type: `Environment`, 84 | name: `BRIDGE_USERNAME`, 85 | value: `{{__MACADDRESS}}` 86 | }, 87 | { 88 | type: `Environment`, 89 | name: `BRIDGE_PIN` 90 | }, 91 | { 92 | type: `Environment`, 93 | name: `BRIDGE_SETUPID` 94 | }, 95 | { 96 | type: `Directory`, 97 | name: `/app/homebridge`, 98 | style: 'store' 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /app/skeletons/builtin/jellyfin.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `JellyFin`, 3 | description: `The Free Software Media Browser`, 4 | image: `jellyfin/jellyfin`, 5 | uuid: `F301B560-3262-4B4D-B09D-75CBB86B3BB1`, 6 | tags: [ 7 | `Media` 8 | ], 9 | actions: [ 10 | { 11 | type: `Text`, 12 | text: `Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps.` 13 | }, 14 | { 15 | type: `Header`, 16 | title: `Media` 17 | }, 18 | { 19 | type: `SelectShares`, 20 | name: `/media`, 21 | description: `Select the media to share in JellyFin` 22 | } 23 | ], 24 | properties: [ 25 | { 26 | type: `Directory`, 27 | name: `/cache`, 28 | style: `store` 29 | }, 30 | { 31 | type: `Directory`, 32 | name: `/config`, 33 | backup: true 34 | }, 35 | { 36 | type: `Directory`, 37 | name: `/media`, 38 | style: `store` 39 | }, 40 | { 41 | type: `Environment`, 42 | name: `DOTNET_RUNNING_IN_CONTAINER`, 43 | value: `true` 44 | }, 45 | { 46 | type: `Port`, 47 | name: `8096/tcp`, 48 | port: 8096, 49 | protocol: `TCP`, 50 | web: { 51 | type: `newtab`, 52 | path: `` 53 | } 54 | }, 55 | { 56 | type: `Network`, 57 | name: `primary`, 58 | value: `home` 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /app/skeletons/builtin/lancache.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `LAN Cache`, 3 | description: `Cache Windows, iOS, Steam and other downloads for multiple computers`, 4 | images: { 5 | x64: `lancachenet/monolithic` 6 | }, 7 | uuid: `E1052DE9-6D0C-4B7D-827F-6F661CEB1FB0`, 8 | tags: [ 'Caches', `Dns` ], 9 | actions: [ 10 | { 11 | type: `Header`, 12 | title: `Cache configuration` 13 | }, 14 | { 15 | type: `EditEnvironment`, 16 | name: `CACHE_MEM_SIZE`, 17 | description: `In memory cache size` 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `CACHE_DISK_SIZE`, 22 | description: `Disk cache size` 23 | }, 24 | { 25 | type: `EditEnvironment`, 26 | name: `CACHE_MAX_AGE`, 27 | description: `Maximum cache age` 28 | }, 29 | { 30 | type: `EditEnvironment`, 31 | name: `CACHE_DOMAINS_REPO`, 32 | description: `Git repo list of domains to cache` 33 | } 34 | ], 35 | properties: [ 36 | { 37 | type: `Directory`, 38 | name: `/data/cache`, 39 | style: 'store', 40 | }, 41 | { 42 | type: `Directory`, 43 | name: `/data/cachedomains`, 44 | style: 'store', 45 | }, 46 | { 47 | type: `Directory`, 48 | name: `/data/logs`, 49 | style: 'store' 50 | }, 51 | { 52 | type: `Directory`, 53 | name: `/var/www`, 54 | style: 'store' 55 | }, 56 | { 57 | type: `Environment`, 58 | name: `CACHE_MEM_SIZE`, 59 | value: `50m` 60 | }, 61 | { 62 | type: `Environment`, 63 | name: `CACHE_DISK_SIZE`, 64 | value: `50000m` 65 | }, 66 | { 67 | type: `Environment`, 68 | name: `CACHE_MAX_AGE`, 69 | value: `14d` 70 | }, 71 | { 72 | type: `Environment`, 73 | name: `CACHE_DOMAINS_REPO`, 74 | value: `https://github.com/aanon4/cache-domains.git` 75 | }, 76 | { 77 | type: `Environment`, 78 | name: `BEAT_TIME`, 79 | value: `1h` 80 | }, 81 | { 82 | type: `Environment`, 83 | name: `LOGFILE_RETENTION`, 84 | value: `14` 85 | }, 86 | { 87 | type: `Environment`, 88 | name: `NGINX_WORKER_PROCESSES`, 89 | value: `auto` 90 | }, 91 | { 92 | type: `Port`, 93 | name: `80/tcp`, 94 | port: 80, 95 | protocol: `TCP` 96 | }, 97 | { 98 | type: `Port`, 99 | name: `443/tcp`, 100 | port: 443, 101 | protocol: `TCP` 102 | }, 103 | { 104 | type: `Port`, 105 | name: `53/tcp`, 106 | port: 53, 107 | protocol: `UDP`, 108 | dns: true 109 | }, 110 | { 111 | type: `Network`, 112 | name: `primary`, 113 | value: `home` 114 | }, 115 | { 116 | type: `Network`, 117 | name: `secondary`, 118 | value: `dns` 119 | } 120 | ], 121 | secondary: [ 122 | { 123 | images: { 124 | x64: `lancachenet/lancache-dns` 125 | }, 126 | properties: [ 127 | { 128 | type: `Environment`, 129 | name: `USE_GENERIC_CACHE`, 130 | value: `true` 131 | }, 132 | { 133 | type: `Environment`, 134 | name: `LANCACHE_IP`, 135 | value: `{{__HOMEIP}}` 136 | }, 137 | { 138 | type: `Environment`, 139 | name: `LANCACHE_DNSDOMAIN`, 140 | value: `{{__DOMAINNAME}}` 141 | }, 142 | { 143 | type: 'Environment', 144 | name: 'UPSTREAM_DNS', 145 | value: '0.0.0.0' 146 | }, 147 | { 148 | type: `Environment`, 149 | name: `CACHE_DOMAINS_REPO`, 150 | value: `https://github.com/aanon4/cache-domains.git` 151 | } 152 | ] 153 | }, 154 | { 155 | images: { 156 | x64: `lancachenet/sniproxy` 157 | }, 158 | properties: [ 159 | ] 160 | } 161 | ] 162 | } 163 | -------------------------------------------------------------------------------- /app/skeletons/builtin/minio.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `MinIO`, 3 | description: `High Performance Object Storage compatible with Amazon's S3.`, 4 | image: `minio/minio`, 5 | uuid: `8994692D-BB01-4320-AE45-B3DD27A6DF8F`, 6 | tags: [ 7 | `Storage` 8 | ], 9 | actions: [ 10 | { 11 | type: `Header`, 12 | title: `Configure` 13 | }, 14 | { 15 | type: `SelectDirectory`, 16 | name: `/data`, 17 | description: `Select storage directory` 18 | }, 19 | { 20 | type: `EditEnvironment`, 21 | name: `MINIO_ACCESS_KEY`, 22 | description: `Access Key (3 or more characters)` 23 | }, 24 | { 25 | type: `EditEnvironment`, 26 | name: `MINIO_SECRET_KEY`, 27 | description: `Secret Key (8 or more characters)` 28 | }, 29 | { 30 | type: `Header`, 31 | title: `Network` 32 | }, 33 | { 34 | type: `Text`, 35 | text: `Select which network this application will use. You probably want home unless this application is being used on a private network.` 36 | }, 37 | { 38 | type: `SelectNetwork`, 39 | name: `primary`, 40 | description: `Select network` 41 | } 42 | ], 43 | properties: [ 44 | { 45 | type: `Arguments`, 46 | value: [ 47 | `server`, 48 | `/data` 49 | ] 50 | }, 51 | { 52 | type: `Directory`, 53 | name: `/data`, 54 | style: `store` 55 | }, 56 | { 57 | type: `Environment`, 58 | name: `MINIO_ACCESS_KEY` 59 | }, 60 | { 61 | type: `Environment`, 62 | name: `MINIO_SECRET_KEY` 63 | }, 64 | { 65 | type: `Environment`, 66 | name: `MINIO_UPDATE`, 67 | value: `off` 68 | }, 69 | { 70 | type: `Environment`, 71 | name: `MINIO_ACCESS_KEY_FILE`, 72 | value: `access_key` 73 | }, 74 | { 75 | type: `Environment`, 76 | name: `MINIO_SECRET_KEY_FILE`, 77 | value: `secret_key` 78 | }, 79 | { 80 | type: `Environment`, 81 | name: `MINIO_KMS_MASTER_KEY_FILE`, 82 | value: `kms_master_key` 83 | }, 84 | { 85 | type: `Environment`, 86 | name: `MINIO_SSE_MASTER_KEY_FILE`, 87 | value: `sse_master_key` 88 | }, 89 | { 90 | type: `Port`, 91 | name: `9000/tcp`, 92 | port: 9000, 93 | protocol: `TCP`, 94 | web: { 95 | tab: `inline`, 96 | widget: `inline`, 97 | path: `/` 98 | } 99 | }, 100 | { 101 | type: `Network`, 102 | name: `primary`, 103 | value: `home` 104 | } 105 | ], 106 | monitor: { 107 | cmd: `cd /data;du -d0 *`, 108 | init: ` 109 |
110 | 111 |
112 | 154 | ` 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/skeletons/builtin/netproxy.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Network Proxy`, 3 | description: `Send web requests to specific domains via a different network`, 4 | image: `registry.minkebox.net/minkebox/netproxy`, 5 | uuid: `1B2AB39F-2472-47AB-AE40-ECCE9CF58484`, 6 | tags: [ 'Proxy', `Dns` ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Networking` 11 | }, 12 | { 13 | type: 'Text', 14 | text: 'Traffic from the source network, and targetting the domains listed below, will be routed to the target network.' 15 | }, 16 | { 17 | type: `SelectNetwork`, 18 | name: `primary`, 19 | description: `Select target network` 20 | }, 21 | { 22 | type: `Text`, 23 | text: `Select source network for traffic. You probably want home unless this application is being used on a private network.` 24 | }, 25 | { 26 | type: `SelectNetwork`, 27 | name: `secondary`, 28 | description: `Select source network` 29 | }, 30 | { 31 | type: `Header`, 32 | title: `Proxied Domains` 33 | }, 34 | { 35 | type: `EditFileAsTable`, 36 | name: `/etc/dnsmasq.d/proxies.preconf`, 37 | description: `Add the domains (which will automatically include any sub-domains) here. All traffic to these domains will be proxied through the selected network.`, 38 | headers: [ 39 | { name: `Domain name` } 40 | ], 41 | pattern: `{{V[0]}}` 42 | } 43 | ], 44 | properties: [ 45 | { 46 | type: `File`, 47 | style: 'boot', 48 | name: `/etc/dnsmasq.d/proxies.preconf` 49 | }, 50 | { 51 | type: `Port`, 52 | name: `443/tcp`, 53 | port: 443, 54 | protocol: 'TCP', 55 | mdns: { 56 | type: '_https._tcp' 57 | } 58 | }, 59 | { 60 | type: `Port`, 61 | name: `53/tcp`, 62 | port: 53, 63 | protocol: 'TCP' 64 | }, 65 | { 66 | type: `Port`, 67 | name: `53/udp`, 68 | port: 53, 69 | protocol: 'UDP', 70 | dns: true 71 | }, 72 | { 73 | type: `Port`, 74 | name: `80/tcp`, 75 | port: 80, 76 | protocol: 'TCP', 77 | mdns: { 78 | type: '_http._tcp' 79 | } 80 | }, 81 | { 82 | type: `Network`, 83 | name: `primary`, 84 | value: `home` 85 | }, 86 | { 87 | type: `Network`, 88 | name: `secondary`, 89 | value: `home` 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /app/skeletons/builtin/networkshares.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Network Folders`, 3 | description: `Mount SMB/CIFS shares from another machine and make them available locally.`, 4 | image: `registry.minkebox.net/minkebox/networkshares`, 5 | uuid: `08ABD2A5-D5A4-4613-9FBA-F56E73322814`, 6 | tags: [ 'Shares', 'Storage' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Network Shares` 11 | }, 12 | { 13 | type: `Text`, 14 | text: `Enter the network shares which should be imported. These will placed in a /folders directory and shareable with other applications.` 15 | }, 16 | { 17 | type: `EditFileAsTable`, 18 | name: `/mounts`, 19 | description: `Enter share names and credentials. Leave username and password blank if no authentication is required.`, 20 | headers: [ 21 | { 22 | name: `Share UNC name` 23 | }, 24 | { 25 | name: `Local name` 26 | }, 27 | { 28 | name: `Username`, 29 | placeholder: `Guest` 30 | }, 31 | { 32 | name: `Password`, 33 | placeholder: `Guest` 34 | } 35 | ], 36 | pattern: `{{V[0]}},{{V[1]}},{{V[2]}},{{V[3]}} 37 | `, 38 | join: `` 39 | } 40 | ], 41 | properties: [ 42 | { 43 | type: `Feature`, 44 | name: `+SYS_ADMIN` 45 | }, 46 | { 47 | type: `Feature`, 48 | name: `+DAC_READ_SEARCH` 49 | }, 50 | { 51 | type: `File`, 52 | name: `/mounts` 53 | }, 54 | { 55 | type: `Directory`, 56 | name: `/folders`, 57 | shares: [ 58 | { 59 | name: `/`, 60 | description: `Remote directories` 61 | } 62 | ] 63 | }, 64 | { 65 | type: `Network`, 66 | name: `primary`, 67 | value: `home` 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /app/skeletons/builtin/openvpnclient.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `OpenVPN`, 3 | description: `OpenVPN client for use with various OpenVPN servers`, 4 | image: `registry.minkebox.net/minkebox/openvpnclient`, 5 | uuid: `D66A1C55-6224-40BA-A772-006B156CCD55`, 6 | tags: [ 'VPN', 'Security', 'Networking' ], 7 | actions: [ 8 | { 9 | type: `EditEnvironment`, 10 | description: `Enter your VPN username`, 11 | name: `USER`, 12 | placeholder: `Username` 13 | }, 14 | { 15 | type: `EditEnvironment`, 16 | description: `Enter your VPN password`, 17 | name: `PASSWORD`, 18 | placeholder: `Password` 19 | }, 20 | { 21 | type: `EditFile`, 22 | description: `Drop your .ovpn configuration file here`, 23 | name: `/etc/openvpn/config.ovpn` 24 | } 25 | ], 26 | properties: [ 27 | { 28 | type: `Feature`, 29 | name: `+NET_ADMIN` 30 | }, 31 | { 32 | type: `Feature`, 33 | name: `tuntap` 34 | }, 35 | { 36 | type: `Environment`, 37 | name: `USER` 38 | }, 39 | { 40 | type: `Environment`, 41 | name: `PASSWORD` 42 | }, 43 | { 44 | type: `File`, 45 | name: `/etc/openvpn/config.ovpn`, 46 | style: `boot` 47 | }, 48 | { 49 | type: `Directory`, 50 | name: `/leases`, 51 | style: `boot` 52 | }, 53 | { 54 | type: `Network`, 55 | name: `primary`, 56 | value: `home` 57 | }, 58 | { 59 | type: `Network`, 60 | name: `secondary`, 61 | value: `__create` 62 | } 63 | ], 64 | monitor: { 65 | cmd: `echo $(iptables -L RX -x -v -n | awk 'NR == 3 {print $2}') $(iptables -L TX -x -v -n | awk 'NR == 3 {print $2}')`, 66 | target: `helper`, 67 | init: ` 68 |
69 | 70 |
71 | 74 | ` 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/skeletons/builtin/pihole.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Pi Hole`, 3 | description: `Network-wide Ad Blocking`, 4 | image: `pihole/pihole`, 5 | uuid: `7CCA69ED-DA29-4DCE-95DD-A14878568B54`, 6 | tags: [ 7 | `Ad Blocking`, `Networking`, `Dns` 8 | ], 9 | actions: [ 10 | ], 11 | properties: [ 12 | { 13 | type: `Feature`, 14 | name: `localtime`, 15 | }, 16 | { 17 | type: `Directory`, 18 | name: `/etc/pihole`, 19 | style: `store` 20 | }, 21 | { 22 | type: `Directory`, 23 | name: `/etc/dnsmasq.d`, 24 | style: `boot` 25 | }, 26 | { 27 | type: `Environment`, 28 | name: `IPv6`, 29 | value: `{{!!__HOMEIP6}}` 30 | }, 31 | { 32 | type: `Environment`, 33 | name: `WEBPASSWORD`, 34 | value: `` 35 | }, 36 | { 37 | type: `Environment`, 38 | name: `DNS1`, 39 | value: `127.0.0.1` 40 | }, 41 | { 42 | type: `Environment`, 43 | name: `DNS2`, 44 | value: `127.0.0.1` 45 | }, 46 | { 47 | type: `Environment`, 48 | name: `VIRTUAL_HOST`, 49 | value: `{{__HOSTIP}}` 50 | }, 51 | { 52 | type: `Environment`, 53 | name: `DNSMASQ_LISTENING`, 54 | value: `all` 55 | }, 56 | { 57 | type: `Port`, 58 | name: `443/tcp`, 59 | port: 443, 60 | protocol: `TCP` 61 | }, 62 | { 63 | type: `Port`, 64 | name: `53/tcp`, 65 | port: 53, 66 | protocol: `TCP` 67 | }, 68 | { 69 | type: `Port`, 70 | name: `53/udp`, 71 | port: 53, 72 | protocol: `UDP`, 73 | dns: true 74 | }, 75 | { 76 | type: `Port`, 77 | name: `80/tcp`, 78 | port: 80, 79 | protocol: `TCP`, 80 | web: { 81 | tab: `inline`, 82 | widget: `inline`, 83 | path: `/admin/` 84 | } 85 | }, 86 | { 87 | type: `Network`, 88 | name: `primary`, 89 | value: `home` 90 | }, 91 | { 92 | type: `Network`, 93 | name: `secondary`, 94 | value: `dns` 95 | } 96 | ], 97 | monitor: { 98 | cmd: `pihole -c --json`, 99 | init: ` 100 | 105 |
106 |
0
Blocked in last 24 hours
107 |
0%
Percentage Blocked
108 |
0
Domains Being Blocked
109 |
110 | ` 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/skeletons/builtin/plex.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Plex`, 3 | description: `Your favorite movies, TV, music, web shows, podcasts, and more, all streamed to your favorite screens`, 4 | image: `linuxserver/plex`, 5 | uuid: `84EDCC84-C18D-4D7D-8664-859E67FE6128`, 6 | tags: [ 'Media' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Media Selection` 11 | }, 12 | { 13 | type: `Text`, 14 | text: `Plex will automatically create two folders (TV and Movies) to store your media. You can also include other folders by adding them below.` 15 | }, 16 | { 17 | type: `SelectShares`, 18 | name: `/data/Shared`, 19 | description: `Select folders Plex can acccess` 20 | } 21 | ], 22 | properties: [ 23 | { 24 | type: `Directory`, 25 | name: `/config`, 26 | style: `store` 27 | }, 28 | { 29 | type: `Directory`, 30 | name: `/transcode`, 31 | style: `store` 32 | }, 33 | { 34 | type: `Directory`, 35 | name: `/data/TV`, 36 | style: `store`, 37 | shares: [ 38 | { 39 | name: `/`, 40 | description: `TV Shows` 41 | } 42 | ] 43 | }, 44 | { 45 | type: `Directory`, 46 | name: `/data/Movies`, 47 | style: `store`, 48 | shares: [ 49 | { 50 | name: `/`, 51 | description: `Movies` 52 | } 53 | ] 54 | }, 55 | { 56 | type: `Directory`, 57 | name: `/data/Shared`, 58 | style: `temp` 59 | }, 60 | { 61 | type: `Environment`, 62 | name: `PLEX_MEDIA_SERVER_INFO_DEVICE`, 63 | value: `Docker Container (LinuxServer.io) / MinkeBox` 64 | }, 65 | { 66 | type: `Port`, 67 | name: `1900/udp`, 68 | port: 1900, 69 | protocol: `UDP` 70 | }, 71 | { 72 | type: `Port`, 73 | name: `32400/tcp`, 74 | port: 32400, 75 | protocol: `TCP`, 76 | web: { 77 | type: `newtab`, 78 | path: `/web` 79 | } 80 | }, 81 | { 82 | type: `Port`, 83 | name: `32400/udp`, 84 | port: 32400, 85 | protocol: `UDP` 86 | }, 87 | { 88 | type: `Port`, 89 | name: `32469/tcp`, 90 | port: 32469, 91 | protocol: `TCP` 92 | }, 93 | { 94 | type: `Port`, 95 | name: `32469/udp`, 96 | port: 32469, 97 | protocol: `UDP` 98 | }, 99 | { 100 | type: `Port`, 101 | name: `5353/udp`, 102 | port: 5353, 103 | protocol: `UDP` 104 | }, 105 | { 106 | type: `Network`, 107 | name: `primary`, 108 | value: `home` 109 | } 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /app/skeletons/builtin/pptpclient.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `PPTP`, 3 | description: `PPTP client to connect to PPTP servers`, 4 | image: `registry.minkebox.net/minkebox/pptpclient`, 5 | uuid: `11E7B3AF-CE7B-49DF-AFCC-FB561B1628DB`, 6 | tags: [ 'VPN', 'Security', 'Networking' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configure` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | description: `Enter your VPN server name`, 15 | name: `SERVER`, 16 | placeholder: `Server Name` 17 | }, 18 | { 19 | type: `EditEnvironment`, 20 | description: `Enter your VPN username`, 21 | name: `USER`, 22 | placeholder: `Username` 23 | }, 24 | { 25 | type: `EditEnvironment`, 26 | description: `Enter your VPN password`, 27 | name: `PASSWORD`, 28 | placeholder: `Password` 29 | }, 30 | { 31 | type: `Header`, 32 | title: `Advanced`, 33 | visible: `property.Advanced` 34 | }, 35 | { 36 | type: `EditEnvironmentAsCheckbox`, 37 | name: `PAP`, 38 | description: `Enable PAP authentication`, 39 | initValue: false 40 | }, 41 | { 42 | type: `EditEnvironmentAsCheckbox`, 43 | name: `EAP`, 44 | description: `Enable EAP authentication`, 45 | initValue: false 46 | }, 47 | { 48 | type: `EditEnvironmentAsCheckbox`, 49 | name: `CHAP`, 50 | description: `Enable CHAP authentication`, 51 | initValue: false 52 | }, 53 | { 54 | type: `EditEnvironmentAsCheckbox`, 55 | name: `MSCHAP`, 56 | description: `Enable MS-CHAP authentication`, 57 | initValue: true 58 | }, 59 | { 60 | type: `EditEnvironmentAsCheckbox`, 61 | name: `MPPE`, 62 | description: `Enable encryption`, 63 | initValue: true 64 | } 65 | ], 66 | properties: [ 67 | { 68 | type: `Feature`, 69 | name: `privileged` 70 | }, 71 | { 72 | type: `Environment`, 73 | name: `USER` 74 | }, 75 | { 76 | type: `Environment`, 77 | name: `PASSWORD` 78 | }, 79 | { 80 | type: `Environment`, 81 | name: `SERVER` 82 | }, 83 | { 84 | type: `Environment`, 85 | name: `PAP` 86 | }, 87 | { 88 | type: `Environment`, 89 | name: `CHAP` 90 | }, 91 | { 92 | type: `Environment`, 93 | name: `EAP` 94 | }, 95 | { 96 | type: `Environment`, 97 | name: `MSCHAP` 98 | }, 99 | { 100 | type: `Environment`, 101 | name: `MPPE` 102 | }, 103 | { 104 | type: `Directory`, 105 | name: `/leases`, 106 | style: `boot` 107 | }, 108 | { 109 | type: `Network`, 110 | name: `primary`, 111 | value: `home` 112 | }, 113 | { 114 | type: `Network`, 115 | name: `secondary`, 116 | value: `__create` 117 | } 118 | ], 119 | monitor: { 120 | cmd: `echo $(iptables -L RX -x -v -n | awk 'NR == 3 {print $2}') $(iptables -L TX -x -v -n | awk 'NR == 3 {print $2}')`, 121 | target: `helper`, 122 | init: ` 123 |
124 | 125 |
126 | 129 | ` 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/skeletons/builtin/radarr.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Radarr`, 3 | description: `Movies downloader for newsgroup and bittorrent users`, 4 | image: `linuxserver/radarr`, 5 | uuid: `8200F669-0959-4579-8B50-DC176C9BD721`, 6 | tags: [ 7 | `Media` 8 | ], 9 | actions: [ 10 | { 11 | type: `NavButton`, 12 | name: `Open Transmission`, 13 | url: `http://{{__HOMEIP}}:9091/` 14 | }, 15 | { 16 | type: `NavButton`, 17 | name: `Open NZBGet`, 18 | url: `http://{{__HOMEIP}}:6789/` 19 | }, 20 | { 21 | type: `Text`, 22 | text: `Radarr simplifies finding and downloading Movies. 23 | This application is bundled with the Transmission bit torrent client and the NZBGet Usenet client. 24 | They can be accessed using the buttons above.` 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Movies` 29 | }, 30 | { 31 | type: `SelectDirectory`, 32 | name: `/movies`, 33 | description: `Select the folder where we save your Movies` 34 | }, 35 | { 36 | type: `Header`, 37 | title: `Network` 38 | }, 39 | { 40 | type: `Text`, 41 | text: `Select the network to use for downloading content. By default this is your home network, 42 | but you may want to use a VPN network for extra security and privacy.` 43 | }, 44 | { 45 | type: `SelectNetwork`, 46 | name: `primary`, 47 | description: `Select BitTorrent network` 48 | } 49 | ], 50 | properties: [ 51 | { 52 | type: `Directory`, 53 | name: `/config`, 54 | style: `boot`, 55 | backup: true 56 | }, 57 | { 58 | type: `Directory`, 59 | name: `/downloads`, 60 | use: `downloads`, 61 | shares: [ 62 | { 63 | name: `/`, 64 | description: `Downloads` 65 | } 66 | ] 67 | }, 68 | { 69 | type: `Directory`, 70 | name: `/movies`, 71 | style: `store` 72 | }, 73 | { 74 | type: `Environment`, 75 | name: `HOME`, 76 | value: `/root` 77 | }, 78 | { 79 | type: `Port`, 80 | name: `7878/tcp`, 81 | port: 7878, 82 | protocol: `TCP`, 83 | web: { 84 | type: `newtab`, 85 | path: `/` 86 | } 87 | }, 88 | { 89 | type: `Network`, 90 | name: `primary`, 91 | value: `home` 92 | }, 93 | { 94 | type: `Network`, 95 | name: `secondary`, 96 | value: `home` 97 | } 98 | ], 99 | secondary: [ 100 | { 101 | image: `linuxserver/transmission`, 102 | delay: 0, 103 | properties: [ 104 | { 105 | type: `Directory`, 106 | name: `/config`, 107 | style: `boot`, 108 | backup: true 109 | }, 110 | { 111 | type: `Directory`, 112 | name: `/downloads`, 113 | use: `downloads` 114 | } 115 | ] 116 | }, 117 | { 118 | image: `linuxserver/nzbget`, 119 | delay: 0, 120 | properties: [ 121 | { 122 | type: `Directory`, 123 | name: `/config`, 124 | style: `boot`, 125 | backup: true 126 | }, 127 | { 128 | type: `Directory`, 129 | name: `/downloads`, 130 | use: `downloads` 131 | }, 132 | { 133 | type: `File`, 134 | name: `/config/custom-cont-init.d/disable-password`, 135 | mode: 0o777, 136 | value: `sed -i "s/ControlPassword=.*/ControlPassword=/" /config/nzbget.conf` 137 | } 138 | ] 139 | } 140 | ] 141 | } 142 | -------------------------------------------------------------------------------- /app/skeletons/builtin/sftp.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Secure FTP`, 3 | description: `Provide secure ftp (sftp) access to selected folders and files`, 4 | images: { 5 | x64: `amimof/sftp` 6 | }, 7 | uuid: `D6AD83F2-C379-4FD8-81C4-9292620E5D3C`, 8 | tags: [ 'Utilities' ], 9 | actions: [ 10 | { 11 | type: `Header`, 12 | title: `User` 13 | }, 14 | { 15 | type: `EditEnvironment`, 16 | name: `SSH_USERNAME`, 17 | description: `Select the user's login name`, 18 | initValue: `sftpuser` 19 | }, 20 | { 21 | type: `EditEnvironment`, 22 | name: `SSH_PASSWORD`, 23 | description: `Select the user's password`, 24 | initValue: `sftppassword` 25 | }, 26 | { 27 | type: `Header`, 28 | title: `Folders` 29 | }, 30 | { 31 | type: `SelectShares`, 32 | name: `/home/{{SSH_USERNAME}}`, 33 | description: `Select the folders to share with this user` 34 | }, 35 | { 36 | type: 'Header', 37 | title: 'Network', 38 | visible: `property.Advanced` 39 | }, 40 | { 41 | type: 'Text', 42 | text: 'Select which network this application will use. You probably want home unless this application is being used on a VPN or private network.' 43 | }, 44 | { 45 | type: 'SelectNetwork', 46 | name: 'primary', 47 | description: 'Select network' 48 | }, 49 | { 50 | type: `Header`, 51 | title: `Advanced` 52 | }, 53 | { 54 | type: `EditEnvironment`, 55 | name: `SSH_PORT`, 56 | description: `Select the SSH port (usually 22)`, 57 | initValue: 22 58 | }, 59 | { 60 | type: `EditEnvironmentAsCheckbox`, 61 | name: `NAT`, 62 | description: `Make available on the Internet with the name
{{__GLOBALNAME}}
` 63 | } 64 | ], 65 | properties: [ 66 | { 67 | type: `Environment`, 68 | name: `SSH_USERNAME` 69 | }, 70 | { 71 | type: `Environment`, 72 | name: `SSH_USERID` 73 | }, 74 | { 75 | type: `Environment`, 76 | name: `SSH_DATADIR_NAME`, 77 | value: `../../tmp/ignore` 78 | }, 79 | { 80 | type: `Environment`, 81 | name: `SSH_GENERATE_HOSTKEYS`, 82 | value: `true` 83 | }, 84 | { 85 | type: `Environment`, 86 | name: `SSH_PASSWORD` 87 | }, 88 | { 89 | type: `Environment`, 90 | name: `SSH_PORT` 91 | }, 92 | { 93 | type: `Environment`, 94 | name: `NAT` 95 | }, 96 | { 97 | type: `Environment`, 98 | name: `LOGLEVEL` 99 | }, 100 | { 101 | type: `Environment`, 102 | name: `DEBUG` 103 | }, 104 | { 105 | type: `Directory`, 106 | name: `/home/{{SSH_USERNAME}}`, 107 | style: `temp` 108 | }, 109 | { 110 | type: `Port`, 111 | name: `SSH_PORT`, 112 | port: `{{SSH_PORT}}`, 113 | protocol: `TCP`, 114 | nat: `{{NAT}}`, 115 | mdns: { 116 | type: `_sftp-ssh._tcp` 117 | } 118 | }, 119 | { 120 | type: `Directory`, 121 | name: `/etc/ssh/host_keys`, 122 | style: 'boot' 123 | }, 124 | { 125 | type: `Network`, 126 | name: `primary`, 127 | value: `home` 128 | } 129 | ] 130 | } 131 | -------------------------------------------------------------------------------- /app/skeletons/builtin/shadowsocks.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Shadowsocks`, 3 | description: `A secure socks5 proxy, designed to protect your Internet traffic`, 4 | images: { 5 | x64: `shadowsocks/shadowsocks-libev` 6 | }, 7 | uuid: `6C6D77BF-A8F5-44B1-B5E9-BEA868B9DF57`, 8 | tags: [ 9 | `Proxy`, `VPN`, `Networking` 10 | ], 11 | actions: [ 12 | { 13 | type: `Header`, 14 | title: `Configuration` 15 | }, 16 | { 17 | type: `EditEnvironment`, 18 | name: `SERVER_PORT`, 19 | description: `TCP port to access the proxy`, 20 | id: `port`, 21 | initValue: `{{__RANDOMPORTS(1)}}` 22 | }, 23 | { 24 | type: `EditEnvironment`, 25 | name: `PASSWORD`, 26 | description: `Password`, 27 | id: `pass` 28 | }, 29 | { 30 | type: `EditEnvironment`, 31 | name: `DNS_ADDRS`, 32 | description: `DNS server`, 33 | visible: `property.Advanced` 34 | }, 35 | { 36 | type: `EditEnvironment`, 37 | name: `TIMEOUT`, 38 | description: `Connection timeout (seconds)`, 39 | visible: `property.Advanced` 40 | }, 41 | { 42 | type: `EditEnvironment`, 43 | name: `ENCRYPTION_METHOD`, 44 | description: `Encryption method`, 45 | options: [ 46 | { 47 | name: `aes-256-gcm`, 48 | value: `aes-256-gcm` 49 | }, 50 | { 51 | name: `aes-192-gcm`, 52 | value: `aes-192-gcm` 53 | }, 54 | { 55 | name: `aes-128-gcm`, 56 | value: `aes-128-gcm` 57 | }, 58 | { 59 | name: `chacha20-ietf-poly1305`, 60 | value: `chacha20-ietf-poly1305` 61 | } 62 | ], 63 | id: `meth`, 64 | visible: `property.Advanced` 65 | }, 66 | { 67 | type: `Header`, 68 | title: `Client Configuration` 69 | }, 70 | { 71 | type: `Text`, 72 | text: `To add this proxy to a client, use the ss:// URI below:` 73 | }, 74 | { 75 | type: `Text`, 76 | text: ``, 77 | id: `ss` 78 | }, 79 | { 80 | type: `Text`, 81 | text: `{{__GLOBALNAME}}`, 82 | id: `gn`, 83 | visible: false 84 | }, 85 | { 86 | type: `Script`, 87 | script: ` 88 | const ss = document.querySelector('#ss'); 89 | const pass = document.querySelector('#pass input'); 90 | const port = document.querySelector('#port input'); 91 | const meth = document.querySelector('#meth select'); 92 | const globalname = document.querySelector('#gn').innerText; 93 | setInterval(function() { 94 | const uri = 'ss://' + btoa( 95 | meth.value + ':' + pass.value + '@' + globalname + ':' + port.value 96 | ) + ''; 97 | if (uri != ss.innerHTML) { 98 | ss.innerHTML = uri; 99 | } 100 | }, 1000);` 101 | }, 102 | { 103 | type: `Text`, 104 | text: `UPnP is not available on your network. Please manually forward TCP port {{SERVER_PORT}} from your router to {{__HOMEIP}}{{__HOMEIP6 ? "and " + __HOMEIP6 : ""}}.`, 105 | visible: `!property.UPnPAvailable` 106 | } 107 | ], 108 | properties: [ 109 | { 110 | type: `Feature`, 111 | name: `localtime` 112 | }, 113 | { 114 | type: `Environment`, 115 | name: `PASSWORD` 116 | }, 117 | { 118 | type: `Environment`, 119 | name: `SERVER_NAME`, 120 | value: `0.0.0.0` 121 | }, 122 | { 123 | type: `Environment`, 124 | name: `SERVER_PORT` 125 | }, 126 | { 127 | type: `Environment`, 128 | name: `ENCRYPTION_METHOD`, 129 | value: `aes-256-gcm` 130 | }, 131 | { 132 | type: `Environment`, 133 | name: `TIMEOUT`, 134 | value: `300` 135 | }, 136 | { 137 | type: `Environment`, 138 | name: `DNS_ADDRS`, 139 | value: `{{__DNSSERVER}}` 140 | }, 141 | { 142 | type: `Port`, 143 | name: `SERVER_PORT`, 144 | port: `{{SERVER_PORT}}`, 145 | protocol: `TCP`, 146 | nat: true 147 | }, 148 | { 149 | type: `Network`, 150 | name: `primary`, 151 | value: `home` 152 | } 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /app/skeletons/builtin/sonarr.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Sonarr`, 3 | description: `Smart PVR for newsgroup and bittorrent users`, 4 | image: `linuxserver/sonarr`, 5 | uuid: `42150D45-9105-4263-BB92-93AF87559337`, 6 | tags: [ 7 | `Media` 8 | ], 9 | actions: [ 10 | { 11 | type: `NavButton`, 12 | name: `Open Transmission`, 13 | url: `http://{{__HOMEIP}}:9091/` 14 | }, 15 | { 16 | type: `NavButton`, 17 | name: `Open NZBGet`, 18 | url: `http://{{__HOMEIP}}:6789/` 19 | }, 20 | { 21 | type: `Text`, 22 | text: `Sonarr is a smart PVR client which simplifies finding and downloading TV show. 23 | This application is bundled with the Transmission bit torrent client and the NZBGet Usenet client. 24 | They can be access using the buttons above.` 25 | }, 26 | { 27 | type: `Header`, 28 | title: `TV shows` 29 | }, 30 | { 31 | type: `SelectDirectory`, 32 | name: `/tv`, 33 | description: `Select the folder where we save your TV shows` 34 | }, 35 | { 36 | type: `Header`, 37 | title: `Network` 38 | }, 39 | { 40 | type: `Text`, 41 | text: `Select the network to use for downloading content. By default this is your home network, 42 | but you may want to use a VPN network for extra security and privacy.` 43 | }, 44 | { 45 | type: `SelectNetwork`, 46 | name: `primary`, 47 | description: `Select BitTorrent network` 48 | } 49 | ], 50 | properties: [ 51 | { 52 | type: `Directory`, 53 | name: `/config`, 54 | style: `boot`, 55 | backup: true 56 | }, 57 | { 58 | type: `Directory`, 59 | name: `/downloads`, 60 | use: `downloads`, 61 | shares: [ 62 | { 63 | name: `/`, 64 | description: `Downloads` 65 | } 66 | ] 67 | }, 68 | { 69 | type: `Directory`, 70 | name: `/tv`, 71 | style: `store` 72 | }, 73 | { 74 | type: `Environment`, 75 | name: `HOME`, 76 | value: `/root` 77 | }, 78 | { 79 | type: `Environment`, 80 | name: `LANGUAGE`, 81 | value: `en_US.UTF-8` 82 | }, 83 | { 84 | type: `Environment`, 85 | name: `TERM`, 86 | value: `xterm` 87 | }, 88 | { 89 | type: `Environment`, 90 | name: `XDG_CONFIG_HOME`, 91 | value: `/config/xdg` 92 | }, 93 | { 94 | type: `Environment`, 95 | name: `SONARR_BRANCH`, 96 | value: `phantom-develop` 97 | }, 98 | { 99 | type: `Port`, 100 | name: `8989/tcp`, 101 | port: 8989, 102 | protocol: `TCP`, 103 | web: { 104 | type: `newtab`, 105 | path: `/` 106 | } 107 | }, 108 | { 109 | type: `Network`, 110 | name: `primary`, 111 | value: `home` 112 | }, 113 | { 114 | type: `Network`, 115 | name: `secondary`, 116 | value: `home` 117 | } 118 | ], 119 | secondary: [ 120 | { 121 | image: `linuxserver/transmission`, 122 | delay: 0, 123 | properties: [ 124 | { 125 | type: `Directory`, 126 | name: `/config`, 127 | style: `boot`, 128 | backup: true 129 | }, 130 | { 131 | type: `Directory`, 132 | name: `/downloads`, 133 | use: `downloads` 134 | } 135 | ] 136 | }, 137 | { 138 | image: `linuxserver/nzbget`, 139 | delay: 0, 140 | properties: [ 141 | { 142 | type: `Directory`, 143 | name: `/config`, 144 | style: `boot`, 145 | backup: true 146 | }, 147 | { 148 | type: `Directory`, 149 | name: `/downloads`, 150 | use: `downloads` 151 | }, 152 | { 153 | type: `File`, 154 | name: `/config/custom-cont-init.d/disable-password`, 155 | mode: 0o777, 156 | value: `sed -i "s/ControlPassword=.*/ControlPassword=/" /config/nzbget.conf` 157 | } 158 | ] 159 | } 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /app/skeletons/builtin/syncthing.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `SyncThing`, 3 | description: `Continuous and secure file synchronization between multiple computers anywhere`, 4 | image: `linuxserver/syncthing`, 5 | uuid: `010B271D-68A8-4E03-8612-1E94F1DD9FA3`, 6 | tags: [ 'Sync', 'Shares' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Sync` 11 | }, 12 | { 13 | type: `Text`, 14 | text: `Select which folders you want to synchronize. The specific synchronization is controlled in the app itself.` 15 | }, 16 | { 17 | type: `SelectShares`, 18 | name: `/Sync/Folders`, 19 | description: `Select folders` 20 | }, 21 | { 22 | type: `Text`, 23 | text: `Select which applications you want to synchronize.` 24 | }, 25 | { 26 | type: `SelectBackups`, 27 | name: `/Sync/Applications`, 28 | description: `Select applications` 29 | } 30 | ], 31 | properties: [ 32 | { 33 | type: `Environment`, 34 | name: `PUID`, 35 | value: `0` 36 | }, 37 | { 38 | type: `Environment`, 39 | name: `HOME`, 40 | value: `/` 41 | }, 42 | { 43 | type: `Directory`, 44 | name: `/Sync` 45 | }, 46 | { 47 | type: `Directory`, 48 | name: `/Sync/Folders`, 49 | style: `temp` 50 | }, 51 | { 52 | type: `Directory`, 53 | name: `/Sync/Applications`, 54 | style: `temp` 55 | }, 56 | { 57 | type: `Directory`, 58 | name: `/config`, 59 | style: `boot` 60 | }, 61 | { 62 | type: `Port`, 63 | name: `8384/tcp`, 64 | port: 8384, 65 | protocol: `TCP`, 66 | web: { 67 | tab: `inline`, 68 | path: `/` 69 | }, 70 | mdns: { 71 | type: `_http._tcp` 72 | } 73 | }, 74 | { 75 | type: `Port`, 76 | name: `22000/tcp`, 77 | port: 22000, 78 | protocol: `TCP`, 79 | }, 80 | { 81 | type: `Port`, 82 | name: `21027/udp`, 83 | port: 21027, 84 | protocol: `UDP`, 85 | }, 86 | { 87 | type: `Network`, 88 | name: `primary`, 89 | value: `home` 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /app/skeletons/builtin/torrelay.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Tor Relay`, 3 | description: `Create a Tor middle/guard relay node`, 4 | image: `registry.minkebox.net/minkebox/tor-relay`, 5 | uuid: `1852E3FB-091D-4170-AB40-DC3F8D84F19E`, 6 | tags: [ 'Tor', 'Security', 'Networking' ], 7 | actions: [ 8 | { 9 | type: `Help`, 10 | text: `A guard relay is the first relay in the chain of 3 relays building a Tor circuit. A middle relay is neither a 11 | guard nor an exit, but acts as the second hop between the two. To become a guard, a relay has to be stable and fast 12 | (at least 2MByte/s) otherwise it will remain a middle relay.

13 | Guard and middle relays usually do not receive abuse complaints. All relays will be listed in the public list 14 | of Tor relays, so may be blocked by certain services that don't understand how Tor works or deliberately want to 15 | censor Tor users. If you are running a relay from home and have one static IP, you may want to consider running a 16 | bridge instead so that your non-Tor traffic doesn't get blocked as though it's coming from Tor. If you have a 17 | dynamic IP address or multiple static IPs, this isn't as much of an issue.

18 | A non-exit Tor relay requires minimal maintenance efforts and bandwidth usage can be highly customized in 19 | the tor configuration. The so called "exit policy" of the relay decides if it is a relay allowing clients to exit 20 | or not. A non-exit relay does not allow exiting in its exit policy.` 21 | }, 22 | { 23 | type: `Header`, 24 | title: `Configuration` 25 | }, 26 | { 27 | type: `EditEnvironment`, 28 | name: `NICKNAME`, 29 | description: `Nickname for the node (1-19 characters)`, 30 | initValue: `MinkeBoxTor`, 31 | validate: `[a-zA-Z0-9]{1,19}` 32 | }, 33 | { 34 | type: `EditEnvironment`, 35 | name: `EMAIL`, 36 | description: `Public email associated with this node` 37 | }, 38 | { 39 | type: `EditEnvironment`, 40 | name: `BANDWIDTH`, 41 | description: `Available bandwidth (in MBits)` 42 | }, 43 | { 44 | type: `EditEnvironment`, 45 | name: `ORPort`, 46 | description: `Select the port number for incoming connections`, 47 | initValue: `{{__RANDOMPORTS(1)}}` 48 | }, 49 | { 50 | type: `SelectNetwork`, 51 | name: `primary`, 52 | description: `Select the network used to connect to the global Tor network` 53 | }, 54 | { 55 | type: `Header`, 56 | title: `Advanced`, 57 | visible: `property.Advanced` 58 | }, 59 | { 60 | type: `EditEnvironmentAsCheckbox`, 61 | name: `EXIT`, 62 | description: `Allow traffic to exit from this relay. Use with caution.` 63 | } 64 | ], 65 | properties: [ 66 | { 67 | type: `Feature`, 68 | name: `ddns` 69 | }, 70 | { 71 | type: `Directory`, 72 | name: `/root/.tor`, 73 | style: `store`, 74 | backup: true 75 | }, 76 | { 77 | type: `Directory`, 78 | name: `/var/lib/tor`, 79 | style: `store`, 80 | backup: true 81 | }, 82 | { 83 | type: `File`, 84 | name: `/etc/tor/torrc`, 85 | value: `{{torrc}}`, 86 | style: `boot` 87 | }, 88 | { 89 | type: `Port`, 90 | name: `ORPort`, 91 | protocol: `TCP`, 92 | port: `ORPort`, 93 | nat: true 94 | }, 95 | { 96 | type: `Network`, 97 | name: `primary`, 98 | value: `home`, 99 | bandwidth: `{{BANDWIDTH}}` 100 | } 101 | ], 102 | monitor: { 103 | cmd: `echo $(iptables -L RX -x -v -n | awk 'NR == 3 {print $2}') $(iptables -L TX -x -v -n | awk 'NR == 3 {print $2}')`, 104 | target: 'helper', 105 | init: ` 106 |

107 | 108 |
109 | 112 | ` 113 | }, 114 | constants: [ 115 | { 116 | name: `torrc`, 117 | value: ` 118 | ContactInfo {{EMAIL}} 119 | ORPort {{ORPort}} 120 | Address {{__GLOBALNAME}} 121 | Nickname {{NICKNAME}} 122 | ExitRelay {{EXIT ? 1 : 0}} 123 | SocksPort 0 124 | ControlSocket 0 125 | BandwidthRate {{BANDWIDTH}}MBits 126 | BandwidthBurst {{BANDWIDTH}}MBits 127 | RelayBandwidthRate {{BANDWIDTH}}MBits 128 | RelayBandwidthBurst {{BANDWIDTH}}MBits 129 | ` 130 | } 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /app/skeletons/builtin/torservices.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Tor Services`, 3 | description: `Create a Tor service node to connect your applications to the Tor network.`, 4 | image: `registry.minkebox.net/minkebox/tor-services`, 5 | uuid: `8A498764-EA8D-4366-9DC4-B7EC8F0E215A`, 6 | tags: [ 'Tor', 'Security', 'Networking' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Configuration` 11 | }, 12 | { 13 | type: `EditEnvironment`, 14 | name: `BANDWIDTH`, 15 | description: `Available bandwidth (in MBits)` 16 | }, 17 | { 18 | type: `Header`, 19 | title: `Networks` 20 | }, 21 | { 22 | type: `Text`, 23 | text: `Select which networks Tor will use. You probably want home for both in most cases. If not, the source network should be the network which is shared with the websites and applications you wish to proxy. 24 | The target network should be the network use to connect to the rest of the global Tor network.` 25 | }, 26 | { 27 | type: `SelectNetwork`, 28 | name: `secondary`, 29 | description: `Select the source network Tor will find services to publish from` 30 | }, 31 | { 32 | type: `SelectNetwork`, 33 | name: `primary`, 34 | description: `Select the target network Tor will use to connect to the global Tor network` 35 | }, 36 | { 37 | type: `Header`, 38 | title: `Websites` 39 | }, 40 | { 41 | type: `SelectWebsites`, 42 | name: `WEBSITES`, 43 | description: `Select the websites you wish to publish on Tor`, 44 | pattern: `{{V[4]}}#{{__LOOKUPIP(V[1])}}#{{V[2]}}#{{V[1]}}`, 45 | join: ` ` 46 | }, 47 | { 48 | type: `Header`, 49 | title: `Services`, 50 | }, 51 | { 52 | type: `Text`, 53 | text: `Your published apps, with associated Onion addresses, are listed below.` 54 | }, 55 | { 56 | type: `ShowFileAsTable`, 57 | name: `/var/lib/tor/services`, 58 | description: `Onion Addresses`, 59 | headers: [ 60 | { name: `Host` }, 61 | { name: `Onion` } 62 | ] 63 | } 64 | ], 65 | properties: [ 66 | { 67 | type: `Feature`, 68 | name: `+NET_ADMIN` 69 | }, 70 | { 71 | type: `Environment`, 72 | name: `WEBSITES` 73 | }, 74 | { 75 | type: `Directory`, 76 | name: `/root/.tor`, 77 | style: `store`, 78 | backup: true 79 | }, 80 | { 81 | type: `Directory`, 82 | name: `/var/lib/tor`, 83 | style: `store`, 84 | backup: true 85 | }, 86 | { 87 | type: `File`, 88 | name: `/etc/tor/torrc.tmpl`, 89 | value: `{{torrc}}`, 90 | style: `boot` 91 | }, 92 | { 93 | type: `Network`, 94 | name: `primary`, 95 | value: `home` 96 | }, 97 | { 98 | type: `Network`, 99 | name: `secondary`, 100 | value: `home`, 101 | create: true 102 | } 103 | ], 104 | monitor: { 105 | cmd: `echo $(iptables -L RX -x -v -n | awk 'NR == 3 {print $2}') $(iptables -L TX -x -v -n | awk 'NR == 3 {print $2}')`, 106 | target: `helper`, 107 | init: ` 108 |
109 | 110 |
111 | 114 | ` 115 | }, 116 | constants: [ 117 | { 118 | name: `torrc`, 119 | value: ` 120 | ExitRelay 0 121 | SocksPort 0 122 | ControlSocket 0 123 | BandwidthRate {{BANDWIDTH}}MBits 124 | BandwidthBurst {{BANDWIDTH}}MBits 125 | ` 126 | } 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /app/skeletons/builtin/upnpmonitor.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `UPnP Monitor`, 3 | description: `Monitor the open UPnP ports on your firewall.`, 4 | image: `registry.minkebox.net/minkebox/upnpmonitor`, 5 | uuid: `83271E99-6703-4739-B347-38C1B52B7706`, 6 | tags: [ 'Networking', 'Utilities' ], 7 | actions: [ 8 | { 9 | type: `Header`, 10 | title: `Active Ports` 11 | }, 12 | { 13 | type: `ShowFileAsTable`, 14 | name: `/tmp/active.display`, 15 | description: `Ports currently active on the firewall`, 16 | headers: [ 17 | { name: `Firewall Port` }, 18 | { name: `Target Host` }, 19 | { name: `Target Port` }, 20 | { name: `Note` } 21 | ] 22 | }, 23 | { 24 | type: `Header`, 25 | title: `Inactive Ports` 26 | }, 27 | { 28 | type: `ShowFileAsTable`, 29 | name: `/tmp/inactive.display`, 30 | description: `Any port that once was active on the firewall`, 31 | headers: [ 32 | { name: `Firewall Port` }, 33 | { name: `Target Host` }, 34 | { name: `Target Port` }, 35 | { name: `Note` } 36 | ] 37 | }, 38 | { 39 | type: `Header`, 40 | title: `Network`, 41 | visible: `property.Advanced` 42 | }, 43 | { 44 | type: `Text`, 45 | text: `Select which network this application will use. You probably want home unless this application is being used on a private network.` 46 | }, 47 | { 48 | type: `SelectNetwork`, 49 | name: `primary`, 50 | description: `Select network` 51 | } 52 | ], 53 | properties: [ 54 | { 55 | type: `File`, 56 | name: `/tmp/active.display`, 57 | style: 'boot' 58 | }, 59 | { 60 | type: `File`, 61 | name: `/tmp/inactive.display`, 62 | style: 'boot' 63 | }, 64 | { 65 | type: `Network`, 66 | name: `primary`, 67 | value: `home` 68 | } 69 | ], 70 | monitor: { 71 | cmd: `wc -l /tmp/active /tmp/inactive | sed "s/^ *\\([0-9]*\\).*$/\\1/" | head -n2`, 72 | init: ` 73 | 78 |
79 |
0
Active Ports
80 |
0
Inactive Ports
81 |
82 | ` 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/skeletons/builtin/wiki.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Wiki.js`, 3 | description: `A modern, lightweight and powerful wiki app`, 4 | images: { 5 | x64: `requarks/wiki:2`, 6 | arm64: `requarks/wiki:2.1-arm` 7 | }, 8 | uuid: `828D4811-E001-409B-9BDC-A440B9A1A736`, 9 | tags: [ 10 | 'Wiki', 11 | 'Web' 12 | ], 13 | delay: 10, 14 | actions: [ 15 | { 16 | type: `Header`, 17 | title: `Nothing to configuration` 18 | } 19 | ], 20 | properties: [ 21 | { 22 | type: `Environment`, 23 | name: `DB_TYPE`, 24 | value: `postgres` 25 | }, 26 | { 27 | type: `Environment`, 28 | name: `DB_HOST`, 29 | value: `localhost` 30 | }, 31 | { 32 | type: `Environment`, 33 | name: `DB_PORT`, 34 | value: 5432 35 | }, 36 | { 37 | type: `Environment`, 38 | name: `DB_USER`, 39 | value: `wikijs` 40 | }, 41 | { 42 | type: `Environment`, 43 | name: `DB_PASS`, 44 | value: `wikijsrocks` 45 | }, 46 | { 47 | type: `Environment`, 48 | name: `DB_NAME`, 49 | value: `wiki` 50 | }, 51 | { 52 | type: `Port`, 53 | name: `3000/tcp`, 54 | port: 3000, 55 | protocol: `TCP`, 56 | web: { 57 | type: 'newtab', 58 | path: '/' 59 | } 60 | }, 61 | { 62 | type: `Network`, 63 | name: `primary`, 64 | value: `home` 65 | } 66 | ], 67 | secondary: [ 68 | { 69 | image: `postgres:11-alpine`, 70 | delay: 0, 71 | properties: [ 72 | { 73 | type: `Environment`, 74 | name: `POSTGRES_DB`, 75 | value: `wiki` 76 | }, 77 | { 78 | type: `Environment`, 79 | name: `POSTGRES_USER`, 80 | value: `wikijs` 81 | }, 82 | { 83 | type: `Environment`, 84 | name: `POSTGRES_PASSWORD`, 85 | value: `wikijsrocks` 86 | }, 87 | { 88 | type: 'Directory', 89 | name: '/var/lib/postgresql/data', 90 | shares: [ 91 | { name: '/', description: 'Wiki.js Postgres DB' } 92 | ], 93 | backup: true 94 | } 95 | ] 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /app/skeletons/builtin/wireguardclient.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Wireguard Peer`, 3 | description: `Fast, Modern, Secure VPN Tunnel`, 4 | image: `registry.minkebox.net/minkebox/wireguardclient`, 5 | uuid: `4F3784DF-8426-4051-AAA2-D4551B055D1B`, 6 | tags: [ 'VPN', 'Security', 'Networking' ], 7 | actions: [ 8 | { 9 | type: `EditFile`, 10 | description: `Drop or enter your configuration here`, 11 | name: `/etc/wireguard/wg0.conf` 12 | } 13 | ], 14 | properties: [ 15 | { 16 | type: `Feature`, 17 | name: `privileged` 18 | }, 19 | { 20 | type: `Feature`, 21 | name: `ddns` 22 | }, 23 | { 24 | type: `Directory`, 25 | name: `/etc/wireguard`, 26 | style: `boot`, 27 | }, 28 | { 29 | type: `Directory`, 30 | name: `/leases`, 31 | style: `boot` 32 | }, 33 | { 34 | type: `File`, 35 | name: `/etc/wireguard/wg0.conf`, 36 | style: `boot` 37 | }, 38 | { 39 | type: `Network`, 40 | name: `primary`, 41 | value: `home` 42 | }, 43 | { 44 | type: `Network`, 45 | name: `secondary`, 46 | value: `__create` 47 | } 48 | ], 49 | monitor: { 50 | cmd: `echo $(iptables -L RX -x -v -n | awk 'NR == 3 {print $2}') $(iptables -L TX -x -v -n | awk 'NR == 3 {print $2}')`, 51 | target: `helper`, 52 | init: ` 53 |
54 | 55 |
56 | 59 | ` 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/skeletons/builtin/wordpress.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Wordpress`, 3 | description: `Open source software you can use to create a beautiful website, blog, or app.`, 4 | uuid: `9A6C3BAB-68EC-49BB-9914-D4F7AAFB7CA6`, 5 | image: `wordpress:latest`, 6 | tags: [ 7 | `Blog`, 8 | `Web` 9 | ], 10 | delay: 5, 11 | actions: [ 12 | { 13 | type: `Text`, 14 | text: `All configuration takes place in the Wordpress app.` 15 | }, 16 | { 17 | type: `SetEnvironment`, 18 | name: `DB_NAME`, 19 | value: `wp` 20 | }, 21 | { 22 | type: `SetEnvironment`, 23 | name: `DB_ROOT_PASSWORD`, 24 | value: `wp-password` 25 | } 26 | ], 27 | properties: [ 28 | { 29 | type: `Environment`, 30 | name: `WORDPRESS_DB_HOST`, 31 | value: `127.0.0.1` 32 | }, 33 | { 34 | type: `Environment`, 35 | name: `WORDPRESS_DB_NAME`, 36 | value: `{{DB_NAME}}` 37 | }, 38 | { 39 | type: `Environment`, 40 | name: `WORDPRESS_DB_USER`, 41 | value: `root` 42 | }, 43 | { 44 | type: `Environment`, 45 | name: `WORDPRESS_DB_PASSWORD`, 46 | value: `{{DB_ROOT_PASSWORD}}` 47 | }, 48 | { 49 | type: `File`, 50 | name: `/usr/local/etc/php/conf.d/conf.ini` 51 | }, 52 | { 53 | type: `Directory`, 54 | name: `/var/www/html`, 55 | use: `wp-app` 56 | }, 57 | { 58 | type: `Port`, 59 | name: `80/tcp`, 60 | protocol: `TCP`, 61 | port: 80, 62 | web: { 63 | path: `/`, 64 | tab: `newtab` 65 | } 66 | }, 67 | { 68 | type: `Network`, 69 | name: `primary`, 70 | value: `home` 71 | } 72 | ], 73 | secondary: [ 74 | { 75 | image: `mysql:latest`, 76 | properties: [ 77 | { 78 | type: `Arguments`, 79 | value: [ 80 | `--default_authentication_plugin=mysql_native_password`, 81 | `--character-set-server=utf8mb4`, 82 | `--collation-server=utf8mb4_unicode_ci` 83 | ] 84 | }, 85 | { 86 | type: `Environment`, 87 | name: `MYSQL_DATABASE`, 88 | value: `{{DB_NAME}}` 89 | }, 90 | { 91 | type: `Environment`, 92 | name: `MYSQL_ROOT_PASSWORD`, 93 | value: `{{DB_ROOT_PASSWORD}}` 94 | }, 95 | { 96 | type: `Directory`, 97 | name: `/docker-entrypoint-initdb.d` 98 | }, 99 | { 100 | type: `Directory`, 101 | name: `/var/lib/mysql`, 102 | use: `db_data` 103 | } 104 | ] 105 | } 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /app/skeletons/disabled/ntopng.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `ntopng`, 3 | description: `High-Speed Web-based Traffic Analysis and Flow Collection`, 4 | image: `registry.minkebox.net/minkebox/ntopng`, 5 | actions: [ 6 | ], 7 | properties: [ 8 | { 9 | type: `Feature`, 10 | name: `privileged` 11 | }, 12 | { 13 | type: `Arguments`, 14 | value: [ 15 | `--interface`, 16 | `br0`, 17 | `--disable-autologout`, 18 | `--disable-login`, 19 | `1`, 20 | `--dns-mode`, 21 | `1`, 22 | `--community`, 23 | `--local-networks`, 24 | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` 25 | ] 26 | }, 27 | { 28 | type: `Port`, 29 | name: `3000/tcp`, 30 | port: 3000, 31 | protocol: `TCP`, 32 | web: { 33 | type: `newtab`, 34 | path: `/` 35 | } 36 | }, 37 | { 38 | type: `Network`, 39 | name: `primary`, 40 | value: `host` 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /app/skeletons/disabled/privatenetwork.skeleton: -------------------------------------------------------------------------------- 1 | { 2 | name: `Private Network`, 3 | description: `Secure, private network between you and a friend`, 4 | image: `registry.minkebox.net/minkebox/privatenetwork`, 5 | tags: [ 'Private Networks' ], 6 | actions: [ 7 | { 8 | type: `Header`, 9 | title: `Configure` 10 | }, 11 | { 12 | type: `Text`, 13 | text: `A secure network requires a Server and a Client. The Server creates the network and the Client connects.` 14 | }, 15 | { 16 | type: `Environment`, 17 | name: `MODE`, 18 | options: [ 19 | { 20 | name: `Server`, 21 | value: `SERVER` 22 | }, 23 | { 24 | name: `Client`, 25 | value: `CLIENT` 26 | } 27 | ], 28 | description: `Select network mode` 29 | }, 30 | { 31 | type: `Header`, 32 | title: `Server`, 33 | visible: `property['Environment#MODE'] == 'SERVER'` 34 | }, 35 | { 36 | type: `DownloadFile`, 37 | description: `Generated client private network configuration file`, 38 | name: `/etc/config.ovpn` 39 | }, 40 | { 41 | type: `Header`, 42 | title: `Client`, 43 | visible: `property['Environment#MODE'] == 'CLIENT'` 44 | }, 45 | { 46 | type: `EditFile`, 47 | description: `Drop your private network configuration file here`, 48 | name: `/etc/config.ovpn`, 49 | enabled: 'property.FirstUse' 50 | } 51 | ], 52 | properties: [ 53 | { 54 | type: `Feature`, 55 | name: `+NET_ADMIN` 56 | }, 57 | { 58 | type: `Feature`, 59 | name: `ddns` 60 | }, 61 | { 62 | type: `Environment`, 63 | name: `MODE`, 64 | value: `CLIENT` 65 | }, 66 | { 67 | type: `Directory`, 68 | name: `/etc/status`, 69 | style: 'boot', 70 | }, 71 | { 72 | type: `Directory`, 73 | name: `/etc/openvpn`, 74 | style: 'boot' 75 | }, 76 | { 77 | type: `File`, 78 | name: `/etc/config.ovpn`, 79 | style: 'boot' 80 | }, 81 | { 82 | type: `Network`, 83 | name: `primary`, 84 | value: `home` 85 | }, 86 | { 87 | type: `Network`, 88 | name: `secondary`, 89 | value: `__create` 90 | } 91 | ], 92 | monitor: { 93 | cmd: `ifconfig tun0 | grep "RX bytes" | tr '\\n' ' ' | sed "s/^.*RX bytes:\\([0-9]*\\).*TX bytes:\\([0-9]*\\).*$/\\1 \\2/"`, 94 | polling: 5, 95 | parser: ` 96 | const rxtx = input.split(' '); 97 | if (rxtx.length == 2) { 98 | const now = Date.now() / 1000; 99 | if (!state) { 100 | state = { 101 | rx: [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ], 102 | tx: [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ], 103 | last: rxtx, 104 | then: now - 1 105 | }; 106 | } 107 | state.rx.shift(); 108 | state.tx.shift(); 109 | state.rx.push((rxtx[0] - state.last[0]) * 8 / 1000000 / (now - state.then)); 110 | state.tx.push((rxtx[1] - state.last[1]) * 8 / 1000000 / (now - state.then)); 111 | output.rx = state.rx.slice(-1)[0].toFixed(1); 112 | output.tx = state.tx.slice(-1)[0].toFixed(1); 113 | state.last = rxtx; 114 | state.then = now; 115 | } 116 | output.graph = { 117 | traffic: { 118 | type: 'line', 119 | data: { 120 | labels: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63], 121 | datasets: [ 122 | { label: 'RX', data: state.rx, borderColor: '#88cce7', backgroundColor: '#88cce7', fill: false, pointRadius: 0 }, 123 | { label: 'TX', data: state.tx, borderColor: '#41b376', backgroundColor: '#41b376', fill: false, pointRadius: 0 } 124 | ] 125 | }, 126 | options: { 127 | animation: { duration: 0 }, 128 | maintainAspectRatio: false, 129 | adaptive: true, 130 | title: { display: true, text: 'Bandwidth (Mb/s)' }, 131 | scales: { 132 | xAxes: [{ 133 | display: false 134 | }], 135 | yAxes: [{ 136 | ticks: { beginAtZero: true } 137 | }] 138 | } 139 | } 140 | } 141 | }; 142 | `, 143 | minwidth: '400px', 144 | template: `{{{graph.traffic}}}` 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/test/environment.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Environment', async function() { 4 | 5 | require('./fixture/system.fixture')(); 6 | 7 | describe('MinkeApp.expandEnvironment', async function() { 8 | 9 | require('./fixture/minkeapp.fixture')(); 10 | 11 | it('Empty environment', async function() { 12 | const env = await this.app.expandEnvironment({ 13 | }); 14 | assert.equal(Object.keys(env).length, 0); 15 | }); 16 | 17 | it('Single, null enviroment entry', async function() { 18 | const env = await this.app.expandEnvironment({ 19 | thing: {} 20 | }); 21 | assert.equal(env.thing.value, ''); 22 | }); 23 | 24 | it('Single, default enviroment entry', async function() { 25 | const env = await this.app.expandEnvironment({ 26 | thing: { value: '"thang"' } 27 | }); 28 | assert.equal(env.thing.value, 'thang'); 29 | }); 30 | 31 | it('System property: __MACADDRESS', async function() { 32 | const env = await this.app.expandEnvironment({ 33 | thing: { value: '__MACADDRESS' } 34 | }); 35 | assert.equal(env.thing.value, '5A:92:20:46:9E:8B'); 36 | }); 37 | 38 | describe('Strings', function() { 39 | 40 | it('"abc" + __MACADDRESS + "def"', async function() { 41 | const env = await this.app.expandEnvironment({ 42 | thing: { value: '"abc" + __MACADDRESS + "def"' } 43 | }); 44 | assert.equal(env.thing.value, 'abc5A:92:20:46:9E:8Bdef'); 45 | }); 46 | 47 | }); 48 | 49 | describe('Numbers', function() { 50 | 51 | it('1 + 2', async function() { 52 | const env = await this.app.expandEnvironment({ 53 | thing: { value: '1 + 2' } 54 | }); 55 | assert.equal(env.thing.value, 3); 56 | }); 57 | 58 | }); 59 | 60 | describe('Bools', function() { 61 | 62 | it('true && true', async function() { 63 | const env = await this.app.expandEnvironment({ 64 | thing: { value: 'true && true' } 65 | }); 66 | assert.equal(env.thing.value, true); 67 | }); 68 | 69 | }); 70 | 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /app/test/expandstring.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Expand', async function() { 4 | 5 | require('./fixture/system.fixture')(); 6 | 7 | describe('MinkeApp.expandString', async function() { 8 | 9 | require('./fixture/minkeapp.fixture')(); 10 | 11 | it('Empty string', async function() { 12 | const str = await this.app.expandString(''); 13 | assert.equal(str, ''); 14 | }); 15 | 16 | it('Simple string', async function() { 17 | const str = await this.app.expandString('hello'); 18 | assert.equal(str, 'hello'); 19 | }); 20 | 21 | it('Quoted string', async function() { 22 | const str = await this.app.expandString('"hello"'); 23 | assert.equal(str, '"hello"'); 24 | }); 25 | 26 | it('{{__MACADDRESS}}', async function() { 27 | const str = await this.app.expandString('{{__MACADDRESS}}'); 28 | assert.equal(str, '5A:92:20:46:9E:8B'); 29 | }); 30 | 31 | it('abc{{__MACADDRESS}}def', async function() { 32 | const str = await this.app.expandString('abc{{__MACADDRESS}}def'); 33 | assert.equal(str, 'abc5A:92:20:46:9E:8Bdef'); 34 | }); 35 | 36 | describe('Functions', function() { 37 | 38 | it('{{__RANDOMHEX(16)}}', async function() { 39 | const str = await this.app.expandString('{{__RANDOMHEX(16)}}'); 40 | assert.equal(str.length, 16); 41 | }); 42 | 43 | it('{{__RANDOMPORTS(1)}}', async function() { 44 | const str = await this.app.expandString('{{__RANDOMPORTS(1)}}'); 45 | assert.notEqual(str, 0); 46 | assert.equal(parseInt(str), str); 47 | }); 48 | }); 49 | 50 | describe('Newlines', function() { 51 | 52 | it('abc\\ndef', async function() { 53 | const str = await this.app.expandString('abc\ndef'); 54 | assert.equal(str, 'abc\ndef'); 55 | }); 56 | 57 | it('{{__MACADDRESS}}\\n', async function() { 58 | const str = await this.app.expandString('{{__MACADDRESS}}\n'); 59 | assert.equal(str, '5A:92:20:46:9E:8B\n'); 60 | }); 61 | 62 | it('{{"\\n"}}', async function() { 63 | const str = await this.app.expandString('{{"\n"}}'); 64 | assert.equal(str, '\n'); 65 | }); 66 | 67 | }); 68 | 69 | describe('Expression', function() { 70 | 71 | it('{{1 + 2}}', async function() { 72 | const str = await this.app.expandString('{{1 + 2}}'); 73 | assert.equal(str, 3); 74 | }); 75 | 76 | it('{{"1" + "2"}}', async function() { 77 | const str = await this.app.expandString('{{"1" + "2"}}'); 78 | assert.equal(str, '12'); 79 | }); 80 | 81 | }) 82 | 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /app/test/filesystem.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Filesystem', async function() { 4 | 5 | require('./fixture/system.fixture')(); 6 | require('./fixture/minkeapp.fixture')(); 7 | require('./fixture/filesystem.fixture')(); 8 | 9 | describe('makeFile', function() { 10 | 11 | it('simple file', async function() { 12 | await this.fs._makeFile(this.app, { 13 | target: '/a/file/somewhere', 14 | src: '/a/file/somewhere', 15 | value: 'hello', 16 | mode: 0o666 17 | }); 18 | assert.ok(this.fs.mocks.mkdirSync.called); 19 | assert.ok(this.fs.mocks.writeFileSync.called); 20 | assert.equal(this.fs.mocks.writeFileSync.firstCall.args[1], 'hello'); 21 | }); 22 | 23 | it('substitute file contents', async function() { 24 | this.app._vars['/a/file/somewhere'] = { type: 'String', value: 'goodbye' }; 25 | await this.fs._makeFile(this.app, { 26 | target: '/a/file/somewhere', 27 | src: '/a/file/somewhere', 28 | value: 'hello', 29 | mode: 0o666 30 | }); 31 | assert.ok(this.fs.mocks.mkdirSync.called); 32 | assert.ok(this.fs.mocks.writeFileSync.called); 33 | assert.equal(this.fs.mocks.writeFileSync.firstCall.args[1], 'goodbye'); 34 | }); 35 | 36 | it('substitute file default contents', async function() { 37 | this.app._vars['/a/file/somewhere'] = { type: 'String', defaultValue: 'goodbye' }; 38 | await this.fs._makeFile(this.app, { 39 | target: '/a/file/somewhere', 40 | src: '/a/file/somewhere', 41 | value: 'hello', 42 | mode: 0o666 43 | }); 44 | assert.ok(this.fs.mocks.mkdirSync.called); 45 | assert.ok(this.fs.mocks.writeFileSync.called); 46 | assert.equal(this.fs.mocks.writeFileSync.firstCall.args[1], 'goodbye'); 47 | }); 48 | 49 | it('substitute file contents with variable', async function() { 50 | this.app._vars.stuff = { type: 'String', value: '-' }; 51 | this.app._vars['/a/file/somewhere'] = { type: 'String', defaultValue: 'good{{stuff}}bye' }; 52 | await this.app.createJS(); 53 | await this.fs._makeFile(this.app, { 54 | target: '/a/file/somewhere', 55 | src: '/a/file/somewhere', 56 | value: 'hello', 57 | mode: 0o666 58 | }); 59 | assert.ok(this.fs.mocks.mkdirSync.called); 60 | assert.ok(this.fs.mocks.writeFileSync.called); 61 | assert.equal(this.fs.mocks.writeFileSync.firstCall.args[1], 'good-bye'); 62 | }); 63 | 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /app/test/fixture/dns.fixture.js: -------------------------------------------------------------------------------- 1 | 2 | const sinon = require('sinon'); 3 | const mock = require('mock-require'); 4 | 5 | module.exports = function() { 6 | 7 | beforeEach(function() { 8 | mock('fs', { 9 | writeFileSync: sinon.fake() 10 | }); 11 | mock('child_process', { 12 | spawnSync: sinon.fake() 13 | }); 14 | const DNS = mock.reRequire('../../DNS'); 15 | mock.stop('fs'); 16 | mock.stop('child_process'); 17 | this.dns = DNS; 18 | }); 19 | 20 | afterEach(function() { 21 | delete this.dns; 22 | }); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/test/fixture/filesystem.fixture.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | const mock = require('mock-require'); 3 | 4 | module.exports = function() { 5 | 6 | beforeEach(async function() { 7 | const mocks = { 8 | mkdirSync: sinon.spy(), 9 | existsSync: sinon.fake.returns(false), 10 | writeFileSync: sinon.spy() 11 | }; 12 | mock('fs', mocks); 13 | const Filesystem = mock.reRequire('../../Filesystem'); 14 | mock.stop('fs'); 15 | this.fs = Filesystem.create(this.app); 16 | this.fs.mocks = mocks; 17 | }); 18 | 19 | afterEach(function() { 20 | delete this.fs; 21 | }); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/test/fixture/minkeapp.fixture.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | const mock = require('mock-require'); 3 | 4 | module.exports = function() { 5 | 6 | beforeEach(async function() { 7 | mock('fs', { 8 | mkdirSync: sinon.fake(), 9 | readdirSync: sinon.fake.returns([]), 10 | existsSync: sinon.fake.returns(false) 11 | }); 12 | const MinkeApp = mock.reRequire('../../MinkeApp'); 13 | mock.stop('fs'); 14 | 15 | MinkeApp._network = { network: {} }; 16 | const app = await (new MinkeApp().createFromJSON({ binds: [], vars: '', networks: { primary: { name: 'home'} , secondary: { name: 'none' } } })); 17 | app._globalId = '1FB44E7C-7E63-4739-A1F6-569220469E8B'; 18 | await app.createJS(); 19 | this.app = app; 20 | }); 21 | 22 | afterEach(function() { 23 | delete this.app; 24 | }); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/test/fixture/minkesetup.fixture.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | const mock = require('mock-require'); 3 | 4 | module.exports = function() { 5 | 6 | beforeEach(async function() { 7 | let reason = null; 8 | const mocks = { 9 | readFileSync: sinon.stub().withArgs('/minke/minke-restart-reason').callsFake(function(name) { return reason; }), 10 | writeFileSync: sinon.stub().withArgs('/minke/minke-restart-reason').callsFake(function(name, v) { reason = v; }) 11 | }; 12 | mock('fs', mocks); 13 | const MinkeSetup = mock.reRequire('../../MinkeSetup'); 14 | mock.stop('fs'); 15 | const app = new MinkeSetup(null, {}, {}); 16 | this.MinkeSetup = MinkeSetup; 17 | this.app = app; 18 | }); 19 | 20 | afterEach(function() { 21 | delete this.MinkeSetup; 22 | delete this.app; 23 | }); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/test/fixture/skeletons.fixture.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | const mock = require('mock-require'); 3 | const FS = require('fs'); 4 | 5 | 6 | module.exports = function() { 7 | 8 | mock('fs', { 9 | mkdirSync: sinon.fake(), 10 | readFileSync: FS.readFileSync, 11 | readdirSync: FS.readdirSync, 12 | existsSync: FS.existsSync, 13 | lstatSync: FS.lstatSync 14 | }); 15 | const Skeletons = mock.reRequire('../../Skeletons'); 16 | mock.stop('fs'); 17 | 18 | beforeEach(async function() { 19 | this.skeletons = Skeletons; 20 | }); 21 | 22 | afterEach(function() { 23 | delete this.skeletons; 24 | }); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/test/fixture/system.fixture.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | const mock = require('mock-require'); 3 | 4 | // Force production config 5 | mock('../../Config-Development', require('../../Config-Production')); 6 | 7 | module.exports = function() { 8 | global.DEBUG = false; 9 | global.SYSTEM = true; 10 | global.Root = { 11 | emit: sinon.fake() 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /app/test/images.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | beforeEach(function() { 4 | this.images = require('../Images'); 5 | }); 6 | afterEach(function() { 7 | delete this.images; 8 | }); 9 | 10 | describe('Images', async function() { 11 | 12 | require('./fixture/system.fixture')(); 13 | 14 | describe('withTag', function() { 15 | 16 | it('simple name', function() { 17 | assert.equal(this.images.withTag('ubuntu'), 'ubuntu:latest'); 18 | }); 19 | 20 | it('long name', function() { 21 | assert.equal(this.images.withTag('registry.somewhere.com/application'), 'registry.somewhere.com/application:latest'); 22 | }); 23 | 24 | it('explicit tag', function() { 25 | assert.equal(this.images.withTag('ubuntu:edge'), 'ubuntu:edge'); 26 | }); 27 | 28 | describe('Minke specific', function() { 29 | 30 | it('minke latest', function() { 31 | assert.equal(this.images.withTag('registry.minkebox.net/minkebox/minke'), 'registry.minkebox.net/minkebox/minke:latest'); 32 | }); 33 | 34 | it('minke-helper latest', function() { 35 | assert.equal(this.images.withTag('registry.minkebox.net/minkebox/minke-helper'), 'registry.minkebox.net/minkebox/minke-helper:latest'); 36 | }); 37 | 38 | }); 39 | 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /app/test/restart.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Restart', async function() { 4 | 5 | require('./fixture/system.fixture')(); 6 | 7 | describe('MinkeSetup', function() { 8 | require('./fixture/minkesetup.fixture')(); 9 | 10 | it('Default reason', function() { 11 | assert.equal(this.MinkeSetup.restartReason(), 'restart'); 12 | }); 13 | 14 | it('Change reason', function() { 15 | assert.equal(this.MinkeSetup.restartReason('exit'), 'restart'); 16 | assert.equal(this.MinkeSetup.restartReason(), 'exit'); 17 | }); 18 | 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /app/utils/Barrier.js: -------------------------------------------------------------------------------- 1 | module.exports = function(fn) { 2 | return async function() { 3 | const self = this; 4 | const args = arguments; 5 | async function exec() { 6 | try { 7 | return await fn.apply(self, args); 8 | } 9 | finally { 10 | const next = fn.__barrier.shift(); 11 | if (next) { 12 | setImmediate(next); 13 | } 14 | else { 15 | fn.__barrier = null; 16 | } 17 | } 18 | } 19 | if (!fn.__barrier) { 20 | fn.__barrier = []; 21 | return await exec(); 22 | } 23 | else { 24 | return new Promise((resolve, reject) => { 25 | fn.__barrier.push(async () => { 26 | try { 27 | resolve(await exec()); 28 | } 29 | catch (e) { 30 | reject(e); 31 | } 32 | }); 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/utils/Events.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | const Events = function() { 4 | this._emitter = new EventEmitter(); 5 | this._eventState = {}; 6 | } 7 | 8 | Events.prototype = { 9 | 10 | on: function(event, listener) { 11 | this._emitter.on(event, listener); 12 | if (this._emitter.listenerCount(event) === 1) { 13 | this._emitter.emit(`${event}.start`); 14 | } 15 | }, 16 | 17 | off: function(event, listener) { 18 | this._emitter.off(event, listener); 19 | if (this._emitter.listenerCount(event) === 0) { 20 | this._emitter.emit(`${event}.stop`); 21 | } 22 | }, 23 | 24 | emit: function(event, data) { 25 | this._eventState[event] = data; 26 | this._emitter.emit(event, data); 27 | } 28 | 29 | }; 30 | 31 | module.exports = Events; 32 | -------------------------------------------------------------------------------- /app/utils/Flatten.js: -------------------------------------------------------------------------------- 1 | module.exports = function flatten(arr) { 2 | return arr.reduce(function (flat, toFlatten) { 3 | return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten); 4 | }, []); 5 | } 6 | -------------------------------------------------------------------------------- /assets/MinkeBoxBigLong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/MinkeBoxBigLong.png -------------------------------------------------------------------------------- /assets/MinkeBoxLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/MinkeBoxLogo.png -------------------------------------------------------------------------------- /assets/MinkeBoxLogoBorderless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/MinkeBoxLogoBorderless.png -------------------------------------------------------------------------------- /assets/MinkeBoxLong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/MinkeBoxLong.png -------------------------------------------------------------------------------- /assets/cross-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/cross-icon.png -------------------------------------------------------------------------------- /assets/install/AboutToCreate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/AboutToCreate.png -------------------------------------------------------------------------------- /assets/install/AllDone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/AllDone.png -------------------------------------------------------------------------------- /assets/install/Applications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Applications.png -------------------------------------------------------------------------------- /assets/install/AtomPC.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/AtomPC.jpeg -------------------------------------------------------------------------------- /assets/install/ClickAway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/ClickAway.png -------------------------------------------------------------------------------- /assets/install/Disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Disk.png -------------------------------------------------------------------------------- /assets/install/DiskImageAdded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/DiskImageAdded.png -------------------------------------------------------------------------------- /assets/install/DiskImageSelection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/DiskImageSelection.png -------------------------------------------------------------------------------- /assets/install/Disks After.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Disks After.png -------------------------------------------------------------------------------- /assets/install/Disks Before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Disks Before.png -------------------------------------------------------------------------------- /assets/install/Disks During.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Disks During.png -------------------------------------------------------------------------------- /assets/install/DownloadInstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/DownloadInstall.png -------------------------------------------------------------------------------- /assets/install/EnableEFI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/EnableEFI.png -------------------------------------------------------------------------------- /assets/install/Etcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Etcher.png -------------------------------------------------------------------------------- /assets/install/FirstBoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/FirstBoot.png -------------------------------------------------------------------------------- /assets/install/FixNetwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/FixNetwork.png -------------------------------------------------------------------------------- /assets/install/InitialVM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/InitialVM.png -------------------------------------------------------------------------------- /assets/install/InstallApps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/InstallApps.png -------------------------------------------------------------------------------- /assets/install/InternetSpeedTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/InternetSpeedTest.png -------------------------------------------------------------------------------- /assets/install/MainConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/MainConfig.png -------------------------------------------------------------------------------- /assets/install/Memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Memory.png -------------------------------------------------------------------------------- /assets/install/NewVM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/NewVM.png -------------------------------------------------------------------------------- /assets/install/Screen Shot 2019-04-03 at 4.14.48 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Screen Shot 2019-04-03 at 4.14.48 PM.png -------------------------------------------------------------------------------- /assets/install/Select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Select.png -------------------------------------------------------------------------------- /assets/install/SpeedTestEnter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/SpeedTestEnter.png -------------------------------------------------------------------------------- /assets/install/SpeedTestOpened.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/SpeedTestOpened.png -------------------------------------------------------------------------------- /assets/install/SpeedTestStart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/SpeedTestStart.png -------------------------------------------------------------------------------- /assets/install/StartUp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/StartUp.png -------------------------------------------------------------------------------- /assets/install/Win10Find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/Win10Find.png -------------------------------------------------------------------------------- /assets/install/iu 6.47.28 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/iu 6.47.28 PM.png -------------------------------------------------------------------------------- /assets/install/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/install/overview.png -------------------------------------------------------------------------------- /assets/overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minkebox/minke/f98c31006b7f042af15158757af7e2a4cf90a29f/assets/overview.jpg -------------------------------------------------------------------------------- /extras/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | minkebox: 4 | container_name: minkebox 5 | image: registry.minkebox.net/minkebox/minke:${TAG-latest} 6 | volumes: 7 | - /var/run/docker.sock:/var/run/docker.sock:rprivate 8 | - ${VOL_ROOT-/tmp/minkebox}:/minke:rshared 9 | - ${VOL_STORE-/dev/null}:/mnt/store:rshared 10 | - ${VOL_NATIVE-/dev/null}:/mnt/native/host:rshared 11 | privileged: true 12 | networks: 13 | home: 14 | ipv4_address: ${IP} 15 | networks: 16 | home: 17 | name: home 18 | driver: macvlan 19 | ipam: 20 | config: 21 | - subnet: ${IP}/24 22 | driver_opts: 23 | parent: ${NETWORK-eth0} 24 | -------------------------------------------------------------------------------- /extras/minkebox: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Static IP assigned to MinkeBox 4 | IP=$1 5 | 6 | # MinkeBox storage 7 | VOL=$2 8 | VOL2=$3 9 | 10 | # Overrides 11 | 12 | if [ "${VOL2}" != "" -a "${VOL2}" != "-" ]; then 13 | EXTRA_VOL="--mount type=bind,source=${VOL2},target=/mnt/store,bind-propagation=rshared" 14 | fi 15 | for i in $4 $5 $6 $7 $8; do 16 | if [ "${i}" ]; then 17 | EXTRA_VOL="${EXTRA_VOL} --mount type=bind,source=${i},target=/mnt/native/$(basename ${i}),bind-propagation=rshared" 18 | fi 19 | done 20 | 21 | if [ "$(whoami)" != "root" -o "${IP}" = "" -o "${VOL}" = "" ]; then 22 | echo "Usage: sudo $0 [ | -] [ ...]" 23 | exit 1 24 | fi 25 | 26 | MINKEBOX="registry.minkebox.net/minkebox/minke" 27 | 28 | # Fetch MinkeBox if not already downloaded 29 | if [ "$(docker image ls -q ${MINKEBOX})" = "" -o "$(docker image ls -q ${MINKEBOX}-helper)" = "" ];then 30 | docker pull ${MINKEBOX} 31 | docker pull ${MINKEBOX}-helper 32 | fi 33 | 34 | if [ "${TZ}" = "" -a -e /etc/timezone ]; then 35 | TZ=$(cat /etc/timezone) 36 | fi 37 | 38 | # Extract information about the host network 39 | default_route=$(ip route get 1.1.1.1 | head -1) 40 | gw=$(echo $default_route | cut -d' ' -f3) 41 | dev=$(echo $default_route | cut -d' ' -f5) 42 | host=$(echo $default_route | cut -d' ' -f7) 43 | 44 | # Create a macvlan network called 'home' to use as MinkeBox's home network. There's a little mucking 45 | # around to make sure MinkeBox can talk to the docker host. 46 | if [ ! -e /sys/class/net/home ]; then 47 | ip link add home link ${dev} type macvlan mode bridge 48 | ip addr add ${host} dev home 49 | ip link set home up 50 | route=$(ip route | grep "dev ${dev} " | grep -v default) 51 | cidr=$(echo $route | cut -d' ' -f1) 52 | if [ "${route}" != "" ]; then 53 | ip route del $(echo $route) 54 | ip route add ${cidr} dev home 55 | fi 56 | fi 57 | 58 | # Recreate the docker network 59 | docker network rm home > /dev/null 60 | docker network create --driver=macvlan --subnet=${host}/24 --gateway=${gw} -o "parent=home" home > /dev/null 61 | 62 | docker container rm minke 2> /dev/null 63 | docker run -d --name minke --privileged \ 64 | -e TZ="${TZ}" \ 65 | --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ 66 | --mount type=bind,source=${VOL},target=/minke,bind-propagation=rshared \ 67 | ${EXTRA_VOL} \ 68 | --network=home \ 69 | --ip=${IP} \ 70 | ${MINKEBOX} > /dev/null 71 | -------------------------------------------------------------------------------- /startup.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Setup timezone 4 | if [ "${TZ}" != "" ]; then 5 | echo ${TZ} > /etc/timezone 6 | fi 7 | if [ ! -e /etc/timezone ]; then 8 | echo 'America/Los_Angeles' > /etc/timezone 9 | fi 10 | cp /usr/share/zoneinfo/$(cat /etc/timezone) /etc/localtime 11 | 12 | # Start syncing time. Delay this for 60 seconds to give the MinkeBox DNS time to startup. 13 | echo "servers pool.ntp.org" > /etc/ntpd.conf 14 | (sleep 60 ; ntpd -s -f /etc/ntpd.conf) & 15 | 16 | # Use our own DNS 17 | echo "nameserver 127.0.0.1" > /etc/resolv.conf 18 | 19 | # MinkeBox 20 | trap "killall node" INT TERM 21 | /usr/bin/node --expose-gc --inspect /app/index.js & 22 | wait "$!" 23 | wait "$!" 24 | 25 | # Restart if testing (so we can debug inside the docker container) 26 | while [ -f /tmp/minke-testing ]; do 27 | /usr/bin/node --expose-gc /app/index.js 28 | done 29 | --------------------------------------------------------------------------------