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
14 changes: 7 additions & 7 deletions platform/CTFd/api/v1/challenges.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,19 +631,19 @@ def post(self):
request_data = request.get_json()

challenge_id = request_data.get("challenge_id")
# Check for preview flag in request data (for both admin and regular users)
preview = request_data.get("preview", False)
# Allow preview for SQL challenges
if preview:

# Check for test flag in request data (for testing without recording submission)
test = request_data.get("test", False)

# Allow test mode for SQL challenges
if test:
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()

# Check if challenge is hidden and user is not admin
if challenge.state == "hidden" and not is_admin():
abort(403)

# Only allow preview for SQL challenges
# Only allow test mode for SQL challenges
if challenge.type == "sql":
chal_class = get_chal_class(challenge.type)
response = chal_class.attempt(challenge, request)
Expand Down
10 changes: 5 additions & 5 deletions platform/CTFd/plugins/sql_challenges/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
FROM golang:1.22-alpine AS builder
FROM golang:1.23-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
# Copy source code (needed for go mod tidy to detect dependencies)
COPY sql_judge_server.go .

# Download dependencies and tidy
RUN go mod download && go mod tidy

# Build the application
RUN go build -o sql-judge-server sql_judge_server.go

Expand Down
54 changes: 31 additions & 23 deletions platform/CTFd/plugins/sql_challenges/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,26 +211,26 @@ def attempt(cls, challenge, request):
"""
data = request.form or request.get_json()
submission = data.get("submission", "").strip()
is_preview = data.get("preview", False) # Check if this is just a preview/test
is_test = data.get("test", False) # Check if this is test mode (checks correctness but doesn't record)

# Get user information from request
user_id = str(data.get("user_id", ""))
user_name = data.get("user_name", "")
client_ip = get_ip()

# Debug logging
import logging
logging.info(f"SQL Challenge attempt - Preview: {is_preview}, User ID: {user_id}, User Name: {user_name}, IP: {client_ip}")
logging.info(f"SQL Challenge attempt - Test mode: {is_test}, User ID: {user_id}, User Name: {user_name}, IP: {client_ip}")

if not submission:
return ChallengeResponse(
status="incorrect",
message="Please provide a SQL query"
)
# Check deadline only for actual submissions, not previews

# Check deadline only for actual submissions, not test mode
# Use deadline_utc (raw datetime) for comparison, not the property (which returns a string)
if not is_preview and challenge.deadline_utc and datetime.utcnow() > challenge.deadline_utc:
if not is_test and challenge.deadline_utc and datetime.utcnow() > challenge.deadline_utc:
return ChallengeResponse(
status="incorrect",
message="Submission deadline has passed"
Expand All @@ -240,17 +240,17 @@ def attempt(cls, challenge, request):
try:
import requests
import json

# Use Go MySQL server
go_server_url = os.environ.get('SQL_JUDGE_SERVER_URL', 'http://localhost:8080')
if is_preview:
# For preview, only execute the user query without comparing

if is_test:
# For test mode, check correctness but format message differently
response = requests.post(
f"{go_server_url}/judge",
json={
'init_query': challenge.init_query,
'solution_query': submission, # Use user query as solution to get its result
'solution_query': challenge.solution_query,
'user_query': submission,
'user_id': user_id,
'user_name': user_name,
Expand All @@ -259,26 +259,34 @@ def attempt(cls, challenge, request):
},
timeout=10
)

if response.status_code == 200:
result = response.json()

if not result.get('success'):
return ChallengeResponse(
status="incorrect",
message=f"[PREVIEW]\nError: {result.get('error', 'Unknown error')}"
message=f"[TEST]\nError: {result.get('error', 'Unknown error')}"
)
# Just show the query result without grading

# Format results for display
user_result_str = json.dumps(result['user_result'])
return ChallengeResponse(
status="incorrect",
message=f"[PREVIEW]\nQuery executed successfully:\n\n[USER_RESULT]\n{user_result_str}\n[/USER_RESULT]"
)

if result['match']:
return ChallengeResponse(
status="correct",
message=f"[TEST]\n✅ Correct! Your query produces the expected result.\n\n[USER_RESULT]\n{user_result_str}\n[/USER_RESULT]"
)
else:
expected_result_str = json.dumps(result['expected_result'])
return ChallengeResponse(
status="incorrect",
message=f"[TEST]\n❌ Incorrect. Your query does not produce the expected result.\n\n[USER_RESULT]\n{user_result_str}\n[/USER_RESULT]\n\n[EXPECTED_RESULT]\n{expected_result_str}\n[/EXPECTED_RESULT]"
)
else:
return ChallengeResponse(
status="incorrect",
message=f"[PREVIEW]\nSQL judge server error: HTTP {response.status_code}"
message=f"[TEST]\nSQL judge server error: HTTP {response.status_code}"
)
else:
# Normal submission - compare with solution
Expand Down
11 changes: 8 additions & 3 deletions platform/CTFd/plugins/sql_challenges/assets/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ <h5>Test Results:</h5>
The state of the challenge (visible, hidden)
</small>
</label>
<select class="form-control challenge-state" name="state">
<select class="form-control" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>
Expand All @@ -97,6 +97,11 @@ <h5>Test Results:</h5>
</label>
<input type="number" class="form-control challenge-max-attempts" name="max_attempts" placeholder="0" value="{{ challenge.max_attempts }}">
</div>

<button type="submit" class="btn btn-primary">Update</button>

<div class="d-flex gap-8">
<button type="submit" class="btn btn-primary mr-2">Update</button>
<a href="/challenges/sql/{{ challenge.id }}" class="btn btn-info" target="_blank">
<i class="fas fa-external-link-alt me-1"></i> View Challenge Page
</a>
</div>
</form>
2 changes: 1 addition & 1 deletion platform/CTFd/plugins/sql_challenges/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22

toolchain go1.24.5

require github.com/dolthub/go-mysql-server v0.18.1
require github.com/dolthub/go-mysql-server v0.20.0

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions platform/CTFd/plugins/sql_challenges/sql_judge_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func executeQuery(initQueries []string, query string, req *QueryRequest) (*Query
// }
// }

_, iter, err := engine.Query(ctx, stmt)
_, iter, _, err := engine.Query(ctx, stmt)
if err != nil {
if len(stmt) > logMaxLength {
log.Printf("Failed to execute (truncated): %s...", stmt[:logMaxLength])
Expand All @@ -317,7 +317,7 @@ func executeQuery(initQueries []string, query string, req *QueryRequest) (*Query
}

// Execute the main query
schema, iter, err := engine.Query(ctx, query)
schema, iter, _, err := engine.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("query error: %v", err)
}
Expand Down
Loading