diff options
Diffstat (limited to 'vnext/server/middleware')
-rw-r--r-- | vnext/server/middleware/android.js | 43 | ||||
-rw-r--r-- | vnext/server/middleware/android.spec.js | 33 | ||||
-rw-r--r-- | vnext/server/middleware/event.js | 7 | ||||
-rw-r--r-- | vnext/server/middleware/host-meta.js | 42 | ||||
-rw-r--r-- | vnext/server/middleware/legacy.json | 142 | ||||
-rw-r--r-- | vnext/server/middleware/mastodon.js | 23 | ||||
-rw-r--r-- | vnext/server/middleware/mastodon.spec.js | 13 | ||||
-rw-r--r-- | vnext/server/middleware/rememberme.js | 50 | ||||
-rw-r--r-- | vnext/server/middleware/urlexpand.js | 3 | ||||
-rw-r--r-- | vnext/server/middleware/webfinger.js | 59 | ||||
-rw-r--r-- | vnext/server/middleware/webfinger.spec.js | 28 |
11 files changed, 439 insertions, 4 deletions
diff --git a/vnext/server/middleware/android.js b/vnext/server/middleware/android.js new file mode 100644 index 00000000..ae5f8fe8 --- /dev/null +++ b/vnext/server/middleware/android.js @@ -0,0 +1,43 @@ +import debug from 'debug' +var log = debug('android') + +import * as legacy_data from './legacy.json' + +const releases_url = 'https://api.github.com/repos/Juick/Juick-Android/releases' + +/** + * Return android releases + * @type {import('express').RequestParamHandler} + */ +const releases = async (req, res) => { + let agent = req.headers['user-agent'] || 'unknown' + let android_version = parse_agent_android_sdk_version(agent) + log(`releases request from ${android_version || agent}`) + if (android_version > 0) { + if (is_legacy_android(android_version)) { + log('responding with legacy stub') + return res.json([legacy_data]) + } else { + log('redirecting to Github') + return res.redirect(releases_url) + } + } + return res.json([]) +} + +const parse_agent_android_sdk_version = (agent = '') => { + let version = agent.split(' ', 3) + let is_android_app = version.length == 3 && version[2].startsWith('Android') + if (is_android_app) { + let android_version = version[2].split('/') + let is_valid_version = android_version.length == 2 + return is_valid_version ? +(android_version[1]) : NaN + } + return NaN +} + +const is_legacy_android = (version) => { + return version < 24 +} + +export default releases diff --git a/vnext/server/middleware/android.spec.js b/vnext/server/middleware/android.spec.js new file mode 100644 index 00000000..19d380d7 --- /dev/null +++ b/vnext/server/middleware/android.spec.js @@ -0,0 +1,33 @@ +import request from 'supertest' + +import { app } from '../app' + +describe('Releases helper', () => { + it('Should respond with empty array to unknown user agents', async () => { + return request(app) + .get('/api/apps/android/releases') + .expect(200) + .then(response => { + expect(response.body).toStrictEqual([]) + }) + }) + it('Should respond with a single legacy version data to old Android app', async () => { + return request(app) + .get('/api/apps/android/releases') + .set('User-Agent', 'Juick/100 okhttp/3.12 Android/19') + .expect(200) + .then(response => { + expect(response.body.length).toBe(1) + expect(response.body[0].name).toBe('3.1.216') + }) + }) + it('Should redirect to Github when Android version is ok', async () => { + return request(app) + .get('/api/apps/android/releases') + .set('User-Agent', 'Juick/100 okhttp/3.12 Android/24') + .expect(302) + .then(response => { + expect(response.redirect).toBe(true) + }) + }) +}) diff --git a/vnext/server/middleware/event.js b/vnext/server/middleware/event.js index 8280f32b..623ee9a9 100644 --- a/vnext/server/middleware/event.js +++ b/vnext/server/middleware/event.js @@ -1,3 +1,5 @@ +import config from 'config' + import { simpleParser } from 'mailparser' import { isPM, isReply, isService } from '../common/MessageUtils' import { sendTelegramNotification } from '../durov' @@ -8,7 +10,7 @@ import { send } from '../hms' var log = debug('event') /** @type {number[]} */ -const allSandboxIds = [] +const allSandboxIds = config.get('service.sandboxIds') /** * handle message event @@ -26,8 +28,9 @@ function processMessageEvent(msg) { subscribers(new URLSearchParams(JSON.parse(JSON.stringify(params)))).then(users => { return users.map(user => { log(`${user.uname}: ${user.unreadCount}`) + let tokenTypes = msg.service ? ['mpns', 'apns', 'fcm'] : ['mpns', 'apns', 'fcm', 'web'] let [sandboxTokens, productionTokens] = (user.tokens || []) - .filter(t => ['mpns', 'apns', 'fcm', 'web'].includes(t.type)) + .filter(t => tokenTypes.includes(t.type)) .map(t => t.type === 'web' ? JSON.parse(t.token) : t.token) .reduce((result, element) => { allSandboxIds.includes(user.uid) diff --git a/vnext/server/middleware/host-meta.js b/vnext/server/middleware/host-meta.js new file mode 100644 index 00000000..0bca925a --- /dev/null +++ b/vnext/server/middleware/host-meta.js @@ -0,0 +1,42 @@ +import config from 'config' + +const baseURL = config.get('service.baseURL') + +/** + * host-meta endpoint + * @type {import('express').RequestParamHandler} + */ +export const xmlMeta = async (_req, res) => { + return res.set('Content-Type', 'text/xml') + .send(`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="${baseURL}/.well-known/webfinger?resource={uri}"/></XRD>`) +} + +/** + * host-meta.json endpoint + * @type {import('express').RequestParamHandler} + */ +export const jsonMeta = async (_req, res) => { + return res.json({ + 'links': [ + { + 'rel': 'lrdd', + 'template': `${baseURL}/.well-known/webfinger?resource={uri}` + } + ] + }) +} + +/** + * nodeinfo endpoint + * @type {import('express').RequestParamHandler} + */ +export const nodeinfo = async (_req, res) => { + return res.json({ + 'links': [ + { + 'rel': 'https://nodeinfo.diaspora.software/ns/schema/2.0', + 'href': `${baseURL}/.well-known/nodeinfo/2.0` + } + ] + }) +} diff --git a/vnext/server/middleware/legacy.json b/vnext/server/middleware/legacy.json new file mode 100644 index 00000000..840708e6 --- /dev/null +++ b/vnext/server/middleware/legacy.json @@ -0,0 +1,142 @@ +{ + "url": "https://api.github.com/repos/juick/Juick-Android/releases/164308344", + "assets_url": "https://api.github.com/repos/juick/Juick-Android/releases/164308344/assets", + "upload_url": "https://uploads.github.com/repos/juick/Juick-Android/releases/164308344/assets{?name,label}", + "html_url": "https://github.com/juick/Juick-Android/releases/tag/v3.1.216", + "id": 164308344, + "author": { + "login": "vitalyster", + "id": 1052407, + "node_id": "MDQ6VXNlcjEwNTI0MDc=", + "avatar_url": "https://avatars.githubusercontent.com/u/1052407?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/vitalyster", + "html_url": "https://github.com/vitalyster", + "followers_url": "https://api.github.com/users/vitalyster/followers", + "following_url": "https://api.github.com/users/vitalyster/following{/other_user}", + "gists_url": "https://api.github.com/users/vitalyster/gists{/gist_id}", + "starred_url": "https://api.github.com/users/vitalyster/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/vitalyster/subscriptions", + "organizations_url": "https://api.github.com/users/vitalyster/orgs", + "repos_url": "https://api.github.com/users/vitalyster/repos", + "events_url": "https://api.github.com/users/vitalyster/events{/privacy}", + "received_events_url": "https://api.github.com/users/vitalyster/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "RE_kwDOADJkWc4JyyV4", + "tag_name": "v3.1.216", + "target_commitish": "master", + "name": "3.1.216", + "draft": false, + "prerelease": true, + "created_at": "2024-07-07T19:47:32Z", + "published_at": "2024-07-07T19:51:29Z", + "assets": [ + { + "url": "https://api.github.com/repos/juick/Juick-Android/releases/assets/178215973", + "id": 178215973, + "node_id": "RA_kwDOADJkWc4Kn1wl", + "name": "Juick-free-v3.1.216.apk", + "label": null, + "uploader": { + "login": "vitalyster", + "id": 1052407, + "node_id": "MDQ6VXNlcjEwNTI0MDc=", + "avatar_url": "https://avatars.githubusercontent.com/u/1052407?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/vitalyster", + "html_url": "https://github.com/vitalyster", + "followers_url": "https://api.github.com/users/vitalyster/followers", + "following_url": "https://api.github.com/users/vitalyster/following{/other_user}", + "gists_url": "https://api.github.com/users/vitalyster/gists{/gist_id}", + "starred_url": "https://api.github.com/users/vitalyster/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/vitalyster/subscriptions", + "organizations_url": "https://api.github.com/users/vitalyster/orgs", + "repos_url": "https://api.github.com/users/vitalyster/repos", + "events_url": "https://api.github.com/users/vitalyster/events{/privacy}", + "received_events_url": "https://api.github.com/users/vitalyster/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 7807858, + "download_count": 2, + "created_at": "2024-07-07T19:51:12Z", + "updated_at": "2024-07-07T19:51:25Z", + "browser_download_url": "https://github.com/juick/Juick-Android/releases/download/v3.1.216/Juick-free-v3.1.216.apk" + }, + { + "url": "https://api.github.com/repos/juick/Juick-Android/releases/assets/178215894", + "id": 178215894, + "node_id": "RA_kwDOADJkWc4Kn1vW", + "name": "Juick-google-v3.1.216.apk", + "label": null, + "uploader": { + "login": "vitalyster", + "id": 1052407, + "node_id": "MDQ6VXNlcjEwNTI0MDc=", + "avatar_url": "https://avatars.githubusercontent.com/u/1052407?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/vitalyster", + "html_url": "https://github.com/vitalyster", + "followers_url": "https://api.github.com/users/vitalyster/followers", + "following_url": "https://api.github.com/users/vitalyster/following{/other_user}", + "gists_url": "https://api.github.com/users/vitalyster/gists{/gist_id}", + "starred_url": "https://api.github.com/users/vitalyster/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/vitalyster/subscriptions", + "organizations_url": "https://api.github.com/users/vitalyster/orgs", + "repos_url": "https://api.github.com/users/vitalyster/repos", + "events_url": "https://api.github.com/users/vitalyster/events{/privacy}", + "received_events_url": "https://api.github.com/users/vitalyster/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 8421195, + "download_count": 7, + "created_at": "2024-07-07T19:50:49Z", + "updated_at": "2024-07-07T19:51:04Z", + "browser_download_url": "https://github.com/juick/Juick-Android/releases/download/v3.1.216/Juick-google-v3.1.216.apk" + }, + { + "url": "https://api.github.com/repos/juick/Juick-Android/releases/assets/178215877", + "id": 178215877, + "node_id": "RA_kwDOADJkWc4Kn1vF", + "name": "Juick-huawei-v3.1.216.apk", + "label": null, + "uploader": { + "login": "vitalyster", + "id": 1052407, + "node_id": "MDQ6VXNlcjEwNTI0MDc=", + "avatar_url": "https://avatars.githubusercontent.com/u/1052407?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/vitalyster", + "html_url": "https://github.com/vitalyster", + "followers_url": "https://api.github.com/users/vitalyster/followers", + "following_url": "https://api.github.com/users/vitalyster/following{/other_user}", + "gists_url": "https://api.github.com/users/vitalyster/gists{/gist_id}", + "starred_url": "https://api.github.com/users/vitalyster/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/vitalyster/subscriptions", + "organizations_url": "https://api.github.com/users/vitalyster/orgs", + "repos_url": "https://api.github.com/users/vitalyster/repos", + "events_url": "https://api.github.com/users/vitalyster/events{/privacy}", + "received_events_url": "https://api.github.com/users/vitalyster/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 8275073, + "download_count": 3, + "created_at": "2024-07-07T19:50:27Z", + "updated_at": "2024-07-07T19:50:41Z", + "browser_download_url": "https://github.com/juick/Juick-Android/releases/download/v3.1.216/Juick-huawei-v3.1.216.apk" + } + ], + "tarball_url": "https://api.github.com/repos/juick/Juick-Android/tarball/v3.1.216", + "zipball_url": "https://api.github.com/repos/juick/Juick-Android/zipball/v3.1.216", + "body": "* Bugfixes and performance improvements" +} diff --git a/vnext/server/middleware/mastodon.js b/vnext/server/middleware/mastodon.js new file mode 100644 index 00000000..e29fd736 --- /dev/null +++ b/vnext/server/middleware/mastodon.js @@ -0,0 +1,23 @@ +import { getMonthlyActiveUsers } from '../db/Users' + +/** + * Return content for embedding + * @type {import('express').RequestParamHandler} + */ +export const instance = async (req, res) => { + const activeUsers = await getMonthlyActiveUsers() + res.json({ + 'domain': 'juick.com', + 'title': 'Microblogging service', + 'description': 'Juick', + 'version': '2.x', + 'contact': { + 'email': 'support@juick.com' + }, + 'usage': { + 'users': { + 'active_month': activeUsers + } + } + }) +} diff --git a/vnext/server/middleware/mastodon.spec.js b/vnext/server/middleware/mastodon.spec.js new file mode 100644 index 00000000..561303bc --- /dev/null +++ b/vnext/server/middleware/mastodon.spec.js @@ -0,0 +1,13 @@ +import request from 'supertest' +import { app } from '../app' + +describe('Mastodon API middleware', () => { + it('Inactive users should not be included in Instance response', async () => { + return request(app) + .get('/api/v2/instance') + .expect(200) + .then(response => { + expect(response.body.usage.users.active_month).toStrictEqual(1) + }) + }) +}) diff --git a/vnext/server/middleware/rememberme.js b/vnext/server/middleware/rememberme.js new file mode 100644 index 00000000..1c0a2ee4 --- /dev/null +++ b/vnext/server/middleware/rememberme.js @@ -0,0 +1,50 @@ +import config from 'config' +import { createHash } from 'node:crypto' +import debug from 'debug' +import { getUserByName } from '../db/Users' +const log = debug('auth') + +const auth_key = config.get('service.auth.key') +const JUICK_COOKIE_NAME = 'juick-remember-me' + +export const rememberMeParser = (req, _res, next) => { + if (req.cookies && !!req.cookies[JUICK_COOKIE_NAME]) { + validate_cookie(req.cookies[JUICK_COOKIE_NAME]).then(visitor => { + req.visitor = visitor + setImmediate(next) + }).catch(() => { + setImmediate(next) + }) + } else { + setImmediate(next) + } +} + +const validate_cookie = async (cookie) => { + const [ username, expiry_time, , signature ] = Buffer.from(cookie, 'base64').toString('ascii').split(':', 4) + if (is_token_expired(expiry_time)) { + log(`Token expired at: ${new Date(expiry_time)}`) + return '' + } + const user = await getUserByName(username) + if (!user) { + log(`User not found: ${username}`) + return '' + } + const expected_signature = make_token_signature(expiry_time, username, user['passw']) + if (expected_signature === signature) { + log(`Signature verified: ${username}`) + return username + } + log(`Invalid token signature: ${username}`) + return '' +} + +const make_token_signature = (expiry_time, username, password) => { + const data = `${username}:${expiry_time}:{noop}${password}:${auth_key}` + return createHash('sha256').update(data).digest('hex') +} + +const is_token_expired = (expiry_time) => { + return new Date(expiry_time) < new Date() +} diff --git a/vnext/server/middleware/urlexpand.js b/vnext/server/middleware/urlexpand.js index a99f80a7..fd7ab3bb 100644 --- a/vnext/server/middleware/urlexpand.js +++ b/vnext/server/middleware/urlexpand.js @@ -2,8 +2,7 @@ import { expandShortenedLink } from '../../src/api' /** * Expand URLs - * @param {import("next").NextApiRequest} req - * @param {import("next").NextApiResponse} res + * @type {import('express').RequestParamHandler} */ export default function urlExpand(req, res) { let url = (req.query.url || '').toString() diff --git a/vnext/server/middleware/webfinger.js b/vnext/server/middleware/webfinger.js new file mode 100644 index 00000000..873387b3 --- /dev/null +++ b/vnext/server/middleware/webfinger.js @@ -0,0 +1,59 @@ +import config from 'config' +import addrparser from 'address-rfc2822' +import debug from 'debug' +var log = debug('webfinger') + +import { getUserByName } from '../db/Users' + +const baseUrl = config.get('service.baseURL') + +/** + * WebFinger endpoint + * @type {import('express').RequestParamHandler} + */ +export const webfinger = async (req, res) => { + const resource = req.query.resource + if (resource) { + const acct = resource.substring(5) // drop "acct:" + const addresses = parseAddress(acct) + if (addresses && addresses.length == 1) { + const address = addresses[0] + const ourAddress = new URL(baseUrl) + if (address.host() === ourAddress.hostname) { + const name = addresses[0].user() + const user = await getUserByName(name) + if (user) { + return res.json({ + subject: resource, + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: `${baseUrl}/u/${user.nick}` + } + ] + }) + } else { + log(`User not found: ${name}`) + return res.status(404).end() + } + } else { + log(`Address not found: ${address.host()}`) + return res.status(404).end() + } + } else { + log(`Invalid resource: ${resource}`) + return res.status(400).end() + } + } + log('Missing `resource` param') + res.status(400) +} + +const parseAddress = (address = '') => { + try { + return addrparser.parse(address, { startAt: 'address' }) + } catch { + return undefined + } +} diff --git a/vnext/server/middleware/webfinger.spec.js b/vnext/server/middleware/webfinger.spec.js new file mode 100644 index 00000000..d1b198e6 --- /dev/null +++ b/vnext/server/middleware/webfinger.spec.js @@ -0,0 +1,28 @@ +import request from 'supertest' + +import { app } from '../app' + +describe('WebFinger middleware', () => { + it('Existing user response should have a subject and links', async () => { + const resource = 'acct:ugnich@example.lan' + const response = await request(app) + .get(`/.well-known/webfinger?resource=${resource}`) + expect(response.status).toStrictEqual(200) + expect(response.body.subject).toStrictEqual(resource) + expect(response.body.links.length).toStrictEqual(1) + expect(response.body.links[0].href).toStrictEqual('https://example.lan/u/ugnich') + }) + it('Unknown user should return 404', async () => { + const resource = 'acct:durov@example.lan' + const response = await request(app) + .get(`/.well-known/webfinger?resource=${resource}`) + expect(response.status).toStrictEqual(404) + }) + it('Invalid input should return 400', async () => { + const resource = ';DROP TABLE users' + const response = await request(app) + .get(`/.well-known/webfinger?resource=${resource}`) + expect(response.status).toStrictEqual(400) + }) +}) + |