Compare commits

...

2 Commits

Author SHA1 Message Date
Florent Beauchamp
5299c101c2 fix: add missing service worker file 2021-10-07 17:55:20 +02:00
Florent Beauchamp
83ca34807d feat(xo-web): send notification 2021-10-07 10:27:52 +02:00
7 changed files with 214 additions and 63 deletions

View File

@@ -9,8 +9,8 @@ export default class Logs {
{
filter: [process.env.DEBUG, filter],
level,
transport,
},
transport
}
])
})
}

View File

@@ -123,6 +123,7 @@
"uuid": "^8.3.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^1.2.0",
"web-push": "^3.4.5",
"ws": "^7.1.2",
"xdg-basedir": "^4.0.0",
"xen-api": "^0.34.3",

View File

@@ -20,6 +20,7 @@ import proxyConsole from './proxy-console.mjs'
import pw from 'pw'
import serveStatic from 'serve-static'
import stoppable from 'stoppable'
import webpush from 'web-push'
import WebServer from 'http-server-plus'
import WebSocket from 'ws'
import xdg from 'xdg-basedir'
@@ -71,8 +72,8 @@ configure([
{
filter: process.env.DEBUG,
level: 'info',
transport: transportConsole(),
},
transport: transportConsole()
}
])
const log = createLogger('xo:main')
@@ -84,7 +85,7 @@ const DEPRECATED_ENTRIES = ['users', 'servers']
async function loadConfiguration() {
const config = await appConf.load(APP_NAME, {
appDir: APP_DIR,
ignoreUnknownFormats: true,
ignoreUnknownFormats: true
})
log.info('Configuration loaded.')
@@ -105,7 +106,7 @@ async function updateLocalConfig(diff) {
const localConfig = await fse.readFile(LOCAL_CONFIG_FILE).then(JSON.parse, () => ({}))
merge(localConfig, diff)
await fse.outputFile(LOCAL_CONFIG_FILE, JSON.stringify(localConfig), {
mode: 0o600,
mode: 0o600
})
}
@@ -135,8 +136,8 @@ async function createExpressApp(config) {
saveUninitialized: false,
secret: sessionSecret,
store: new MemoryStore({
checkPeriod: 24 * 3600 * 1e3,
}),
checkPeriod: 24 * 3600 * 1e3
})
})
)
@@ -174,7 +175,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
res.send(
signInPage({
error: req.flash('error')[0],
strategies,
strategies
})
)
})
@@ -193,7 +194,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
signInPage({
error: req.flash('error')[0],
otp: true,
strategies,
strategies
})
)
})
@@ -219,7 +220,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
const { user, isPersistent } = req.session
const token = await xo.createAuthenticationToken({
expiresIn: isPersistent ? PERMANENT_VALIDITY : SESSION_VALIDITY,
userId: user.id,
userId: user.id
})
res.cookie('token', token.id, {
@@ -227,7 +228,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
// a session (non-permanent) cookie must not have an expiration date
// because it must not survive browser restart
...(isPersistent ? { expires: new Date(token.expiration) } : undefined),
...(isPersistent ? { expires: new Date(token.expiration) } : undefined)
})
delete req.session.isPersistent
@@ -286,6 +287,30 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
}
})
)
// ==============================================================
const publicVapidKey = 'BDAqBcWLLjbzGSMjVqlhZmU88uiAVascwXn5mbiuMVFpsXiJixtIxVpu06pIX1b8cjXKYawsv-FuGhp9oH_1dwc'
const privateVapidKey = 'b1QTbeDFOeu0th23w9bDEpLHfkSKGvXJ3VQq50gHEcQ'
webpush.setVapidDetails('mailto:example@yourdomain.org', publicVapidKey, privateVapidKey)
// subscribe route
express.use(createExpress.json())
express.post('/service-worker-subscribe', (req, res) => {
// get push subscription object from the request
const subscription = req.body
// send status 201 for the request
res.status(201).json({})
// create paylod: specified the detals of the push notification
const payload = JSON.stringify({
title: 'Titre de ma notification from server',
body: 'Contenu de ma notification',
url: 'https://www.vates.fr'
})
// pass the object into sendNotification fucntion and catch any error
webpush.sendNotification(subscription, payload).catch(err => console.error(err))
})
}
// ===================================================================
@@ -319,14 +344,14 @@ async function registerPlugin(pluginPath, pluginName) {
getDataDir: () => {
const dir = `${datadir}/${pluginName}`
return fse.ensureDir(dir).then(() => dir)
},
}
})
: factory
;[instance, configurationSchema, configurationPresets, testSchema] = await Promise.all([
handleFactory(factory),
handleFactory(configurationSchema),
handleFactory(configurationPresets),
handleFactory(testSchema),
handleFactory(testSchema)
])
await this.registerPlugin(
@@ -363,11 +388,11 @@ async function registerPluginsInPath(path, prefix) {
})
await Promise.all(
files.map(name => {
if (name.startsWith(prefix)) {
files
.filter(name => name.startsWith(prefix))
.map(name => {
return registerPluginWrapper.call(this, `${path}/${name}`, name.slice(prefix.length))
}
})
})
)
}
@@ -376,7 +401,7 @@ async function registerPlugins(xo) {
[new URL('../node_modules', import.meta.url).pathname, '/usr/local/lib/node_modules'].map(path =>
Promise.all([
registerPluginsInPath.call(xo, path, 'xo-server-'),
registerPluginsInPath.call(xo, `${path}/@xen-orchestra`, 'server-'),
registerPluginsInPath.call(xo, `${path}/@xen-orchestra`, 'server-')
])
)
)
@@ -418,7 +443,7 @@ async function makeWebServerListen(
const pems = await genSelfSignedCert()
await Promise.all([
fse.outputFile(cert, pems.cert, { flag: 'wx', mode: 0o400 }),
fse.outputFile(key, pems.key, { flag: 'wx', mode: 0o400 }),
fse.outputFile(key, pems.key, { flag: 'wx', mode: 0o400 })
])
log.info('new certificate generated', { cert, key })
opts.cert = pems.cert
@@ -464,7 +489,7 @@ const setUpProxies = (express, opts, xo) => {
.createServer({
changeOrigin: true,
ignorePath: true,
xfwd: true,
xfwd: true
})
.on('error', (error, req, res) => {
// `res` can be either a `ServerResponse` or a `Socket` (which does not have
@@ -478,7 +503,7 @@ const setUpProxies = (express, opts, xo) => {
const { method, url } = req
log.error('failed to proxy request', {
error,
req: { method, url },
req: { method, url }
})
})
@@ -494,7 +519,7 @@ const setUpProxies = (express, opts, xo) => {
proxy.web(req, res, {
agent: new URL(target).hostname === 'localhost' ? undefined : xo.httpAgent,
target: target + url.slice(prefix.length),
target: target + url.slice(prefix.length)
})
return
@@ -506,7 +531,7 @@ const setUpProxies = (express, opts, xo) => {
// WebSocket proxy.
const webSocketServer = new WebSocket.Server({
noServer: true,
noServer: true
})
xo.hooks.on('stop', () => fromCallback.call(webSocketServer, 'close'))
@@ -519,7 +544,7 @@ const setUpProxies = (express, opts, xo) => {
proxy.ws(req, socket, head, {
agent: new URL(target).hostname === 'localhost' ? undefined : xo.httpAgent,
target: target + url.slice(prefix.length),
target: target + url.slice(prefix.length)
})
return
@@ -546,7 +571,7 @@ const setUpApi = (webServer, xo, config) => {
const webSocketServer = new WebSocket.Server({
...config.apiWebSocketOptions,
noServer: true,
noServer: true
})
xo.hooks.on('stop', () => fromCallback.call(webSocketServer, 'close'))
@@ -614,7 +639,7 @@ const CONSOLE_PROXY_PATH_RE = /^\/api\/consoles\/(.*)$/
const setUpConsoleProxy = (webServer, xo) => {
const webSocketServer = new WebSocket.Server({
noServer: true,
noServer: true
})
xo.hooks.on('stop', () => fromCallback.call(webSocketServer, 'close'))
@@ -644,7 +669,7 @@ const setUpConsoleProxy = (webServer, xo) => {
timestamp: Date.now(),
userId: user.id,
userIp: remoteAddress,
userName: user.name,
userName: user.name
}
if (vm.is_control_domain) {
@@ -663,7 +688,7 @@ const setUpConsoleProxy = (webServer, xo) => {
socket.on('close', () => {
xo.emit('xo:audit', 'consoleClosed', {
...data,
timestamp: Date.now(),
timestamp: Date.now()
})
log.info(`- Console proxy (${user.name} - ${remoteAddress})`)
})
@@ -710,7 +735,7 @@ export default async function main(args) {
blocked((time, stack) => {
logPerf.info(`blocked for ${ms(time)}`, {
time,
stack,
stack
})
}, options)
}
@@ -742,7 +767,7 @@ export default async function main(args) {
appVersion: APP_VERSION,
config,
httpServer: webServer,
safeMode,
safeMode
})
// Register web server close on XO stop.

View File

@@ -25,28 +25,28 @@ const gulp = require('gulp')
// ===================================================================
function lazyFn(factory) {
let fn = function () {
let fn = function() {
fn = factory()
return fn.apply(this, arguments)
}
return function () {
return function() {
return fn.apply(this, arguments)
}
}
// -------------------------------------------------------------------
const livereload = lazyFn(function () {
const livereload = lazyFn(function() {
const livereload = require('gulp-refresh')
livereload.listen({
port: LIVERELOAD_PORT,
port: LIVERELOAD_PORT
})
return livereload
})
const pipe = lazyFn(function () {
const pipe = lazyFn(function() {
let current
function pipeCore(streams) {
let i, n, stream
@@ -63,7 +63,7 @@ const pipe = lazyFn(function () {
}
const push = Array.prototype.push
return function (streams) {
return function(streams) {
try {
if (!(streams instanceof Array)) {
streams = []
@@ -79,7 +79,7 @@ const pipe = lazyFn(function () {
}
})
const resolvePath = lazyFn(function () {
const resolvePath = lazyFn(function() {
return require('path').resolve
})
@@ -87,7 +87,7 @@ const resolvePath = lazyFn(function () {
// Similar to `gulp.src()` but the pattern is relative to `SRC_DIR`
// and files are automatically watched when not in production mode.
const src = lazyFn(function () {
const src = lazyFn(function() {
function resolve(path) {
return path ? resolvePath(SRC_DIR, path) : SRC_DIR
}
@@ -100,7 +100,7 @@ const src = lazyFn(function () {
base: base,
cwd: base,
passthrough: opts && opts.passthrough,
sourcemaps: opts && opts.sourcemaps,
sourcemaps: opts && opts.sourcemaps
})
}
: function src(pattern, opts) {
@@ -111,11 +111,11 @@ const src = lazyFn(function () {
base: base,
cwd: base,
passthrough: opts && opts.passthrough,
sourcemaps: opts && opts.sourcemaps,
sourcemaps: opts && opts.sourcemaps
}),
require('gulp-watch')(pattern, {
base: base,
cwd: base,
cwd: base
}),
require('gulp-plumber')()
)
@@ -125,13 +125,13 @@ const src = lazyFn(function () {
// Similar to `gulp.dest()` but the output directory is relative to
// `DIST_DIR` and default to `./`, and files are automatically live-
// reloaded when not in production mode.
const dest = lazyFn(function () {
const dest = lazyFn(function() {
function resolve(path) {
return path ? resolvePath(DIST_DIR, path) : DIST_DIR
}
const opts = {
sourcemaps: '.',
sourcemaps: '.'
}
return PRODUCTION
@@ -162,7 +162,7 @@ function browserify(path, opts) {
// Required by Watchify.
cache: {},
packageCache: {},
packageCache: {}
})
const plugins = opts.plugins
@@ -178,7 +178,7 @@ function browserify(path, opts) {
bundler = require('watchify')(bundler, {
// do not watch in `node_modules`
// https://github.com/browserify/watchify#options
ignoreWatch: true,
ignoreWatch: true
})
}
@@ -189,7 +189,7 @@ function browserify(path, opts) {
path = resolvePath(SRC_DIR, path)
let stream = new (require('readable-stream'))({
objectMode: true,
objectMode: true
})
let write
@@ -204,28 +204,28 @@ function browserify(path, opts) {
new (require('vinyl'))({
base: SRC_DIR,
contents: buffer,
path: path,
path: path
})
)
})
}
if (PRODUCTION) {
write = function (data) {
write = function(data) {
stream.push(data)
stream.push(null)
}
} else {
stream = require('gulp-plumber')().pipe(stream)
write = function (data) {
write = function(data) {
stream.push(data)
}
bundler.on('update', bundle)
}
stream._read = function () {
this._read = function () {}
stream._read = function() {
this._read = function() {}
bundle()
}
@@ -240,7 +240,7 @@ gulp.task(function buildPages() {
require('gulp-pug')(),
DEVELOPMENT &&
require('gulp-embedlr')({
port: LIVERELOAD_PORT,
port: LIVERELOAD_PORT
}),
dest()
)
@@ -255,10 +255,10 @@ gulp.task(function buildScripts() {
'modular-cssify',
{
css: DIST_DIR + '/modules.css',
from: undefined,
},
],
],
from: undefined
}
]
]
}),
require('gulp-sourcemaps').init({ loadMaps: true }),
PRODUCTION && require('gulp-terser')(),
@@ -275,18 +275,18 @@ gulp.task(function buildStyles() {
dest()
)
})
gulp.task(function copyAssets() {
return pipe(
src(['assets/**/*', 'favicon.*']),
src('fontawesome-webfont.*', {
base: __dirname + '/../../node_modules/font-awesome/fonts', // eslint-disable-line no-path-concat
passthrough: true,
base: path.join(__dirname, '/../../node_modules/font-awesome/fonts'), // eslint-disable-line no-path-concat
passthrough: true
}),
src(['!*.css', 'font-mfizz.*'], {
base: __dirname + '/../../node_modules/font-mfizz/dist', // eslint-disable-line no-path-concat
passthrough: true,
passthrough: true
}),
src(['serviceworker.js']),
dest()
)
})

View File

@@ -5,6 +5,7 @@ import React, { Component } from 'react'
import ReactNotify from 'react-notify'
import { connectStore } from 'utils'
import { isAdmin } from 'selectors'
import fetch from './fetch'
let instance
@@ -12,8 +13,47 @@ export let error
export let info
export let success
const publicVapidKey = 'BDAqBcWLLjbzGSMjVqlhZmU88uiAVascwXn5mbiuMVFpsXiJixtIxVpu06pIX1b8cjXKYawsv-FuGhp9oH_1dwc'
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
// register the service worker, register our push api, send the notification
async function registerNotificationServiceWorker() {
// register service worker
const register = await navigator.serviceWorker.register('/serviceworker.js', {
scope: '/'
})
// register push
const subscription = await register.pushManager.subscribe({
userVisibleOnly: true,
// public vapid key
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
})
// Send push notification
await fetch('/service-worker-subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'content-type': 'application/json'
}
})
}
@connectStore({
isAdmin,
isAdmin
})
export class Notification extends Component {
componentDidMount() {
@@ -21,6 +61,11 @@ export class Notification extends Component {
throw new Error('Notification is a singleton!')
}
instance = this
// check if the serveice worker can work in the current browser
if ('serviceWorker' in navigator) {
registerNotificationServiceWorker()
}
}
componentWillUnmount() {

View File

@@ -0,0 +1,27 @@
const version = 7;
self.addEventListener('install', () => {
console.log(`Installation du service worker v${version}`);
return self.skipWaiting();
});
self.addEventListener('activate', () => console.log(`Activation du service worker v${version}`));
self.addEventListener('push', event => {
const dataJSON = event.data.json();
console.log(dataJSON)
const notificationOptions = {
body: dataJSON.body,
data: {
url: dataJSON.url,
}
};
return self.registration.showNotification(dataJSON.title, notificationOptions);
});
self.addEventListener('notificationclick', event => {
const url = event.notification.data.url;
event.notification.close();
event.waitUntil(clients.openWindow(url));
});

View File

@@ -2680,7 +2680,7 @@ asap@^2.0.6, asap@~2.0.3:
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
asn1.js@^5.0.0, asn1.js@^5.2.0:
asn1.js@^5.0.0, asn1.js@^5.2.0, asn1.js@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
@@ -3474,6 +3474,11 @@ buffer-crc32@^0.2.13, buffer-crc32@~0.2.3:
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
@@ -5808,6 +5813,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -8310,6 +8322,13 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
http_ece@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75"
integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==
dependencies:
urlsafe-base64 "~1.0.0"
https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
@@ -9900,6 +9919,23 @@ just-reduce-object@^1.0.3:
resolved "https://registry.yarnpkg.com/just-reduce-object/-/just-reduce-object-1.1.0.tgz#d29d172264f8511c74462de30d72d5838b6967e6"
integrity sha512-nGyg7N9FEZsyrGQNilkyVLxKPsf96iel5v0DrozQ19ML+96HntyS/53bOP68iK/kZUGvsL3FKygV8nQYYhgTFw==
jwa@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
dependencies:
jwa "^2.0.0"
safe-buffer "^5.0.1"
keycode@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
@@ -16513,6 +16549,11 @@ url@^0.11.0, url@~0.11.0:
punycode "1.3.2"
querystring "0.2.0"
urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6"
integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY=
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
@@ -16938,6 +16979,18 @@ wcwidth@^1.0.1:
dependencies:
defaults "^1.0.3"
web-push@^3.4.5:
version "3.4.5"
resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1"
integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g==
dependencies:
asn1.js "^5.3.0"
http_ece "1.1.0"
https-proxy-agent "^5.0.0"
jws "^4.0.0"
minimist "^1.2.5"
urlsafe-base64 "^1.0.0"
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"