diff --git a/src/routes/apps.js b/src/routes/apps.js index 4ec2c4974..2517d6f79 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -17,6 +17,7 @@ exports = module.exports = { stopApp: stopApp, startApp: startApp, exec: exec, + execWebSocket: execWebSocket, cloneApp: cloneApp }; @@ -459,6 +460,49 @@ function exec(req, res, next) { }); } +function execWebSocket(ws, req) { + assert.strictEqual(typeof req.params.id, 'string'); + + debug('Execing into app id:%s and cmd:%s', req.params.id, req.query.cmd); + + var cmd = null; + if (req.query.cmd) { + cmd = safe.JSON.parse(req.query.cmd); + if (!util.isArray(cmd) || cmd.length < 1) return console.error(new HttpError(400, 'cmd must be array with atleast size 1')); + } + + var columns = req.query.columns ? parseInt(req.query.columns, 10) : null; + if (isNaN(columns)) return console.error(new HttpError(400, 'columns must be a number')); + + var rows = req.query.rows ? parseInt(req.query.rows, 10) : null; + if (isNaN(rows)) return console.error(new HttpError(400, 'rows must be a number')); + + var tty = req.query.tty === 'true' ? true : false; + + // req.clearTimeout(); + + apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { + if (error && error.reason === AppsError.NOT_FOUND) return console.error(new HttpError(404, 'No such app')); + if (error && error.reason === AppsError.BAD_STATE) return console.error(new HttpError(409, error.message)); + if (error) return console.error(new HttpError(500, error)); + + console.log('Connected to terminal'); + + + duplexStream.on('data', function (data) { + ws.send(data.toString()); + }); + + ws.on('message', function (msg) { + duplexStream.write(msg); + }); + + ws.on('close', function () { + // Clean things up, if any? + }); + }); +} + function listBackups(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); diff --git a/src/server.js b/src/server.js index ee3cac983..dfede95fc 100644 --- a/src/server.js +++ b/src/server.js @@ -28,6 +28,9 @@ function initializeExpressSync() { var app = express(); var httpServer = http.createServer(app); + // enabled websocket handling + require('express-ws')(app, httpServer); + var QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx) FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart @@ -56,7 +59,7 @@ function initializeExpressSync() { router.del = router.delete; // amend router.del for readability further on app - .use(middleware.timeout(REQUEST_TIMEOUT)) + // .use(middleware.timeout(REQUEST_TIMEOUT)) .use(json) .use(urlencoded) .use(middleware.cookieParser()) @@ -189,6 +192,7 @@ function initializeExpressSync() { router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream); router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs); router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec); + router.ws ('/api/v1/apps/:id/exec', routes.apps.execWebSocket); router.post('/api/v1/apps/:id/clone', appsScope, routes.user.requireAdmin, routes.apps.cloneApp); // settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above) @@ -232,22 +236,23 @@ function initializeExpressSync() { // we rely on nginx for timeouts on the TCP level (see client_header_timeout) httpServer.setTimeout(0); - // upgrade handler - httpServer.on('upgrade', function (req, socket, head) { - // create a node response object for express - var res = new http.ServerResponse({}); - res.assignSocket(socket); - res.sendUpgradeHandshake = function () { // could extend express.response as well - socket.write('HTTP/1.1 101 TCP Handshake\r\n' + - 'Upgrade: tcp\r\n' + - 'Connection: Upgrade\r\n' + - '\r\n'); - }; - // route through express middleware. if we provide no callback, express will provide a 'finalhandler' - // TODO: it's not clear if socket needs to be destroyed - app(req, res); - }); + // upgrade handler + // httpServer.on('upgrade', function (req, socket, head) { + // // create a node response object for express + // var res = new http.ServerResponse({}); + // res.assignSocket(socket); + // res.sendUpgradeHandshake = function () { // could extend express.response as well + // socket.write('HTTP/1.1 101 TCP Handshake\r\n' + + // 'Upgrade: tcp\r\n' + + // 'Connection: Upgrade\r\n' + + // '\r\n'); + // }; + + // // route through express middleware. if we provide no callback, express will provide a 'finalhandler' + // // TODO: it's not clear if socket needs to be destroyed + // app(req, res); + // }); return httpServer; } diff --git a/webadmin/src/index.html b/webadmin/src/index.html index d6d8fd6d6..2fd39cc37 100644 --- a/webadmin/src/index.html +++ b/webadmin/src/index.html @@ -3,7 +3,7 @@ - + Cloudron @@ -72,6 +72,12 @@ + + + + + + diff --git a/webadmin/src/js/client.js b/webadmin/src/js/client.js index a687bb7a8..dc512a635 100644 --- a/webadmin/src/js/client.js +++ b/webadmin/src/js/client.js @@ -120,7 +120,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', this._installedApps = []; this._clientId = '<%= oauth.clientId %>'; this._clientSecret = '<%= oauth.clientSecret %>'; - this.apiOrigin = '<%= oauth.apiOrigin %>'; + // window.location fallback for websocket connections which do not have relative uris + this.apiOrigin = '<%= oauth.apiOrigin %>' || window.location.origin; this.avatar = ''; this.resetAvatar(); diff --git a/webadmin/src/theme.scss b/webadmin/src/theme.scss index 655601520..dec5707a7 100644 --- a/webadmin/src/theme.scss +++ b/webadmin/src/theme.scss @@ -125,6 +125,11 @@ html, body { overflow: auto; } +#ng-view { + display: flex; + flex-direction: column; +} + .shadow { box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1); } @@ -137,8 +142,14 @@ html, body { } .content { - max-width: 970px; + width: 720px; + max-width: 720px; margin: 0 auto; + + &.content-large { + width: 970px; + max-width: 970px; + } } .navbar { @@ -454,11 +465,6 @@ h1, h2, h3 { margin-bottom: 0; } -.section-header { - max-width: 720px; - margin: 0 auto; -} - .card { background-color: white; max-width: 720px; @@ -1086,21 +1092,12 @@ footer { .logs-main { text-align: left; width: 100%; - height: 100%; + flex-grow: 1; display: flex; flex-direction: column; - .log-line-container { - flex-grow: 1; - background-color: black; - color: white; - overflow: auto; - border-style: solid; - border-width: 0 15px; - border-color: $body-bg; - padding: 5px; - font-family: monospace; - margin-bottom: 20px; + .logs-controls { + margin-top: 25px; } .log-line { @@ -1114,4 +1111,37 @@ footer { color: #00FFFF; } } + + .logs-and-term-container { + flex-grow: 1; + margin-left: calc(8.33% + 15px); + margin-right: calc(8.33% + 15px); + margin-bottom: 20px; + background-color: black; + color: white; + overflow: auto; + padding: 5px; + font-family: monospace; + } + + .ng-isolate-scope { + display: inline-block; + float: left; + } + + select { + display: inline-block; + width: 250px; + margin-left: 20px; + } + + .uib-tab.active { + a { + background-color: white; + + &:hover, &:focus { + background-color: white; + } + } + } } diff --git a/webadmin/src/views/account.html b/webadmin/src/views/account.html index 8cb6788a8..03d1f6ae3 100644 --- a/webadmin/src/views/account.html +++ b/webadmin/src/views/account.html @@ -101,69 +101,68 @@ -
-
-

Account

-
-
+
-
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - -
Username{{ user.username }}    
Display name{{ user.displayName }}
Email{{ user.email }}
Password recovery email{{ user.alternateEmail }}
-
- -
-
-
-
-
+
+

Account

+
-
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
Username{{ user.username }}    
Display name{{ user.displayName }}
Email{{ user.email }}
Password recovery email{{ user.alternateEmail }}
+
+ +
+
+
+
+
-
-
-

Sessions

-
-
+
-
-
-
-
-

You are logged into {{ activeClients.length + 1 }} app(s), including this session.

- -
-

Active Apps:

-

{{ client.name }} - {{client.activeTokens.length}} time(s)

-
-
- -
-
-
+
+

Sessions

+
+ +
+
+
+
+

You are logged into {{ activeClients.length + 1 }} app(s), including this session.

+ +
+

Active Apps:

+

{{ client.name }} - {{client.activeTokens.length}} time(s)

+
+
+ +
+
+
+
diff --git a/webadmin/src/views/apps.html b/webadmin/src/views/apps.html index 26c2a17ce..7c8650705 100644 --- a/webadmin/src/views/apps.html +++ b/webadmin/src/views/apps.html @@ -361,7 +361,7 @@ } -
+
diff --git a/webadmin/src/views/certs.html b/webadmin/src/views/certs.html index 32576b293..68e807fd0 100644 --- a/webadmin/src/views/certs.html +++ b/webadmin/src/views/certs.html @@ -87,160 +87,156 @@
-
-
-

Domain & Certificates

-
-
- -
-
-

Domain

-
-
- -
-
-
-

To use a custom domain, configure your domain to use Route53. Moving to a custom domain will retain all your apps and data and will take around 15 minutes.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Domain name{{ config.fqdn }}
DNS provider{{ dnsConfig.provider }}
-
- No DNS provider is configured. All DNS records need to be setup manually. - To avoid manual setup for each installed app, set a DNS API provider. -
-
- Wildcard DNS provider is configured. Always ensure there is a wildcard DNS record for this server's IP. -
-
- No DNS provider configured. All DNS records need to be setup manually and all DNS checks are skipped. -
Access key id{{ dnsConfig.accessKeyId || 'unset' }}
Secret access keyhidden
DigitalOcean tokenhidden

-
-
-
- -
-
-

SSL Certificates

-
-
- -
-
-
- Certificates can only by set for custom domains. -
-
- -
-
-
-
-

Certificates are automatically obtained and renewed from Let’s Encrypt. See the current rate limit here.

-
- -

This wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.

-
{{ defaultCert.error }}
-
Upload successful
-
-
- - - - - -
-
-
-
- - - - - -
-
- -
-
-
-
-
-
-
-
- -

This certificate will be used for this Settings application.

-
{{ adminCert.error }}
-
Upload successful
-
-
- - - - - -
-
-
-
- - - - - -
-
- -
-
-
-
+
+
+

Domain & Certificates

+
+ +
+

Domain

+
+ +
+
+
+

To use a custom domain, configure your domain to use Route53. Moving to a custom domain will retain all your apps and data and will take around 15 minutes.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Domain name{{ config.fqdn }}
DNS provider{{ dnsConfig.provider }}
+
+ No DNS provider is configured. All DNS records need to be setup manually. + To avoid manual setup for each installed app, set a DNS API provider. +
+
+ Wildcard DNS provider is configured. Always ensure there is a wildcard DNS record for this server's IP. +
+
+ No DNS provider configured. All DNS records need to be setup manually and all DNS checks are skipped. +
Access key id{{ dnsConfig.accessKeyId || 'unset' }}
Secret access keyhidden
DigitalOcean tokenhidden

+
+
+
+ +
+

SSL Certificates

+
+ +
+
+
+ Certificates can only by set for custom domains. +
+
+ +
+
+
+
+

Certificates are automatically obtained and renewed from Let’s Encrypt. See the current rate limit here.

+
+ +

This wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.

+
{{ defaultCert.error }}
+
Upload successful
+
+
+ + + + + +
+
+
+
+ + + + + +
+
+ +
+
+
+
+
+
+
+
+ +

This certificate will be used for this Settings application.

+
{{ adminCert.error }}
+
Upload successful
+
+
+ + + + + +
+
+
+
+ + + + + +
+
+ +
+
+
+
+
diff --git a/webadmin/src/views/email.html b/webadmin/src/views/email.html index c42259f49..4e7c43ef6 100644 --- a/webadmin/src/views/email.html +++ b/webadmin/src/views/email.html @@ -1,211 +1,203 @@