import dns from "dns"; import Router from "koa-router"; import { getBaseDomain, parseDomain } from "@shared/utils/domains"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; import parseMentionUrl from "@shared/utils/parseMentionUrl"; import { isInternalUrl } from "@shared/utils/urls"; import { NotFoundError, ValidationError } from "@server/errors"; import auth from "@server/middlewares/authentication"; import { rateLimiter } from "@server/middlewares/rateLimiter"; import validate from "@server/middlewares/validate"; import { Document, Share, Team, User } from "@server/models"; import { authorize } from "@server/policies"; import { presentDocument, presentMention } from "@server/presenters/unfurls"; import presentUnfurl from "@server/presenters/unfurls/unfurl"; import { APIContext } from "@server/types"; import { CacheHelper } from "@server/utils/CacheHelper"; import { Hook, PluginManager } from "@server/utils/PluginManager"; import { RateLimiterStrategy } from "@server/utils/RateLimiter"; import * as T from "./schema"; const router = new Router(); const plugins = PluginManager.getHooks(Hook.UnfurlProvider); router.post( "urls.unfurl", rateLimiter(RateLimiterStrategy.OneThousandPerHour), auth(), validate(T.UrlsUnfurlSchema), async (ctx: APIContext) => { const { url, documentId } = ctx.input.body; const { user: actor } = ctx.state.auth; const urlObj = new URL(url); // Mentions if (urlObj.protocol === "mention:") { if (!documentId) { throw ValidationError("Document ID is required to unfurl a mention"); } const { modelId: userId } = parseMentionUrl(url); const [user, document] = await Promise.all([ User.findByPk(userId), Document.findByPk(documentId, { userId: actor.id, }), ]); if (!user) { throw NotFoundError("Mentioned user does not exist"); } if (!document) { throw NotFoundError("Document does not exist"); } authorize(actor, "read", user); authorize(actor, "read", document); ctx.body = await presentMention(user, document); return; } // Internal resources if (isInternalUrl(url) || parseDomain(url).host === actor.team.domain) { const previewDocumentId = parseDocumentSlug(url); if (previewDocumentId) { const document = previewDocumentId ? await Document.findByPk(previewDocumentId, { userId: actor.id }) : undefined; if (!document) { throw NotFoundError("Document does not exist"); } authorize(actor, "read", document); ctx.body = presentDocument(document, actor); return; } return (ctx.response.status = 204); } // External resources const cachedData = await CacheHelper.getData( CacheHelper.getUnfurlKey(actor.teamId, url) ); if (cachedData) { return (ctx.body = presentUnfurl(cachedData)); } for (const plugin of plugins) { const data = await plugin.value.unfurl(url, actor); if (data) { if ("error" in data) { return (ctx.response.status = 204); } else { await CacheHelper.setData( CacheHelper.getUnfurlKey(actor.teamId, url), data, plugin.value.cacheExpiry ); return (ctx.body = presentUnfurl(data)); } } } return (ctx.response.status = 204); } ); router.post( "urls.validateCustomDomain", rateLimiter(RateLimiterStrategy.OneHundredPerHour), auth(), validate(T.UrlsCheckCnameSchema), async (ctx: APIContext) => { const { hostname } = ctx.input.body; const [team, share] = await Promise.all([ Team.findOne({ where: { domain: hostname, }, }), Share.findOne({ where: { domain: hostname, }, }), ]); if (team || share) { throw ValidationError("Domain is already in use"); } let addresses; try { addresses = await new Promise((resolve, reject) => { dns.resolveCname(hostname, (err, addresses) => { if (err) { return reject(err); } return resolve(addresses); }); }); } catch (err) { if (err.code === "ENOTFOUND") { throw NotFoundError("No CNAME record found"); } throw ValidationError("Invalid domain"); } if (addresses.length === 0) { throw ValidationError("No CNAME record found"); } const address = addresses[0]; const likelyValid = address.endsWith(getBaseDomain()); if (!likelyValid) { throw ValidationError("CNAME is not configured correctly"); } ctx.body = { success: true, }; } ); export default router;