diff --git a/keyserver/package.json b/keyserver/package.json index 24802b7b6..12a398923 100644 --- a/keyserver/package.json +++ b/keyserver/package.json @@ -1,102 +1,103 @@ { "name": "keyserver", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "main": "dist/keyserver", "scripts": { "clean": "rm -rf dist/ && rm -rf node_modules/ && mkdir dist", "babel-build": ". bash/source-nvm.sh && yarn --silent babel src/ --out-dir dist/ --config-file ./babel.config.cjs --verbose --ignore 'src/landing/flow-typed','src/landing/node_modules','src/landing/package.json','src/lib/flow-typed','src/lib/node_modules','src/lib/package.json','src/web/flow-typed','src/web/node_modules','src/web/package.json','src/web/dist','src/web/webpack.config.js','src/web/account-bar.react.js','src/web/app.react.js','src/web/calendar','src/web/chat','src/web/flow','src/web/loading-indicator.react.js','src/web/modals','src/web/root.js','src/web/router-history.js','src/web/script.js','src/web/selectors/chat-selectors.js','src/web/selectors/entry-selectors.js','src/web/splash','src/web/vector-utils.js','src/web/vectors.react.js'", "rsync": "rsync -rLpmuv --exclude '*/package.json' --exclude '*/node_modules/*' --include '*.json' --include '*.cjs' --include '*.node' --exclude '*.*' src/ dist/", "prod-build": "yarn babel-build && yarn rsync && yarn update-geoip", "update-geoip": "yarn script dist/scripts/update-geoip.js", "prod": "node --trace-warnings --loader=./loader.mjs dist/keyserver", "dev-rsync": "yarn --silent chokidar --initial --silent -s 'src/**/*.json' 'src/**/*.cjs' -c 'yarn rsync > /dev/null 2>&1'", "dev": ". bash/source-nvm.sh && yarn concurrently --names=\"BABEL,RSYNC,NODEM\" -c \"bgBlue.bold,bgMagenta.bold,bgGreen.bold\" \"yarn babel-build --source-maps --watch\" \"yarn dev-rsync\" \". bash/source-nvm.sh && NODE_ENV=development nodemon -e js,json,cjs --watch dist --no-warnings=ExperimentalWarning --loader=./loader.mjs dist/keyserver\"", "script": ". bash/source-nvm.sh && NODE_ENV=development node --loader=./loader.mjs", "test": "jest" }, "devDependencies": { "@babel/cli": "^7.13.14", "@babel/core": "^7.13.14", "@babel/node": "^7.13.13", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "babel-jest": "^26.6.3", "chokidar-cli": "^2.1.0", "concurrently": "^5.3.0", "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "internal-ip": "4.3.0", "jest": "^26.6.3", "nodemon": "^2.0.4" }, "dependencies": { "@babel/runtime": "^7.13.10", "@matrix-org/olm": "3.2.14", "@parse/node-apn": "^3.2.0", "@vingle/bmp-js": "^0.2.5", "JSONStream": "^1.3.5", "common-tags": "^1.7.2", "cookie-parser": "^1.4.3", "dateformat": "^3.0.3", + "detect-browser": "^4.0.4", "ethers": "^5.7.2", "express": "^4.17.3", "express-ws": "^4.0.0", "firebase-admin": "^10.1.0", "geoip-lite": "^1.4.5", "invariant": "^2.2.4", "landing": "0.0.1", "lib": "0.0.1", "lodash": "^4.17.21", "multer": "^1.4.1", "mysql2": "^2.3.3", "node-schedule": "^2.1.0", "nodemailer": "^6.6.1", "rust-node-addon": "0.0.1", "react": "18.1.0", "react-dom": "18.1.0", "react-html-email": "^3.0.0", "react-redux": "^7.1.1", "react-router": "^5.2.0", "redis": "^3.1.1", "redux": "^4.0.4", "replacestream": "^4.0.3", "rereadable-stream": "^1.4.5", "sharp": "^0.30.5", "siwe": "^1.1.6", "sql-template-strings": "^2.2.2", "stream-combiner": "^0.2.2", "tcomb": "^3.2.29", "twin-bcrypt": "^2.1.1", "uuid": "^3.3.3", "web": "0.0.1", "web-push": "^3.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.5", "utf-8-validate": "^5.0.7" }, "nodemonConfig": { "delay": "200" }, "jest": { "roots": [ "/src" ], "transform": { "\\.js$": "babel-jest" }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ] } } diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js index d1d34f3fb..3eb367f1d 100644 --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -1,192 +1,199 @@ // @flow import olm from '@matrix-org/olm'; import cluster from 'cluster'; import cookieParser from 'cookie-parser'; +import { detect as detectBrowser } from 'detect-browser'; import express from 'express'; import expressWs from 'express-ws'; import os from 'os'; import stores from 'lib/facts/stores.js'; import './cron/cron.js'; import { migrate } from './database/migrations.js'; import { jsonEndpoints } from './endpoints.js'; import { emailSubscriptionResponder } from './responders/comm-landing-responders.js'; import { jsonHandler, httpGetHandler, downloadHandler, htmlHandler, uploadHandler, } from './responders/handlers.js'; import landingHandler from './responders/landing-handler.js'; import { errorReportDownloadResponder } from './responders/report-responders.js'; import { createNewVersionResponder, markVersionDeployedResponder, } from './responders/version-responders.js'; import { websiteResponder } from './responders/website-responders.js'; import { webWorkerResponder } from './responders/webworker-responders.js'; import { onConnection } from './socket/socket.js'; import { multerProcessor, multimediaUploadResponder, uploadDownloadResponder, } from './uploads/uploads.js'; import { prefetchAllURLFacts, getSquadCalURLFacts, getLandingURLFacts, getCommAppURLFacts, } from './utils/urls.js'; (async () => { await olm.init(); await prefetchAllURLFacts(); const squadCalBaseRoutePath = getSquadCalURLFacts()?.baseRoutePath; const landingBaseRoutePath = getLandingURLFacts()?.baseRoutePath; const commAppBaseRoutePath = getCommAppURLFacts()?.baseRoutePath; const compiledFolderOptions = process.env.NODE_ENV === 'development' ? undefined : { maxAge: '1y', immutable: true }; if (cluster.isMaster) { const didMigrationsSucceed: boolean = await migrate(); if (!didMigrationsSucceed) { // The following line uses exit code 2 to ensure nodemon exits // in a dev environment, instead of restarting. Context provided // in https://github.com/remy/nodemon/issues/751 process.exit(2); } const cpuCount = os.cpus().length; for (let i = 0; i < cpuCount; i++) { cluster.fork(); } cluster.on('exit', () => cluster.fork()); } else { const server = express(); expressWs(server); server.use(express.json({ limit: '250mb' })); server.use(cookieParser()); const setupAppRouter = router => { router.use('/images', express.static('images')); router.use('/fonts', express.static('fonts')); router.use('/misc', express.static('misc')); router.use( '/.well-known', express.static( '.well-known', // Necessary for apple-app-site-association file { setHeaders: res => res.setHeader('Content-Type', 'application/json'), }, ), ); router.use( '/compiled', express.static('app_compiled', compiledFolderOptions), ); router.use('/', express.static('icons')); for (const endpoint in jsonEndpoints) { // $FlowFixMe Flow thinks endpoint is string const responder = jsonEndpoints[endpoint]; const expectCookieInvalidation = endpoint === 'log_out'; router.post( `/${endpoint}`, jsonHandler(responder, expectCookieInvalidation), ); } router.get( '/create_version/:deviceType/:codeVersion', httpGetHandler(createNewVersionResponder), ); router.get( '/mark_version_deployed/:deviceType/:codeVersion', httpGetHandler(markVersionDeployedResponder), ); router.get( '/download_error_report/:reportID', downloadHandler(errorReportDownloadResponder), ); router.get( '/upload/:uploadID/:secret', downloadHandler(uploadDownloadResponder), ); // This endpoint should be handled by the mobile app. If the server // receives this request, it means that the app is not installed and we // should redirect the user to a place from which the app can be // downloaded. router.get('/invite/:secret', (req, res) => { + const userAgent = req.get('User-Agent'); + const detectionResult = detectBrowser(userAgent); + const redirectUrl = + detectionResult.os === 'Android OS' + ? stores.googlePlayUrl + : stores.appStoreUrl; res.writeHead(301, { - Location: stores.appStoreUrl, + Location: redirectUrl, }); res.end(); }); // $FlowFixMe express-ws has side effects that can't be typed router.ws('/ws', onConnection); router.get('/worker/:worker', webWorkerResponder); router.get('*', htmlHandler(websiteResponder)); router.post( '/upload_multimedia', multerProcessor, uploadHandler(multimediaUploadResponder), ); }; // Note - the order of router declarations matters. On prod we have // squadCalBaseRoutePath configured to '/', which means it's a catch-all. If // we call server.use on squadCalRouter first, it will catch all requests // and prevent commAppRouter and landingRouter from working correctly. So we // make sure that squadCalRouter goes last if (landingBaseRoutePath) { const landingRouter = express.Router(); landingRouter.use('/images', express.static('images')); landingRouter.use('/fonts', express.static('fonts')); landingRouter.use( '/compiled', express.static('landing_compiled', compiledFolderOptions), ); landingRouter.use('/', express.static('landing_icons')); landingRouter.post('/subscribe_email', emailSubscriptionResponder); landingRouter.get('*', landingHandler); server.use(landingBaseRoutePath, landingRouter); } if (commAppBaseRoutePath) { const commAppRouter = express.Router(); setupAppRouter(commAppRouter); server.use(commAppBaseRoutePath, commAppRouter); } if (squadCalBaseRoutePath) { const squadCalRouter = express.Router(); setupAppRouter(squadCalRouter); server.use(squadCalBaseRoutePath, squadCalRouter); } const listenAddress = (() => { if (process.env.COMM_LISTEN_ADDR) { return process.env.COMM_LISTEN_ADDR; } else if (process.env.NODE_ENV === 'development') { return undefined; } else { return 'localhost'; } })(); server.listen(parseInt(process.env.PORT, 10) || 3000, listenAddress); } })(); diff --git a/lib/facts/stores.js b/lib/facts/stores.js index 6342956fe..ead5ad590 100644 --- a/lib/facts/stores.js +++ b/lib/facts/stores.js @@ -1,7 +1,9 @@ // @flow const stores = { appStoreUrl: 'https://apps.apple.com/us/app/comm-messenger/id1574433435', + googlePlayUrl: + 'https://play.google.com/store/apps/details?id=app.comm.android', }; export default stores;