Skip to content

Commit 3a239e8

Browse files
Merge pull request #6 from ethereum/fredriksvantes-patch-1
Refactoring
2 parents 92d551d + 5a48538 commit 3a239e8

File tree

1 file changed

+111
-69
lines changed

1 file changed

+111
-69
lines changed

server.py

+111-69
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from flask import Flask, render_template, request, jsonify
88
from flask_recaptcha import ReCaptcha
9+
from flask_limiter import Limiter
10+
from flask_limiter.util import get_remote_address
911

1012
from sendgrid import SendGridAPIClient
1113
from sendgrid.helpers.mail import (Mail, Attachment, FileContent, FileName, FileType, Disposition)
@@ -14,93 +16,140 @@
1416

1517
load_dotenv()
1618

17-
if not set(['RECAPTCHASITEKEY', 'RECAPTCHASECRETKEY', 'SENDGRIDAPIKEY', 'SENDGRIDFROMEMAIL']).issubset(os.environ):
18-
print("Failed to start. Please set the environment variables RECAPTCHASITEKEY, RECAPTCHASECRETKEY, SENDGRIDAPIKEY, and SENDGRIDFROMEMAIL")
19-
exit(1)
20-
21-
RECAPTCHASITEKEY = os.environ['RECAPTCHASITEKEY']
22-
RECAPTCHASECRETKEY = os.environ['RECAPTCHASECRETKEY']
23-
SENDGRIDAPIKEY = os.environ['SENDGRIDAPIKEY']
24-
FROMEMAIL = os.environ['SENDGRIDFROMEMAIL']
25-
26-
# this needs to be reflected in the `templates/index.html` file
27-
NUMBER_OF_ATTACHMENTS = int(os.environ.get('NUMBEROFATTACHMENTS', '10'))
28-
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
29-
30-
app = Flask(__name__)
31-
app.config['RECAPTCHA_SITE_KEY'] = RECAPTCHASITEKEY
32-
app.config['RECAPTCHA_SECRET_KEY'] = RECAPTCHASECRETKEY
33-
app.config['MAX_CONTENT_LENGTH'] = 15 * 1024 * 1024 # 15 Mb limit
34-
recaptcha = ReCaptcha(app)
35-
36-
log_file = os.environ.get('LOG_FILE', '')
37-
if log_file:
38-
logging.basicConfig(filename=log_file, level=logging.INFO)
39-
else:
40-
logging.basicConfig(level=logging.INFO)
19+
class Config:
20+
MAX_CONTENT_LENGTH = 15 * 1024 * 1024 # 15 MB
21+
EMAIL_DOMAIN = "@ethereum.org"
22+
DEFAULT_RECIPIENT_EMAIL = "[email protected]"
23+
NUMBER_OF_ATTACHMENTS = int(os.getenv('NUMBEROFATTACHMENTS', 10))
24+
DEBUG_MODE = os.getenv('DEBUG', 'False').lower() == 'true'
25+
SECRET_KEY = os.getenv('SECRET_KEY', 'you-should-set-a-secret-key')
26+
27+
def validate_env_vars(required_vars):
28+
"""
29+
Validates that all required environment variables are set.
30+
"""
31+
missing_vars = [var for var in required_vars if var not in os.environ]
32+
if missing_vars:
33+
raise EnvironmentError(f"Missing required environment variables: {', '.join(missing_vars)}")
34+
35+
def sanitize_filename(filename):
36+
"""
37+
Sanitizes the filename to prevent directory traversal and other issues.
38+
"""
39+
return filename.replace("..", "").replace("/", "").replace("\\", "")
4140

4241
def parse_form(form):
42+
"""
43+
Parses the form data to extract the message, recipient, and attachments.
44+
"""
4345
text = form['message']
4446
recipient = form['recipient']
4547

4648
all_attachments = []
47-
for i in range(NUMBER_OF_ATTACHMENTS):
48-
attachment = form['attachment-%s' % i]
49-
filename = form['filename-%s' % i].encode('ascii', 'ignore').decode() # remove non-ascii characters
49+
for i in range(Config.NUMBER_OF_ATTACHMENTS):
50+
attachment = form.get(f'attachment-{i}')
51+
filename = form.get(f'filename-{i}', '').encode('ascii', 'ignore').decode() # remove non-ascii characters
5052
if not attachment:
5153
continue
52-
all_attachments.append((filename, attachment))
54+
sanitized_filename = sanitize_filename(filename)
55+
all_attachments.append((sanitized_filename, attachment))
5356
return text, recipient, all_attachments
5457

5558
def valid_recipient(recipient):
56-
if recipient in ['legal', 'devcon', 'esp', 'security', 'oleh']:
57-
return True
58-
return False
59+
"""
60+
Checks if the recipient is valid.
61+
"""
62+
valid_recipients = ['legal', 'devcon', 'esp', 'security', 'oleh']
63+
return recipient in valid_recipients
5964

6065
def get_identifier(recipient, now=None, randint=None):
66+
"""
67+
Generates a unique identifier based on the recipient, current timestamp, and a random number.
68+
"""
6169
if now is None:
6270
now = datetime.now()
6371
if randint is None:
6472
randint = Random().randint(1000, 9999)
65-
return '%s:%s:%s' % (recipient, now.strftime('%Y:%m:%d:%H:%M:%S'), randint)
73+
return f'{recipient}:{now.strftime("%Y:%m:%d:%H:%M:%S")}:{randint}'
6674

67-
def create_email(toEmail, identifier, text, all_attachments):
75+
def create_email(to_email, identifier, text, all_attachments):
76+
"""
77+
Creates an email message with attachments.
78+
"""
6879
plain_text = text.replace('<br />', '\n')
6980
message = Mail(
70-
from_email=FROMEMAIL,
71-
to_emails=toEmail,
72-
subject='Secure Form Submission %s' % identifier,
73-
plain_text_content=plain_text)
74-
75-
for item in all_attachments:
76-
filename = item['filename']
77-
attachment = item['attachment']
81+
from_email=FROMEMAIL,
82+
to_emails=to_email,
83+
subject=f'Secure Form Submission {identifier}',
84+
plain_text_content=plain_text
85+
)
7886

87+
for filename, attachment in all_attachments:
7988
encoded_file = base64.b64encode(attachment.encode("utf-8")).decode()
80-
attachedFile = Attachment(
89+
attached_file = Attachment(
8190
FileContent(encoded_file),
8291
FileName(filename + '.pgp'),
8392
FileType('application/pgp-encrypted'),
8493
Disposition('attachment')
8594
)
86-
message.add_attachment(attachedFile)
95+
message.add_attachment(attached_file)
8796
return message
8897

98+
def validate_recaptcha(recaptcha_response):
99+
"""
100+
Validates the ReCaptcha response.
101+
"""
102+
if not recaptcha.verify(response=recaptcha_response):
103+
raise ValueError('Error: ReCaptcha verification failed!')
104+
105+
def send_email(message):
106+
"""
107+
Sends the email using SendGrid.
108+
"""
109+
sg = SendGridAPIClient(SENDGRIDAPIKEY)
110+
response = sg.send(message)
111+
if response.status_code not in [200, 201, 202]:
112+
raise ValueError(f"Error: Failed to send email. Status code: {response.status_code}")
113+
114+
# Validate required environment variables
115+
required_env_vars = ['RECAPTCHASITEKEY', 'RECAPTCHASECRETKEY', 'SENDGRIDAPIKEY', 'SENDGRIDFROMEMAIL']
116+
validate_env_vars(required_env_vars)
117+
118+
RECAPTCHASITEKEY = os.environ['RECAPTCHASITEKEY']
119+
RECAPTCHASECRETKEY = os.environ['RECAPTCHASECRETKEY']
120+
SENDGRIDAPIKEY = os.environ['SENDGRIDAPIKEY']
121+
FROMEMAIL = os.environ['SENDGRIDFROMEMAIL']
122+
123+
app = Flask(__name__)
124+
app.config.from_object(Config)
125+
recaptcha = ReCaptcha(app)
126+
127+
# Initialize rate limiting
128+
limiter = Limiter(get_remote_address, app=app, default_limits=["200 per day", "50 per hour"])
129+
130+
# Configure logging
131+
log_file = os.environ.get('LOG_FILE', '')
132+
133+
if log_file:
134+
logging.basicConfig(filename=log_file, level=logging.INFO)
135+
else:
136+
logging.basicConfig(level=logging.INFO)
137+
89138
@app.route('/', methods=['GET'])
90139
def index():
91-
return render_template('index.html', notice='', hascaptcha=not DEBUG, attachments_number=NUMBER_OF_ATTACHMENTS, recaptcha_sitekey=RECAPTCHASITEKEY)
140+
return render_template('index.html', notice='', hascaptcha=not Config.DEBUG_MODE, attachments_number=Config.NUMBER_OF_ATTACHMENTS, recaptcha_sitekey=RECAPTCHASITEKEY)
92141

93142
@app.route('/submit-encrypted-data', methods=['POST'])
143+
@limiter.limit("5 per minute")
94144
def submit():
95145
try:
96146
# Parse JSON data from request
97147
data = request.get_json()
98148

99-
# Won't even look on Captcha for debug mode
100-
if not DEBUG:
101-
if not recaptcha.verify(response=data['g-recaptcha-response']):
102-
raise ValueError('Error: ReCaptcha verification failed! You would need to re-submit the request.')
103-
149+
# Validate ReCaptcha unless in debug mode
150+
if not Config.DEBUG_MODE:
151+
validate_recaptcha(data['g-recaptcha-response'])
152+
104153
# Extract fields from JSON data
105154
message = data['message']
106155
recipient = data['recipient']
@@ -111,44 +160,37 @@ def submit():
111160

112161
if not valid_recipient(recipient):
113162
raise ValueError('Error: Invalid recipient!')
114-
163+
115164
# Get submission statistics
116165
date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
117166
message_length = len(message)
118167
file_count = len(files)
119-
120-
toEmail = "[email protected]" if recipient == 'legal' else recipient + "@ethereum.org"
168+
169+
to_email = Config.DEFAULT_RECIPIENT_EMAIL if recipient == 'legal' else recipient + Config.EMAIL_DOMAIN
121170
identifier = get_identifier(recipient)
122-
# toEmail = "[email protected]"
123171

124172
log_data = f"{date} - message to: {recipient}, identifier: {identifier}, length: {message_length}, file count: {file_count}"
125173
logging.info(log_data)
126174

127-
message = create_email(toEmail, identifier, message, files)
175+
message = create_email(to_email, identifier, message, files)
128176

129-
if DEBUG:
130-
print("Attempt to send email to %s" % toEmail)
177+
if Config.DEBUG_MODE:
178+
print(f"Attempt to send email to {to_email}")
131179
print(message.get())
132180
else:
133-
sg = SendGridAPIClient(SENDGRIDAPIKEY)
134-
response = sg.send(message)
135-
if not response.status_code in [200, 201, 202]:
136-
logging.error("Failed to send email: %s" % response.body)
137-
logging.error("Headers: %s" % response.headers)
138-
raise ValueError('Error: Failed to send email. Please try again later. Code: %s' % response.status_code)
139-
140-
notice = 'Thank you! The relevant team was notified of your submission. You could use a following identifier to refer to it in correspondence: <b>' + identifier + '</b>'
141-
181+
send_email(message)
182+
183+
notice = f'Thank you! The relevant team was notified of your submission. You could use the following identifier to refer to it in correspondence: <b>{identifier}</b>'
184+
142185
# Return success response
143186
return jsonify({'status': 'success', 'message': notice})
144-
187+
145188
except Exception as e:
146189
# Log error message and return failure response
147-
error_message = str(e)
148-
print(error_message)
190+
error_message = "An unexpected error occurred. Please try again later."
191+
logging.error(f"Internal error: {str(e)}")
149192
return jsonify({'status': 'failure', 'message': error_message})
150193

151-
152194
@app.errorhandler(413)
153195
def error413(e):
154196
return render_template('413.html'), 413

0 commit comments

Comments
 (0)