11"""
2- Solution controller - Solution uploading.
2+ Solution controller - Solution uploading and viewing .
33"""
44
55import os
66from html import escape as html_escape
7- from flask import Blueprint , render_template , request , redirect , flash , session , abort
7+ from flask import Blueprint , render_template , request , redirect , flash , session , abort , current_app
88from werkzeug .utils import secure_filename
99import bleach
1010from app .models .crackme import crackme_by_hexid
11- from app .models .solution import solution_create , solutions_by_user_and_crackme
11+ from app .models .solution import solution_create , solution_exists , solution_by_hexid
1212from app .models .notification import notification_add
1313from app .models .errors import ErrNoResult
1414from app .services .recaptcha import verify as verify_recaptcha
1515from app .services .limiter import limit
16- from app .services .view import FLASH_ERROR , FLASH_SUCCESS
16+ from app .services .view import FLASH_ERROR , is_valid_hexid
1717from app .services .archive import is_archive_password_protected , is_pe_file
1818from app .services .discord import notify_new_solution
19+ from app .services .crypto import get_obfuscation_key_base64 , get_obfuscation_salt
1920from app .controllers .decorators import login_required
2021
2122solution_bp = Blueprint ('solution' , __name__ )
2223
23- # Upload folder for solutions
2424UPLOAD_FOLDER = 'tmp/solution'
2525MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
26+ MAX_INFO_LENGTH = 200
27+ MAX_CONTENT_LENGTH = 50000
28+ MIN_CONTENT_LENGTH = 200
2629
2730
28- @ solution_bp . route ( '/upload/solution/<hexidcrackme>' , methods = [ 'GET' ])
29- @ login_required
30- def upload_solution_get ( hexidcrackme ):
31- """Display the solution upload form."""
31+ def _get_crackme_or_abort ( hexid ):
32+ """Fetch crackme by hexid, abort with 404/500 on error."""
33+ if not is_valid_hexid ( hexid ):
34+ abort ( 404 )
3235 try :
33- crackme = crackme_by_hexid (hexidcrackme )
36+ return crackme_by_hexid (hexid )
3437 except ErrNoResult :
3538 abort (404 )
3639 except Exception as e :
3740 print (f"Error getting crackme: { e } " )
3841 abort (500 )
3942
43+
44+ def _get_solution_or_abort (hexid ):
45+ """Fetch solution by hexid, abort with 404/500 on error."""
46+ if not is_valid_hexid (hexid ):
47+ abort (404 )
48+ try :
49+ return solution_by_hexid (hexid )
50+ except ErrNoResult :
51+ abort (404 )
52+ except Exception as e :
53+ print (f"Error getting solution: { e } " )
54+ abort (500 )
55+
56+
57+ def _send_notifications_and_render_success (username , crackme ):
58+ """Send notifications and render the success page after solution creation."""
59+ try :
60+ notification_add (username , f"Your solution for '{ html_escape (crackme ['name' ])} ' is waiting approval!" )
61+ notify_new_solution (username , crackme ['name' ])
62+ except Exception as e :
63+ print (f"Notification error: { e } " )
64+
65+ return render_template ('submission/success.html' ,
66+ submission_type = 'Writeup' ,
67+ name = crackme ['name' ],
68+ username = username )
69+
70+
71+ @solution_bp .route ('/upload/solution/<hexidcrackme>' , methods = ['GET' ])
72+ @login_required
73+ def upload_solution_get (hexidcrackme ):
74+ """Display the solution upload form."""
75+ crackme = _get_crackme_or_abort (hexidcrackme )
4076 return render_template ('solution/create.html' ,
4177 hexidcrackme = hexidcrackme ,
4278 username = crackme .get ('author' , '' ),
@@ -47,95 +83,157 @@ def upload_solution_get(hexidcrackme):
4783@login_required
4884@limit ("20 per day" , key_func = lambda : session .get ('name' ))
4985def upload_solution_post (hexidcrackme ):
50- """Handle solution upload."""
86+ """Handle solution file upload."""
87+ _get_crackme_or_abort (hexidcrackme ) # Validate crackme exists
5188 username = session .get ('name' )
52-
53- info = bleach .clean (request .form .get ('info' , '' ))
89+ redirect_url = f'/upload/solution/{ hexidcrackme } '
5490
5591 # Check if user already submitted a solution
5692 try :
57- solutions_by_user_and_crackme (username , hexidcrackme )
58- flash ("You've already submitted a solution to this crackme" , FLASH_ERROR )
59- return redirect (f'/upload/solution/ { hexidcrackme } ' )
93+ if solution_exists (username , hexidcrackme ):
94+ flash ("You've already submitted a solution to this crackme" , FLASH_ERROR )
95+ return redirect (redirect_url )
6096 except ErrNoResult :
61- pass # No existing solution, continue
97+ abort ( 404 )
6298
63- # Validate reCAPTCHA
6499 if not verify_recaptcha (request ):
65100 flash ('reCAPTCHA invalid!' , FLASH_ERROR )
66- return redirect (f'/upload/solution/ { hexidcrackme } ' )
101+ return redirect (redirect_url )
67102
68- # Check for file
69- if 'file' not in request .files :
103+ # Validate file presence
104+ if 'file' not in request .files or request . files [ 'file' ]. filename == '' :
70105 flash ('Field missing: file' , FLASH_ERROR )
71- return redirect (f'/upload/solution/ { hexidcrackme } ' )
106+ return redirect (redirect_url )
72107
73108 file = request .files ['file' ]
74- if file .filename == '' :
75- flash ('Field missing: file' , FLASH_ERROR )
76- return redirect (f'/upload/solution/{ hexidcrackme } ' )
77109
78- # Check file size from header
110+ # Check file size ( header and actual)
79111 if file .content_length and file .content_length > MAX_FILE_SIZE :
80112 flash ('This file is too large!' , FLASH_ERROR )
81- return redirect (f'/upload/solution/ { hexidcrackme } ' )
113+ return redirect (redirect_url )
82114
83- # Read file data
84115 try :
85116 data = file .read ()
86- if len (data ) > MAX_FILE_SIZE :
87- flash ('This file is too large!' , FLASH_ERROR )
88- return redirect (f'/upload/solution/{ hexidcrackme } ' )
89117 except Exception as e :
90118 print (f"Error reading file: { e } " )
91119 abort (500 )
92120
121+ if len (data ) > MAX_FILE_SIZE :
122+ flash ('This file is too large!' , FLASH_ERROR )
123+ return redirect (redirect_url )
124+
93125 # Check for password-protected archives
94126 if is_archive_password_protected (data ):
95127 flash ('Password-protected archives are not allowed. Do NOT add a password yourself - the server handles this automatically.' , FLASH_ERROR )
96- return redirect (f'/upload/solution/ { hexidcrackme } ' )
128+ return redirect (redirect_url )
97129
98- # Check for PE files (patched binaries are not allowed)
130+ # Check for PE files (patched binaries not allowed)
99131 if is_pe_file (file .filename , data ):
100- flash ('Executable files (PE binaries) are not allowed as solutions. '
101- 'Please submit a writeup that analyzes the algorithm instead of a patched binary.' , FLASH_ERROR )
102- return redirect (f'/upload/solution/{ hexidcrackme } ' )
132+ flash ('Executable files (PE binaries) are not allowed as solutions. Please submit a writeup that analyzes the algorithm instead of a patched binary.' , FLASH_ERROR )
133+ return redirect (redirect_url )
134+
135+ info = bleach .clean (request .form .get ('info' , '' ))
136+ if len (info ) > MAX_INFO_LENGTH :
137+ flash (f'Info field exceeds maximum length of { MAX_INFO_LENGTH } characters.' , FLASH_ERROR )
138+ return redirect (redirect_url )
103139
104- # Secure filename (fallback to "unnamed" if filename has only unsafe characters)
105140 original_filename = secure_filename (file .filename ) or "unnamed"
106141
107- # Create solution
108142 try :
109- solution = solution_create (info , username , hexidcrackme , original_filename )
143+ solution , crackme = solution_create (info , username , hexidcrackme , original_filename )
110144 except Exception as e :
111145 print (f"Error creating solution: { e } " )
112146 abort (500 )
113147
114- # Create path using hexid only
115- safe_path = os .path .join (UPLOAD_FOLDER , solution ['hexid' ])
116-
117- # Ensure upload directory exists
148+ # Save uploaded file
118149 os .makedirs (UPLOAD_FOLDER , exist_ok = True )
119-
120- # Save file
121150 try :
122- with open (safe_path , 'wb' ) as f :
151+ with open (os . path . join ( UPLOAD_FOLDER , solution [ 'hexid' ]) , 'wb' ) as f :
123152 f .write (data )
124153 except Exception as e :
125154 print (f"File write error: { e } " )
126155 flash ('An error occurred on the server. Please try again later.' , FLASH_ERROR )
127- return redirect (f'/upload/solution/{ hexidcrackme } ' )
156+ return redirect (redirect_url )
157+
158+ return _send_notifications_and_render_success (username , crackme )
159+
128160
129- # Send notification
161+ @solution_bp .route ('/solution/<hexid>' , methods = ['GET' ])
162+ @login_required
163+ def view_solution (hexid ):
164+ """Display a solution's writeup page."""
165+ solution = _get_solution_or_abort (hexid )
166+ salt = get_obfuscation_salt (current_app .config )
167+
168+ return render_template ('solution/read.html' ,
169+ solution = solution ,
170+ obfuscation_key = get_obfuscation_key_base64 (hexid , salt ))
171+
172+
173+ @solution_bp .route ('/upload/solution/<hexidcrackme>/editor' , methods = ['GET' ])
174+ @login_required
175+ def editor_solution_get (hexidcrackme ):
176+ """Display the web-based markdown editor for writing solutions."""
177+ crackme = _get_crackme_or_abort (hexidcrackme )
178+ return render_template ('solution/editor.html' ,
179+ hexidcrackme = hexidcrackme ,
180+ username = crackme .get ('author' , '' ),
181+ crackmename = crackme .get ('name' , '' ),
182+ min_content_length = MIN_CONTENT_LENGTH ,
183+ max_content_length = MAX_CONTENT_LENGTH )
184+
185+
186+ @solution_bp .route ('/upload/solution/<hexidcrackme>/editor' , methods = ['POST' ])
187+ @login_required
188+ @limit ("20 per day" , key_func = lambda : session .get ('name' ))
189+ def editor_solution_post (hexidcrackme ):
190+ """Handle solution submission from the web editor."""
191+ _get_crackme_or_abort (hexidcrackme ) # Validate crackme exists
192+ username = session .get ('name' )
193+ redirect_url = f'/upload/solution/{ hexidcrackme } /editor'
194+
195+ # Check if user already submitted a solution
130196 try :
131- crackme = crackme_by_hexid (hexidcrackme )
132- notification_add (username , f"Your solution for '{ html_escape (crackme ['name' ])} ' is waiting approval!" )
133- # Send Discord notification
134- notify_new_solution (username , crackme ['name' ])
197+ if solution_exists (username , hexidcrackme ):
198+ flash ("You've already submitted a solution to this crackme" , FLASH_ERROR )
199+ return redirect (redirect_url )
200+ except ErrNoResult :
201+ abort (404 )
202+
203+ if not verify_recaptcha (request ):
204+ flash ('reCAPTCHA invalid!' , FLASH_ERROR )
205+ return redirect (redirect_url )
206+
207+ content = request .form .get ('content' , '' ).strip ()
208+
209+ # Validate content length
210+ if len (content ) < MIN_CONTENT_LENGTH :
211+ flash (f'Your writeup is too short. Please write at least { MIN_CONTENT_LENGTH } characters.' , FLASH_ERROR )
212+ return redirect (redirect_url )
213+
214+ if len (content ) > MAX_CONTENT_LENGTH :
215+ flash (f'Your writeup exceeds the maximum length of { MAX_CONTENT_LENGTH :,} characters.' , FLASH_ERROR )
216+ return redirect (redirect_url )
217+
218+ info = bleach .clean (request .form .get ('info' , '' ))
219+ if len (info ) > MAX_INFO_LENGTH :
220+ flash (f'Info field exceeds maximum length of { MAX_INFO_LENGTH } characters.' , FLASH_ERROR )
221+ return redirect (redirect_url )
222+
223+ try :
224+ solution , crackme = solution_create (info , username , hexidcrackme , original_filename = 'writeup.md' , has_markdown = True )
135225 except Exception as e :
136- print (f"Notification error: { e } " )
226+ print (f"Error creating solution: { e } " )
227+ abort (500 )
137228
138- return render_template ('submission/success.html' ,
139- submission_type = 'Writeup' ,
140- name = crackme ['name' ],
141- username = username )
229+ # Save markdown content
230+ os .makedirs (UPLOAD_FOLDER , exist_ok = True )
231+ try :
232+ with open (os .path .join (UPLOAD_FOLDER , solution ['hexid' ]), 'w' , encoding = 'utf-8' ) as f :
233+ f .write (content )
234+ except Exception as e :
235+ print (f"File write error: { e } " )
236+ flash ('An error occurred on the server. Please try again later.' , FLASH_ERROR )
237+ return redirect (redirect_url )
238+
239+ return _send_notifications_and_render_success (username , crackme )
0 commit comments