From ae53484abd64d06367fc0e6f52373e2f8a85a907 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 19 Jan 2020 21:30:25 -0500 Subject: [PATCH] feat: admin ssl - renew cert + toggle redirection btn --- client/components/admin/admin-ssl.vue | 330 +++++++++++------- package.json | 8 +- server/app/data.yml | 2 + server/controllers/{letsencrypt.js => ssl.js} | 13 + server/core/servers.js | 40 ++- server/graph/resolvers/system.js | 56 +++ server/graph/schemas/system.graphql | 14 + server/helpers/error.js | 16 + server/master.js | 4 +- yarn.lock | 46 +-- 10 files changed, 380 insertions(+), 149 deletions(-) rename server/controllers/{letsencrypt.js => ssl.js} (65%) diff --git a/client/components/admin/admin-ssl.vue b/client/components/admin/admin-ssl.vue index d65503c9..40eea64b 100644 --- a/client/components/admin/admin-ssl.vue +++ b/client/components/admin/admin-ssl.vue @@ -8,163 +8,259 @@ .headline.primary--text.animated.fadeInLeft {{ $t('admin:ssl.title') }} .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:ssl.subtitle') }} v-spacer - v-btn.animated.fadeInDown(color='success', depressed, @click='save', large) - v-icon(left) mdi-check - span {{$t('common:actions.apply')}} + v-btn.animated.fadeInDown( + v-if='info.sslProvider === `letsencrypt`' + color='black' + dark + depressed + @click='renewCertificate' + large + :loading='loadingRenew' + ) + v-icon(left) mdi-cached + span {{$t('admin:ssl.renewCertificate')}} v-form.pt-3 v-layout(row wrap) v-flex(lg6 xs12) - v-form - v-card.animated.fadeInUp - v-toolbar(color='primary', dark, dense, flat) - v-toolbar-title.subtitle-1 {{ $t('admin:ssl.provider') }} - v-card-text - v-select( - :items='providers' - outlined - :label='$t(`admin:ssl.provider`)' - required - :counter='255' - v-model='config.provider' - prepend-icon='mdi-handshake' - :hint='$t(`admin:ssl.providerHint`)' - persistent-hint - ) - v-text-field.mt-3( - outlined - :label='$t(`admin:ssl.domain`)' - required - :counter='255' - v-model='config.domain' - prepend-icon='mdi-earth' - :hint='$t(`admin:ssl.domainHint`)' - persistent-hint - :disabled='config.provider === ``' - ) - - v-card.animated.fadeInUp.wait-p2s.mt-3(v-if='config.provider !== ``') - v-toolbar(color='primary', dark, dense, flat) - v-toolbar-title.subtitle-1 {{$t('admin:ssl.providerOptions')}} - v-card-text --- + v-card.animated.fadeInUp + v-subheader {{ $t('admin:ssl.currentState') }} + v-list(two-line, dense) + v-list-item + v-list-item-avatar + v-icon.indigo.white--text mdi-handshake + v-list-item-content + v-list-item-title {{ $t(`admin:ssl.provider`) }} + v-list-item-subtitle {{ providerTitle }} + template(v-if='info.sslProvider === `letsencrypt`') + v-list-item + v-list-item-avatar + v-icon.indigo.white--text mdi-application + v-list-item-content + v-list-item-title {{ $t(`admin:ssl.domain`) }} + v-list-item-subtitle {{ info.sslDomain }} + v-list-item + v-list-item-avatar + v-icon.indigo.white--text mdi-at + v-list-item-content + v-list-item-title {{ $t('admin:ssl.subscriberEmail') }} + v-list-item-subtitle {{ info.sslSubscriberEmail }} + v-list-item + v-list-item-avatar + v-icon.indigo.white--text mdi-calendar-remove-outline + v-list-item-content + v-list-item-title {{ $t('admin:ssl.expiration') }} + v-list-item-subtitle {{ info.sslExpirationDate | moment('calendar') }} + v-list-item + v-list-item-avatar + v-icon.indigo.white--text mdi-traffic-light + v-list-item-content + v-list-item-title {{ $t(`admin:ssl.status`) }} + v-list-item-subtitle {{ info.sslStatus }} v-flex(lg6 xs12) v-card.animated.fadeInUp.wait-p2s - v-toolbar(color='primary', dark, dense, flat) - v-toolbar-title.subtitle-1 {{ $t('admin:ssl.ports') }} - v-card-text - v-row - v-col(cols='6') - v-text-field( - outlined - :label='$t(`admin:ssl.httpPort`)' - v-model='config.httpPort' - prepend-icon='mdi-lock-open-variant-outline' - :hint='$t(`admin:ssl.httpPortHint`)' - persistent-hint + v-subheader {{ $t('admin:ssl.ports') }} + v-list(two-line, dense) + v-list-item + v-list-item-avatar + v-icon.blue.white--text mdi-lock-open-variant + v-list-item-content + v-list-item-title {{ $t(`admin:ssl.httpPort`) }} + v-list-item-subtitle {{ info.httpPort }} + template(v-if='info.httpsPort > 0') + v-divider + v-list-item + v-list-item-avatar + v-icon.green.white--text mdi-lock + v-list-item-content + v-list-item-title {{ $t(`admin:ssl.httpsPort`) }} + v-list-item-subtitle {{ info.httpsPort }} + v-divider + v-list-item + v-list-item-avatar + v-icon.indigo.white--text mdi-sign-direction + v-list-item-content + v-list-item-title {{ $t(`admin:ssl.httpPortRedirect`) }} + v-list-item-subtitle {{ info.httpRedirection }} + v-list-item-action + v-btn.red--text( + v-if='info.httpRedirection' + depressed + :color='$vuetify.theme.dark ? `red darken-4` : `red lighten-5`' + :class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`' + @click='toggleRedir' + :loading='loadingRedir' ) - v-col(cols='6') - v-checkbox( - :label='$t(`admin:ssl.httpPortRedirect`)' - v-model='config.httpRedirect' - :hint='$t(`admin:ssl.httpPortRedirectHint`)' - :disabled='config.provider === ``' - persistent-hint - color='primary' + v-icon(left) mdi-power + span {{$t('admin:ssl.httpPortRedirectTurnOff')}} + v-btn.green--text( + v-else + depressed + :color='$vuetify.theme.dark ? `green darken-4` : `green lighten-5`' + :class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`' + @click='toggleRedir' + :loading='loadingRedir' ) - v-col(cols='6') - v-text-field( - outlined - :label='$t(`admin:ssl.httpsPort`)' - v-model='config.httpsPort' - prepend-icon='mdi-lock' - :hint='$t(`admin:ssl.httpsPortHint`)' - persistent-hint - :disabled='config.provider === ``' - ) - v-card-text.grey(:class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-4`') - .caption {{$t(`admin:ssl.writableConfigFileWarning`)}} + v-icon(left) mdi-power + span {{$t('admin:ssl.httpPortRedirectTurnOn')}} + + v-dialog( + v-model='loadingRenew' + persistent + max-width='450' + ) + v-card(color='black', dark) + v-card-text.pa-10.text-center + semipolar-spinner.animated.fadeIn( + :animation-duration='1500' + :size='65' + color='#FFF' + style='margin: 0 auto;' + ) + .mt-5.body-1.white--text {{$t('admin:ssl.renewCertificateLoadingTitle')}} + .caption.mt-4 {{$t('admin:ssl.renewCertificateLoadingSubtitle')}} diff --git a/package.json b/package.json index 69fe621c..9b3728b6 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "express": "4.17.1", "express-brute": "1.0.1", "express-session": "1.17.0", - "file-type": "13.1.0", + "file-type": "13.1.1", "filesize": "6.0.1", "fs-extra": "8.1.0", "getos": "3.1.1", @@ -121,7 +121,7 @@ "node-2fa": "1.1.2", "node-cache": "5.1.0", "nodemailer": "6.4.2", - "objection": "1.6.11", + "objection": "2.1.2", "passport": "0.4.1", "passport-auth0": "1.3.1", "passport-azure-ad": "4.2.1", @@ -158,7 +158,7 @@ "scim-query-filter-parser": "2.0.4", "semver": "7.1.1", "serve-favicon": "2.5.0", - "simple-git": "1.129.0", + "simple-git": "1.130.0", "solr-node": "1.2.1", "sqlite3": "4.1.1", "ssh2": "0.8.7", @@ -188,7 +188,7 @@ "@babel/plugin-syntax-import-meta": "^7.8.3", "@babel/polyfill": "^7.8.3", "@babel/preset-env": "^7.8.3", - "@mdi/font": "4.7.95", + "@mdi/font": "4.8.95", "@panter/vue-i18next": "0.15.1", "@requarks/ckeditor5": "12.4.0-wiki.14", "@vue/babel-preset-app": "4.1.2", diff --git a/server/app/data.yml b/server/app/data.yml index 2baef4d4..f313d25a 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -54,6 +54,8 @@ defaults: securityHSTSDuration: 300 securityCSP: false securityCSPDirectives: '' + server: + sslRedir: false flags: ldapdebug: false sqllog: false diff --git a/server/controllers/letsencrypt.js b/server/controllers/ssl.js similarity index 65% rename from server/controllers/letsencrypt.js rename to server/controllers/ssl.js index 8d4d39f4..4ce60494 100644 --- a/server/controllers/letsencrypt.js +++ b/server/controllers/ssl.js @@ -1,6 +1,7 @@ const express = require('express') const router = express.Router() const _ = require('lodash') +const qs = require('querystring') /* global WIKI */ @@ -22,4 +23,16 @@ router.get('/.well-known/acme-challenge/:token', (req, res, next) => { } }) +/** + * Redirect to HTTPS if HTTP Redirection is enabled + */ +router.all('/*', (req, res, next) => { + if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) { + let query = (!_.isEmpty(req.query)) ? `?${qs.stringify(req.query)}` : `` + return res.redirect(`https://${req.hostname}${req.originalUrl}${query}`) + } else { + next() + } +}) + module.exports = router diff --git a/server/core/servers.js b/server/core/servers.js index 0cca5315..92cd903a 100644 --- a/server/core/servers.js +++ b/server/core/servers.js @@ -46,7 +46,7 @@ module.exports = { }) this.servers.http.on('connection', conn => { - let connKey = `${conn.remoteAddress}:${conn.remotePort}` + let connKey = `http:${conn.remoteAddress}:${conn.remotePort}` this.connections.set(connKey, conn) conn.on('close', () => { this.connections.delete(connKey) @@ -108,7 +108,7 @@ module.exports = { }) this.servers.https.on('connection', conn => { - let connKey = `${conn.remoteAddress}:${conn.remotePort}` + let connKey = `https:${conn.remoteAddress}:${conn.remotePort}` this.connections.set(connKey, conn) conn.on('close', () => { this.connections.delete(connKey) @@ -135,11 +135,17 @@ module.exports = { /** * Close all active connections */ - closeConnections () { - for (const conn of this.connections.values()) { + closeConnections (mode = 'all') { + for (const [key, conn] of this.connections) { + if (mode !== `all` && key.indexOf(`${mode}:`) !== 0) { + continue + } conn.destroy() + this.connections.delete(key) + } + if (mode === 'all') { + this.connections.clear() } - this.connections.clear() }, /** * Stop all servers @@ -155,5 +161,29 @@ module.exports = { this.servers.https = null } this.servers.graph = null + }, + /** + * Restart Server + */ + async restartServer (srv = 'https') { + this.closeConnections(srv) + switch (srv) { + case 'http': + if (this.servers.http) { + await Promise.fromCallback(cb => { this.servers.http.close(cb) }) + this.servers.http = null + } + this.startHTTP() + break + case 'https': + if (this.servers.https) { + await Promise.fromCallback(cb => { this.servers.https.close(cb) }) + this.servers.https = null + } + this.startHTTPS() + break + default: + throw new Error('Cannot restart server: Invalid designation') + } } } diff --git a/server/graph/resolvers/system.js b/server/graph/resolvers/system.js index 7b867d1b..1d2417a3 100644 --- a/server/graph/resolvers/system.js +++ b/server/graph/resolvers/system.js @@ -220,6 +220,38 @@ module.exports = { } catch (err) { return graphHelper.generateError(err) } + }, + /** + * Set HTTPS Redirection State + */ + async setHTTPSRedirection (obj, args, context) { + _.set(WIKI.config, 'server.sslRedir', args.enabled) + await WIKI.configSvc.saveToDb(['server']) + return { + responseResult: graphHelper.generateSuccess('HTTP Redirection state set successfully.') + } + }, + /** + * Renew SSL Certificate + */ + async renewHTTPSCertificate (obj, args, context) { + try { + if (!WIKI.config.ssl.enabled) { + throw new WIKI.Error.SystemSSLDisabled() + } else if (WIKI.config.ssl.provider !== `letsencrypt`) { + throw new WIKI.Error.SystemSSLRenewInvalidProvider() + } else if (!WIKI.servers.le) { + throw new WIKI.Error.SystemSSLLEUnavailable() + } else { + await WIKI.servers.le.requestCertificate() + await WIKI.servers.restartServer('https') + return { + responseResult: graphHelper.generateSuccess('SSL Certificate renewed successfully.') + } + } + } catch (err) { + return graphHelper.generateError(err) + } } }, SystemInfo: { @@ -266,6 +298,15 @@ module.exports = { hostname () { return os.hostname() }, + httpPort () { + return WIKI.servers.servers.http ? _.get(WIKI.servers.servers.http.address(), 'port', 0) : 0 + }, + httpRedirection () { + return _.get(WIKI.config, 'server.sslRedir', false) + }, + httpsPort () { + return WIKI.servers.servers.https ? _.get(WIKI.servers.servers.https.address(), 'port', 0) : 0 + }, latestVersion () { return WIKI.system.updates.version }, @@ -293,6 +334,21 @@ module.exports = { ramTotal () { return filesize(os.totalmem()) }, + sslDomain () { + return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.domain : null + }, + sslExpirationDate () { + return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null + }, + sslProvider () { + return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null + }, + sslStatus () { + return 'OK' + }, + sslSubscriberEmail () { + return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.subscriberEmail : null + }, telemetry () { return WIKI.telemetry.enabled }, diff --git a/server/graph/schemas/system.graphql b/server/graph/schemas/system.graphql index 2d8400a5..5b418340 100644 --- a/server/graph/schemas/system.graphql +++ b/server/graph/schemas/system.graphql @@ -40,6 +40,12 @@ type SystemMutation { mongoDbConnString: String! groupMode: SystemImportUsersGroupMode! ): SystemImportUsersResponse @auth(requires: ["manage:system"]) + + setHTTPSRedirection( + enabled: Boolean! + ): DefaultResponse @auth(requires: ["manage:system"]) + + renewHTTPSCertificate: DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- @@ -65,6 +71,9 @@ type SystemInfo { dbVersion: String @auth(requires: ["manage:system"]) groupsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"]) hostname: String @auth(requires: ["manage:system"]) + httpPort: Int @auth(requires: ["manage:system"]) + httpRedirection: Boolean @auth(requires: ["manage:system"]) + httpsPort: Int @auth(requires: ["manage:system"]) latestVersion: String @auth(requires: ["manage:system"]) latestVersionReleaseDate: Date @auth(requires: ["manage:system"]) nodeVersion: String @auth(requires: ["manage:system"]) @@ -72,6 +81,11 @@ type SystemInfo { pagesTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"]) platform: String @auth(requires: ["manage:system"]) ramTotal: String @auth(requires: ["manage:system"]) + sslDomain: String @auth(requires: ["manage:system"]) + sslExpirationDate: Date @auth(requires: ["manage:system"]) + sslProvider: String @auth(requires: ["manage:system"]) + sslStatus: String @auth(requires: ["manage:system"]) + sslSubscriberEmail: String @auth(requires: ["manage:system"]) telemetry: Boolean @auth(requires: ["manage:system"]) telemetryClientId: String @auth(requires: ["manage:system"]) upgradeCapable: Boolean @auth(requires: ["manage:system"]) diff --git a/server/helpers/error.js b/server/helpers/error.js index bd24165d..560162be 100644 --- a/server/helpers/error.js +++ b/server/helpers/error.js @@ -165,6 +165,22 @@ module.exports = { message: 'An unexpected error occured during search operation.', code: 4001 }), + SystemGenericError: CustomError('SystemGenericError', { + message: 'An unexpected error occured.', + code: 7001 + }), + SystemSSLDisabled: CustomError('SystemSSLDisabled', { + message: 'SSL is not enabled.', + code: 7002 + }), + SystemSSLLEUnavailable: CustomError('SystemSSLLEUnavailable', { + message: 'Let\'s Encrypt is not initialized.', + code: 7004 + }), + SystemSSLRenewInvalidProvider: CustomError('SystemSSLRenewInvalidProvider', { + message: 'Current provider does not support SSL certificate renewal.', + code: 7003 + }), UserCreationFailed: CustomError('UserCreationFailed', { message: 'An unexpected error occured during user creation.', code: 1009 diff --git a/server/master.js b/server/master.js index 604264bb..7459c137 100644 --- a/server/master.js +++ b/server/master.js @@ -59,10 +59,10 @@ module.exports = async () => { })) // ---------------------------------------- - // Let's Encrypt Challenge + // SSL Handlers // ---------------------------------------- - app.use('/', ctrl.letsencrypt) + app.use('/', ctrl.ssl) // ---------------------------------------- // Passport Authentication diff --git a/yarn.lock b/yarn.lock index 6a44273a..f2d9c251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2052,10 +2052,10 @@ resolved "https://registry.yarnpkg.com/@log4js-node/log4js-api/-/log4js-api-1.0.2.tgz#7a8143fb33f077df3e579dca7f18fea74a02ec8b" integrity sha512-6SJfx949YEWooh/CUPpJ+F491y4BYJmknz4hUN1+RHvKoUEynKbRmhnwbk/VLmh4OthLLDNCyWXfbh4DG1cTXA== -"@mdi/font@4.7.95": - version "4.7.95" - resolved "https://registry.yarnpkg.com/@mdi/font/-/font-4.7.95.tgz#46fddf35aad64dd623a8b1837f78ca4ed7bc48b1" - integrity sha512-/SWooHIFz2dXkQJk3VhEXSbBplOU1lIkGSELAmw0peFEgR8KPqyM//M3vD8WDZETuEOSRVhVqLevP3okrsM5dw== +"@mdi/font@4.8.95": + version "4.8.95" + resolved "https://registry.yarnpkg.com/@mdi/font/-/font-4.8.95.tgz#e026255815fbac2d5155fdf5cd3069b8dc599900" + integrity sha512-mfEjd6kkuheZ15CBU7g/q+De9+dah/SEgVH0uZsgCJTSYa+CkXIen35aNyHoixgcEfPV4Or0NLJvyYM5CXUnbQ== "@opencensus/web-types@0.0.7": version "0.0.7" @@ -5821,6 +5821,11 @@ date-utils@*: resolved "https://registry.yarnpkg.com/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" integrity sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q= +db-errors@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/db-errors/-/db-errors-0.2.3.tgz#a6a38952e00b20e790f2695a6446b3c65497ffa2" + integrity sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng== + de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" @@ -6970,13 +6975,13 @@ file-loader@5.0.2: loader-utils "^1.2.3" schema-utils "^2.5.0" -file-type@13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-13.1.0.tgz#5cfeade4745fad9504bb1435b5b5003af272b5a8" - integrity sha512-nr4fSvwYSlQl7YmaWS8rsvDrAm6VgCeb2ysHh18+YBSH4RxewhPKUQrj2XRuEMBNnH6E4xw+yWTL7+jiMrh6GA== +file-type@13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-13.1.1.tgz#439ceea3a50a3929b21d18d9c1c77fd7f2a63aa8" + integrity sha512-HEb3tepyq8KzKSFEGMJSIxqn8uC1n3AM8OKME5+BIgq0bErRzcDPOdmnyPKtfjStSpIvuk0Rle8mvuG4824caQ== dependencies: readable-web-to-node-stream "^2.0.0" - strtok3 "^5.0.1" + strtok3 "^5.0.2" token-types "^2.0.0" typedarray-to-buffer "^3.1.5" @@ -10648,14 +10653,13 @@ object.values@^1.1.0: function-bind "^1.1.1" has "^1.0.3" -objection@1.6.11: - version "1.6.11" - resolved "https://registry.yarnpkg.com/objection/-/objection-1.6.11.tgz#6755c15300277eee76c44faf4295704d8e2e02e2" - integrity sha512-/W6iR6+YvFg1U4k5DyX1MrY+xqodDM8AAOU1J0b3HlptsNw8V3uDHjZgTi1cFPPe5+ZeTTMvhIFhNiUP6+nqYQ== +objection@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/objection/-/objection-2.1.2.tgz#da42e743ef69eabbb29bbf50efbe6f49acc854ba" + integrity sha512-55tkg1C8iFfsEz07N3peKXU4COve+jRTaUlSXAGlcC2Kk/7ONntREnwErQp+0483g7eexEbc8EJ7CDPu/RmYoQ== dependencies: - ajv "^6.10.0" - bluebird "^3.5.5" - lodash "^4.17.11" + ajv "^6.10.2" + db-errors "^0.2.3" offline-plugin@5.0.7: version "5.0.7" @@ -13813,10 +13817,10 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= -simple-git@1.129.0: - version "1.129.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.129.0.tgz#eddd2611d2bf41c77e1d08cd70c0b7f3af785040" - integrity sha512-XbzNmugMTeV2crZnPl+b1ZJn+nqXCUNyrZxDXpLM0kHL3B85sbPlpd8q9I4qtAHI9D2FxTB6w4BuiAGKYtyzKw== +simple-git@1.130.0: + version "1.130.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.130.0.tgz#b689c4163bc021df563a81f256de54482005195d" + integrity sha512-gQsPA1uuAkGUa6S+yG4NRknKHVEV+Vnp437w8dJpDpzjtEH566WRSz5z6DoIxlBFaLC7Xwypznsuf1S/J0gtFg== dependencies: debug "^4.0.1" @@ -14313,7 +14317,7 @@ striptags@3.1.1: resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.1.1.tgz#c8c3e7fdd6fb4bb3a32a3b752e5b5e3e38093ebd" integrity sha1-yMPn/db7S7OjKjt1LltePjgJPr0= -strtok3@^5.0.1: +strtok3@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-5.0.2.tgz#bb81f1f56742e16f1a30ccce5dc3d9498aa5475a" integrity sha512-EFeVpFC5qDsqPEJSrIYyS/ueFBknGhgSK9cW+YAJF/cgJG/KSjoK7X6rK5xnpcLe7y1LVkVFCXWbAb+ClNKzKQ==