Skip to content

Commit df36772

Browse files
authored
chore: add turnstile for recaptcha (#289)
1 parent 8922085 commit df36772

File tree

7 files changed

+101
-2
lines changed

7 files changed

+101
-2
lines changed

control-service/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ func (s *server) initializeHttpServer() {
125125
s.server.POST("/service/control/share/create", s.handleShareCreate)
126126
}
127127

128+
func getTurnstileIP(c echo.Context) string {
129+
cfConnectingIP := c.Request().Header.Get("CF-Connecting-IP")
130+
if cfConnectingIP != "" {
131+
return cfConnectingIP
132+
}
133+
return c.RealIP()
134+
}
135+
128136
func (s *server) handleRun(c echo.Context) error {
129137
var req *workertypes.WorkerRequestPayload
130138
if err := c.Bind(&req); err != nil {
@@ -138,6 +146,14 @@ func (s *server) handleRun(c echo.Context) error {
138146
})
139147
}
140148

149+
log.Printf("Validating turnstile")
150+
if err := ValidateTurnstile(c.Request().Context(), req.Token, getTurnstileIP(c), os.Getenv("TURNSTILE_SECRET_KEY")); err != nil {
151+
log.Printf("Could not validate turnstile: %v", err)
152+
return c.JSON(http.StatusUnauthorized, echo.Map{
153+
"error": err.Error(),
154+
})
155+
}
156+
log.Printf("Validated turnstile successfully")
141157
log.Printf("Obtaining worker")
142158
var worker *Worker
143159
select {

control-service/turnstile.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"log"
9+
"net/http"
10+
"time"
11+
)
12+
13+
type TurnstileResponse struct {
14+
Success bool `json:"success"`
15+
ErrorCodes []string `json:"error-codes,omitempty"`
16+
}
17+
18+
var turnStileHttpClient = &http.Client{Timeout: 15 * time.Second}
19+
20+
func ValidateTurnstile(ctx context.Context, token string, remoteIP string, secretKey string) error {
21+
if secretKey == "" {
22+
log.Printf("warning: Turnstile secretKey is empty, skipping validation")
23+
return nil
24+
}
25+
if remoteIP == "" {
26+
log.Printf("warning: Turnstile remoteIP is empty, skipping validation")
27+
return nil
28+
}
29+
requestBody, err := json.Marshal(map[string]string{
30+
"secret": secretKey,
31+
"response": token,
32+
"remoteip": remoteIP,
33+
})
34+
if err != nil {
35+
return fmt.Errorf("failed to marshal request body: %w", err)
36+
}
37+
38+
req, err := http.NewRequestWithContext(ctx, "POST", "https://challenges.cloudflare.com/turnstile/v0/siteverify", bytes.NewBuffer(requestBody))
39+
if err != nil {
40+
return fmt.Errorf("failed to create request: %w", err)
41+
}
42+
req.Header.Set("Content-Type", "application/json")
43+
44+
resp, err := turnStileHttpClient.Do(req)
45+
if err != nil {
46+
return fmt.Errorf("failed to send request: %w", err)
47+
}
48+
defer resp.Body.Close()
49+
50+
if resp.StatusCode != http.StatusOK {
51+
return fmt.Errorf("unexpected status code: %v", resp.StatusCode)
52+
}
53+
54+
var result TurnstileResponse
55+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
56+
return fmt.Errorf("failed to parse response: %w", err)
57+
}
58+
59+
if !result.Success {
60+
return fmt.Errorf("turnstile validation failed: %v", result.ErrorCodes)
61+
}
62+
63+
return nil
64+
}

frontend/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<meta name="description" content="An interactive playground for Playwright with various examples available."/>
99
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎭</text></svg>" />
1010
<title>Try Playwright</title>
11+
<script defer src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
1112
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-34156117-11"></script>
1213
<script>
1314
if (!window.location.search.includes("no-analytics")) {

frontend/src/components/App/index.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,40 @@ import styles from './index.module.css'
1212
import CodeLanguageSelector from '../CodeLanguageSelector';
1313
import useDarkMode from '../../hooks/useDarkMode';
1414

15+
const VITE_TURNSTILE_SITEKEY = '0x4AAAAAAA_K0T_2LZ0rgUtv';
16+
1517
const App: React.FunctionComponent = () => {
1618
const { code, onChangeRightPanelMode, codeLanguage, onLanguageChange } = useContext(CodeContext)
1719
const [loading, setLoading] = useState<boolean>(false)
1820
const [resp, setResponse] = useState<ExecutionResponse|null>(null)
1921
const handleExecutionRef = useRef<() => Promise<void>>()
2022
const [darkMode] = useDarkMode()
23+
const turnstileRef = useRef<HTMLDivElement>(null)
2124

2225
const handleExecution = async (): Promise<void> => {
2326
setLoading(true)
2427
setResponse(null)
2528

2629
trackEvent()
27-
setResponse(await runCode(code, codeLanguage))
30+
const turnstileToken = await new Promise<string>((resolve) => {
31+
try {
32+
(window as any).turnstile.reset();
33+
} catch (error) {}
34+
(window as any).turnstile.execute(turnstileRef.current, {
35+
sitekey: VITE_TURNSTILE_SITEKEY,
36+
callback: (token: string) => resolve(token),
37+
'error-callback': () => resolve(''),
38+
});
39+
});
40+
setResponse(await runCode(code, codeLanguage, turnstileToken))
2841
setLoading(false)
2942
onChangeRightPanelMode(false)
3043
}
3144
handleExecutionRef.current = handleExecution
3245

3346
return (
3447
<CustomProvider theme={darkMode ? 'dark' : 'light'}>
48+
<div ref={turnstileRef} style={{ display: 'none' }} />
3549
<Header />
3650
<Grid fluid className={styles.grid}>
3751
<Col xs={24} md={12}>

frontend/src/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type ExecutionResponse = Partial<{
1616
output: string;
1717
}>
1818

19-
export const runCode = async (code: string, codeLanguage: CodeLanguage): Promise<ExecutionResponse> => {
19+
export const runCode = async (code: string, codeLanguage: CodeLanguage, turnstileToken: string): Promise<ExecutionResponse> => {
2020
if (codeLanguage === CodeLanguage.PLAYWRIGHT_TEST)
2121
codeLanguage = CodeLanguage.JAVASCRIPT
2222
const resp = await fetch("/service/control/run", {
@@ -27,6 +27,7 @@ export const runCode = async (code: string, codeLanguage: CodeLanguage): Promise
2727
body: JSON.stringify({
2828
code,
2929
language: codeLanguage,
30+
token: turnstileToken,
3031
})
3132
})
3233

internal/workertypes/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type WorkerResponsePayload struct {
1616
}
1717

1818
type WorkerRequestPayload struct {
19+
Token string `json:"token"`
1920
Code string `json:"code"`
2021
Language WorkerLanguage `json:"language"`
2122
}

k8/control-deployment.yaml.tpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ spec:
2929
value: https://[email protected]/5479806
3030
- name: WORKER_IMAGE_TAG
3131
value: ${DOCKER_TAG}
32+
- name: TURNSTILE_SECRET_KEY
33+
value: "${TURNSTILE_SECRET_KEY}"
3234
image: ghcr.io/mxschmitt/try-playwright/control-service:${DOCKER_TAG}
3335
name: control
3436
imagePullPolicy: IfNotPresent

0 commit comments

Comments
 (0)