source: indico/indico/web/http_api/handlers.py @ 1ea4c5

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

[IMP] Use iterators for event lists

  • Property mode set to 100644
File size: 9.3 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, LimitExceededException
36from indico.web.http_api.auth import APIKeyHolder
37from indico.web.http_api.cache import RequestCache
38from indico.web.http_api.responses import HTTPAPIResult, HTTPAPIError
39from indico.web.http_api.util import remove_lists, get_query_parameter
40from indico.web.http_api import API_MODE_KEY, API_MODE_ONLYKEY, API_MODE_SIGNED, API_MODE_ONLYKEY_SIGNED, API_MODE_ALL_SIGNED
41from indico.web.wsgi import webinterface_handler_config as apache
42from indico.util.metadata.serializer import Serializer
43
44# indico legacy imports
45from MaKaC.common import DBMgr
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 for each detail level
51MAX_RECORDS = {
52    'events': 10000,
53    'contributions': 500,
54    'subcontributions': 500,
55    'sessions': 100,
56}
57
58# Valid URLs for export handlers. the last group has to be the response type
59EXPORT_URL_MAP = {
60    r'/export/(event|categ)/(\w+(?:-\w+)*)\.(\w+)$': 'handler_event_categ'
61}
62
63# Compile regexps
64EXPORT_URL_MAP = dict((re.compile(pathRe), handlerFunc) for pathRe, handlerFunc in EXPORT_URL_MAP.iteritems())
65RE_REMOVE_EXTENSION = re.compile(r'\.(\w+)$')
66
67
68def validateSignature(key, signature, path, query, timestamp=None):
69    if timestamp is None:
70        timestamp = int(time.time())
71    ts = timestamp / 300
72    candidates = []
73    for i in xrange(-1, 2):
74        h = hmac.new(key, '%s?%s&%d' % (path, query, ts + i), hashlib.sha1)
75        candidates.append(h.hexdigest())
76    if signature not in candidates:
77        raise HTTPAPIError('Signature invalid (check system clock)', apache.HTTP_FORBIDDEN)
78
79
80def getAK(apiKey, signature, path, query):
81    minfo = HelperMaKaCInfo.getMaKaCInfoInstance()
82    apiMode = minfo.getAPIMode()
83    if not apiKey:
84        if apiMode in (API_MODE_ONLYKEY, API_MODE_ONLYKEY_SIGNED, API_MODE_ALL_SIGNED):
85            raise HTTPAPIError('API key is missing', apache.HTTP_FORBIDDEN)
86        return None, True
87    akh = APIKeyHolder()
88    if not akh.hasKey(apiKey):
89        raise HTTPAPIError('Invalid API key', apache.HTTP_FORBIDDEN)
90    ak = akh.getById(apiKey)
91    if ak.isBlocked():
92        raise HTTPAPIError('API key is blocked', apache.HTTP_FORBIDDEN)
93    # Signature validation
94    onlyPublic = False
95    if signature:
96        validateSignature(ak.getSignKey(), signature, path, query)
97    elif apiMode in (API_MODE_SIGNED, API_MODE_ALL_SIGNED):
98        raise HTTPAPIError('Signature missing', apache.HTTP_FORBIDDEN)
99    elif apiMode == API_MODE_ONLYKEY_SIGNED:
100        onlyPublic = True
101    return ak, onlyPublic
102
103
104def buildAW(ak, req, onlyPublic=False):
105    aw = AccessWrapper()
106    if ak and not onlyPublic:
107        # If we have an authenticated request, require HTTPS
108        minfo = HelperMaKaCInfo.getMaKaCInfoInstance()
109        if not req.is_https() and minfo.isAPIHTTPSRequired():
110            raise HTTPAPIError('HTTPS is required', apache.HTTP_FORBIDDEN)
111        aw.setUser(ak.getUser())
112    return aw
113
114
115def getExportHandler(path):
116    """Get the export handler, handler args and return type from a path"""
117    func = None
118    match = None
119    for pathRe, handlerFunc in EXPORT_URL_MAP.iteritems():
120        match = pathRe.match(path)
121        if match:
122            func = handlerFunc
123            break
124
125    groups = match and match.groups()
126    if not match or groups[-1] not in ExportInterface.getAllowedFormats():
127        return None, None, None
128    return globals()[func], groups[:-1], groups[-1]
129
130
131def handler_event_categ(dbi, aw, qdata, dtype, idlist):
132    idlist = idlist.split('-')
133
134    expInt = ExportInterface(dbi, aw)
135    tzName = get_query_parameter(qdata, ['tz'], None)
136    detail = get_query_parameter(qdata, ['d', 'detail'], 'events')
137    userLimit = get_query_parameter(qdata, ['n', 'limit'], 0, integer=True)
138    offset = get_query_parameter(qdata, ['O', 'offset'], 0, integer=True)
139    orderBy = get_query_parameter(qdata, ['o', 'order'], 'start')
140    descending = get_query_parameter(qdata, ['c', 'descending'], False)
141
142    if tzName is None:
143        info = HelperMaKaCInfo.getMaKaCInfoInstance()
144        tzName = info.getTimezone()
145
146    tz = pytz.timezone(tzName)
147
148    max = MAX_RECORDS.get(detail, 10000)
149    if userLimit > max:
150        raise HTTPAPIError("You can only request up to %d records per request with the detail level '%s" %
151            (max, detail), apache.HTTP_BAD_REQUEST)
152
153    # impose a hard limit
154    limit = userLimit if userLimit > 0 else max
155
156    if dtype == 'categ':
157        iterator = expInt.category(idlist, tz, offset, limit, detail, orderBy, descending, qdata)
158    elif dtype == 'event':
159        iterator = expInt.event(idlist, tz, offset, limit, detail, orderBy, descending, qdata)
160
161    resultList = []
162    complete = True
163
164    try:
165        for obj in iterator:
166            resultList.append(obj)
167    except LimitExceededException:
168        complete = (limit == userLimit)
169
170    return resultList, complete
171
172def handler(req, **params):
173    path, query = req.URLFields['PATH_INFO'], req.URLFields['QUERY_STRING']
174    # Extract HMAC signature
175    signature = None
176    m = re.search(r'&([0-9a-fA-F]{40})$', query)
177    if m:
178        signature = m.group(1).lower()
179        query = query[:-41]
180
181    # Parse the actual query string
182    qdata = parse_qs(query)
183    no_cache = get_query_parameter(qdata, ['nc', 'nocache'], 'no') == 'yes'
184
185    dbi = DBMgr.getInstance()
186    dbi.startRequest()
187
188    # Copy qdata for the cache key
189    qdata_copy = dict(qdata)
190    cache = RequestCache(HelperMaKaCInfo.getMaKaCInfoInstance().getAPICacheTTL())
191
192    pretty = get_query_parameter(qdata, ['p', 'pretty'], 'no') == 'yes'
193    apiKey = get_query_parameter(qdata, ['ak', 'apikey'], None)
194    onlyPublic = get_query_parameter(qdata, ['op', 'onlypublic'], 'no') == 'yes'
195
196    # Get our handler function and its argument and response type
197    func, args, dformat = getExportHandler(path)
198    if func is None or dformat is None:
199        raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
200
201    ak = error = result = None
202    ts = int(time.time())
203    try:
204        # Validate the API key (and its signature)
205        ak, enforceOnlyPublic = getAK(apiKey, signature, path, query)
206        if enforceOnlyPublic:
207            onlyPublic = True
208        # Create an access wrapper for the API key's user
209        aw = buildAW(ak, req, onlyPublic)
210        # Get rid of API key in cache key if we did not impersonate a user
211        if ak and aw.getUser() is None:
212            qdata_copy.pop('ak', None)
213            qdata_copy.pop('apikey', None)
214
215        obj = None
216        add_to_cache = True
217        cache_path = RE_REMOVE_EXTENSION.sub('', path)
218        if not no_cache:
219            obj = cache.loadObject(cache_path, qdata_copy)
220            if obj is not None:
221                result, complete = obj.getContent()
222                ts = obj.getTS()
223                add_to_cache = False
224        if result is None:
225            # Perform the actual exporting
226            result, complete = func(dbi, aw, qdata, *args)
227        if result is not None and add_to_cache:
228            cache.cacheObject(cache_path, qdata_copy, (result, complete))
229    except HTTPAPIError, e:
230        error = e
231        if e.getCode():
232            req.status = e.getCode()
233
234    if result is None and error is None:
235        # TODO: usage page
236        raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
237    else:
238        if ak and error is None:
239            # Commit only if there was an API key and no error
240            for _retry in xrange(10):
241                dbi.sync()
242                ak.used(req.remote_ip, path, query, not onlyPublic)
243                try:
244                    dbi.endRequest(True)
245                except ConflictError:
246                    pass # retry
247                else:
248                    break
249        else:
250            # No need to commit stuff if we didn't use an API key
251            # (nothing was written)
252            dbi.endRequest(False)
253
254        serializer = Serializer.create(dformat, pretty=pretty,
255                                       **remove_lists(qdata))
256
257        if error:
258            resultFossil = fossilize(error)
259        else:
260            resultFossil = fossilize(HTTPAPIResult(result, path, query, ts, complete))
261        del resultFossil['_fossil']
262
263        req.headers_out['Content-Type'] = serializer.getMIMEType()
264        return serializer(resultFossil)
Note: See TracBrowser for help on using the repository browser.