Skip to content

Commit 5dbce1e

Browse files
committed
Optimize API resolution using a Trie
1 parent 76f6a71 commit 5dbce1e

File tree

1 file changed

+157
-122
lines changed

1 file changed

+157
-122
lines changed

backend/globaleaks/rest/api.py

+157-122
Original file line numberDiff line numberDiff line change
@@ -42,130 +42,179 @@
4242
from globaleaks.utils.sock import isIPAddress
4343

4444
tid_regexp = r'([0-9]+)'
45-
uuid_regexp = r'([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})'
45+
uuid_regexp = r'([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|closed)'
46+
uuid_regexp_or_closed = r'([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})'
4647
key_regexp = r'([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|[a-z_]{0,100})'
4748

4849
api_spec = [
49-
(r'/api/health', health.HealthStatusHandler),
50-
(r'/api/report', report.ReportHandler),
51-
52-
# Public API
53-
(r'/api/public', public.PublicResource),
50+
('/api/health', health.HealthStatusHandler),
51+
('/api/public', public.PublicResource),
52+
('/api/report', report.ReportHandler),
53+
('/api/support', support.SupportHandler),
54+
('/api/wizard', wizard.Wizard),
5455

5556
# Authentication Handlers
56-
(r'/api/auth/token', auth.token.TokenHandler),
57-
(r'/api/auth/authentication', auth.AuthenticationHandler),
58-
(r'/api/auth/type', auth.AuthTypeHandler),
59-
(r'/api/auth/tokenauth', auth.TokenAuthHandler),
60-
(r'/api/auth/receiptauth', auth.ReceiptAuthHandler),
61-
(r'/api/auth/session', auth.SessionHandler),
62-
(r'/api/auth/tenantauthswitch/' + tid_regexp, auth.TenantAuthSwitchHandler),
63-
(r'/api/auth/operatorauthswitch', auth.OperatorAuthSwitchHandler),
57+
('/api/auth/token', auth.token.TokenHandler),
58+
('/api/auth/authentication', auth.AuthenticationHandler),
59+
('/api/auth/type', auth.AuthTypeHandler),
60+
('/api/auth/tokenauth', auth.TokenAuthHandler),
61+
('/api/auth/receiptauth', auth.ReceiptAuthHandler),
62+
('/api/auth/session', auth.SessionHandler),
63+
('/api/auth/tenantauthswitch/', auth.TenantAuthSwitchHandler, r'/api/auth/tenantauthswitch/' + tid_regexp),
64+
('/api/auth/operatorauthswitch', auth.OperatorAuthSwitchHandler),
6465

6566
# User Preferences Handler
66-
(r'/api/user/preferences', user.UserInstance),
67-
(r'/api/user/operations', user.operation.UserOperationHandler),
68-
(r'/api/user/reset/password', user.reset_password.PasswordResetHandler),
69-
(r'/api/user/reset/password/(.+)', user.reset_password.PasswordResetHandler),
70-
(r'/api/user/validate/email/(.+)', user.validate_email.EmailValidation),
67+
('/api/user/preferences', user.UserInstance),
68+
('/api/user/operations', user.operation.UserOperationHandler),
69+
('/api/user/reset/password', user.reset_password.PasswordResetHandler),
70+
('/api/user/reset/password', user.reset_password.PasswordResetHandler, r'/api/user/reset/password/(.+)'),
71+
('/api/user/validate/email', user.validate_email.EmailValidation, r'/api/user/validate/email/(.+)'),
7172

7273
# Receiver Handlers
73-
(r'/api/recipient/operations', recipient.Operations),
74-
(r'/api/recipient/rtips', recipient.TipsCollection),
75-
(r'/api/recipient/rtips/' + uuid_regexp, recipient.rtip.RTipInstance),
76-
(r'/api/recipient/rtips/' + uuid_regexp + r'/comments', recipient.rtip.RTipCommentCollection),
77-
(r'/api/recipient/rtips/' + uuid_regexp + r'/iars', recipient.rtip.IdentityAccessRequestsCollection),
78-
(r'/api/recipient/rtips/' + uuid_regexp + r'/export', recipient.export.ExportHandler),
79-
(r'/api/recipient/rtips/' + uuid_regexp + r'/rfiles', recipient.rtip.ReceiverFileUpload),
80-
(r'/api/recipient/redactions', recipient.rtip.RTipRedactionCollection),
81-
(r'/api/recipient/redactions/' + uuid_regexp, recipient.rtip.RTipRedactionCollection),
82-
(r'/api/recipient/rfiles/' + uuid_regexp, recipient.rtip.ReceiverFileDownload),
83-
(r'/api/recipient/wbfiles/' + uuid_regexp, recipient.rtip.WhistleblowerFileDownload),
74+
('/api/recipient/operations', recipient.Operations),
75+
('/api/recipient/rtips', recipient.TipsCollection),
76+
('/api/recipient/rtips', recipient.rtip.RTipInstance, r'/api/recipient/rtips/' + uuid_regexp),
77+
('/api/recipient/rtips', recipient.rtip.RTipCommentCollection, r'/api/recipient/rtips/' + uuid_regexp + r'/comments'),
78+
('/api/recipient/rtips', recipient.rtip.IdentityAccessRequestsCollection, r'/api/recipient/rtips/' + uuid_regexp + r'/iars'),
79+
('/api/recipient/rtips', recipient.export.ExportHandler, r'/api/recipient/rtips/' + uuid_regexp + r'/export'),
80+
('/api/recipient/rtips', recipient.rtip.ReceiverFileUpload, r'/api/recipient/rtips/' + uuid_regexp + r'/rfiles'),
81+
('/api/recipient/redactions', recipient.rtip.RTipRedactionCollection),
82+
('/api/recipient/redactions', recipient.rtip.RTipRedactionCollection, r'/api/recipient/redactions/' + uuid_regexp),
83+
('/api/recipient/rfiles', recipient.rtip.ReceiverFileDownload, r'/api/recipient/rfiles/' + uuid_regexp),
84+
('/api/recipient/wbfiles', recipient.rtip.WhistleblowerFileDownload, r'/api/recipient/wbfiles/' + uuid_regexp),
8485

8586
# Whistleblower Handlers
86-
(r'/api/whistleblower/operations', whistleblower.wbtip.Operations),
87-
(r'/api/whistleblower/submission', whistleblower.submission.SubmissionInstance),
88-
(r'/api/whistleblower/submission/attachment', whistleblower.attachment.SubmissionAttachment),
89-
(r'/api/whistleblower/wbtip', whistleblower.wbtip.WBTipInstance),
90-
(r'/api/whistleblower/wbtip/comments', whistleblower.wbtip.WBTipCommentCollection),
91-
(r'/api/whistleblower/wbtip/rfiles/' + uuid_regexp, whistleblower.wbtip.ReceiverFileDownload),
92-
(r'/api/whistleblower/wbtip/wbfiles', whistleblower.attachment.PostSubmissionAttachment),
93-
(r'/api/whistleblower/wbtip/wbfiles/' + uuid_regexp, whistleblower.wbtip.WhistleblowerFileDownload),
94-
(r'/api/whistleblower/wbtip/identity', whistleblower.wbtip.WBTipIdentityHandler),
95-
(r'/api/whistleblower/wbtip/fillform', whistleblower.wbtip.WBTipAdditionalQuestionnaire),
87+
('/api/whistleblower/operations', whistleblower.wbtip.Operations),
88+
('/api/whistleblower/submission', whistleblower.submission.SubmissionInstance),
89+
('/api/whistleblower/submission/attachment', whistleblower.attachment.SubmissionAttachment),
90+
('/api/whistleblower/wbtip', whistleblower.wbtip.WBTipInstance),
91+
('/api/whistleblower/wbtip/comments', whistleblower.wbtip.WBTipCommentCollection),
92+
('/api/whistleblower/wbtip/rfiles', whistleblower.wbtip.ReceiverFileDownload, r'/api/whistleblower/wbtip/rfiles/' + uuid_regexp),
93+
('/api/whistleblower/wbtip/wbfiles', whistleblower.attachment.PostSubmissionAttachment),
94+
('/api/whistleblower/wbtip/wbfiles', whistleblower.wbtip.WhistleblowerFileDownload, r'/api/whistleblower/wbtip/wbfiles/' + uuid_regexp),
95+
('/api/whistleblower/wbtip/identity', whistleblower.wbtip.WBTipIdentityHandler),
96+
('/api/whistleblower/wbtip/fillform', whistleblower.wbtip.WBTipAdditionalQuestionnaire),
9697

9798
# Custodian Handlers
98-
(r'/api/custodian/iars', custodian.IdentityAccessRequestsCollection),
99-
(r'/api/custodian/iars/' + uuid_regexp, custodian.IdentityAccessRequestInstance),
99+
('/api/custodian/iars', custodian.IdentityAccessRequestsCollection),
100+
('/api/custodian/iars', custodian.IdentityAccessRequestInstance, r'/api/custodian/iars/' + uuid_regexp),
100101

101102
# Analyst Handlers
102-
(r'/api/analyst/stats', analyst.Statistics),
103+
('/api/analyst/stats', analyst.Statistics),
103104

104105
# Admin Handlers
105-
(r'/api/admin/node', admin.node.NodeInstance),
106-
(r'/api/admin/network', admin.network.NetworkInstance),
107-
(r'/api/admin/users', admin.user.UsersCollection),
108-
(r'/api/admin/users/' + uuid_regexp, admin.user.UserInstance),
109-
(r'/api/admin/contexts', admin.context.ContextsCollection),
110-
(r'/api/admin/contexts/' + uuid_regexp, admin.context.ContextInstance),
111-
(r'/api/admin/questionnaires', admin.questionnaire.QuestionnairesCollection),
112-
(r'/api/admin/questionnaires/duplicate', admin.questionnaire.QuestionnareDuplication),
113-
(r'/api/admin/questionnaires/' + key_regexp, admin.questionnaire.QuestionnaireInstance),
114-
(r'/api/admin/notification', admin.notification.NotificationInstance),
115-
(r'/api/admin/fields', admin.field.FieldsCollection),
116-
(r'/api/admin/fields/' + key_regexp, admin.field.FieldInstance),
117-
(r'/api/admin/steps', admin.step.StepCollection),
118-
(r'/api/admin/steps/' + uuid_regexp, admin.step.StepInstance),
119-
(r'/api/admin/fieldtemplates', admin.field.FieldTemplatesCollection),
120-
(r'/api/admin/fieldtemplates/' + key_regexp, admin.field.FieldTemplateInstance),
121-
(r'/api/admin/redirects', admin.redirect.RedirectCollection),
122-
(r'/api/admin/redirects/' + uuid_regexp, admin.redirect.RedirectInstance),
123-
(r'/api/admin/auditlog', admin.auditlog.AuditLog),
124-
(r'/api/admin/auditlog/access', admin.auditlog.AccessLog),
125-
(r'/api/admin/auditlog/debug', admin.auditlog.DebugLog),
126-
(r'/api/admin/auditlog/jobs', admin.auditlog.JobsTiming),
127-
(r'/api/admin/auditlog/tips', admin.auditlog.TipsCollection),
128-
(r'/api/admin/l10n/(' + '|'.join(LANGUAGES_SUPPORTED_CODES) + ')', admin.l10n.AdminL10NHandler),
129-
(r'/api/admin/config', admin.operation.AdminOperationHandler),
130-
(r'/api/admin/config/csr/gen', admin.https.CSRHandler),
131-
(r'/api/admin/config/acme/run', admin.https.AcmeHandler),
132-
(r'/api/admin/config/tls', admin.https.ConfigHandler),
133-
(r'/api/admin/config/tls/files/(cert|chain|key)', admin.https.FileHandler),
134-
(r'/api/admin/files', admin.file.FileCollection),
135-
(r'/api/admin/files/(.+)', admin.file.FileInstance),
136-
(r'/api/admin/tenants', admin.tenant.TenantCollection),
137-
(r'/api/admin/tenants/' + '([0-9]{1,20})', admin.tenant.TenantInstance),
138-
(r'/api/admin/statuses', admin.submission_statuses.SubmissionStatusCollection),
139-
(r'/api/admin/statuses/' + r'(closed)' + r'/substatuses', admin.submission_statuses.SubmissionSubStatusCollection),
140-
(r'/api/admin/statuses/' + uuid_regexp, admin.submission_statuses.SubmissionStatusInstance),
141-
(r'/api/admin/statuses/' + r'(closed)', admin.submission_statuses.SubmissionStatusInstance),
142-
(r'/api/admin/statuses/' + uuid_regexp + r'/substatuses', admin.submission_statuses.SubmissionSubStatusCollection),
143-
(r'/api/admin/statuses/' + r'(closed)' + r'/substatuses/' + uuid_regexp, admin.submission_statuses.SubmissionSubStatusInstance),
144-
(r'/api/admin/statuses/' + uuid_regexp + r'/substatuses/' + uuid_regexp, admin.submission_statuses.SubmissionSubStatusInstance),
145-
146-
# Services
147-
(r'/api/support', support.SupportHandler),
148-
(r'/api/signup', signup.Signup),
149-
(r'/api/signup/([a-zA-Z0-9_\-]{64})', signup.SignupActivation),
150-
(r'/api/wizard', wizard.Wizard),
106+
('/api/admin/node', admin.node.NodeInstance),
107+
('/api/admin/network', admin.network.NetworkInstance),
108+
('/api/admin/users', admin.user.UsersCollection),
109+
('/api/admin/users', admin.user.UserInstance, r'/api/admin/users/' + uuid_regexp),
110+
('/api/admin/contexts', admin.context.ContextsCollection),
111+
('/api/admin/contexts', admin.context.ContextInstance, r'/api/admin/contexts/' + uuid_regexp),
112+
('/api/admin/questionnaires', admin.questionnaire.QuestionnairesCollection),
113+
('/api/admin/questionnaires', admin.questionnaire.QuestionnaireInstance, r'/api/admin/questionnaires/' + key_regexp),
114+
('/api/admin/questionnaires/duplicate', admin.questionnaire.QuestionnareDuplication),
115+
('/api/admin/notification', admin.notification.NotificationInstance),
116+
('/api/admin/fields', admin.field.FieldsCollection),
117+
('/api/admin/fields', admin.field.FieldInstance, r'/api/admin/fields/' + key_regexp),
118+
('/api/admin/steps', admin.step.StepCollection),
119+
('/api/admin/steps', admin.step.StepInstance, r'/api/admin/steps/' + uuid_regexp),
120+
('/api/admin/fieldtemplates', admin.field.FieldTemplatesCollection),
121+
('/api/admin/fieldtemplates', admin.field.FieldTemplateInstance, r'/api/admin/fieldtemplates/' + key_regexp),
122+
('/api/admin/redirects', admin.redirect.RedirectCollection, r'/api/admin/redirects'),
123+
('/api/admin/redirects', admin.redirect.RedirectInstance, r'/api/admin/redirects/' + uuid_regexp),
124+
('/api/admin/auditlog', admin.auditlog.AuditLog),
125+
('/api/admin/auditlog/access', admin.auditlog.AccessLog),
126+
('/api/admin/auditlog/debug', admin.auditlog.DebugLog),
127+
('/api/admin/auditlog/jobs', admin.auditlog.JobsTiming),
128+
('/api/admin/auditlog/tips', admin.auditlog.TipsCollection),
129+
('/api/admin/l10n/', admin.l10n.AdminL10NHandler, r'/api/admin/l10n/(' + '|'.join(LANGUAGES_SUPPORTED_CODES) + ')'),
130+
('/api/admin/config', admin.operation.AdminOperationHandler),
131+
('/api/admin/config/csr/gen', admin.https.CSRHandler),
132+
('/api/admin/config/acme/run', admin.https.AcmeHandler),
133+
('/api/admin/config/tls', admin.https.ConfigHandler),
134+
('/api/admin/config/tls/files/', admin.https.FileHandler, r'/api/admin/config/tls/files/(cert|chain|key)'),
135+
('/api/admin/files', admin.file.FileCollection),
136+
('/api/admin/files', admin.file.FileInstance, r'/api/admin/files/(.+)'),
137+
('/api/admin/tenants', admin.tenant.TenantCollection),
138+
('/api/admin/tenants', admin.tenant.TenantInstance, r'/api/admin/tenants/' + '([0-9]{1,20})'),
139+
('/api/admin/statuses', admin.submission_statuses.SubmissionStatusCollection),
140+
('/api/admin/statuses', admin.submission_statuses.SubmissionStatusInstance, r'/api/admin/statuses/' + uuid_regexp_or_closed),
141+
('/api/admin/statuses', admin.submission_statuses.SubmissionSubStatusCollection, r'/api/admin/statuses/' + uuid_regexp_or_closed + r'/substatuses'),
142+
('/api/admin/statuses', admin.submission_statuses.SubmissionSubStatusInstance, r'/api/admin/statuses/' + uuid_regexp_or_closed + r'/substatuses/' + uuid_regexp),
143+
144+
# Signup
145+
('/api/signup', signup.Signup),
146+
('/api/signup', signup.SignupActivation, r'/api/signup/([a-zA-Z0-9_\-]{64})'),
151147

152148
# Well known path
153-
(r'/.well-known/acme-challenge/([a-zA-Z0-9_\-]{42,44})', admin.https.AcmeChallengeHandler),
154-
(r'/.well-known/security.txt', security.SecuritytxtHandler),
149+
('/.well-known/acme-challenge', admin.https.AcmeChallengeHandler, r'/\.well-known/acme-challenge/([a-zA-Z0-9_\-]{42,44})'),
150+
('/.well-known/security.txt', security.SecuritytxtHandler),
155151

156152
# Special Files Handlers
157-
(r'/robots.txt', robots.RobotstxtHandler),
158-
(r'/sitemap.xml', sitemap.SitemapHandler),
159-
(r'/s/(.+)', file.FileHandler),
160-
(r'/l10n/(' + '|'.join(LANGUAGES_SUPPORTED_CODES) + ')', l10n.L10NHandler),
153+
('/robots.txt', robots.RobotstxtHandler),
154+
('/sitemap.xml', sitemap.SitemapHandler),
155+
('/s/', file.FileHandler, r'/s/(.+)'),
156+
('/l10n/', l10n.L10NHandler, r'/l10n/(' + '|'.join(LANGUAGES_SUPPORTED_CODES) + ')'),
161157

162158
# Path alias
163-
(r'^(/admin|/login|/submission)$', redirect.SpecialRedirectHandler),
164-
165-
# This handler attempts to route all non routed get requests
166-
(r'/([a-zA-Z0-9_\-\/\.\@]*)', staticfile.StaticFileHandler)
159+
('/admin', redirect.SpecialRedirectHandler),
160+
('/login', redirect.SpecialRedirectHandler),
161+
('/submission', redirect.SpecialRedirectHandler),
167162
]
168163

164+
# Extend the tuples in the API spec that have 2 elements with None
165+
api_spec = [t if len(t) == 3 else (*t, re.escape(t[0])) for t in api_spec]
166+
167+
168+
default_api = staticfile.StaticFileHandler
169+
default_regexp = re.compile(r'/([a-zA-Z0-9_\-\/\.\@]*)')
170+
171+
172+
class TrieNode:
173+
def __init__(self):
174+
self.children = {}
175+
self.descriptors = [] # List of tuples (regexp, handler)
176+
177+
178+
class Trie:
179+
def __init__(self):
180+
self.root = TrieNode()
181+
182+
def insert(self, prefix, regexp, handler):
183+
"""
184+
Insert a route prefix into the Trie.
185+
"""
186+
node = self.root
187+
parts = prefix.strip('/').split('/')
188+
189+
for part in parts:
190+
if part not in node.children:
191+
node.children[part] = TrieNode()
192+
node = node.children[part]
193+
194+
# Attach the full regexp and handler at the leaf node
195+
node.descriptors.append((re.compile(regexp), handler))
196+
197+
def search(self, path):
198+
"""
199+
Search for a matching handler based on the path.
200+
"""
201+
match = None
202+
node = self.root
203+
parts = path.strip('/').split('/')
204+
205+
for part in parts:
206+
if part in node.children:
207+
node = node.children[part]
208+
else:
209+
break
210+
211+
for regexp, handler in node.descriptors:
212+
match = regexp.match(path)
213+
if match:
214+
return match, handler
215+
216+
return default_regexp.match(path), default_api
217+
169218

170219
class APIResourceWrapper(Resource):
171220
isLeaf = True
@@ -180,17 +229,15 @@ class APIResourceWrapper(Resource):
180229

181230
def __init__(self):
182231
Resource.__init__(self)
183-
self.registry = []
232+
self.registry = Trie()
184233
self.handler = None
185234

186-
for tup in api_spec:
187-
pattern, handler = tup
235+
for prefix, handler, regexp in api_spec:
236+
if not regexp.startswith("^"):
237+
regexp = "^" + regexp
188238

189-
if not pattern.startswith("^"):
190-
pattern = "^" + pattern
191-
192-
if not pattern.endswith("$"):
193-
pattern += "$"
239+
if not regexp.endswith("$"):
240+
regexp += "$"
194241

195242
if not hasattr(handler, '_decorated'):
196243
handler._decorated = True
@@ -199,21 +246,10 @@ def __init__(self):
199246
if hasattr(handler, m):
200247
decorators.decorate_method(handler, m)
201248

202-
self.registry.append((re.compile(pattern), handler))
249+
self.registry.insert(prefix, re.compile(regexp), handler)
203250

204251
def resolve_handler(self, path):
205-
match = None
206-
207-
for regexp, handler in self.registry:
208-
try:
209-
match = regexp.match(path)
210-
except UnicodeDecodeError:
211-
match = None
212-
if match:
213-
break
214-
215-
if match:
216-
return match, handler
252+
return self.registry.search(path)
217253

218254
def should_redirect_https(self, request):
219255
if request.isSecure() or \
@@ -326,7 +362,7 @@ def render(self, request):
326362
(State.tenants[1].cache.hostname == '' and isIPAddress(request.hostname)):
327363
request.tid = 1
328364
else:
329-
request.tid = State.tenant_hostname_id_map.get(request.hostname, None)
365+
request.tid = State.tenant_hostname_id_map.get(request.hostname)
330366

331367
if request.tid == 1:
332368
try:
@@ -392,7 +428,6 @@ def render(self, request):
392428
return b''
393429

394430
match, handler = self.resolve_handler(request_path)
395-
396431
if match is None:
397432
self.handle_exception(errors.ResourceNotFound, request)
398433
return b''

0 commit comments

Comments
 (0)