Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/playground/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions docs/deployment/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion internal/server/tplhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import (
)

type TemplateArguments struct {
GoogleTagID string
GoogleTagID string
TurnstileSiteKey string
}

type TemplateFileServer struct {
Expand Down
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useEffect, useState, useRef } from 'react'

interface Props {
siteKey: string | null | undefined
className?: string
onSuccess: (token: string) => void
renderError: (err: string) => React.ReactNode
}

export const TurnstileChallenge: React.FC<Props> = ({ siteKey, className, onSuccess, renderError }) => {
const [err, setErr] = useState<string | null>(null)
const containerRef = useRef<HTMLDivElement | null>(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])

Check warning on line 28 in web/src/components/elements/misc/TurnstileChallenge/TurnstileChallenge.tsx

View workflow job for this annotation

GitHub Actions / test

React Hook useEffect has a missing dependency: 'onSuccess'. Either include it or remove the dependency array. Mutable values like 'containerRef.current' aren't valid dependencies because mutating them doesn't re-render the component

return (
<div className={className}>
{err ? renderError(err) : null}
<div ref={containerRef} />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TurnstileChallenge'
92 changes: 62 additions & 30 deletions web/src/components/features/inspector/RunOutput/RunOutput.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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'

Check failure on line 12 in web/src/components/features/inspector/RunOutput/RunOutput.tsx

View workflow job for this annotation

GitHub Actions / test

'environment' is defined but never used
import { TurnstileChallenge } from '~/components/elements/misc/TurnstileChallenge'
import { useTurnstile } from '~/hooks/turnstile'

const linkStyle = {
root: {
Expand Down Expand Up @@ -38,6 +41,8 @@

export const RunOutput: React.FC = () => {
const theme = useTheme()
const dispatch = useDispatch()
const turnstile = useTurnstile()
const { status, monaco, terminal } = useSelector<State, State>((state) => state)

const { fontSize, renderingBackend } = terminal.settings
Expand All @@ -52,36 +57,63 @@
const fontFamily = useMemo(() => getFontFamily(monaco?.fontFamily ?? DEFAULT_FONT), [monaco])
const isClean = !status?.dirty

return (
<div className="RunOutput" style={styles}>
<div className="RunOutput__content">
{status?.lastError ? (
<div className="RunOutput__container">
<MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
<b className="RunOutput__label">Error</b>
<pre className="RunOutput__errors">{highlightLinks(status.lastError)}</pre>
</MessageBar>
</div>
) : isClean ? (
<div
className="RunOutput__container"
style={{
fontFamily,
fontSize: `${fontSize}px`,
}}
>
Press &quot;Run&quot; to compile program.
</div>
) : (
<ConsoleWrapper
fontFamily={fontFamily}
fontSize={fontSize}
status={status}
backend={renderingBackend}
disableTerminal={terminal.settings.disableTerminalEmulation}
/>
let content: React.ReactNode | null = null
if (status?.lastError) {
content = (
<div className="RunOutput__container">
<MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
<b className="RunOutput__label">Error</b>
<pre className="RunOutput__errors">{highlightLinks(status.lastError)}</pre>
</MessageBar>
</div>
)
} else if (status?.cfChallengeRequested) {
content = (
<TurnstileChallenge
className="RunOutput__container"
siteKey={turnstile?.siteKey}
onSuccess={(token) => {
dispatch(
runFileWithParamsDispatcher({
turnstileToken: token,
}),
)
}}
renderError={(err) => (
<MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
<b className="RunOutput__label">Error</b>
<pre className="RunOutput__errors">{err}</pre>
</MessageBar>
)}
/>
)
} else if (isClean) {
content = (
<div
className="RunOutput__container"
style={{
fontFamily,
fontSize: `${fontSize}px`,
}}
>
Press &quot;Run&quot; to compile program.
</div>
)
} else {
content = (
<ConsoleWrapper
fontFamily={fontFamily}
fontSize={fontSize}
status={status}
backend={renderingBackend}
disableTerminal={terminal.settings.disableTerminalEmulation}
/>
)
}

return (
<div className="RunOutput" style={styles}>
<div className="RunOutput__content">{content}</div>
</div>
)
}
19 changes: 11 additions & 8 deletions web/src/components/pages/PlaygroundPage/PlaygroundPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'))

Expand All @@ -25,13 +26,15 @@ export const PlaygroundPage: React.FC = () => {
}, [snippetID, dispatch])

return (
<div ref={containerRef} className={styles.Playground}>
<LazyAnnouncementBanner />
<Header />
<SuspenseBoundary errorLabel="Failed to load workspace" preloaderText="Loading workspace...">
<LazyPlaygroundContent parentRef={containerRef} />
</SuspenseBoundary>
<ConnectedStatusBar />
</div>
<TurnstileProvider>
<div ref={containerRef} className={styles.Playground}>
<LazyAnnouncementBanner />
<Header />
<SuspenseBoundary errorLabel="Failed to load workspace" preloaderText="Loading workspace...">
<LazyPlaygroundContent parentRef={containerRef} />
</SuspenseBoundary>
<ConnectedStatusBar />
</div>
</TurnstileProvider>
)
}
1 change: 1 addition & 0 deletions web/src/environment.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
12 changes: 12 additions & 0 deletions web/src/hooks/turnstile.tsx
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions web/src/pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<meta name="description" content="Better Go Playground with autocomplete and syntax highlight support" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="darkreader-lock" />
<% if (PROD) { %> {{ if .TurnstileSiteKey }}
<meta name="turnstile" content="{{ .TurnstileSiteKey }}" />
{{ end }} <% } %>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<title>Better Go Playground</title>
Expand Down Expand Up @@ -105,6 +108,7 @@
}
}
</style>
<% if (PROD { %><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script><% } %>
</head>

<body>
Expand Down
22 changes: 22 additions & 0 deletions web/src/providers/turnstile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { createContext, useMemo } from 'react'

export interface TurnstileContextValue {
siteKey: string | null
}

export const TurnstileContext = createContext<TurnstileContextValue | null>(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 <TurnstileContext.Provider value={contextValue}>{children}</TurnstileContext.Provider>
}
Loading
Loading