Merge branch 'configdb' into 'main'

Add PleromaConfigDB module, include sentryDsn in CSP

See merge request soapbox-pub/ditto!579
This commit is contained in:
Alex Gleason 2024-11-15 02:43:50 +00:00
commit 7108b62193
6 changed files with 446 additions and 33 deletions

291
fixtures/config-db.json Normal file
View file

@ -0,0 +1,291 @@
{
"configs": [{
"db": [
":soapbox_fe"
],
"group": ":pleroma",
"key": ":frontend_configurations",
"value": [
{
"tuple": [
":pleroma_fe",
{
":alwaysShowSubjectInput": true,
":background": "/images/city.jpg",
":collapseMessageWithSubject": false,
":disableChat": false,
":greentext": false,
":hideFilteredStatuses": false,
":hideMutedPosts": false,
":hidePostStats": false,
":hideSitename": false,
":hideUserStats": false,
":loginMethod": "password",
":logo": "/static/logo.svg",
":logoMargin": ".1em",
":logoMask": true,
":minimalScopesMode": false,
":noAttachmentLinks": false,
":nsfwCensorImage": "",
":postContentType": "text/plain",
":redirectRootLogin": "/main/friends",
":redirectRootNoLogin": "/main/all",
":scopeCopy": true,
":showFeaturesPanel": true,
":showInstanceSpecificPanel": false,
":sidebarRight": false,
":subjectLineBehavior": "email",
":theme": "pleroma-dark",
":webPushNotifications": false
}
]
},
{
"tuple": [
":soapbox_fe",
{
"aboutPages": {},
"ads": [
{
"card": {
"height": 564,
"image": "https://media.gleasonator.com/3c331456d0d0f9f9ad91eab0efbb4df22a044f92bdf6ef349b26de97db5ca3bd.png",
"type": "link",
"url": "https://www.veganbodybuilding.com/",
"width": 564
}
},
{
"card": {
"height": 250,
"image": "https://media.gleasonator.com/22590c7cb3edd8ac82660301be980c6fcad6b96a320e24f709ad0571a29ea0aa.png",
"type": "link",
"url": "https://poa.st",
"width": 300
}
}
],
"allowedEmoji": [
"👍",
"⚡",
"❤",
"😂",
"😯",
"😢",
"😡"
],
"authenticatedProfile": false,
"banner": "",
"betaPages": {},
"brandColor": "#1ca82b",
"colors": {
"accent": {
"100": "#eafae7",
"200": "#caf4c3",
"300": "#6bdf58",
"400": "#55da40",
"50": "#f4fdf3",
"500": "#2bd110",
"600": "#27bc0e",
"700": "#209d0c",
"800": "#0d3f05",
"900": "#082803"
},
"accent-blue": "#199727",
"danger": {
"100": "#fee2e2",
"200": "#fecaca",
"300": "#fca5a5",
"400": "#f87171",
"50": "#fef2f2",
"500": "#ef4444",
"600": "#dc2626",
"700": "#b91c1c",
"800": "#991b1b",
"900": "#7f1d1d"
},
"gradient-end": "#2bd110",
"gradient-start": "#1ca82b",
"gray": {
"100": "#f1f6f2",
"200": "#dde8de",
"300": "#9ebfa2",
"400": "#91b595",
"50": "#f8faf8",
"500": "#75a37a",
"600": "#69936e",
"700": "#4c504c",
"800": "#233125",
"900": "#161f17"
},
"greentext": "#789922",
"primary": {
"100": "#e8f6ea",
"200": "#c6e9ca",
"300": "#60c26b",
"400": "#49b955",
"50": "#f4fbf4",
"500": "#1ca82b",
"600": "#199727",
"700": "#157e20",
"800": "#08320d",
"900": "#052008"
},
"secondary": {
"100": "#eafae7",
"200": "#cef4c3",
"300": "#7cdf58",
"400": "#71da40",
"50": "#f9fdf3",
"500": "#359713",
"600": "#4ebc0e",
"700": "#2f9d0c",
"800": "#173f05",
"900": "#282828"
},
"success": {
"100": "#dcfce7",
"200": "#bbf7d0",
"300": "#86efac",
"400": "#4ade80",
"50": "#f0fdf4",
"500": "#22c55e",
"600": "#16a34a",
"700": "#15803d",
"800": "#166534",
"900": "#14532d"
}
},
"copyright": "♥2022. Copying is an act of love. Please copy and share.",
"cryptoAddresses": [
{
"address": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n",
"ticker": "btc"
},
{
"address": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717",
"ticker": "eth"
},
{
"address": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D",
"ticker": "doge"
},
{
"address": "0x541a45cb212b57f41393427fb15335fc89c35851",
"ticker": "ubq"
},
{
"address": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK",
"ticker": "xmr"
},
{
"address": "ltc1qda645jdf4jszwxcvsn32ykdhemvlx7yl9n5gz9",
"ticker": "ltc"
},
{
"address": "bitcoincash:qpcfnm9w8uemax38yqhyg58zn2ptpf6szvkr0n48a7",
"ticker": "bch"
},
{
"address": "XnB5p4JvL3So91A1c1MERozZEjeMSsAD7J",
"ticker": "dash"
},
{
"address": "t1PHZX5ZjY7y61iC19A958W9hdyH3SiLJuF",
"ticker": "zec"
},
{
"address": "0xB81BAEE10d163404a1c60045a872a0da9E258465",
"ticker": "etc"
},
{
"address": "AGTLRXapPYpxt3PLdiXEs8y4kLw6Qy3C4t",
"ticker": "btg"
},
{
"address": "SbQcFUDi7kKyxkmskzW3w74x68H5eUrg76",
"ticker": "dgb"
},
{
"address": "N7nompUVxz5ATrzRVTzw7CaAJoSiVtEcQx",
"ticker": "nmc"
},
{
"address": "3AQcUgCbF6ymiR4HGCU8ANx9SqbzL6nx8r",
"ticker": "vtc"
}
],
"cryptoDonatePanel": {
"limit": 1
},
"customCss": [],
"defaultSettings": {
"themeMode": "system"
},
"displayFqn": true,
"extensions": {
"ads": {
"enabled": false,
"interval": 40,
"provider": "soapbox"
},
"patron": {
"enabled": true
}
},
"features": {
"accountAliases": true
},
"feedInjection": true,
"gdpr": false,
"greentext": true,
"logo": "https://media.gleasonator.com/0c760b3ecdbc993ba47b785d0adecf0ec71fd9c59808e27d0665b9f77a32d8de.png",
"mediaPreview": false,
"mobilePages": {},
"navlinks": {
"homeFooter": [
{
"title": "About",
"url": "/about"
},
{
"title": "Terms of Service",
"url": "/about/tos"
},
{
"title": "Privacy Policy",
"url": "/about/privacy"
},
{
"title": "DMCA",
"url": "/about/dmca"
},
{
"title": "Source Code",
"url": "/about#opensource"
}
]
},
"promoPanel": {
"items": [
{
"icon": "music",
"text": "Gleasonator theme song",
"url": "https://media.gleasonator.com/custom/261905_gleasonator_song.mp3"
}
]
},
"redirectRootNoLogin": "",
"sentryDsn": "https://95c1dd3284d7284134928059844ba086@o4505999744499712.ingest.sentry.io/4505999904931840",
"singleUserMode": false,
"singleUserModeProfile": "",
"tileServer": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"tileServerAttribution": "© OpenStreetMap Contributors",
"verifiedCanEditName": true,
"verifiedIcon": ""
}
]
}
]
}]
}

View file

@ -1,18 +1,18 @@
import { NSchema as n, NStore } from '@nostrify/nostrify';
import { z } from 'zod'; import { z } from 'zod';
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts'; import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts'; import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts';
import { lookupPubkey } from '@/utils/lookup.ts'; import { lookupPubkey } from '@/utils/lookup.ts';
import { getPleromaConfigs } from '@/utils/pleroma.ts';
const frontendConfigController: AppController = async (c) => { const frontendConfigController: AppController = async (c) => {
const store = await Storages.db(); const store = await Storages.db();
const configs = await getConfigs(store, c.req.raw.signal); const configDB = await getPleromaConfigs(store, c.req.raw.signal);
const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations'); const frontendConfig = configDB.get(':pleroma', ':frontend_configurations');
if (frontendConfig) { if (frontendConfig) {
const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array();
@ -28,7 +28,7 @@ const frontendConfigController: AppController = async (c) => {
const configController: AppController = async (c) => { const configController: AppController = async (c) => {
const store = await Storages.db(); const store = await Storages.db();
const configs = await getConfigs(store, c.req.raw.signal); const configs = await getPleromaConfigs(store, c.req.raw.signal);
return c.json({ configs, need_reboot: false }); return c.json({ configs, need_reboot: false });
}; };
@ -37,17 +37,10 @@ const updateConfigController: AppController = async (c) => {
const { pubkey } = Conf; const { pubkey } = Conf;
const store = await Storages.db(); const store = await Storages.db();
const configs = await getConfigs(store, c.req.raw.signal); const configs = await getPleromaConfigs(store, c.req.raw.signal);
const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json()); const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json());
for (const { group, key, value } of newConfigs) { configs.merge(newConfigs);
const index = configs.findIndex((c) => c.group === group && c.key === key);
if (index === -1) {
configs.push({ group, key, value });
} else {
configs[index].value = value;
}
}
await createAdminEvent({ await createAdminEvent({
kind: 30078, kind: 30078,
@ -70,24 +63,6 @@ const pleromaAdminDeleteStatusController: AppController = async (c) => {
return c.json({}); return c.json({});
}; };
async function getConfigs(store: NStore, signal: AbortSignal): Promise<PleromaConfig[]> {
const { pubkey } = Conf;
const [event] = await store.query([{
kinds: [30078],
authors: [pubkey],
'#d': ['pub.ditto.pleroma.config'],
limit: 1,
}], { signal });
try {
const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content);
return n.json().pipe(configSchema.array()).catch([]).parse(decrypted);
} catch (_e) {
return [];
}
}
const pleromaAdminTagSchema = z.object({ const pleromaAdminTagSchema = z.object({
nicknames: z.string().array(), nicknames: z.string().array(),
tags: z.string().array(), tags: z.string().array(),

View file

@ -1,15 +1,39 @@
import { AppMiddleware } from '@/app.ts'; import { AppMiddleware } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts';
import { Storages } from '@/storages.ts';
import { getPleromaConfigs } from '@/utils/pleroma.ts';
let configDBCache: Promise<PleromaConfigDB> | undefined;
export const cspMiddleware = (): AppMiddleware => { export const cspMiddleware = (): AppMiddleware => {
return async (c, next) => { return async (c, next) => {
const store = await Storages.db();
if (!configDBCache) {
configDBCache = getPleromaConfigs(store);
}
const { host, protocol, origin } = Conf.url; const { host, protocol, origin } = Conf.url;
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
const configDB = await configDBCache;
const sentryDsn = configDB.getIn(':pleroma', ':frontend_configurations', ':soapbox_fe', 'sentryDsn');
const connectSrc = ["'self'", 'blob:', origin, `${wsProtocol}//${host}`];
if (typeof sentryDsn === 'string') {
try {
const dsn = new URL(sentryDsn);
connectSrc.push(dsn.origin);
} catch {
// Ignore
}
}
const policies = [ const policies = [
'upgrade-insecure-requests', 'upgrade-insecure-requests',
`script-src 'self'`, `script-src 'self'`,
`connect-src 'self' blob: ${origin} ${wsProtocol}//${host}`, `connect-src ${connectSrc.join(' ')}`,
`media-src 'self' https:`, `media-src 'self' https:`,
`img-src 'self' data: blob: https:`, `img-src 'self' data: blob: https:`,
`default-src 'none'`, `default-src 'none'`,

View file

@ -0,0 +1,27 @@
import { assertEquals } from '@std/assert';
import data from '~/fixtures/config-db.json' with { type: 'json' };
import { PleromaConfig } from '@/schemas/pleroma-api.ts';
import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts';
Deno.test('PleromaConfigDB.getIn', () => {
const configDB = new PleromaConfigDB(data.configs as PleromaConfig[]);
assertEquals(
configDB.get(':pleroma', ':frontend_configurations')?.value,
configDB.getIn(':pleroma', ':frontend_configurations'),
);
assertEquals(configDB.getIn(':pleroma', ':frontend_configurations', ':bleroma'), undefined);
assertEquals(
configDB.getIn(':pleroma', ':frontend_configurations', ':soapbox_fe', 'colors', 'primary', '500'),
'#1ca82b',
);
assertEquals(
configDB.getIn(':pleroma', ':frontend_configurations', ':soapbox_fe', 'colors', 'primary', '99999999'),
undefined,
);
});

View file

@ -0,0 +1,67 @@
import type { ElixirTuple, ElixirValue, PleromaConfig } from '@/schemas/pleroma-api.ts';
export class PleromaConfigDB {
constructor(private configs: PleromaConfig[]) {}
get(group: string, key: string): PleromaConfig | undefined {
return this.configs.find((c) => c.group === group && c.key === key);
}
getIn(group: string, key: string, ...paths: string[]): ElixirValue | undefined {
const config = this.get(group, key);
if (!config) return undefined;
let value = config.value;
for (const path of paths) {
if (Array.isArray(value)) {
const tuple = value.find((item): item is ElixirTuple => {
return PleromaConfigDB.isTuple(item) && item.tuple[0] === path;
});
if (tuple) {
value = tuple.tuple[1];
} else {
return;
}
} else if (PleromaConfigDB.isTuple(value) && value.tuple[0] === path) {
value = value.tuple[1];
} else if (!PleromaConfigDB.isTuple(value) && value && typeof value === 'object' && path in value) {
value = value[path];
} else {
return;
}
}
return value;
}
set(group: string, key: string, value: PleromaConfig): void {
const index = this.configs.findIndex((c) => c.group === group && c.key === key);
if (index === -1) {
this.configs.push(value);
} else {
this.configs[index] = value;
}
}
merge(configs: PleromaConfig[]): void {
for (const { group, key, value } of configs) {
this.set(group, key, { group, key, value });
}
}
toJSON(): PleromaConfig[] {
return this.configs;
}
private static isTuple(value: ElixirValue): value is ElixirTuple {
return Boolean(
value &&
typeof value === 'object' &&
'tuple' in value &&
Array.isArray(value.tuple) &&
value.tuple.length === 2 &&
typeof value.tuple[0] === 'string',
);
}
}

29
src/utils/pleroma.ts Normal file
View file

@ -0,0 +1,29 @@
import { NSchema as n, NStore } from '@nostrify/nostrify';
import { Conf } from '@/config.ts';
import { configSchema } from '@/schemas/pleroma-api.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts';
export async function getPleromaConfigs(store: NStore, signal?: AbortSignal): Promise<PleromaConfigDB> {
const { pubkey } = Conf;
const [event] = await store.query([{
kinds: [30078],
authors: [pubkey],
'#d': ['pub.ditto.pleroma.config'],
limit: 1,
}], { signal });
if (!event) {
return new PleromaConfigDB([]);
}
try {
const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content);
const configs = n.json().pipe(configSchema.array()).catch([]).parse(decrypted);
return new PleromaConfigDB(configs);
} catch (_e) {
return new PleromaConfigDB([]);
}
}