source: indico/indico/web/http_api/handlers.py @ c004bc

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

[IMP] Support persistent api requests (w/o tstamp)

  • Property mode set to 100644
File size: 9.8 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
30import urllib
31from urlparse import parse_qs
32from ZODB.POSException import ConflictError
33
34# indico imports
35from indico.web.http_api import HTTPAPIHook
36from indico.web.http_api.auth import APIKeyHolder
37from indico.web.http_api.cache import RequestCache
38from indico.web.http_api.fossils import IHTTPAPIExportResultFossil
39from indico.web.http_api.responses import HTTPAPIResult, HTTPAPIError
40from indico.web.http_api.util import remove_lists, get_query_parameter
41from indico.web.http_api import API_MODE_ONLYKEY, API_MODE_SIGNED, API_MODE_ONLYKEY_SIGNED, API_MODE_ALL_SIGNED
42from indico.web.wsgi import webinterface_handler_config as apache
43from indico.util.metadata.serializer import Serializer
44from indico.util.network import _get_remote_ip
45
46# indico legacy imports
47from MaKaC.common import DBMgr
48from MaKaC.common.logger import Logger
49from MaKaC.common.fossilize import fossilize
50from MaKaC.accessControl import AccessWrapper
51from MaKaC.common.info import HelperMaKaCInfo
52
53
54# Remove the extension at the end or before the querystring
55RE_REMOVE_EXTENSION = re.compile(r'\.(\w+)(?:$|(?=\?))')
56
57
58def normalizeQuery(path, query, remove=('signature',), separate=False):
59    """Normalize request path and query so it can be used for caching and signing
60
61    Returns a string consisting of path and sorted query string.
62    Dynamic arguments like signature and timestamp are removed from the query string.
63    """
64    queryParams = remove_lists(parse_qs(query))
65    if remove:
66        for key in remove:
67            queryParams.pop(key, None)
68    sortedQuery = sorted(queryParams.items(), key=lambda x: x[0].lower())
69    if separate:
70        return path, sortedQuery and urllib.urlencode(sortedQuery)
71    elif sortedQuery:
72        return '%s?%s' % (path, urllib.urlencode(sortedQuery))
73    else:
74        return path
75
76
77def validateSignature(ak, signature, timestamp, path, query):
78    ttl = HelperMaKaCInfo.getMaKaCInfoInstance().getAPISignatureTTL()
79    if not timestamp and not ak.isPersistentAllowed():
80        raise HTTPAPIError('Signature invalid (no timestamp)', apache.HTTP_FORBIDDEN)
81    elif timestamp and abs(timestamp - int(time.time())) > ttl:
82        raise HTTPAPIError('Signature invalid (bad timestamp)', apache.HTTP_FORBIDDEN)
83    digest = hmac.new(ak.getSignKey(), normalizeQuery(path, query), hashlib.sha1).hexdigest()
84    if signature != digest:
85        raise HTTPAPIError('Signature invalid', apache.HTTP_FORBIDDEN)
86
87
88def checkAK(apiKey, signature, timestamp, path, query):
89    minfo = HelperMaKaCInfo.getMaKaCInfoInstance()
90    apiMode = minfo.getAPIMode()
91    if not apiKey:
92        if apiMode in (API_MODE_ONLYKEY, API_MODE_ONLYKEY_SIGNED, API_MODE_ALL_SIGNED):
93            raise HTTPAPIError('API key is missing', apache.HTTP_FORBIDDEN)
94        return None, True
95    akh = APIKeyHolder()
96    if not akh.hasKey(apiKey):
97        raise HTTPAPIError('Invalid API key', apache.HTTP_FORBIDDEN)
98    ak = akh.getById(apiKey)
99    if ak.isBlocked():
100        raise HTTPAPIError('API key is blocked', apache.HTTP_FORBIDDEN)
101    # Signature validation
102    onlyPublic = False
103    if signature:
104        validateSignature(ak, signature, timestamp, path, query)
105    elif apiMode in (API_MODE_SIGNED, API_MODE_ALL_SIGNED):
106        raise HTTPAPIError('Signature missing', apache.HTTP_FORBIDDEN)
107    elif apiMode == API_MODE_ONLYKEY_SIGNED:
108        onlyPublic = True
109    return ak, onlyPublic
110
111
112def buildAW(ak, req, onlyPublic=False):
113    aw = AccessWrapper()
114    if ak and not onlyPublic:
115        # If we have an authenticated request, require HTTPS
116        minfo = HelperMaKaCInfo.getMaKaCInfoInstance()
117        if not req.is_https() and minfo.isAPIHTTPSRequired():
118            raise HTTPAPIError('HTTPS is required', apache.HTTP_FORBIDDEN)
119        aw.setUser(ak.getUser())
120    return aw
121
122def handler(req, **params):
123    logger = Logger.get('httpapi')
124    path, query = req.URLFields['PATH_INFO'], req.URLFields['QUERY_STRING']
125    if req.method == 'POST':
126        # Convert POST data to a query string
127        queryParams = dict(req.form)
128        for key, value in queryParams.iteritems():
129            queryParams[key] = [str(value)]
130        query = urllib.urlencode(remove_lists(queryParams))
131    else:
132        # Parse the actual query string
133        queryParams = parse_qs(query)
134
135    dbi = DBMgr.getInstance()
136    dbi.startRequest()
137
138    mode = path.split('/')[1]
139
140    cache = RequestCache(HelperMaKaCInfo.getMaKaCInfoInstance().getAPICacheTTL())
141
142    apiKey = get_query_parameter(queryParams, ['ak', 'apikey'], None)
143    signature = get_query_parameter(queryParams, ['signature'])
144    timestamp = get_query_parameter(queryParams, ['timestamp'], 0, integer=True)
145    no_cache = get_query_parameter(queryParams, ['nc', 'nocache'], 'no') == 'yes'
146    pretty = get_query_parameter(queryParams, ['p', 'pretty'], 'no') == 'yes'
147    onlyPublic = get_query_parameter(queryParams, ['op', 'onlypublic'], 'no') == 'yes'
148
149    # Disable caching if we are not exporting
150    if mode != 'export':
151        no_cache = True
152
153    # Get our handler function and its argument and response type
154    func, dformat = HTTPAPIHook.parseRequest(path, queryParams)
155    if func is None or dformat is None:
156        raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
157
158    ak = error = result = None
159    ts = int(time.time())
160    typeMap = {}
161    try:
162        # Validate the API key (and its signature)
163        ak, enforceOnlyPublic = checkAK(apiKey, signature, timestamp, path, query)
164        if enforceOnlyPublic:
165            onlyPublic = True
166        # Create an access wrapper for the API key's user
167        aw = buildAW(ak, req, onlyPublic)
168        # Get rid of API key in cache key if we did not impersonate a user
169        if ak and aw.getUser() is None:
170            cache_key = normalizeQuery(path, query, remove=('ak', 'apiKey', 'signature', 'timestamp', 'nc', 'nocache'))
171        else:
172            cache_key = normalizeQuery(path, query, remove=('signature', 'timestamp', 'nc', 'nocache'))
173
174        obj = None
175        addToCache = True
176        cache_key = RE_REMOVE_EXTENSION.sub('', cache_key)
177        if not no_cache:
178            obj = cache.loadObject(cache_key)
179            if obj is not None:
180                result, extra, complete, typeMap = obj.getContent()
181                ts = obj.getTS()
182                addToCache = False
183        if result is None:
184            # Perform the actual exporting
185            res = func(aw, req)
186            if isinstance(res, tuple) and len(res) == 4:
187                result, extra, complete, typeMap = res
188            else:
189                result, extra, complete, typeMap = res, {}, True, {}
190        if result is not None and addToCache:
191            cache.cacheObject(cache_key, (result, extra, complete, typeMap))
192    except HTTPAPIError, e:
193        error = e
194        if e.getCode():
195            req.status = e.getCode()
196            if req.status == apache.HTTP_METHOD_NOT_ALLOWED:
197                req.headers_out['Allow'] = 'GET' if req.method == 'POST' else 'POST'
198
199    if result is None and error is None:
200        # TODO: usage page
201        raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
202    else:
203        if ak and error is None:
204            # Commit only if there was an API key and no error
205            for _retry in xrange(10):
206                dbi.sync()
207                normPath, normQuery = normalizeQuery(path, query, remove=('signature', 'timestamp'), separate=True)
208                ak.used(_get_remote_ip(req), normPath, normQuery, not onlyPublic)
209                try:
210                    dbi.endRequest(True)
211                except ConflictError:
212                    pass # retry
213                else:
214                    break
215        else:
216            # No need to commit stuff if we didn't use an API key
217            # (nothing was written)
218            dbi.endRequest(False)
219
220        # Log successful POST api requests
221        if error is None and req.method == 'POST':
222            logger.info('API request: %s?%s' % (path, query))
223
224        serializer = Serializer.create(dformat, pretty=pretty, typeMap=typeMap,
225                                       **remove_lists(queryParams))
226
227        if error:
228            resultFossil = fossilize(error)
229        else:
230            iface = None
231            if mode == 'export':
232                iface = IHTTPAPIExportResultFossil
233            resultFossil = fossilize(HTTPAPIResult(result, path, query, ts, complete, extra), iface)
234
235        del resultFossil['_fossil']
236
237        try:
238
239            if error and not serializer.schemaless:
240                # if our serializer has a specific schema (HTML, ICAL, etc...)
241                # use JSON, since it is universal
242                serializer = Serializer.create('json')
243                # set text/plain, so that it is visible in all browsers
244                req.headers_out['Content-Type'] = 'text/plain'
245            else:
246                req.headers_out['Content-Type'] = serializer.getMIMEType()
247
248            return serializer(resultFossil)
249        except:
250            logger.exception('Serialization error in request %s?%s' % (path, query))
251            raise
Note: See TracBrowser for help on using the repository browser.