1# -*- coding: iso-8859-1 -*-
2"""
3    MoinMoin - Utility functions for the web-layer
4
5    @copyright: 2003-2008 MoinMoin:ThomasWaldmann,
6                2008-2008 MoinMoin:FlorianKrupicka
7    @license: GNU GPL, see COPYING for details.
8"""
9import time
10
11from werkzeug.utils import redirect
12from werkzeug.exceptions import abort
13from werkzeug.http import cookie_date
14from werkzeug.wrappers import Response
15
16from MoinMoin import caching
17from MoinMoin import log
18from MoinMoin import wikiutil
19from MoinMoin.Page import Page
20from MoinMoin.web.exceptions import Forbidden, SurgeProtection
21
22logging = log.getLogger(__name__)
23
24def check_forbidden(request):
25    """ Simple action and host access checks
26
27    Spider agents are checked against the called actions,
28    hosts against the blacklist. Raises Forbidden if triggered.
29    """
30    args = request.args
31    action = args.get('action')
32    if ((args or request.method != 'GET') and
33        action not in ['rss_rc', 'show', 'sitemap'] and
34        not (action == 'AttachFile' and args.get('do') == 'get')):
35        if request.isSpiderAgent:
36            raise Forbidden()
37    if request.cfg.hosts_deny:
38        remote_addr = request.remote_addr
39        for host in request.cfg.hosts_deny:
40            if host[-1] == '.' and remote_addr.startswith(host):
41                logging.debug("hosts_deny (net): %s" % remote_addr)
42                raise Forbidden()
43            if remote_addr == host:
44                logging.debug("hosts_deny (ip): %s" % remote_addr)
45                raise Forbidden()
46    return False
47
48def check_surge_protect(request, kick=False, action=None, username=None):
49    """ Check for excessive requests
50
51    Raises a SurgeProtection exception on wiki overuse.
52
53    @param request: a moin request object
54    @param kick: immediately ban this user
55    @param action: specify the action explicitly (default: request.action)
56    @param username: give username (for action == 'auth-name')
57    """
58    limits = request.cfg.surge_action_limits
59    if not limits:
60        return False
61
62    remote_addr = request.remote_addr or ''
63    if remote_addr.startswith('127.'):
64        return False
65
66    validuser = request.user.valid
67    current_action = action or request.action
68    if current_action == 'auth-ip':
69        # for checking if some specific ip tries to authenticate too often,
70        # not considering the username it tries to authenticate as (could
71        # be many different names)
72        if current_action not in limits:
73            # if admin did not add this key to the limits configuration, do nothing
74            return False
75        current_id = remote_addr
76    elif current_action == 'auth-name':
77        # for checking if some username tries to authenticate too often,
78        # not considering the ip the request comes from (could be a distributed
79        # attack on a high-privilege user)
80        if current_action not in limits:
81            # if admin did not add this key to the limits configuration, do nothing
82            return False
83        current_id = username
84    else:
85        # general case
86        current_id = validuser and request.user.name or remote_addr
87
88    default_limit = limits.get('default', (30, 60))
89
90    now = int(time.time())
91    surgedict = {}
92    surge_detected = False
93
94    try:
95        # if we have common farm users, we could also use scope='farm':
96        cache = caching.CacheEntry(request, 'surgeprotect', 'surge-log', scope='wiki', use_encode=True)
97        if cache.exists():
98            data = cache.content()
99            data = data.split("\n")
100            for line in data:
101                try:
102                    id, t, action, surge_indicator = line.split("\t")
103                    t = int(t)
104                    maxnum, dt = limits.get(action, default_limit)
105                    if t >= now - dt:
106                        events = surgedict.setdefault(id, {})
107                        timestamps = events.setdefault(action, [])
108                        timestamps.append((t, surge_indicator))
109                except StandardError:
110                    pass
111
112        maxnum, dt = limits.get(current_action, default_limit)
113        events = surgedict.setdefault(current_id, {})
114        timestamps = events.setdefault(current_action, [])
115        surge_detected = len(timestamps) > maxnum
116
117        surge_indicator = surge_detected and "!" or ""
118        timestamps.append((now, surge_indicator))
119        if surge_detected:
120            if len(timestamps) < maxnum * 2:
121                timestamps.append((now + request.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
122
123        if current_action not in ('cache', 'AttachFile', ): # don't add cache/AttachFile accesses to all or picture galleries will trigger SP
124            action = 'all' # put a total limit on user's requests
125            maxnum, dt = limits.get(action, default_limit)
126            events = surgedict.setdefault(current_id, {})
127            timestamps = events.setdefault(action, [])
128
129            if kick: # ban this guy, NOW
130                timestamps.extend([(now + request.cfg.surge_lockout_time, "!")] * (2 * maxnum))
131
132            surge_detected = surge_detected or len(timestamps) > maxnum
133
134            surge_indicator = surge_detected and "!" or ""
135            timestamps.append((now, surge_indicator))
136            if surge_detected:
137                if len(timestamps) < maxnum * 2:
138                    timestamps.append((now + request.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
139
140        data = []
141        for id, events in surgedict.items():
142            for action, timestamps in events.items():
143                for t, surge_indicator in timestamps:
144                    data.append("%s\t%d\t%s\t%s" % (id, t, action, surge_indicator))
145        data = "\n".join(data)
146        cache.update(data)
147    except StandardError:
148        pass
149
150    if surge_detected and validuser and request.user.auth_method in request.cfg.auth_methods_trusted:
151        logging.info("Trusted user %s would have triggered surge protection if not trusted.", request.user.name)
152        return False
153    elif surge_detected:
154        logging.warning("Surge Protection: action=%s id=%s (ip: %s)", current_action, current_id, remote_addr)
155        raise SurgeProtection(retry_after=request.cfg.surge_lockout_time)
156    else:
157        return False
158
159def redirect_last_visited(request):
160    pagetrail = request.user.getTrail()
161    if pagetrail:
162        # Redirect to last page visited
163        last_visited = pagetrail[-1]
164        wikiname, pagename = wikiutil.split_interwiki(last_visited)
165        if wikiname != request.cfg.interwikiname and wikiname != 'Self':
166            wikitag, wikiurl, wikitail, error = wikiutil.resolve_interwiki(request, wikiname, pagename)
167            url = wikiurl + wikiutil.quoteWikinameURL(wikitail)
168        else:
169            url = Page(request, pagename).url(request)
170    else:
171        # Or to localized FrontPage
172        url = wikiutil.getFrontPage(request).url(request)
173    url = request.getQualifiedURL(url)
174    return abort(redirect(url))
175
176class UniqueIDGenerator(object):
177    def __init__(self, pagename=None):
178        self.unique_stack = []
179        self.include_stack = []
180        self.include_id = None
181        self.page_ids = {None: {}}
182        self.pagename = pagename
183
184    def push(self):
185        """
186        Used by the TOC macro, this ensures that the ID namespaces
187        are reset to the status when the current include started.
188        This guarantees that doing the ID enumeration twice results
189        in the same results, on any level.
190        """
191        self.unique_stack.append((self.page_ids, self.include_id))
192        self.include_id, pids = self.include_stack[-1]
193        self.page_ids = {}
194        for namespace in pids:
195            self.page_ids[namespace] = pids[namespace].copy()
196
197    def pop(self):
198        """
199        Used by the TOC macro to reset the ID namespaces after
200        having parsed the page for TOC generation and after
201        printing the TOC.
202        """
203        self.page_ids, self.include_id = self.unique_stack.pop()
204        return self.page_ids, self.include_id
205
206    def begin(self, base):
207        """
208        Called by the formatter when a document begins, which means
209        that include causing nested documents gives us an include
210        stack in self.include_id_stack.
211        """
212        pids = {}
213        for namespace in self.page_ids:
214            pids[namespace] = self.page_ids[namespace].copy()
215        self.include_stack.append((self.include_id, pids))
216        self.include_id = self(base)
217        # if it's the page name then set it to None so we don't
218        # prepend anything to IDs, but otherwise keep it.
219        if self.pagename and self.pagename == self.include_id:
220            self.include_id = None
221
222    def end(self):
223        """
224        Called by the formatter when a document ends, restores
225        the current include ID to the previous one and discards
226        the page IDs state we kept around for push().
227        """
228        self.include_id, pids = self.include_stack.pop()
229
230    def __call__(self, base, namespace=None):
231        """
232        Generates a unique ID using a given base name. Appends a running count to the base.
233
234        Needs to stay deterministic!
235
236        @param base: the base of the id
237        @type base: unicode
238        @param namespace: the namespace for the ID, used when including pages
239
240        @returns: a unique (relatively to the namespace) ID
241        @rtype: unicode
242        """
243        if not isinstance(base, unicode):
244            base = unicode(str(base), 'ascii', 'ignore')
245        if not namespace in self.page_ids:
246            self.page_ids[namespace] = {}
247        count = self.page_ids[namespace].get(base, -1) + 1
248        self.page_ids[namespace][base] = count
249        if not count:
250            return base
251        return u'%s-%d' % (base, count)
252
253FATALTMPL = """
254<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
255<html>
256<head><title>%(title)s</title></head>
257<body><h1>%(title)s</h1>
258<pre>
259%(body)s
260</pre></body></html>
261"""
262def fatal_response(error):
263    """ Create a response from MoinMoin.error.FatalError instances. """
264    html = FATALTMPL % dict(title=error.__class__.__name__,
265                            body=str(error))
266    return Response(html, status=500, mimetype='text/html')
267