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