handle cross-origin cookies with a custom header X-Set-Cookie, fix #24

Cookies set from a different domain can not be read by JS executed in
the current domain. As a workaround, Isso sends both a Set-Cookie and
X-Set-Cookie header. The former is used by the browser to make the
HTTP request to the API, the latter is read by `embed.min.js` to
determine if a comment can be edited or deleted.

When a comment is deleted, the server sends an expired cookies in
Set-Cookie and X-Set-Cookie.
This commit is contained in:
Martin Zimmermann 2013-11-05 12:20:17 +01:00
parent 05c8b571e2
commit 6691810316
4 changed files with 28 additions and 9 deletions

View File

@ -76,6 +76,11 @@ define(["q"], function(Q) {
function onload() { function onload() {
var rule = url.replace(endpoint, "").split("?", 1)[0]; var rule = url.replace(endpoint, "").split("?", 1)[0];
var cookie = xhr.getResponseHeader("X-Set-Cookie");
if (cookie && cookie.match(/^isso-/)) {
document.cookie = cookie;
}
if (rule in rules && rules[rule].indexOf(xhr.status) === -1) { if (rule in rules && rules[rule].indexOf(xhr.status) === -1) {
response.reject(xhr.responseText); response.reject(xhr.responseText);

View File

@ -241,7 +241,7 @@ define(["app/text/html", "app/dom", "app/utils", "app/api", "app/markup", "app/i
// remove edit and delete buttons when cookie is gone // remove edit and delete buttons when cookie is gone
var clear = function(button) { var clear = function(button) {
if (! utils.cookie(comment.id)) { if (! utils.cookie("isso-" + comment.id)) {
$(button, footer).remove(); $(button, footer).remove();
} else { } else {
setTimeout(function() { clear(button); }, 15*1000); setTimeout(function() { clear(button); }, 15*1000);
@ -253,14 +253,14 @@ define(["app/text/html", "app/dom", "app/utils", "app/api", "app/markup", "app/i
// show direct reply to own comment when cookie is max aged // show direct reply to own comment when cookie is max aged
var show = function(el) { var show = function(el) {
if (utils.cookie(comment.id)) { if (utils.cookie("isso-" + comment.id)) {
setTimeout(function() { show(el); }, 15*1000); setTimeout(function() { show(el); }, 15*1000);
} else { } else {
footer.append(el); footer.append(el);
} }
}; };
if (utils.cookie(comment.id)) { if (utils.cookie("isso-" + comment.id)) {
show($("a.reply", footer).detach()); show($("a.reply", footer).detach());
} }
}; };

View File

@ -5,10 +5,11 @@ import json
import time import time
import hashlib import hashlib
import logging import logging
import sqlite3 import functools
from itsdangerous import SignatureExpired, BadSignature from itsdangerous import SignatureExpired, BadSignature
from werkzeug.http import dump_cookie
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from werkzeug.exceptions import BadRequest, Forbidden, NotFound from werkzeug.exceptions import BadRequest, Forbidden, NotFound
@ -118,9 +119,13 @@ def new(app, environ, request, uri):
# success! # success!
logger.info('comment created: %s', json.dumps(rv)) logger.info('comment created: %s', json.dumps(rv))
resp = Response(json.dumps(rv), 202 if rv["mode"] == 2 else 201, cookie = functools.partial(dump_cookie,
content_type='application/json') value=app.sign([rv["id"], checksum]),
resp.set_cookie(str(rv["id"]), app.sign([rv["id"], checksum]), max_age=app.conf.getint('general', 'max-age')) 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 return resp
@ -176,8 +181,13 @@ def single(app, environ, request, id):
checksum = hashlib.md5(rv["text"].encode('utf-8')).hexdigest() checksum = hashlib.md5(rv["text"].encode('utf-8')).hexdigest()
rv["text"] = app.markdown(rv["text"]) rv["text"] = app.markdown(rv["text"])
cookie = functools.partial(dump_cookie,
value=app.sign([rv["id"], checksum]),
max_age=app.conf.getint('general', 'max-age'))
resp = Response(json.dumps(rv), 200, content_type='application/json') resp = Response(json.dumps(rv), 200, content_type='application/json')
resp.set_cookie(str(rv["id"]), app.sign([rv["id"], checksum]), max_age=app.conf.getint('general', 'max-age')) resp.headers.add("Set-Cookie", cookie(str(rv["id"])))
resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"]))
return resp return resp
if request.method == 'DELETE': if request.method == 'DELETE':
@ -192,8 +202,11 @@ def single(app, environ, request, id):
logger.info('comment %i deleted', id) logger.info('comment %i deleted', id)
cookie = functools.partial(dump_cookie, expires=0, max_age=0)
resp = Response(json.dumps(rv), 200, content_type='application/json') resp = Response(json.dumps(rv), 200, content_type='application/json')
resp.delete_cookie(str(id), path='/') resp.headers.add("Set-Cookie", cookie(str(id)))
resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id))
return resp return resp

View File

@ -38,6 +38,7 @@ class CORSMiddleWare(object):
headers.append(("Access-Control-Allow-Headers", "Origin, Content-Type")) headers.append(("Access-Control-Allow-Headers", "Origin, Content-Type"))
headers.append(("Access-Control-Allow-Credentials", "true")) headers.append(("Access-Control-Allow-Credentials", "true"))
headers.append(("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")) headers.append(("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"))
headers.append(("Access-Control-Expose-Headers", "X-Set-Cookie"))
return start_response(status, headers, exc_info) return start_response(status, headers, exc_info)
if environ.get("REQUEST_METHOD") == "OPTIONS": if environ.get("REQUEST_METHOD") == "OPTIONS":