source: indico/indico/web/http_api/handlers.py @ 9c31e72

hello-world-walkthroughipv6v0.98-seriesv0.98.2v0.98.3v0.98b2v0.99v1.0v1.1
Last change on this file since 9c31e72 was 9c31e72, checked in by Jose Benito <jose.benito.gonzalez@…>, 21 months ago

[IMP] Require HTTPS for authenticated requests

  • Property mode set to 100644
File size: 8.0 KB
Line 
1# -*- coding: utf-8 -*-
2##
3##
4## This file is part of CDS Indico.
5## Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 CERN.
6##
7## CDS Indico is free software; you can redistribute it and/or
8## modify it under the terms of the GNU General Public License as
9## published by the Free Software Foundation; either version 2 of the
10## License, or (at your option) any later version.
11##
12## CDS Indico is distributed in the hope that it will be useful, but
13## WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15## General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with CDS Indico; if not, write to the Free Software Foundation, Inc.,
19## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20
21"""
22HTTP API - Handlers
23"""
24
25# python stdlib imports
26import hashlib
27import hmac
28import re
29import time
30from urlparse import parse_qs
31from ZODB.POSException import ConflictError
32import pytz
33
34# indico imports
35from indico.web.http_api import ExportInterface
36from indico.web.http_api.auth import APIKeyHolder
37from indico.web.http_api.cache import RequestCache
38from indico.web.http_api.fossils import IHTTPAPIResultFossil, IHTTPAPIErrorFossil
39from indico.web.http_api.util import remove_lists, get_query_parameter
40from indico.web.wsgi import webinterface_handler_config as apache
41from indico.util.metadata.serializer import Serializer
42
43# indico legacy imports
44from MaKaC.common import DBMgr
45from MaKaC.common.Configuration import Config
46from MaKaC.common.fossilize import fossilizes, fossilize, Fossilizable
47from MaKaC.accessControl import AccessWrapper
48from MaKaC.common.info import HelperMaKaCInfo
49
50# Maximum number of records that will get exported
51MAX_RECORDS = 20000
52
53# Valid URLs for export handlers. the last group has to be the response type
54EXPORT_URL_MAP = {
55    r'/export/(event|categ)/(\w+(?:-\w+)*)\.(\w+)$': 'handler_event_categ'
56}
57
58# Compile regexps
59EXPORT_URL_MAP = dict((re.compile(pathRe), handlerFunc) for pathRe, handlerFunc in EXPORT_URL_MAP.iteritems())
60
61class HTTPAPIError(Exception, Fossilizable):
62    fossilizes(IHTTPAPIErrorFossil)
63
64    def getMessage(self):
65        return self.message
66
67
68class HTTPAPIResult(Fossilizable):
69    fossilizes(IHTTPAPIResultFossil)
70
71    def __init__(self, results, path='', query='', ts=None):
72        if ts is None:
73            ts = int(time.time())
74        self._results = results
75        self._path = path
76        self._query = query
77        self._ts = ts
78
79    def getTS(self):
80        return self._ts
81
82    def getURL(self):
83        prefix = Config.getInstance().getBaseSecureURL()
84        if self._query:
85            return prefix + self._path + '?' + self._query
86        return prefix + self._path
87
88    def getResults(self):
89        return self._results
90
91def validateSignature(req, key, signature, path, query, timestamp=None):
92    if timestamp is None:
93        timestamp = int(time.time())
94    if not signature:
95        req.status = apache.HTTP_FORBIDDEN
96        raise HTTPAPIError('Signature missing')
97    ts = timestamp / 300
98    candidates = []
99    for i in xrange(-1, 2):
100        h = hmac.new(key, '%s?%s&%d' % (path, query, ts + i), hashlib.sha1)
101        candidates.append(h.hexdigest())
102    if signature not in candidates:
103        req.status = apache.HTTP_FORBIDDEN
104        raise HTTPAPIError('Signature invalid (check system clock)')
105
106def getAK(apiKey, signature, path, query, req):
107    if not apiKey:
108        return None
109    if not req.is_https():
110        req.status = apache.HTTP_FORBIDDEN
111        raise HTTPAPIError('HTTPS is required')
112    akh = APIKeyHolder()
113    if not akh.hasKey(apiKey):
114        req.status = apache.HTTP_FORBIDDEN
115        raise HTTPAPIError('Invalid API key')
116    ak = akh.getById(apiKey)
117    if ak.isBlocked():
118        req.status = apache.HTTP_FORBIDDEN
119        raise HTTPAPIError('API key is blocked')
120    validateSignature(req, ak.getSignKey(), signature, path, query)
121    return ak
122
123def buildAW(ak):
124    aw = AccessWrapper()
125    if ak:
126        aw.setUser(ak.getUser())
127    return aw
128
129def getExportHandler(path):
130    """Get the export handler, handler args and return type from a path"""
131    func = None
132    match = None
133    for pathRe, handlerFunc in EXPORT_URL_MAP.iteritems():
134        match = pathRe.match(path)
135        if match:
136            func = handlerFunc
137            break
138
139    groups = match and match.groups()
140    if not match or groups[-1] not in ExportInterface.getAllowedFormats():
141        return None, None, None
142    return globals()[func], groups[:-1], groups[-1]
143
144def handler_event_categ(dbi, aw, qdata, dtype, idlist):
145    idlist = idlist.split('-')
146
147    expInt = ExportInterface(dbi, aw)
148    tzName = get_query_parameter(qdata, ['tz'], None)
149    detail = get_query_parameter(qdata, ['d', 'detail'], 'events')
150    limit = get_query_parameter(qdata, ['n', 'limit'], 0, integer=True)
151
152    if tzName is None:
153        info = HelperMaKaCInfo.getMaKaCInfoInstance()
154        tzName = info.getTimezone()
155
156    tz = pytz.timezone(tzName)
157
158    # impose a hard limit
159    limit = limit if limit > 0 else MAX_RECORDS
160
161    if dtype == 'categ':
162        return expInt.category(idlist, tz, limit, detail, qdata)
163    elif dtype == 'event':
164        return expInt.event(idlist, tz, limit, detail, qdata)
165
166def handler(req, **params):
167    path, query = req.URLFields['PATH_INFO'], req.URLFields['QUERY_STRING']
168    # Extract HMAC signature
169    signature = None
170    m = re.search(r'&([0-9a-fA-F]{40})$', query)
171    if m:
172        signature = m.group(1).lower()
173        query = query[:-41]
174
175    # Parse the actual query string
176    qdata = parse_qs(query)
177    no_cache = get_query_parameter(qdata, ['nc', 'nocache'], 'no') == 'yes'
178
179    # Copy qdata for the cache key
180    qdata_copy = dict(qdata)
181    cache = RequestCache()
182    obj = None
183    if not no_cache:
184        obj = cache.loadObject(path, qdata_copy)
185
186    add_to_cache = True
187
188    dbi = DBMgr.getInstance()
189    dbi.startRequest()
190
191    pretty = get_query_parameter(qdata, ['p', 'pretty'], 'no') == 'yes'
192    apiKey = get_query_parameter(qdata, ['ak', 'apikey'], None)
193
194    # Get our handler function and its argument and response type
195    func, args, dformat = getExportHandler(path)
196    if func is None or dformat is None:
197        raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
198
199    ak = error = results = resp = None
200    try:
201        # Validate the API key (and its signature)
202        ak = getAK(apiKey, signature, path, query, req)
203        if obj is not None:
204            resp = obj.getContent()
205            add_to_cache = False
206        else:
207            # Create an access wrapper for the API key's user
208            aw = buildAW(ak)
209            # Perform the actual exporting
210            results = func(dbi, aw, qdata, *args)
211    except HTTPAPIError, e:
212        error = e
213        add_to_cache = False
214
215    if results is None and error is None and resp is None:
216        # TODO: usage page
217        raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
218    elif resp is None:
219        serializer = Serializer.create(dformat, pretty=pretty,
220                                       **remove_lists(qdata))
221
222        if error:
223            resultFossil = fossilize(error)
224        else:
225            resultFossil = fossilize(HTTPAPIResult(results, path, query))
226        del resultFossil['_fossil']
227        result = serializer(resultFossil)
228
229        if ak and error is None:
230            # Commit only if there was an API key and no error
231            for _retry in xrange(10):
232                dbi.sync()
233                ak.used(req.remote_ip, path, query)
234                try:
235                    dbi.endRequest(True)
236                except ConflictError:
237                    pass # retry
238                else:
239                    break
240        else:
241            # No need to commit stuff if we didn't use an API key
242            # (nothing was written)
243            dbi.endRequest(False)
244        resp = serializer.getMIMEType(), result
245
246    if add_to_cache:
247        cache.cacheObject(path, qdata_copy, resp)
248    req.headers_out['Content-Type'] = resp[0]
249    return resp[1]
Note: See TracBrowser for help on using the repository browser.