def token_request(self, session): # Token Request as per # http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint params = { 'grant_type': "authorization_code", 'code': session['code'], 'redirect_uri': session['redirect_uri'], } auth_string = "%s:%s" % (session['client_id'], session['client_secret']) headers = { 'Content-Type': "application/x-www-form-urlencoded", 'Authorization': "Basic %s" % (base64.b64encode( (auth_string.encode("ascii")).decode("ascii"))), } token_url = "%s/oauth/token" % (session['issuer']) try: with http.curl("POST", token_url, urlencode(params), extra_headers=headers) as resp: assert resp and resp.getcode() == 200 ans = json.loads(resp.read()) session['access_token'] = ans['access_token'] except (AssertionError, ValueError, KeyError): return False return True
def make_app(conf=None): try: import uwsgi except ImportError: class App(Isso, ThreadedMixin): pass else: class App(Isso, uWSGIMixin): pass isso = App(conf) for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to HTTP server") break else: logger.warn("unable to connect to HTTP server") app = ProxyFix( wsgi.SubURI( SharedDataMiddleware( isso.wsgi_app, { '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/') }))) return app
def make_app(conf=None): try: import uwsgi except ImportError: class App(Isso, ThreadedMixin): pass else: class App(Isso, uWSGIMixin): pass isso = App(conf) for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to HTTP server") break else: logger.warn("unable to connect to HTTP server") if isso.conf.getboolean("server", "profile"): from werkzeug.contrib.profiler import ProfilerMiddleware isso = ProfilerMiddleware(isso, sort_by=("cumtime", ), restrictions=("isso/(?!lib)", )) app = ProxyFix( wsgi.SubURI( wsgi.CORSMiddleware( SharedDataMiddleware(isso, { '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/')}), list(isso.conf.getiter("general", "host"))))) return app
def sendmsg(self, title, body): if self.moderated: TA_view = body["v"] TA_delete = body["d"] TA_activate = body["a"] body = body["rv"] if PY2K: if title: title = title.encode("utf-8") body = body.encode("utf-8") if self.moderated: # Need to review comments, use the TalkAdmin interface. host = "http://sc.ftqq.com" path = "/webhook/%s?%s" % (self.conf.get("takey"), urlencode(dict( TA_action_on=1, TA_title=title, TA_content=body, TA_view=TA_view, TA_delete=TA_delete, TA_activate=TA_activate ))) else: host = "https://sc.ftqq.com" path = "/%s.send?%s" % (self.conf.get("sckey"), urlencode(dict( text=title, desp=body ))) with curl("POST", host, path, 5) as resp: if resp: try: result = json.loads(resp.read()) except (TypeError, ValueError): logger.error("Illegal response structure, request error.") else: if result.get("errno") != 0: logger.warn(result)
def make_app(conf=None): try: import uwsgi except ImportError: class App(Isso, ThreadedMixin): pass else: class App(Isso, uWSGIMixin): pass isso = App(conf) for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to HTTP server") break else: logger.warn("unable to connect to HTTP server") app = ProxyFix(wsgi.SubURI(SharedDataMiddleware(isso.wsgi_app, { '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/') }))) return app
def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False): if not any((threading, multiprocessing, uwsgi)): raise RuntimeError("either set threading, multiprocessing or uwsgi") if threading: class App(Isso, ThreadedMixin): pass elif multiprocessing: class App(Isso, ProcessMixin): pass else: class App(Isso, uWSGIMixin): pass isso = App(conf) # check HTTP server connection for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to %s", host) break else: logger.warn( "unable to connect to your website, Isso will probably not " "work correctly. Please make sure, Isso can reach your " "website via HTTP(S).") wrapper = [local_manager.make_middleware] if isso.conf.getboolean("server", "profile"): wrapper.append( partial(ProfilerMiddleware, sort_by=("cumulative", ), restrictions=("isso/(?!lib)", 10))) wrapper.append( partial(SharedDataMiddleware, exports={ '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/'), '/demo': join(dirname(__file__), 'demo/') })) wrapper.append( partial(wsgi.CORSMiddleware, origin=origin(isso.conf.getiter("general", "host")), allowed=("Origin", "Referer", "Content-Type"), exposed=("X-Set-Cookie", "Date"))) wrapper.extend([wsgi.SubURI, ProxyFix]) if werkzeug.version.startswith("0.8"): wrapper.append(wsgi.LegacyWerkzeugMiddleware) return reduce(lambda x, f: f(x), wrapper, isso)
def discovery(self, session): # Provider Discovery as per # http://openid.net/specs/openid-connect-discovery-1_0.html id_normed, wf_host = self.id_normalize(session['identifier']) params = { 'resource': id_normed, 'rel': "http://openid.net/specs/connect/1.0/issuer", } try: assert wf_host with http.curl( "GET", "https://%s/.well-known/webfinger?%s" % (wf_host, urlencode(params))) as resp: assert resp and resp.getcode() == 200 ans = json.loads(resp.read()) for link in ans['links']: if link['rel'] == "http://openid.net/specs/connect/1.0/issuer": session['issuer'] = link['href'] u = urlsplit(session['issuer']) assert u.scheme == "https" assert u.hostname assert not u.query and not u.fragment break assert 'issuer' in session except (AssertionError, ValueError, KeyError): return False try: with http.curl( "GET", session['issuer'].rstrip("/") + "/.well-known/openid-configuration") as resp: assert resp and resp.getcode() == 200 ans = json.loads(resp.read()) assert ans['issuer'] == session['issuer'] session['registration_endpoint'] = ans['registration_endpoint'] session['userinfo_endpoint'] = ans['userinfo_endpoint'] except (AssertionError, ValueError, KeyError): return False return True
def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False): if not any((threading, multiprocessing, uwsgi)): raise RuntimeError("either set threading, multiprocessing or uwsgi") if threading: class App(Isso, ThreadedMixin): pass elif multiprocessing: class App(Isso, ProcessMixin): pass else: class App(Isso, uWSGIMixin): pass isso = App(conf) # check HTTP server connection for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to %s", host) break else: logger.warn("unable to connect to your website, Isso will probably not " "work correctly. Please make sure, Isso can reach your " "website via HTTP(S).") wrapper = [local_manager.make_middleware] if isso.conf.getboolean("server", "profile"): wrapper.append(partial(ProfilerMiddleware, sort_by=("cumulative", ), restrictions=("isso/(?!lib)", 10))) wrapper.append(partial(SharedDataMiddleware, exports={ '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/'), '/demo': join(dirname(__file__), 'demo/') })) wrapper.append(partial(wsgi.CORSMiddleware, origin=origin(isso.conf.getiter("general", "host")), allowed=("Origin", "Referer", "Content-Type"), exposed=("X-Set-Cookie", "Date"))) wrapper.extend([wsgi.SubURI, ProxyFix, wsgi.LegacyWerkzeugMiddleware]) if werkzeug.version.startswith("0.8"): wrapper.append(wsgi.LegacyWerkzeugMiddleware) return reduce(lambda x, f: f(x), wrapper, isso)
def update_google_certs(cls): if API.google_certs: for key in API.google_certs: if datetime.utcnow() <= API.google_certs[key].not_valid_after: return with http.curl('GET', API.GOOGLE_CERT_URL, timeout=5) as resp: try: assert resp and resp.getcode() == 200 pems = json.loads(resp.read()) API.google_certs = {key: load_pem_x509_certificate(pems[key].encode("ascii"), default_backend()) for key in pems} except (AssertionError, ValueError): logger.warn("failed to renew Google certificates")
def validate_fb_token(self, token, uid): appid = self.facebook_conf.get("app-id") req_url = "%s/debug_token?input_token=%s&access_token=%s|%s" % (API.FACEBOOK_GRAPH_HOST, token, appid, self.facebook_conf.get("app-secret")) with http.curl('GET', req_url, timeout=5) as resp: try: assert resp and resp.getcode() == 200 data = json.loads(resp.read())["data"] assert data["is_valid"] assert data["user_id"] == uid assert data["app_id"] == appid assert datetime.utcnow() <= datetime.utcfromtimestamp(data["expires_at"]) return True except (AssertionError, ValueError, KeyError): return False
def userinfo_request(self, session): # UserInfo Request as per # http://openid.net/specs/openid-connect-core-1_0.html#UserInfo headers = { 'Authorization': "Bearer %s" % (session['access_token']), } try: with http.curl("GET", session['userinfo_endpoint'], extra_headers=headers) as resp: assert resp and resp.getcode() == 200 session['userinfo'] = json.loads(resp.read()) except (AssertionError, ValueError): return False return True
def update_google_certs(cls): if API.google_certs: for key in API.google_certs: if datetime.utcnow() <= API.google_certs[key].not_valid_after: return with http.curl('GET', API.GOOGLE_CERT_URL, timeout=5) as resp: try: assert resp and resp.getcode() == 200 pems = json.loads(resp.read()) API.google_certs = { key: load_pem_x509_certificate(pems[key].encode("ascii"), default_backend()) for key in pems } except (AssertionError, ValueError): logger.warn("failed to renew Google certificates")
def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False): if not any((threading, multiprocessing, uwsgi)): raise RuntimeError("either set threading, multiprocessing or uwsgi") if threading: class App(Isso, ThreadedMixin): pass elif multiprocessing: class App(Isso, ProcessMixin): pass else: class App(Isso, uWSGIMixin): pass isso = App(conf) # show session-key (to see that it changes randomely if unset) logger.info("session-key = %s", isso.conf.get("general", "session-key")) # check HTTP server connection for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to %s", host) break else: logger.warn("unable to connect to %s", ", ".join(conf.getiter("general", "host"))) wrapper = [local_manager.make_middleware] if isso.conf.getboolean("server", "profile"): wrapper.append(partial(ProfilerMiddleware, sort_by=("cumulative", ), restrictions=("isso/(?!lib)", 10))) wrapper.append(partial(SharedDataMiddleware, exports={ '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/')})) wrapper.append(partial(wsgi.CORSMiddleware, origin=origin(isso.conf.getiter("general", "host")), allowed=("Origin", "Content-Type"), exposed=("X-Set-Cookie", "Date"))) wrapper.extend([wsgi.SubURI, ProxyFix]) return reduce(lambda x, f: f(x), wrapper, isso)
def validate_fb_token(self, token, uid): appid = self.facebook_conf.get("app-id") req_url = "%s/debug_token?input_token=%s&access_token=%s|%s" % ( API.FACEBOOK_GRAPH_HOST, token, appid, self.facebook_conf.get("app-secret")) with http.curl('GET', req_url, timeout=5) as resp: try: assert resp and resp.getcode() == 200 data = json.loads(resp.read())["data"] assert data["is_valid"] assert data["user_id"] == uid assert data["app_id"] == appid assert datetime.utcnow() <= datetime.utcfromtimestamp( data["expires_at"]) return True except (AssertionError, ValueError, KeyError): return False
def make_app(conf=None): try: import uwsgi except ImportError: class App(Isso, ThreadedMixin): pass else: class App(Isso, uWSGIMixin): pass isso = App(conf) for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to HTTP server") break else: logger.warn("unable to connect to HTTP server") if isso.conf.getboolean("server", "profile"): from werkzeug.contrib.profiler import ProfilerMiddleware isso = ProfilerMiddleware(isso, sort_by=("cumtime", ), restrictions=("isso/(?!lib)", )) app = ProxyFix( wsgi.SubURI( wsgi.CORSMiddleware( SharedDataMiddleware( isso, { '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/') }), list(isso.conf.getiter("general", "host"))))) return app
def make_app(conf=None): if uwsgi: class App(Isso, uWSGIMixin): pass elif gevent or sys.argv[0].endswith("isso"): class App(Isso, ThreadedMixin): pass else: class App(Isso, ProcessMixin): pass isso = App(conf) for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: if resp is not None: logger.info("connected to %s", host) break else: logger.warn("unable to connect to %s", ", ".join(conf.getiter("general", "host"))) wrapper = [local_manager.make_middleware] if isso.conf.getboolean("server", "profile"): wrapper.append(partial(ProfilerMiddleware, sort_by=("cumtime", ), restrictions=("isso/(?!lib)", 10))) wrapper.append(partial(SharedDataMiddleware, exports={ '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/')})) wrapper.append(partial(wsgi.CORSMiddleware, origin=origin(isso.conf.getiter("general", "host")))) wrapper.extend([wsgi.SubURI, ProxyFix]) return reduce(lambda x, f: f(x), wrapper, isso)
def dyn_register(self, session): # Dynamic Client Registration as per # http://openid.net/specs/openid-connect-registration-1_0.html reg_data = { 'client_name': "Isso", 'redirect_uris': [session['redirect_uri']], } headers = { 'Content-Type': "application/json", } try: with http.curl("POST", session['registration_endpoint'], body=json.dumps(reg_data), extra_headers=headers) as resp: # HTTP status should be 201 on success, but is returned as 200 from some implementations assert resp and 200 <= resp.getcode() < 300 ans = json.loads(resp.read()) session['client_id'] = ans['client_id'] session['client_secret'] = ans['client_secret'] except (AssertionError, ValueError, KeyError): return False return True
def new(self, environ, request, uri): data = request.get_json() for field in set(data.keys()) - API.ACCEPT: data.pop(field) for key in ("author", "email", "website", "parent"): data.setdefault(key, None) valid, reason = API.verify(data) if not valid: return BadRequest(reason) for field in ("author", "email"): if data.get(field) is not None: data[field] = cgi.escape(data[field]) data['mode'] = 2 if self.moderated else 1 data['remote_addr'] = utils.anonymize(str(request.remote_addr)) with self.isso.lock: if uri not in self.threads: with http.curl('GET', local("origin"), uri) as resp: if resp and resp.status == 200: uri, title = parse.thread(resp.read(), id=uri) else: return NotFound('URI does not exist') thread = self.threads.new(uri, title) self.signal("comments.new:new-thread", thread) else: thread = self.threads[uri] # notify extensions that the new comment is about to save self.signal("comments.new:before-save", thread, data) valid, reason = self.guard.validate(uri, data) if not valid: self.signal("comments.new:guard", reason) raise Forbidden(reason) with self.isso.lock: rv = self.comments.add(uri, data) # notify extension, that the new comment has been successfully saved self.signal("comments.new:after-save", thread, rv) cookie = functools.partial(dump_cookie, value=self.isso.sign([rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) rv["hash"] = pbkdf2(rv['email'] or rv['remote_addr'], self.isso.salt, 1000, 6).decode("utf-8") self.cache.set('hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash']) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) # success! self.signal("comments.new:finish", thread, rv) resp = JSON(rv, 202 if rv["mode"] == 2 else 201) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp
def new(self, environ, request, uri): data = request.get_json() for field in set(data.keys()) - API.ACCEPT: data.pop(field) for key in ("author", "email", "website", "parent"): data.setdefault(key, None) valid, reason = API.verify(data) if not valid: return BadRequest(reason) for field in ("author", "email", "website"): if data.get(field) is not None: data[field] = cgi.escape(data[field]) if data.get("website"): data["website"] = normalize(data["website"]) data['mode'] = 2 if self.moderated else 1 data['remote_addr'] = utils.anonymize(str(request.remote_addr)) with self.isso.lock: if uri not in self.threads: if 'title' not in data: with http.curl('GET', local("origin"), uri) as resp: if resp and resp.status == 200: uri, title = parse.thread(resp.read(), id=uri) else: return NotFound('URI does not exist %s') else: title = data['title'] thread = self.threads.new(uri, title) self.signal("comments.new:new-thread", thread) else: thread = self.threads[uri] # notify extensions that the new comment is about to save self.signal("comments.new:before-save", thread, data) valid, reason = self.guard.validate(uri, data) if not valid: self.signal("comments.new:guard", reason) raise Forbidden(reason) with self.isso.lock: rv = self.comments.add(uri, data) # notify extension, that the new comment has been successfully saved self.signal("comments.new:after-save", thread, rv) cookie = functools.partial(dump_cookie, value=self.isso.sign( [rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) rv["hash"] = self.hash(rv['email'] or rv['remote_addr']) self.cache.set( 'hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash']) rv = self._add_gravatar_image(rv) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) # success! self.signal("comments.new:finish", thread, rv) resp = JSON(rv, 202 if rv["mode"] == 2 else 201) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp
def new(self, environ, request, uri, key): data = request.get_json() # check access keys rv = self.db.execute(['SELECT uri FROM access WHERE key = ? ;'], (key, )).fetchall() if not rv or rv[0][0] != uri: raise Forbidden for field in set(data.keys()) - API.ACCEPT: data.pop(field) for key in ("author", "parent"): data.setdefault(key, None) if data['parent'] is not None: data.setdefault('place', None) valid, reason = API.verify(data) if not valid: return BadRequest(reason) escaped = dict((key, cgi.escape(value) if value is not None else None) for key, value in data.items()) added = {} if escaped.get("website") is not None: added["website"] = normalize(escaped["website"]) added['mode'] = 2 if self.moderated else 1 prepared = dict(escaped) prepared.update(added) with self.isso.lock: if uri in self._threads: thread = self._threads[uri] else: if 'title' in prepared: title = prepared['title'] else: with http.curl('GET', local("origin"), uri) as resp: if resp and resp.status == 200: uri, title = parse.thread(resp.read(), id=uri) else: return NotFound('URI does not exist %s') thread = self._threads.new(uri, title) self.signal("comments.new:new-thread", thread) # notify extensions that the new comment is about to save self.signal("comments.new:before-save", thread, prepared) valid, reason = self.guard.validate(uri, prepared) if not valid: self.signal("comments.new:guard", reason) raise Forbidden(reason) with self.isso.lock: rv = self.comments.add(uri, prepared) # notify extension, that the new comment has been successfully saved self.signal("comments.new:after-save", thread, rv) cookie = functools.partial(dump_cookie, value=self.isso.sign( [rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) rv["hash"] = self.hash(rv['remote_addr']) self.cache.set('hash', (rv['remote_addr']).encode('utf-8'), rv['hash']) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) # success! self.signal("comments.new:finish", thread, rv) resp = JSON(rv, 202 if rv["mode"] == 2 else 201) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp
def new(app, environ, request, uri): data = request.get_json() for field in set(data.keys()) - set(['text', 'author', 'website', 'email', 'parent']): data.pop(field) if "text" not in data or data["text"] is None or len(data["text"]) < 3: raise BadRequest("no text given") if "id" in data and not isinstance(data["id"], int): raise BadRequest("parent id must be an integer") if len(data.get("email") or "") > 254: raise BadRequest("http://tools.ietf.org/html/rfc5321#section-4.5.3") for field in ("author", "email"): if data.get(field): data[field] = cgi.escape(data[field]) data['mode'] = (app.conf.getboolean('moderation', 'enabled') and 2) or 1 data['remote_addr'] = utils.anonymize(str(request.remote_addr)) with app.lock: if uri not in app.db.threads: for host in app.conf.getiter('general', 'host'): with http.curl('GET', host, uri) as resp: if resp and resp.status == 200: title = parse.title(resp.read()) break else: return Response('URI does not exist', 404) app.db.threads.new(uri, title) logger.info('new thread: %s -> %s', uri, title) else: title = app.db.threads[uri].title try: with app.lock: rv = app.db.comments.add(uri, data) except db.IssoDBException: raise Forbidden host = list(app.conf.getiter('general', 'host'))[0].rstrip("/") href = host + uri + "#isso-%i" % rv["id"] deletion = host + environ["SCRIPT_NAME"] + "/delete/" + app.sign(str(rv["id"])) activation = None if app.conf.getboolean('moderation', 'enabled'): activation = host + environ["SCRIPT_NAME"] + "/activate/" + app.sign(str(rv["id"])) app.notify(title, notify.format(rv, href, utils.anonymize(str(request.remote_addr)), activation_key=activation, deletion_key=deletion)) # save checksum of text into cookie, so mallory can't modify/delete a comment, if # he add a comment, then removed it but not the signed cookie. checksum = hashlib.md5(rv["text"].encode('utf-8')).hexdigest() rv["text"] = app.markdown(rv["text"]) rv["hash"] = str(pbkdf2(rv['email'] or rv['remote_addr'], app.salt, 1000, 6)) app.cache.set('hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash']) for key in set(rv.keys()) - FIELDS: rv.pop(key) # success! logger.info('comment created: %s', json.dumps(rv)) cookie = functools.partial(dump_cookie, value=app.sign([rv["id"], checksum]), max_age=app.conf.getint('general', 'max-age')) resp = Response(json.dumps(rv), 202 if rv["mode"] == 2 else 201, content_type='application/json') resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp
def new(app, environ, request, uri): data = request.get_json() for field in set(data.keys()) - set( ['text', 'author', 'website', 'email', 'parent']): data.pop(field) if not data.get("text"): raise BadRequest("no text given") if "id" in data and not isinstance(data["id"], int): raise BadRequest("parent id must be an integer") for field in ("author", "email"): if data.get(field): data[field] = cgi.escape(data[field]) data['mode'] = (app.conf.getboolean('moderation', 'enabled') and 2) or 1 data['remote_addr'] = utils.anonymize(str(request.remote_addr)) with app.lock: if uri not in app.db.threads: for host in app.conf.getiter('general', 'host'): with http.curl('GET', host, uri) as resp: if resp and resp.status == 200: title = parse.title(resp.read()) break else: return Response('URI does not exist', 404) app.db.threads.new(uri, title) logger.info('new thread: %s -> %s', uri, title) else: title = app.db.threads[uri].title try: with app.lock: rv = app.db.comments.add(uri, data) except db.IssoDBException: raise Forbidden host = list(app.conf.getiter('general', 'host'))[0].rstrip("/") href = host + uri + "#isso-%i" % rv["id"] deletion = host + environ["SCRIPT_NAME"] + "/delete/" + app.sign( str(rv["id"])) activation = None if app.conf.getboolean('moderation', 'enabled'): activation = host + environ["SCRIPT_NAME"] + "/activate/" + app.sign( str(rv["id"])) app.notify( title, notify.format(rv, href, utils.anonymize(str(request.remote_addr)), activation_key=activation, deletion_key=deletion)) # save checksum of text into cookie, so mallory can't modify/delete a comment, if # he add a comment, then removed it but not the signed cookie. checksum = hashlib.md5(rv["text"].encode('utf-8')).hexdigest() rv["text"] = app.markdown(rv["text"]) rv["hash"] = str( pbkdf2(rv.get('email') or rv['remote_addr'], app.salt, 1000, 6)) for key in set(rv.keys()) - FIELDS: rv.pop(key) # success! logger.info('comment created: %s', json.dumps(rv)) resp = Response(json.dumps(rv), 202 if rv["mode"] == 2 else 201, content_type='application/json') resp.set_cookie(str(rv["id"]), app.sign([rv["id"], checksum]), max_age=app.conf.getint('general', 'max-age')) return resp
def new(self, environ, request, uri): logger.debug("got uri :%s" % uri) logger.debug("got eviron :%s" % str(environ)) logger.debug("got request :%s" % pprint.pformat(request.__dict__)) data = request.get_json() logger.debug("got data :%s" % data) for field in set(data.keys()) - API.ACCEPT: f = data.pop(field) logger.debug("skip data %s:%s" % (field, f )) for key in ("author", "email", "website", "parent"): data.setdefault(key, None) valid, reason = API.verify(data) if not valid: return BadRequest(reason) for field in ("author", "email", "website"): if data.get(field) is not None: data[field] = cgi.escape(data[field]) if data.get("website"): data["website"] = normalize(data["website"]) data['mode'] = 2 if self.moderated else 1 logger.debug("request.remote_addr %s" % (request.remote_addr )) data['remote_addr'] = utils.anonymize(str(request.remote_addr)) with self.isso.lock: if uri not in self.threads: org = str(local("origin")) if org == '<LocalProxy unbound>': org = environ['HTTP_ORIGIN'] logger.debug("got origin %s" % (org)) with http.curl('GET', org, uri) as resp: if resp and resp.status == 200: uri, title = parse.thread(resp.read(), id=uri) else: return NotFound('URI does not exist') thread = self.threads.new(uri, title) self.signal("comments.new:new-thread", thread) else: thread = self.threads[uri] # notify extensions that the new comment is about to save self.signal("comments.new:before-save", thread, data) valid, reason = self.guard.validate(uri, data) if not valid: self.signal("comments.new:guard", reason) raise Forbidden(reason) with self.isso.lock: rv = self.comments.add(uri, data) # notify extension, that the new comment has been successfully saved self.signal("comments.new:after-save", thread, rv) cookie = functools.partial(dump_cookie, value=self.isso.sign([rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) rv["hash"] = self.hash(rv['email'] or rv['remote_addr']) self.cache.set('hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash']) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) # success! self.signal("comments.new:finish", thread, rv) resp = JSON(rv, 202 if rv["mode"] == 2 else 201) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp