1 # Copyright (C) 2010-2016 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Basic WSGI Application object for REST server."""
23 from base64
import b64decode
24 from falcon
import API
, HTTPUnauthorized
25 from falcon
.routing
import create_http_method_map
26 from mailman
import public
27 from mailman
.config
import config
28 from mailman
.database
.transaction
import transactional
29 from mailman
.rest
.root
import Root
30 from wsgiref
.simple_server
import (
31 WSGIRequestHandler
, WSGIServer
, make_server
as wsgi_server
)
34 log
= logging
.getLogger('mailman.http')
39 REALM
= 'mailman3-rest'
42 class AdminWSGIServer(WSGIServer
):
43 """Server class that integrates error handling with our log files."""
45 def handle_error(self
, request
, client_address
):
46 # Interpose base class method so that the exception gets printed to
47 # our log file rather than stderr.
48 log
.exception('REST server exception during request from %s',
56 def write(self
, message
):
57 self
._buffer
.append(message
)
60 self
._buffer
.insert(0, 'REST request handler error:\n')
61 log
.error(EMPTYSTRING
.join(self
._buffer
))
65 class AdminWebServiceWSGIRequestHandler(WSGIRequestHandler
):
66 """Handler class which just logs output to the right place."""
68 def log_message(self
, format
, *args
):
69 """See `BaseHTTPRequestHandler`."""
70 log
.info('%s - - %s', self
.address_string(), format
% args
)
73 # Return a fake stderr object that will actually write its output to
79 """Falcon middleware object for Mailman's REST API.
81 This does two things. It sets the API version on the resource
82 object, and it verifies that the proper authentication has been
85 def process_resource(self
, request
, response
, resource
, params
):
86 # Check the authorization credentials.
88 if request
.auth
is not None and request
.auth
.startswith('Basic '):
89 # b64decode() returns bytes, but we require a str.
90 credentials
= b64decode(request
.auth
[6:]).decode('utf-8')
91 username
, password
= credentials
.split(':', 1)
92 if (username
== config
.webservice
.admin_user
and
93 password
== config
.webservice
.admin_pass
):
97 raise HTTPUnauthorized(
99 'REST API authorization failed',
100 challenges
=['Basic realm=Mailman3'])
104 def __init__(self
, root
):
107 def add_route(self
, uri_template
, method_map
, resource
):
108 # We don't need this method for object-based routing.
109 raise NotImplementedError
112 segments
= uri
.split(SLASH
)
113 # Since the path is always rooted at /, skip the first segment, which
114 # will always be the empty string.
116 this_segment
= segments
.pop(0)
117 resource
= self
._root
120 # Plumb the API through to all child resources.
121 api
= getattr(resource
, 'api', None)
122 # See if any of the resource's child links match the next segment.
123 for name
in dir(resource
):
124 if name
.startswith('__') and name
.endswith('__'):
126 attribute
= getattr(resource
, name
, MISSING
)
127 assert attribute
is not MISSING
, name
128 matcher
= getattr(attribute
, '__matcher__', MISSING
)
129 if matcher
is MISSING
:
132 if isinstance(matcher
, str):
133 # Is the matcher string a regular expression or plain
134 # string? If it starts with a caret, it's a regexp.
135 if matcher
.startswith('^'):
136 cre
= re
.compile(matcher
)
137 # Search against the entire remaining path.
138 tmp_segments
= segments
[:]
139 tmp_segments
.insert(0, this_segment
)
140 remaining_path
= SLASH
.join(tmp_segments
)
141 mo
= cre
.match(remaining_path
)
144 context
, segments
, **mo
.groupdict())
145 elif matcher
== this_segment
:
146 result
= attribute(context
, segments
)
148 # The matcher is a callable. It returns None if it
149 # doesn't match, and if it does, it returns a 3-tuple
150 # containing the positional arguments, the keyword
151 # arguments, and the remaining segments. The attribute is
152 # then called with these arguments. Note that the matcher
153 # wants to see the full remaining path components, which
154 # includes the current hop.
155 tmp_segments
= segments
[:]
156 tmp_segments
.insert(0, this_segment
)
157 matcher_result
= matcher(tmp_segments
)
158 if matcher_result
is not None:
159 positional
, keyword
, segments
= matcher_result
161 context
, segments
, *positional
, **keyword
)
162 # The attribute could return a 2-tuple giving the resource and
163 # remaining path segments, or it could just return the result.
164 # Of course, if the result is None, then the matcher did not
168 elif isinstance(result
, tuple):
169 resource
, segments
= result
172 # See if the context set an API and set it on the next
173 # resource in the chain, falling back to the parent resource's
174 # API if there is one.
175 resource
.api
= context
.pop('api', api
)
176 # The method could have truncated the remaining segments,
177 # meaning, it's consumed all the path segments, or this is the
178 # last path segment. In that case the resource we're left at
180 if len(segments
) == 0:
181 # We're at the end of the path, so the root must be the
183 method_map
= create_http_method_map(resource
)
184 return resource
, method_map
, context
185 this_segment
= segments
.pop(0)
188 # None of the attributes matched this path component, so the
190 return None, None, None
193 class RootedAPI(API
):
194 def __init__(self
, root
, *args
, **kws
):
197 middleware
=Middleware(),
198 router
=ObjectRouter(root
),
200 # Let Falcon parse the form data into the request object's
202 self
.req_options
.auto_parse_form_urlencoded
= True
203 # Don't ignore empty query parameters.
204 self
.req_options
.keep_blank_qs_values
= True
206 # Override the base class implementation to wrap a transactional
207 # handler around the call, so that the current transaction is
208 # committed if no errors occur, and aborted otherwise.
210 def __call__(self
, environ
, start_response
):
211 return super().__call
__(environ
, start_response
)
215 def make_application():
216 """Create the WSGI application.
218 Use this if you want to integrate Mailman's REST server with your own WSGI
221 return RootedAPI(Root())
226 """Create the Mailman REST server.
228 Use this if you just want to run Mailman's wsgiref-based REST server.
230 host
= config
.webservice
.hostname
231 port
= int(config
.webservice
.port
)
232 server
= wsgi_server(
233 host
, port
, make_application(),
234 server_class
=AdminWSGIServer
,
235 handler_class
=AdminWebServiceWSGIRequestHandler
)