add support for private groups at /g/
The code checks for: - Presence of all three URL arguments: `a`, `a1`, `a2` - Verifies that `a`(admin key...) is 132 characters long - Verifies that `a2`(chat key...) is 169 characters long - Verifies that `a1` does not contain HTML before rendering Each case is tested, including the valid one. Signed-off-by: Jakub Sokołowski <jakub@status.im>
This commit is contained in:
parent
8d74ea7969
commit
5fd8dd2466
|
@ -11,7 +11,7 @@ const router = express.Router()
|
|||
/* Helper for generating pages */
|
||||
const genPage = (req, res, options) => {
|
||||
/* Protection against XSS attacks */
|
||||
if (!utils.isValidUrl(options.mainTarget)) {
|
||||
if (!utils.isValidUrl(options.mainTarget, useStd3ASCII=!options.whitespace)) {
|
||||
handleError(`Input contains HTML: ${options.mainTarget}`)(req, res)
|
||||
return
|
||||
}
|
||||
|
@ -77,7 +77,6 @@ const handleSite = (req, res) => {
|
|||
const handleChatKey = (req, res) => {
|
||||
let chatKey = req.params[0]
|
||||
let uncompressedKey = chatKey
|
||||
|
||||
try {
|
||||
if (!chatKey.startsWith('0x')) { /* decompress/deserialize key */
|
||||
uncompressedKey = utils.decompressKey(chatKey)
|
||||
|
@ -123,13 +122,47 @@ const handlePublicChannel = (req, res) => {
|
|||
const chatName = req.params[0]
|
||||
genPage(req, res, {
|
||||
title: `Join #${chatName} in Status`,
|
||||
info: `Join public channel <span class="inline-block align-bottom w-32 truncate">#${chatName}</span> on Status.`,
|
||||
info: `Join public channel <span class="inline-block align-bottom w-32 truncate">#${chatName}</span> in Status.`,
|
||||
mainTarget: chatName,
|
||||
headerName: `#${chatName}`,
|
||||
path: req.originalUrl,
|
||||
})
|
||||
}
|
||||
|
||||
/* This verifies all 3 required URL arguments are present */
|
||||
const validateGroupChatArgs = (args) => {
|
||||
const requiredKeys = ['a', 'a1', 'a2']
|
||||
const argsHasKey = key => Object.keys(args).includes(key)
|
||||
if (!requiredKeys.every(argsHasKey)) {
|
||||
throw Error('Missing arguments!')
|
||||
}
|
||||
if (args.a == null || args.a.length != 132) {
|
||||
throw Error('Admin public key invalid!')
|
||||
}
|
||||
if (args.a2 == null || args.a2.length != 169) {
|
||||
throw Error('Group public key invalid!')
|
||||
}
|
||||
}
|
||||
|
||||
/* Open Group Chat in Status */
|
||||
const handleGroupChat = (req, res) => {
|
||||
try {
|
||||
validateGroupChatArgs(req.query)
|
||||
} catch(ex) {
|
||||
handleError(`Invalid group chat URL: ${ex.message}`)(req, res)
|
||||
return
|
||||
}
|
||||
let groupName = req.query.a1
|
||||
genPage(req, res, {
|
||||
title: `Join "${groupName}" group chat in Status`,
|
||||
info: `Join group chat <span class="inline-block align-bottom w-32 truncate">${groupName}</span> in Status.`,
|
||||
whitespace: true, /* Allow whitespace in group names */
|
||||
mainTarget: groupName,
|
||||
headerName: groupName,
|
||||
path: req.originalUrl,
|
||||
})
|
||||
}
|
||||
|
||||
router.get('/.well-known/assetlinks.json', (req, res) => {
|
||||
res.json(assetLinks)
|
||||
})
|
||||
|
@ -163,6 +196,8 @@ router.get(/^\/chat\/public\/([a-z0-9-]+)$/, handlePublicChannel) /* Legacy */
|
|||
router.get(/^\/chat\/public\/([a-zA-Z0-9-]+)$/, handleRedirect)
|
||||
router.get(/^\/([a-zA-Z0-9-]+)$/, (req, res) => res.redirect(req.originalUrl.toLowerCase()))
|
||||
|
||||
router.get('/g/:group(*)', handleGroupChat)
|
||||
|
||||
/* Catchall for everything else */
|
||||
router.get('*', (req, res, next) => {
|
||||
if (req.query.redirect) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { test } from 'zora'
|
||||
import crypto from 'crypto'
|
||||
import cheerio from 'cheerio'
|
||||
import request from 'supertest'
|
||||
import app from '../app'
|
||||
|
@ -7,16 +8,8 @@ import assetLinks from '../resources/assetlinks.json'
|
|||
import appleSiteAssociation from '../resources/apple-app-site-association.json'
|
||||
|
||||
const host = 'join.status.im'
|
||||
const chatKey = 'e139115a1acc72510388fcf7e1cf492784c9a839888b25271465f4f1baa38c2d3997f8fd78828eb8628bc3bb55ababd884c6002d18330d59c404cc9ce3e4fb35'
|
||||
const multibaseKey = 'fe70103e139115a1acc72510388fcf7e1cf492784c9a839888b25271465f4f1baa38c2d'
|
||||
const compressedKey = 'zQ3shuoHL7WZEfKdexM6EyDRDhXBgcKz5SVw79stVMpmeyUvG'
|
||||
const chatName = 'Lavender Trivial Goral'
|
||||
|
||||
const srv = request(app)
|
||||
|
||||
const get = (path) => (
|
||||
srv.get(path).set('Host', host)
|
||||
)
|
||||
const get = (path) => srv.get(path).set('Host', host)
|
||||
|
||||
/* helpers for querying returned HTML */
|
||||
const q = (res, query) => cheerio.load(res.text)(query)
|
||||
|
@ -74,6 +67,9 @@ test('test user ens routes', t => {
|
|||
})
|
||||
|
||||
test('test chat key routes', t => {
|
||||
const chatName = 'Lavender Trivial Goral'
|
||||
const chatKey = 'e139115a1acc72510388fcf7e1cf492784c9a839888b25271465f4f1baa38c2d3997f8fd78828eb8628bc3bb55ababd884c6002d18330d59c404cc9ce3e4fb35'
|
||||
|
||||
t.test(`/u/0x04${chatKey.substr(0,8)}... - VALID`, async t => {
|
||||
const res = await get(`/u/0x04${chatKey}`)
|
||||
t.eq(res.statusCode, 200, 'returns 200')
|
||||
|
@ -106,6 +102,9 @@ test('test chat key routes', t => {
|
|||
})
|
||||
|
||||
test('test multibase chat key routes', t => {
|
||||
const chatName = 'Lavender Trivial Goral'
|
||||
const multibaseKey = 'fe70103e139115a1acc72510388fcf7e1cf492784c9a839888b25271465f4f1baa38c2d'
|
||||
|
||||
t.test(`/u/${multibaseKey.substr(0,12)}... - VALID`, async t => {
|
||||
const res = await get(`/u/${multibaseKey}`)
|
||||
t.eq(res.statusCode, 200, 'returns 200')
|
||||
|
@ -123,6 +122,8 @@ test('test multibase chat key routes', t => {
|
|||
})
|
||||
|
||||
test('test compressed chat key routes', t => {
|
||||
const compressedKey = 'zQ3shuoHL7WZEfKdexM6EyDRDhXBgcKz5SVw79stVMpmeyUvG'
|
||||
|
||||
t.test(`/u/${compressedKey.substr(0,12)}... - VALID`, async t => {
|
||||
const res = await get(`/u/${compressedKey}`)
|
||||
t.eq(res.statusCode, 200, 'returns 200')
|
||||
|
@ -145,7 +146,7 @@ test('test public channel routes', t => {
|
|||
t.eq(res.statusCode, 200, 'returns 200')
|
||||
t.eq(meta(res, 'al:ios:url'), 'status-im://status-test', 'contains ios url')
|
||||
t.eq(meta(res, 'al:android:url'), 'status-im://status-test', 'contains android url')
|
||||
t.eq(html(res, 'div#info'), 'Join public channel <span class=\"inline-block align-bottom w-32 truncate\">#status-test</span> on Status.', 'contains prompt')
|
||||
t.eq(html(res, 'div#info'), 'Join public channel <span class=\"inline-block align-bottom w-32 truncate\">#status-test</span> in Status.', 'contains prompt')
|
||||
})
|
||||
|
||||
t.test('/staTus-TesT - UPPER CASE', async t => { /* we don't allow uppercase */
|
||||
|
@ -157,6 +158,42 @@ test('test public channel routes', t => {
|
|||
})
|
||||
})
|
||||
|
||||
test('group chat routes', t => {
|
||||
const groupName = 'Secret%20Club'
|
||||
const groupUUID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
const adminKey = '0x' + crypto.randomBytes(65).toString('hex')
|
||||
const groupKey = groupUUID + '-0x' + crypto.randomBytes(65).toString('hex')
|
||||
|
||||
t.test('/g/args?a1=Secret%20Club&... - VALID', async t => {
|
||||
const res = await get(`/g/args?a=${adminKey}&a1=${groupName}&a2=${groupKey}`)
|
||||
t.eq(res.statusCode, 200, 'returns 200')
|
||||
t.eq(meta(res, 'al:ios:url'), `status-im://g/args?a=${adminKey}&a1=${groupName}&a2=${groupKey}`, 'contains ios url')
|
||||
t.eq(meta(res, 'al:android:url'), `status-im://g/args?a=${adminKey}&a1=${groupName}&a2=${groupKey}`, 'contains android url')
|
||||
t.eq(html(res, 'div#info'), 'Join group chat <span class=\"inline-block align-bottom w-32 truncate\">Secret Club</span> in Status.', 'contains prompt')
|
||||
})
|
||||
|
||||
t.test('/g/args?a1=Secret%20Club&.. - MISSING ARGS', async t => {
|
||||
const res = await get(`/g/args?a=${adminKey}&a=${groupName}`)
|
||||
t.eq(res.statusCode, 400, 'returns 400')
|
||||
t.eq(html(res, 'h3#header'), 'Invalid input format', 'contains warning')
|
||||
t.eq(html(res, 'code#error'), 'Invalid group chat URL: Missing arguments!', 'contains error')
|
||||
})
|
||||
|
||||
t.test('/g/args?a1=Secret%20Club&.. - WRONG ADMIN KEY', async t => {
|
||||
const res = await get(`/g/args?a=${adminKey.substr(0, 130)}&a1=${groupName}&a2=${groupKey}`)
|
||||
t.eq(res.statusCode, 400, 'returns 400')
|
||||
t.eq(html(res, 'h3#header'), 'Invalid input format', 'contains warning')
|
||||
t.eq(html(res, 'code#error'), 'Invalid group chat URL: Admin public key invalid!', 'contains error')
|
||||
})
|
||||
|
||||
t.test('/g/args?a1=Secret%20Club&.. - WRONG CHAT KEY', async t => {
|
||||
const res = await get(`/g/args?a=${adminKey}&a1=${groupName}&a2=${groupKey.substr(0, 160)}`)
|
||||
t.eq(res.statusCode, 400, 'returns 400')
|
||||
t.eq(html(res, 'h3#header'), 'Invalid input format', 'contains warning')
|
||||
t.eq(html(res, 'code#error'), 'Invalid group chat URL: Group public key invalid!', 'contains error')
|
||||
})
|
||||
})
|
||||
|
||||
test('test other routes', t => {
|
||||
t.test('/health', async t => {
|
||||
const res = await get('/health')
|
||||
|
|
|
@ -18,12 +18,13 @@ const makeQrCodeDataUri = (x) => (
|
|||
QRCode.toDataURL(x, {width: 300})
|
||||
)
|
||||
|
||||
const isValidUrl = (text) => {
|
||||
const isValidUrl = (text, useStd3ASCII=true) => {
|
||||
/* Remove protocol prefix */
|
||||
noPrefix = text.replace(/(^\w+:|^)\/\//, '');
|
||||
tokens = noPrefix.split('/')
|
||||
try {
|
||||
uts46.toUnicode(tokens[0], {useStd3ASCII: true})
|
||||
/* useStd3ASCII=true accepts only DNS domain complaint urls */
|
||||
uts46.toUnicode(tokens[0], {useStd3ASCII})
|
||||
} catch(ex) {
|
||||
return false
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue