From 95cc30f31752937a85bece207875bdf9c943c61e Mon Sep 17 00:00:00 2001 From: ryanbetts <84613835+depatchedmode@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:52:54 -0800 Subject: [PATCH 1/2] refactor: antitheft and message validation to modules + moved antitheft from data -> modules + moved validateMessage from data -> modules + renamed validateMessage to validateFrameMessage to start alignment w/ frame.js API --- api/index.js | 6 +++--- {src/data => modules}/antitheft.js | 0 src/data/message.js => modules/validateFrameMessage.js | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) rename {src/data => modules}/antitheft.js (100%) rename src/data/message.js => modules/validateFrameMessage.js (88%) diff --git a/api/index.js b/api/index.js index 74bf0b1..36154b3 100644 --- a/api/index.js +++ b/api/index.js @@ -4,8 +4,8 @@ import { parseRequest, objectToURLSearchParams } from '../modules/utils'; import buildButtons from '../modules/buildButtons'; import buildInputs from '../modules/buildInputs'; import getTargetFrame from '../modules/getTargetFrame'; -import { validateMessage } from '../src/data/message'; -import { isFrameStolen } from '../src/data/antitheft'; +import validateFrameMessage from '../modules/validateFrameMessage'; +import { isFrameStolen } from '../modules/antitheft'; export default async (req, context) => { try { @@ -17,7 +17,7 @@ export default async (req, context) => { if (payload) { payload.referringFrame = from; - payload.validData = await validateMessage(payload.trustedData.messageBytes); + payload.validData = await validateFrameMessage(payload.trustedData.messageBytes); } if (payload?.validData) { diff --git a/src/data/antitheft.js b/modules/antitheft.js similarity index 100% rename from src/data/antitheft.js rename to modules/antitheft.js diff --git a/src/data/message.js b/modules/validateFrameMessage.js similarity index 88% rename from src/data/message.js rename to modules/validateFrameMessage.js index 5432bb0..9987c24 100644 --- a/src/data/message.js +++ b/modules/validateFrameMessage.js @@ -1,4 +1,4 @@ -const validateMessage = async(messageBytes) => { +export default async(messageBytes) => { if (!process.env.FARCASTER_HUB) throw new Error("FARCASTER_HUB is not set"); if (!messageBytes) throw new Error("No data provided"); @@ -15,7 +15,3 @@ const validateMessage = async(messageBytes) => { }) .catch(error => console.error('Error:', error)); } - -export { - validateMessage, -} From f18c5ff2507bc0b592e496a816789d8d19ce311a Mon Sep 17 00:00:00 2001 From: ryanbetts <84613835+depatchedmode@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:58:33 -0800 Subject: [PATCH 2/2] refactor: start using frames.js --- api/index.js | 63 +++++++++++++++------------------ api/og-image.js | 4 +-- api/redirect.js | 6 ++-- modules/antitheft.js | 18 ++++++++-- modules/getTargetFrame.js | 29 ++++++++------- modules/safeDecode.js | 13 ------- modules/sanitize.js | 12 +++++++ modules/validateFrameMessage.js | 17 --------- package.json | 1 + src/data/framer.js | 3 +- src/frames/count.js | 16 ++++----- 11 files changed, 87 insertions(+), 95 deletions(-) delete mode 100644 modules/safeDecode.js create mode 100644 modules/sanitize.js delete mode 100644 modules/validateFrameMessage.js diff --git a/api/index.js b/api/index.js index 36154b3..1a6fae0 100644 --- a/api/index.js +++ b/api/index.js @@ -1,72 +1,65 @@ +import { getFrameMessage } from "frames.js" import landingPage from '../src/landing-page'; -import frames from '../src/frames'; import { parseRequest, objectToURLSearchParams } from '../modules/utils'; import buildButtons from '../modules/buildButtons'; import buildInputs from '../modules/buildInputs'; import getTargetFrame from '../modules/getTargetFrame'; -import validateFrameMessage from '../modules/validateFrameMessage'; -import { isFrameStolen } from '../modules/antitheft'; export default async (req, context) => { try { const requestURL = new URL(req.url); const payload = await parseRequest(req); - let from = requestURL.searchParams.get('frame'); - let buttonId = null; - let frameIsStolen = false; + const frameMessage = payload ? await getFrameMessage(payload, { + hubHttpUrl: process.env.FARCASTER_HUB + }) : {}; - if (payload) { - payload.referringFrame = from; - payload.validData = await validateFrameMessage(payload.trustedData.messageBytes); - } - - if (payload?.validData) { - buttonId = payload.validData.data.frameActionBody.buttonIndex; - frameIsStolen = await isFrameStolen(payload); - } + // extending the frames.js frameMessage object with a few things + // we require + // TODO: see about refactoring this more towards the frames.js style + frameMessage.from = requestURL.searchParams.get('frame'); + frameMessage.requestURL = payload?.untrustedData.url; - const { targetFrameSrc, targetFrameName, redirectUrl } = getTargetFrame(from, buttonId, frames); + const { targetFrame, redirectURL } = await getTargetFrame(frameMessage); - if (redirectUrl) { - return await respondWithRedirect(redirectUrl); - } else if (frameIsStolen) { - return await respondWithFrame('stolen', frames['stolen'], payload); - } else if (targetFrameSrc) { - return await respondWithFrame(targetFrameName, targetFrameSrc, payload); + if (redirectURL) { + return await respondWithRedirect(redirectURL); + } else if (targetFrame) { + return await respondWithFrame(targetFrame, frameMessage); } else { - console.error(`Unknown frame requested: ${targetFrameName}`); + console.error(`Unknown frame requested: ${targetFrame.name}`); } } catch (error) { console.error(`Error processing request: ${error}`); } }; -const respondWithRedirect = (redirectUrl) => { - const internalRedirectUrl = new URL(`${process.env.URL}/redirect`) - internalRedirectUrl.searchParams.set('redirectUrl',redirectUrl); +const respondWithRedirect = (redirectURL) => { + const internalRedirectURL = new URL(`${process.env.URL}/redirect`) + internalRedirectURL.searchParams.set('redirectURL',redirectURL); return new Response('
redirect
', { status: 302, headers: { - 'Location': internalRedirectUrl, + 'Location': internalRedirectURL, }, } ); } -const respondWithFrame = async (targetFrameName, targetFrameSrc, payload) => { +const respondWithFrame = async (targetFrame, frameMessage) => { const searchParams = { - targetFrameName, - payload + targetFrameName: targetFrame.name, + frameMessage } + const host = process.env.URL; const frameContent = { - image: targetFrameSrc.image ? - `${host}/${targetFrameSrc.image}` : + image: targetFrame.image ? + `${host}/${targetFrame.image}` : `${host}/og-image?${objectToURLSearchParams(searchParams)}` || '', - buttons: targetFrameSrc.buttons ? buildButtons(targetFrameSrc.buttons) : '', - inputs: targetFrameSrc.inputs ? buildInputs(targetFrameSrc.inputs) : '', - postURL: `${host}/?frame=${targetFrameName}` + buttons: targetFrame.buttons ? buildButtons(targetFrame.buttons) : '', + inputs: targetFrame.inputs ? buildInputs(targetFrame.inputs) : '', + postURL: `${host}/?frame=${targetFrame.name}` }; return new Response(await landingPage(frameContent), diff --git a/api/og-image.js b/api/og-image.js index 9ac8b33..232fbde 100644 --- a/api/og-image.js +++ b/api/og-image.js @@ -8,8 +8,8 @@ import { URLSearchParamsToObject } from '../modules/utils'; export default async (req, context) => { const url = new URL(req.url); const params = URLSearchParamsToObject(url.searchParams); - const targetFrameSrc = frames[params.targetFrameName]; - const markup = await targetFrameSrc.build(params.payload); + const targetFrame = frames[params.targetFrameName]; + const markup = await targetFrame.build(params.frameMessage); const svg = await satori( html(markup), diff --git a/api/redirect.js b/api/redirect.js index d061bdb..c89f67c 100644 --- a/api/redirect.js +++ b/api/redirect.js @@ -2,14 +2,14 @@ // Redirects to an external URL based on buttonIndex parameter. Used to work // around the same origin policy on frames, which is being removed soon. export default async (req, context) => { - const requestUrl = new URL(req.url); - const redirectUrl = requestUrl.searchParams.get('redirectUrl'); + const requestURL = new URL(req.url); + const redirectURL = requestURL.searchParams.get('redirectURL'); return new Response('
redirect
', { status: 302, headers: { - 'Location': redirectUrl, + 'Location': redirectURL, }, } ); diff --git a/modules/antitheft.js b/modules/antitheft.js index 5557b4c..d196a32 100644 --- a/modules/antitheft.js +++ b/modules/antitheft.js @@ -1,4 +1,5 @@ import { getStore } from '@netlify/blobs'; +import frame from '../api/frame'; // Utility functions to abstract the fetching and setting operations const fetchData = async (key) => { @@ -50,14 +51,25 @@ const removeBoundCast = (castHash) => removeFromList(getBoundCasts, setBoundCast // 1. The castAuthorID is in boundAccounts. // 2. The castHash is in boundCasts. // 3. Both boundCasts & boundAccounts are empty. -const isFrameStolen = async (payload) => { - const { fid: castAuthorID, hash: castHash } = payload.validData.data.frameActionBody.castId; +const isFrameStolen = async (frameMessage) => { + console.log('isFrameStolen', frameMessage); + const { castId, requestURL } = frameMessage; + if (!castId || !requestURL) { + console.log('isFrameStolen:quickExit', castId, requestURL); + return false; + } + + const { fid: castAuthorID, hash: castHash } = castId; const boundCasts = await getBoundCasts(); const boundAccounts = await getBoundAccounts(); const isAuthorAllowed = boundAccounts.includes(castAuthorID) || boundAccounts.length === 0; const isCastAllowed = boundCasts.includes(castHash) || boundCasts.length === 0; - const isFirstParty = payload.untrustedData.url.indexOf(process.env.URL) > -1; + const isFirstParty = requestURL ? requestURL.indexOf(process.env.URL) > -1 : true; + + console.log('isAuthorAllowed', isAuthorAllowed, castAuthorID, boundAccounts); + console.log('isCastAllowed', isCastAllowed, castHash, boundCasts); + console.log('isFirstParty', isFirstParty); return !isFirstParty || !isAuthorAllowed || !isCastAllowed; }; diff --git a/modules/getTargetFrame.js b/modules/getTargetFrame.js index 0f6778f..90a8907 100644 --- a/modules/getTargetFrame.js +++ b/modules/getTargetFrame.js @@ -1,18 +1,23 @@ const DEFAULT_FRAME = 'poster'; +import frames from '../src/frames'; +import { isFrameStolen } from './antitheft'; -export default (name, buttonId, frames) => { +export default async (frameMessage) => { + const frameIsStolen = await isFrameStolen(frameMessage); let targetFrameName = DEFAULT_FRAME; - let redirectUrl = null; - if (name && buttonId) { - const originFrame = frames[name]; - const button = originFrame.buttons[buttonId-1]; + let redirectURL = null; + console.log('getTargetFrame:frameIsStolen', frameIsStolen); + if (frameIsStolen) { + targetFrameName = 'stolen'; + } else if (frameMessage.buttonIndex) { + const originFrame = frames[frameMessage.from]; + const button = originFrame.buttons[frameMessage.buttonIndex-1]; targetFrameName = button.goTo; - redirectUrl = button.url; - } - const targetFrameSrc = frames[targetFrameName]; + redirectURL = button.url; + } + return { - targetFrameSrc, - targetFrameName, - redirectUrl - }; + targetFrame: frames[targetFrameName], + redirectURL + } } \ No newline at end of file diff --git a/modules/safeDecode.js b/modules/safeDecode.js deleted file mode 100644 index be210ef..0000000 --- a/modules/safeDecode.js +++ /dev/null @@ -1,13 +0,0 @@ -import { JSDOM } from 'jsdom'; -import DOMPurify from 'dompurify'; - -export default (inputText) => { - try { - const decodedInputText = atob(inputText); - const window = new JSDOM('').window; - const purify = DOMPurify(window); - return purify.sanitize(decodedInputText); - } catch { - throw new Error(`That ain't no encoded string mfr`) - } -} \ No newline at end of file diff --git a/modules/sanitize.js b/modules/sanitize.js new file mode 100644 index 0000000..de2dc6b --- /dev/null +++ b/modules/sanitize.js @@ -0,0 +1,12 @@ +import { JSDOM } from 'jsdom'; +import DOMPurify from 'dompurify'; + +export default (text) => { + try { + const window = new JSDOM('').window; + const purify = DOMPurify(window); + return purify.sanitize(text); + } catch { + throw new Error(`That ain't no string mfr`) + } +} \ No newline at end of file diff --git a/modules/validateFrameMessage.js b/modules/validateFrameMessage.js deleted file mode 100644 index 9987c24..0000000 --- a/modules/validateFrameMessage.js +++ /dev/null @@ -1,17 +0,0 @@ -export default async(messageBytes) => { - if (!process.env.FARCASTER_HUB) throw new Error("FARCASTER_HUB is not set"); - if (!messageBytes) throw new Error("No data provided"); - - return await fetch(`${process.env.FARCASTER_HUB}/v1/validateMessage`,{ - method: "POST", - headers: { - "Content-Type": "application/octet-stream" - }, - body: Buffer.from(messageBytes, 'hex'), - }) - .then(async(response) => { - const parsedResponse = await response.json(); - return parsedResponse.valid ? parsedResponse.message : false; - }) - .catch(error => console.error('Error:', error)); -} diff --git a/package.json b/package.json index cf35eff..12317f7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@netlify/blobs": "^6.4.2", "dompurify": "^3.0.8", + "frames.js": "^0.1.1", "jsdom": "^24.0.0", "satori": "^0.10.11", "satori-html": "^0.3.2", diff --git a/src/data/framer.js b/src/data/framer.js index 0af9434..2c62b39 100644 --- a/src/data/framer.js +++ b/src/data/framer.js @@ -1,5 +1,6 @@ import { getStore } from '@netlify/blobs'; import { getUsername } from './username'; +import sanitize from '../../modules/sanitize'; const getFramer = async() => { const store = getStore('gameState'); @@ -15,7 +16,7 @@ const getFramer = async() => { const setFramer = async(fid, taunt) => { const store = getStore('gameState'); await store.set('framer', fid); - await store.set('taunt', taunt); + await store.set('taunt', sanitize(taunt)); } export { diff --git a/src/frames/count.js b/src/frames/count.js index 075166d..6a6f6bc 100644 --- a/src/frames/count.js +++ b/src/frames/count.js @@ -1,16 +1,14 @@ import mainLayout from '../layouts/main'; import { getFramer, setFramer } from '../data/framer'; import { getCount, incrementCount } from '../data/count'; -import safeDecode from '../../modules/safeDecode'; -const build = async (payload) => { +const build = async (frameMessage) => { let count = await getCount(); - const validData = payload?.validData; - if (payload.validData && payload.referringFrame == 'count') { + if (frameMessage.from == 'count') { count = await incrementCount(count); - const tauntInput = validData.data.frameActionBody.inputText; - await setFramer(validData.data.fid, tauntInput); + const tauntInput = frameMessage.inputText; + await setFramer(frameMessage.requesterFid, tauntInput); } const { username, taunt } = await getFramer() || ''; @@ -18,7 +16,7 @@ const build = async (payload) => { let tauntOutput; tauntOutput = taunt ? `
- "${safeDecode(taunt)}" + "${taunt}"
` : ''; @@ -35,7 +33,7 @@ const build = async (payload) => { `; - return mainLayout(payload, frameHTML); + return mainLayout(frameMessage, frameHTML); } export const inputs = [ @@ -57,7 +55,7 @@ export const buttons = [ ] export default { - name: 'stolen', + name: 'count', build, buttons, inputs