isso.dispatch now dispatches multiple sites based on relative URLs

The previous approach using a custom X-Custom header did work for the
client-side, but not for activation and deletion links. Now, you need
to add a `name = foo` option to the general section. `isso.dispatch`
then binds this configuration to /foo and can distinguish all API
calls without a special HTTP header.
This commit is contained in:
Martin Zimmermann 2013-12-08 19:03:10 +01:00
parent ac74418179
commit b15f17738e
9 changed files with 48 additions and 43 deletions

View File

@ -38,6 +38,7 @@ session key and hostname. Here are the default values for this section:
[general] [general]
dbpath = /tmp/isso.db dbpath = /tmp/isso.db
name =
host = http://localhost:8080/ host = http://localhost:8080/
max-age = 15m max-age = 15m
session-key = ... ; python: binascii.b2a_hex(os.urandom(24)) session-key = ... ; python: binascii.b2a_hex(os.urandom(24))
@ -47,6 +48,10 @@ dbpath
file location to the SQLite3 database, highly recommended to change this file location to the SQLite3 database, highly recommended to change this
location to a non-temporary location! location to a non-temporary location!
name
required to dispatch :ref:`multiple websites <configure-multiple-sites>`,
not used otherwise.
host host
URL to your website. When you start Isso, it will probe your website with URL to your website. When you start Isso, it will probe your website with
a simple ``GET /`` request to see if it can reach the webserver. If this a simple ``GET /`` request to see if it can reach the webserver. If this

View File

@ -42,25 +42,28 @@ websites on different domains.
The following example uses `gunicorn <http://gunicorn.org/>`_ as WSGI server ( The following example uses `gunicorn <http://gunicorn.org/>`_ as WSGI server (
you can use uWSGI as well). Let's say you maintain two websites, like you can use uWSGI as well). Let's say you maintain two websites, like
foo.example and other.foo: foo.example and other.bar:
.. code-block:: sh .. code-block:: sh
$ cat /etc/isso.d/foo.example.cfg $ cat /etc/isso.d/foo.example.cfg
[general] [general]
name = foo
host = http://foo.example/ host = http://foo.example/
dbpath = /var/lib/isso/foo.example.db dbpath = /var/lib/isso/foo.example.db
$ cat /etc/isso.d/other.foo.cfg $ cat /etc/isso.d/other.bar.cfg
[general] [general]
host = http://other.foo/ name = bar
dbpath = /var/lib/isso/other.foo.db host = http://other.bar/
dbpath = /var/lib/isso/other.bar.db
Then you run Isso using gunicorn: Then you run Isso with gunicorn (separate multiple configuration files by
semicolon):
.. code-block:: sh .. code-block:: sh
$ export ISSO_SETTINGS="/etc/isso.d/foo.example.cfg;/etc/isso.d/other.foo.cfg" $ export ISSO_SETTINGS="/etc/isso.d/foo.example.cfg;/etc/isso.d/other.bar.cfg"
$ gunicorn isso.dispatch -b localhost:8080 $ gunicorn isso.dispatch -b localhost:8080
In your webserver configuration, proxy Isso as usual: In your webserver configuration, proxy Isso as usual:
@ -73,18 +76,18 @@ In your webserver configuration, proxy Isso as usual:
location / { location / {
proxy_pass http://localhost:8080; proxy_pass http://localhost:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
} }
} }
To verify the setup, run: When you now visit http://comments.example/, you will see your different Isso
configuration separated by /`name`.
.. code-block:: sh .. code-block:: text
$ curl -vH "Origin: http://foo.example" http://comments.example/ $ curl http://comments.example/
... /foo
$ curl -vH "Origin: http://other.foo" http://comments.example/ /bar
...
In case of a 418 (I'm a teapot), the setup is *not* correctly configured. Just embed the JavaScript including the new relative path, e.g.
*http://comments.example/foo/js/embed.min.js*. Make sure, you don't mix the
URLs on both sites as it will most likely cause CORS-related errors.

View File

@ -7,6 +7,9 @@
# location to a non-temporary location # location to a non-temporary location
dbpath = /tmp/comments.db dbpath = /tmp/comments.db
# required to dispatch multiple websites, not used otherwise.
name =
# URL to your website. When you start Isso, it will probe your website with a # URL to your website. When you start Isso, it will probe your website with a
# simple GET / request to see if it can reach the webserver. If this fails, Isso # simple GET / request to see if it can reach the webserver. If this fails, Isso
# may not be able check if a web page exists, thus fails to accept new comments. # may not be able check if a web page exists, thus fails to accept new comments.

View File

@ -105,6 +105,7 @@ class Config:
default = [ default = [
"[general]", "[general]",
"name = ",
"dbpath = /tmp/isso.db", "session-key = %r" % binascii.b2a_hex(os.urandom(24)), "dbpath = /tmp/isso.db", "session-key = %r" % binascii.b2a_hex(os.urandom(24)),
"host = http://localhost:8080/", "max-age = 15m", "host = http://localhost:8080/", "max-age = 15m",
"notify = ", "notify = ",

View File

@ -1,3 +1,4 @@
# -*- encoding: utf-8 -*-
import os import os
import logging import logging
@ -7,8 +8,8 @@ try:
except ImportError: except ImportError:
from urllib.parse import urlparse from urllib.parse import urlparse
from werkzeug.wrappers import Request from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.exceptions import ImATeapot from werkzeug.wrappers import Response
from isso import make_app, wsgi from isso import make_app, wsgi
from isso.core import Config from isso.core import Config
@ -16,39 +17,35 @@ from isso.core import Config
logger = logging.getLogger("isso") logger = logging.getLogger("isso")
class Dispatcher(object): class Dispatcher(DispatcherMiddleware):
""" """
A dispatcher to support different websites. Dispatches based on A dispatcher to support different websites. Dispatches based on
HTTP-Host. If HTTP-Host is not provided, display an error message. a relative URI, e.g. /foo.example and /other.bar.
""" """
def __init__(self, *confs): def __init__(self, *confs):
self.isso = {} self.isso = {}
for conf in map(Config.load, confs): for i, conf in enumerate(map(Config.load, confs)):
app = make_app(conf) if not conf.get("general", "name"):
logger.warn("unable to dispatch %r, no 'name' set", confs[i])
continue
for origin in conf.getiter("general", "host"): self.isso["/" + conf.get("general", "name")] = make_app(conf)
self.isso[origin.rstrip("/")] = app
super(Dispatcher, self).__init__(self.default, mounts=self.isso)
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
if Request(environ).url.endswith((".js", ".css")): # clear X-Script-Name as the PATH_INFO is already adjusted
return self.isso.values()[0](environ, start_response) environ.pop('HTTP_X_SCRIPT_NAME', None)
if "HTTP_X_ORIGIN" in environ and "HTTP_ORIGIN" not in environ: return super(Dispatcher, self).__call__(environ, start_response)
environ["HTTP_ORIGIN"] = environ["HTTP_X_ORIGIN"]
origin = environ.get("HTTP_ORIGIN", wsgi.host(environ)) def default(self, environ, start_response):
resp = Response("\n".join(self.isso.keys()), 404, content_type="text/plain")
try:
# logger.info("dispatch %s", origin)
return self.isso[origin](environ, start_response)
except KeyError:
# logger.info("unable to dispatch %s", origin)
resp = ImATeapot("unable to dispatch %s" % origin)
return resp(environ, start_response) return resp(environ, start_response)
@ -61,4 +58,4 @@ else:
logger.fatal("%s: no such file", path) logger.fatal("%s: no such file", path)
break break
else: else:
application = Dispatcher(*confs) application = wsgi.SubURI(Dispatcher(*confs))

View File

@ -87,10 +87,6 @@ define(["q"], function(Q) {
xhr.withCredentials = true; xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Content-Type", "application/json");
if (method === "GET") {
xhr.setRequestHeader("X-Origin", window.location.origin);
}
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
onload(); onload();

0
isso/run.py Normal file
View File

View File

@ -69,7 +69,7 @@ class CORSMiddleware(object):
def add_cors_headers(status, headers, exc_info=None): def add_cors_headers(status, headers, exc_info=None):
headers = Headers(headers) headers = Headers(headers)
headers.add("Access-Control-Allow-Origin", self.origin(environ)) headers.add("Access-Control-Allow-Origin", self.origin(environ))
headers.add("Access-Control-Allow-Headers", "Origin, Content-Type, X-Origin") headers.add("Access-Control-Allow-Headers", "Origin, Content-Type")
headers.add("Access-Control-Allow-Credentials", "true") headers.add("Access-Control-Allow-Credentials", "true")
headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
headers.add("Access-Control-Expose-Headers", "X-Set-Cookie") headers.add("Access-Control-Expose-Headers", "X-Set-Cookie")

View File

@ -26,7 +26,7 @@ def test_simple_CORS():
rv = client.get("/", headers={"ORIGIN": "https://example.tld"}) rv = client.get("/", headers={"ORIGIN": "https://example.tld"})
assert rv.headers["Access-Control-Allow-Origin"] == "https://example.tld" assert rv.headers["Access-Control-Allow-Origin"] == "https://example.tld"
assert rv.headers["Access-Control-Allow-Headers"] == "Origin, Content-Type, X-Origin" assert rv.headers["Access-Control-Allow-Headers"] == "Origin, Content-Type"
assert rv.headers["Access-Control-Allow-Credentials"] == "true" assert rv.headers["Access-Control-Allow-Credentials"] == "true"
assert rv.headers["Access-Control-Allow-Methods"] == "GET, POST, PUT, DELETE" assert rv.headers["Access-Control-Allow-Methods"] == "GET, POST, PUT, DELETE"
assert rv.headers["Access-Control-Expose-Headers"] == "X-Set-Cookie" assert rv.headers["Access-Control-Expose-Headers"] == "X-Set-Cookie"