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 (
+
)
}
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 {