From 5657779e70806d39ceda84d18964d60ef92cc5f4 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:26:29 -0400 Subject: [PATCH 01/10] feat: handle cf-mitigated --- web/src/services/api/client.ts | 8 +++++++- web/src/services/api/models/errors.ts | 14 ++++++++++++++ web/src/services/api/models/index.ts | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 web/src/services/api/models/errors.ts diff --git a/web/src/services/api/client.ts b/web/src/services/api/client.ts index 21b03c01..2725550a 100644 --- a/web/src/services/api/client.ts +++ b/web/src/services/api/client.ts @@ -8,11 +8,12 @@ import { type ShareResponse, type VersionsInfo, type FilesPayload, + CFError, } from './models' import type { IAPIClient } from './interface' export class Client implements IAPIClient { - constructor(private readonly baseUrl: string) {} + constructor(private readonly baseUrl: string) { } /** * Returns server API version. @@ -117,6 +118,11 @@ export class Client implements IAPIClient { return (await rsp.json()) as T } + const cfMitigated = rsp.headers.get('cf-mitigated') + if (cfMitigated && cfMitigated === 'challenge') { + throw new CFError(cfMitigated) + } + const isJson = rsp.headers.get('content-type') if (!isJson) { throw new Error(`${rsp.status} ${rsp.statusText}`) diff --git a/web/src/services/api/models/errors.ts b/web/src/services/api/models/errors.ts new file mode 100644 index 00000000..282dfb8f --- /dev/null +++ b/web/src/services/api/models/errors.ts @@ -0,0 +1,14 @@ +export class CFError extends Error { + public readonly isCFError = true + + constructor(public mitigationType: string) { + super(`Cloudflare WAF returned cf-mitigated: ${mitigationType}`) + } +} + +/** + * Checks whether error is Cloudflare WAF challenge error. + */ +export const isCFError = (err: any): err is CFError => { + return typeof err === 'object' && 'isCFError' in err && err.isCFError +} diff --git a/web/src/services/api/models/index.ts b/web/src/services/api/models/index.ts index ca8b0cb7..42f8e2e9 100644 --- a/web/src/services/api/models/index.ts +++ b/web/src/services/api/models/index.ts @@ -1,3 +1,4 @@ export * from './run' export * from './version' export * from './announcement' +export * from './errors' From f70fbf3fc0493b314c153b0f23048ac7fb74ee5e Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:31:07 -0400 Subject: [PATCH 02/10] feat: add turnstile script --- web/src/pages/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/pages/index.html b/web/src/pages/index.html index e56ba85b..d988abf9 100644 --- a/web/src/pages/index.html +++ b/web/src/pages/index.html @@ -105,6 +105,7 @@ } } + <% if (PROD { %><% } %> From 8705fc1bfaf5f3e98c72ca8800df2b4cbae326ba Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:25:07 -0400 Subject: [PATCH 03/10] chore: add turnstile types --- web/package.json | 1 + web/yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/web/package.json b/web/package.json index 031c70cb..d51d657a 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", + "@types/cloudflare-turnstile": "^0.2.2", "@types/file-saver": "^2.0.7", "@types/jest": "^27.4.0", "@types/node": "^17.0.16", diff --git a/web/yarn.lock b/web/yarn.lock index fb93841d..fbb405d8 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -3098,6 +3098,13 @@ __metadata: languageName: node linkType: hard +"@types/cloudflare-turnstile@npm:^0.2.2": + version: 0.2.2 + resolution: "@types/cloudflare-turnstile@npm:0.2.2" + checksum: 10c0/17dcd6b22cd8c03fc3699b88b1b30d0fac3997557a4189e922aec7f632b8c287b394170cbf09a63598617b3e9a37e65ea6dce3af392e5df02f870e6d6858c0cc + languageName: node + linkType: hard + "@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -10558,6 +10565,7 @@ __metadata: "@testing-library/jest-dom": "npm:^6.5.0" "@testing-library/react": "npm:^12.1.2" "@testing-library/user-event": "npm:^13.5.0" + "@types/cloudflare-turnstile": "npm:^0.2.2" "@types/file-saver": "npm:^2.0.7" "@types/jest": "npm:^27.4.0" "@types/node": "npm:^17.0.16" From 0c36e08578499eac0044be49f378f7fa9132e5ef Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:25:26 -0400 Subject: [PATCH 04/10] feat: propagate turnstile headers --- web/src/services/api/client.ts | 45 +++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/web/src/services/api/client.ts b/web/src/services/api/client.ts index 2725550a..2ab38eb8 100644 --- a/web/src/services/api/client.ts +++ b/web/src/services/api/client.ts @@ -12,8 +12,14 @@ import { } from './models' import type { IAPIClient } from './interface' +export interface RequestOpts { + turnstileToken?: string +} + export class Client implements IAPIClient { - constructor(private readonly baseUrl: string) { } + constructor(private readonly baseUrl: string) { + // Empty comment to workaroud prettier formatting issues. + } /** * Returns server API version. @@ -27,8 +33,8 @@ export class Client implements IAPIClient { * * WASM file can be downloaded using {@link getArtifact} call. */ - async build(files: Record): Promise { - return await this.post(`/v2/compile`, { files }) + async build(files: Record, opts?: RequestOpts): Promise { + return await this.post(`/v2/compile`, { files }, opts) } /** @@ -52,15 +58,20 @@ export class Client implements IAPIClient { * @param backend Go server backend to use. * @returns */ - async run(files: Record, vet: boolean, backend = Backend.Default): Promise { - return await this.post(`/v2/run?vet=${Boolean(vet)}&backend=${backend}`, { files }) + async run( + files: Record, + vet: boolean, + backend = Backend.Default, + opts?: RequestOpts, + ): Promise { + return await this.post(`/v2/run?vet=${Boolean(vet)}&backend=${backend}`, { files }, opts) } /** * Formats Go files. */ - async format(files: Record, backend = Backend.Default): Promise { - return await this.post(`/v2/format?backend=${backend}`, { files }) + async format(files: Record, backend = Backend.Default, opts?: RequestOpts): Promise { + return await this.post(`/v2/format?backend=${backend}`, { files }, opts) } /** @@ -73,8 +84,8 @@ export class Client implements IAPIClient { /** * Uploads a snippet and returns share URL link. */ - async shareSnippet(files: Record): Promise { - return await this.post('/v2/share', { files }) + async shareSnippet(files: Record, opts?: RequestOpts): Promise { + return await this.post('/v2/share', { files }, opts) } /** @@ -100,15 +111,25 @@ export class Client implements IAPIClient { }) } - private async post(uri: string, data: any): Promise { - return await this.doRequest(uri, { + private async post(uri: string, data: any, opts?: RequestOpts): Promise { + const body: RequestInit = { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(data), - }) + } + + if (opts?.turnstileToken?.length) { + if (!body.headers) { + body.headers = {} + } + + body.headers['X-Cf-Turnstile-Response'] = opts.turnstileToken + } + + return await this.doRequest(uri, body) } private async doRequest(uri: string, reqInit?: RequestInit): Promise { From 7e5bf04b762aedfb95dbad19cf800fec9cd54306 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:37:01 -0400 Subject: [PATCH 05/10] feat: handle CF challenge --- web/src/store/actions/actions.ts | 1 + web/src/store/actions/ui.ts | 5 +++++ web/src/store/dispatchers/build/dispatch.ts | 17 +++++++++++++---- web/src/store/reducers.ts | 10 ++++++++++ web/src/store/state.ts | 1 + 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/web/src/store/actions/actions.ts b/web/src/store/actions/actions.ts index 4fa482e0..0297b2dc 100644 --- a/web/src/store/actions/actions.ts +++ b/web/src/store/actions/actions.ts @@ -1,6 +1,7 @@ export enum ActionType { LOADING_STATE_CHANGE = 'LOADING_STATE_CHANGE', ERROR = 'ERROR', + CF_CHALLENGE = 'CF_CHALLENGE', TOGGLE_THEME = 'TOGGLE_THEME', RUN_TARGET_CHANGE = 'RUN_TARGET_CHANGE', MONACO_SETTINGS_CHANGE = 'MONACO_SETTINGS_CHANGE', diff --git a/web/src/store/actions/ui.ts b/web/src/store/actions/ui.ts index a3617cc8..00994a80 100644 --- a/web/src/store/actions/ui.ts +++ b/web/src/store/actions/ui.ts @@ -15,6 +15,11 @@ export const newErrorAction = (err: string) => ({ payload: err, }) +export const newCFChallengeAction = () => ({ + type: ActionType.CF_CHALLENGE, + payload: null, +}) + export const newLoadingAction = (loading = true) => ({ type: ActionType.LOADING_STATE_CHANGE, payload: { loading }, diff --git a/web/src/store/dispatchers/build/dispatch.ts b/web/src/store/dispatchers/build/dispatch.ts index 04e24684..13be1a98 100644 --- a/web/src/store/dispatchers/build/dispatch.ts +++ b/web/src/store/dispatchers/build/dispatch.ts @@ -2,12 +2,13 @@ import { TargetType } from '~/services/config' import { SECOND, setTimeoutNanos } from '~/utils/duration' import { createStdio, GoProcess } from '~/workers/go/client' import { buildGoTestFlags, requiresWasmEnvironment } from '~/lib/sourceutil' -import client, { type EvalEvent, EvalEventKind } from '~/services/api' +import client, { type EvalEvent, EvalEventKind, type RequestOpts, isCFError } from '~/services/api' import { isProjectRequiresGoMod } from '~/services/examples' import type { DispatchFn, StateProvider } from '../../helpers' import { newRemoveNotificationAction, NotificationIDs } from '../../notifications' import { + newCFChallengeAction, newErrorAction, newLoadingAction, newProgramFinishAction, @@ -77,7 +78,11 @@ const dispatchEvalEvents = (dispatch: DispatchFn, events: EvalEvent[]) => { }, programEndTime) } -export const runFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getState: StateProvider) => { +export const runFileWithParamsDispatcher = (opts?: RequestOpts): Dispatcher => { + return (dispatch, getState) => runFileDispatcher(dispatch, getState, opts) +} + +export const runFileDispatcher = async (dispatch: DispatchFn, getState: StateProvider, opts?: RequestOpts) => { dispatch(newRemoveNotificationAction(NotificationIDs.WASMAppExitError)) dispatch(newRemoveNotificationAction(NotificationIDs.GoModMissing)) @@ -119,12 +124,12 @@ export const runFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getSta switch (runTarget) { case TargetType.Server: { // TODO: vet - const res = await client.run(files, false, backend) + const res = await client.run(files, false, backend, opts) dispatchEvalEvents(dispatch, res.events) break } case TargetType.WebAssembly: { - const buildResponse = await client.build(files) + const buildResponse = await client.build(files, opts) const buff = await fetchWasmWithProgress(dispatch, buildResponse.fileName) dispatch(newRemoveNotificationAction(NotificationIDs.WASMAppDownload)) @@ -161,6 +166,10 @@ export const runFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getSta dispatch(newErrorAction(`AppError: Unknown Go runtime type "${runTarget}"`)) } } catch (err: any) { + if (isCFError(err)) { + dispatch(newCFChallengeAction()) + return + } dispatch(newErrorAction(err.message)) } } diff --git a/web/src/store/reducers.ts b/web/src/store/reducers.ts index 9090ae5e..91fc3ac7 100644 --- a/web/src/store/reducers.ts +++ b/web/src/store/reducers.ts @@ -78,11 +78,20 @@ const reducers = { running: false, dirty: true, lastError: a.payload, + cfChallengeRequested: false, + }), + [ActionType.CF_CHALLENGE]: (s: StatusState, a: Action) => ({ + ...s, + loading: false, + running: false, + dirty: true, + cfChallengeRequested: true, }), [ActionType.LOADING_STATE_CHANGE]: (s: StatusState, { payload: { loading } }: Action) => ({ ...s, loading, running: false, + cfChallengeRequested: false, }), [ActionType.EVAL_START]: (s: StatusState, _: Action) => ({ lastError: null, @@ -103,6 +112,7 @@ const reducers = { loading: false, running: false, dirty: true, + cfChallengeRequested: false, }), [ActionType.RUN_TARGET_CHANGE]: (s: StatusState, { payload }: Action) => { // if (payload.target) { diff --git a/web/src/store/state.ts b/web/src/store/state.ts index c0b329af..6c040ca2 100644 --- a/web/src/store/state.ts +++ b/web/src/store/state.ts @@ -18,6 +18,7 @@ export interface StatusState { running?: boolean dirty?: boolean lastError?: string | null + cfChallengeRequested?: boolean events?: EvalEvent[] markers?: Record } From 580154c09773635213ba330126c6f1658939d420 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:22:12 -0400 Subject: [PATCH 06/10] feat: add turnstile wrapper --- .../TurnstileChallenge/TurnstileChallenge.tsx | 36 +++++++++++++++++++ .../elements/misc/TurnstileChallenge/index.ts | 1 + web/src/environment.ts | 4 +++ 3 files changed, 41 insertions(+) create mode 100644 web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx create mode 100644 web/src/components/elements/misc/TurnstileChallenge/index.ts diff --git a/web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx b/web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx new file mode 100644 index 00000000..682d9fa2 --- /dev/null +++ b/web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx @@ -0,0 +1,36 @@ +import React, { useEffect, useState, useRef } from 'react' + +interface Props { + siteKey: string | null + className?: string + onSuccess: (token: string) => void + renderError: (err: string) => React.ReactNode +} + +export const TurnstileChallenge: React.FC = ({ siteKey, className, onSuccess, renderError }) => { + const [err, setErr] = useState(null) + const containerRef = useRef(null) + useEffect(() => { + if (!containerRef.current) { + return + } + + if (!siteKey || !turnstile) { + setErr('WAF challenge requested but Turnstille is not available') + return + } + + turnstile.render(containerRef.current, { + sitekey: siteKey, + 'error-callback': (errMsg) => setErr(errMsg), + callback: onSuccess, + }) + }, [containerRef.current, setErr, siteKey]) + + return ( +
+ {err ? renderError(err) : null} +
+
+ ) +} diff --git a/web/src/components/elements/misc/TurnstileChallenge/index.ts b/web/src/components/elements/misc/TurnstileChallenge/index.ts new file mode 100644 index 00000000..f2259654 --- /dev/null +++ b/web/src/components/elements/misc/TurnstileChallenge/index.ts @@ -0,0 +1 @@ +export * from './TurnstileChallenge' diff --git a/web/src/environment.ts b/web/src/environment.ts index 50ed81d6..ddcead61 100644 --- a/web/src/environment.ts +++ b/web/src/environment.ts @@ -15,6 +15,10 @@ const environment = { issue: import.meta.env.VITE_GITHUB_URL ?? 'https://github.com/x1unix/go-playground/issues/new', donate: import.meta.env.VITE_DONATE_URL ?? 'https://opencollective.com/bttr-go-playground', }, + + turnstile: { + siteKey: import.meta.env.VITE_TURNSTILE_SITE_KEY ?? null, + }, } export default environment From 7de62128846db7886447497db9593261c6e721ef Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:26:57 -0400 Subject: [PATCH 07/10] feat: show challenge captcha --- .../inspector/RunOutput/RunOutput.tsx | 90 ++++++++++++------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/web/src/components/features/inspector/RunOutput/RunOutput.tsx b/web/src/components/features/inspector/RunOutput/RunOutput.tsx index 95eb8904..5a152f5c 100644 --- a/web/src/components/features/inspector/RunOutput/RunOutput.tsx +++ b/web/src/components/features/inspector/RunOutput/RunOutput.tsx @@ -1,14 +1,16 @@ import React, { useMemo } from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { Link, MessageBar, MessageBarType, useTheme } from '@fluentui/react' import { Console, type ConsoleProps } from '~/components/features/inspector/Console' import { FallbackOutput } from 'components/features/inspector/FallbackOutput' -import { type State } from '~/store' +import { runFileWithParamsDispatcher, type State } from '~/store' import { DEFAULT_FONT, getDefaultFontFamily, getFontFamily } from '~/services/fonts' import './RunOutput.css' import { splitStringUrls } from './parser' +import environment from '~/environment' +import { TurnstileChallenge } from '~/components/elements/misc/TurnstileChallenge' const linkStyle = { root: { @@ -38,6 +40,7 @@ const ConsoleWrapper: React.FC = ( export const RunOutput: React.FC = () => { const theme = useTheme() + const dispatch = useDispatch() const { status, monaco, terminal } = useSelector((state) => state) const { fontSize, renderingBackend } = terminal.settings @@ -52,36 +55,63 @@ export const RunOutput: React.FC = () => { const fontFamily = useMemo(() => getFontFamily(monaco?.fontFamily ?? DEFAULT_FONT), [monaco]) const isClean = !status?.dirty - return ( -
-
- {status?.lastError ? ( -
- - Error -
{highlightLinks(status.lastError)}
-
-
- ) : isClean ? ( -
- Press "Run" to compile program. -
- ) : ( - + let content: React.ReactNode | null = null + if (status?.lastError) { + content = ( +
+ + Error +
{highlightLinks(status.lastError)}
+
+
+ ) + } else if (status?.cfChallengeRequested) { + content = ( + { + dispatch( + runFileWithParamsDispatcher({ + turnstileToken: token, + }), + ) + }} + renderError={(err) => ( + + Error +
{err}
+
)} + /> + ) + } else if (isClean) { + content = ( +
+ Press "Run" to compile program.
+ ) + } else { + content = ( + + ) + } + + return ( +
+
{content}
) } From 46fe6cc1381b04bc39c55c6ea7f43c7f10d4b75e Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:30:03 -0400 Subject: [PATCH 08/10] fix: use fallback --- web/src/environment.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/environment.ts b/web/src/environment.ts index ddcead61..3ebdf40e 100644 --- a/web/src/environment.ts +++ b/web/src/environment.ts @@ -1,6 +1,7 @@ /** * Global environment variables */ + const environment = { appVersion: import.meta.env.VITE_VERSION ?? '1.0.0-snapshot', apiUrl: import.meta.env.VITE_LANG_SERVER ?? window.location.origin, @@ -17,7 +18,7 @@ const environment = { }, turnstile: { - siteKey: import.meta.env.VITE_TURNSTILE_SITE_KEY ?? null, + siteKey: import.meta.env.VITE_TURNSTILE_SITE_KEY || null, }, } From beaf5a5b6c894d4bb76e90c5abf62f3973740889 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:41:12 -0400 Subject: [PATCH 09/10] feat: read turnstile site key from meta header --- .../TurnstileChallenge/TurnstileChallenge.tsx | 2 +- .../inspector/RunOutput/RunOutput.tsx | 4 +++- .../pages/PlaygroundPage/PlaygroundPage.tsx | 19 +++++++++------- web/src/environment.ts | 4 ---- web/src/hooks/turnstile.tsx | 12 ++++++++++ web/src/pages/index.html | 3 +++ web/src/providers/turnstile.tsx | 22 +++++++++++++++++++ 7 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 web/src/hooks/turnstile.tsx create mode 100644 web/src/providers/turnstile.tsx diff --git a/web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx b/web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx index 682d9fa2..68622285 100644 --- a/web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx +++ b/web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useRef } from 'react' interface Props { - siteKey: string | null + siteKey: string | null | undefined className?: string onSuccess: (token: string) => void renderError: (err: string) => React.ReactNode diff --git a/web/src/components/features/inspector/RunOutput/RunOutput.tsx b/web/src/components/features/inspector/RunOutput/RunOutput.tsx index 5a152f5c..f76ff1aa 100644 --- a/web/src/components/features/inspector/RunOutput/RunOutput.tsx +++ b/web/src/components/features/inspector/RunOutput/RunOutput.tsx @@ -11,6 +11,7 @@ import './RunOutput.css' import { splitStringUrls } from './parser' import environment from '~/environment' import { TurnstileChallenge } from '~/components/elements/misc/TurnstileChallenge' +import { useTurnstile } from '~/hooks/turnstile' const linkStyle = { root: { @@ -41,6 +42,7 @@ const ConsoleWrapper: React.FC = ( export const RunOutput: React.FC = () => { const theme = useTheme() const dispatch = useDispatch() + const turnstile = useTurnstile() const { status, monaco, terminal } = useSelector((state) => state) const { fontSize, renderingBackend } = terminal.settings @@ -69,7 +71,7 @@ export const RunOutput: React.FC = () => { content = ( { dispatch( runFileWithParamsDispatcher({ diff --git a/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx b/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx index af5df66f..6b01f655 100644 --- a/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx +++ b/web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx @@ -9,6 +9,7 @@ import { ConnectedStatusBar } from '~/components/layout/StatusBar' import styles from './PlaygroundPage.module.css' import { SuspenseBoundary } from '~/components/elements/misc/SuspenseBoundary' import { LazyAnnouncementBanner } from '~/components/layout/AnnouncementBanner' +import { TurnstileProvider } from '~/providers/turnstile.tsx' const LazyPlaygroundContent = lazy(async () => await import('./PlaygroundContainer.tsx')) @@ -25,13 +26,15 @@ export const PlaygroundPage: React.FC = () => { }, [snippetID, dispatch]) return ( -
- -
- - - - -
+ +
+ +
+ + + + +
+
) } diff --git a/web/src/environment.ts b/web/src/environment.ts index 3ebdf40e..61638e07 100644 --- a/web/src/environment.ts +++ b/web/src/environment.ts @@ -16,10 +16,6 @@ const environment = { issue: import.meta.env.VITE_GITHUB_URL ?? 'https://github.com/x1unix/go-playground/issues/new', donate: import.meta.env.VITE_DONATE_URL ?? 'https://opencollective.com/bttr-go-playground', }, - - turnstile: { - siteKey: import.meta.env.VITE_TURNSTILE_SITE_KEY || null, - }, } export default environment diff --git a/web/src/hooks/turnstile.tsx b/web/src/hooks/turnstile.tsx new file mode 100644 index 00000000..d2a2d6ec --- /dev/null +++ b/web/src/hooks/turnstile.tsx @@ -0,0 +1,12 @@ +import { useContext } from 'react' + +import { type TurnstileContextValue, TurnstileContext } from '~/providers/turnstile' + +export const useTurnstile = (): TurnstileContextValue | null => { + const ctx = useContext(TurnstileContext) + if (!ctx) { + return null + } + + return ctx +} diff --git a/web/src/pages/index.html b/web/src/pages/index.html index d988abf9..da69f407 100644 --- a/web/src/pages/index.html +++ b/web/src/pages/index.html @@ -8,6 +8,9 @@ + <% if (PROD) { %> {{ if .TurnstileSiteKey }} + + {{ end }} <% } %> Better Go Playground diff --git a/web/src/providers/turnstile.tsx b/web/src/providers/turnstile.tsx new file mode 100644 index 00000000..45eb8bb3 --- /dev/null +++ b/web/src/providers/turnstile.tsx @@ -0,0 +1,22 @@ +import React, { createContext, useMemo } from 'react' + +export interface TurnstileContextValue { + siteKey: string | null +} + +export const TurnstileContext = createContext(null) + +export const TurnstileProvider: React.FC = ({ children }) => { + const contextValue = useMemo(() => { + const metaTag = document.querySelector('meta[name="turnstile"]') + const siteKey = metaTag?.getAttribute('content') ?? import.meta.env.VITE_TURNSTILE_SITE_KEY + + if (!siteKey?.length) { + return null + } + + return { siteKey } + }, []) + + return {children} +} From bcbf5ac65411d66e507b6139296ececca343a60c Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:54:15 -0400 Subject: [PATCH 10/10] feat: propagate Turnstile params into template --- cmd/playground/main.go | 3 ++- docs/deployment/docker/README.md | 15 +++++++++++++++ internal/config/config.go | 8 ++++++++ internal/server/tplhandler.go | 3 ++- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/cmd/playground/main.go b/cmd/playground/main.go index 71a4e2ed..231db248 100644 --- a/cmd/playground/main.go +++ b/cmd/playground/main.go @@ -102,7 +102,8 @@ func start(logger *zap.Logger, cfg *config.Config) error { // Web UI routes tplVars := server.TemplateArguments{ - GoogleTagID: cfg.Services.GoogleAnalyticsID, + GoogleTagID: cfg.Services.GoogleAnalyticsID, + TurnstileSiteKey: cfg.Services.Turnstile.SiteKey, } if tplVars.GoogleTagID != "" { if err := webutil.ValidateGTag(tplVars.GoogleTagID); err != nil { diff --git a/docs/deployment/docker/README.md b/docs/deployment/docker/README.md index ba84a2a8..c1ce34a9 100644 --- a/docs/deployment/docker/README.md +++ b/docs/deployment/docker/README.md @@ -51,6 +51,21 @@ Use [this tool](../../../cmd/announcements/main.go) to encode a JSON file into a Announcement schema can be found [here](../../../internal/announcements/types.go). +#### Captcha + +If service is deployed behind Cloudflare WAF, some API endpoints can return *403* error with `cf-mitigated: challenge` header due to false-positive WAF detection. + +For such cases, service supports [Cloudflare Turnstile](https://blog.cloudflare.com/en-us/integrating-turnstile-with-the-cloudflare-waf-to-challenge-fetch-requests/) challenges (see [issue](https://github.com/x1unix/go-playground/issues/506)). + +Application will display a Turnstile challenge when WAF returns a challenge request. + +To enable Turnstile, set following environment variables: + +| Name | Description | +| ----------------------- | ----------- | +| `TURNSTILE_SITE_KEY` | Site Key | +| `TURNSTILE_PRIVATE_KEY` | Private Key | + ## Building custom image Use **make** to build Docker image from sources: diff --git a/internal/config/config.go b/internal/config/config.go index 8b9b4011..08f64405 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -92,9 +92,17 @@ func (cfg *BuildConfig) mountFlagSet(f *flag.FlagSet) { f.Var(cmdutil.NewStringsListValue(&cfg.BypassEnvVarsList), "permit-env-vars", "Comma-separated allow list of environment variables passed to Go compiler tool") } +type TurnstileConfig struct { + SiteKey string `envconfig:"TURNSTILE_SITE_KEY" json:"siteKey"` + PrivateKey string `envconfig:"TURNSTILE_PRIVATE_KEY" json:"privateKey"` +} + type ServicesConfig struct { // GoogleAnalyticsID is Google Analytics tag ID (optional) GoogleAnalyticsID string `envconfig:"APP_GTAG_ID" json:"googleAnalyticsID"` + + // Turnstile contains Cloudflare Turnstile configuration. + Turnstile TurnstileConfig `json:"turnstile"` } func (cfg *ServicesConfig) mountFlagSet(f *flag.FlagSet) { diff --git a/internal/server/tplhandler.go b/internal/server/tplhandler.go index d6385c86..4d2bfe30 100644 --- a/internal/server/tplhandler.go +++ b/internal/server/tplhandler.go @@ -14,7 +14,8 @@ import ( ) type TemplateArguments struct { - GoogleTagID string + GoogleTagID string + TurnstileSiteKey string } type TemplateFileServer struct {