Compare commits

..

4 Commits

Author SHA1 Message Date
2d45466f66 Print version at server startup 2013-03-04 15:43:45 -05:00
c6a0e6e96f More complete CORS handling, including preflight requests (hopefully) 2013-03-04 15:40:35 -05:00
79755dc624 Fix Allow: header by switching to cherrypy's built in tools.allow().
Replaces custom tools.allow_methods which didn't return the Allow: header.
2013-03-04 14:08:37 -05:00
c512631184 bulkdata: Build up rows and write to disk all at once 2013-03-03 12:03:44 -05:00
7 changed files with 94 additions and 53 deletions

View File

@@ -53,6 +53,7 @@ def main():
# Print info # Print info
if not args.quiet: if not args.quiet:
print "Version: %s" % nilmdb.__version__
print "Database: %s" % (os.path.realpath(args.database)) print "Database: %s" % (os.path.realpath(args.database))
if args.address == '0.0.0.0' or args.address == '::': if args.address == '0.0.0.0' or args.address == '::':
host = socket.getfqdn() host = socket.getfqdn()

View File

@@ -221,9 +221,11 @@ class File(object):
# An optimized verison of append, to avoid flushing the file # An optimized verison of append, to avoid flushing the file
# and resizing the mmap after each data point. # and resizing the mmap after each data point.
try: try:
rows = []
for i in xrange(count): for i in xrange(count):
row = dataiter.next() row = dataiter.next()
self._f.write(packer(*row)) rows.append(packer(*row))
self._f.write("".join(rows))
finally: finally:
self._f.flush() self._f.flush()
self.size = self._f.tell() self.size = self._f.tell()

View File

@@ -71,16 +71,51 @@ def exception_to_httperror(*expected):
# care of that. # care of that.
return decorator.decorator(wrapper) return decorator.decorator(wrapper)
# Custom Cherrypy tools # Custom CherryPy tools
def allow_methods(methods):
method = cherrypy.request.method.upper() def CORS_allow(methods):
if method not in methods: """This does several things:
if method in cherrypy.request.methods_with_bodies:
cherrypy.request.body.read() Handles CORS preflight requests.
allowed = ', '.join(methods) Adds Allow: header to all requests.
cherrypy.response.headers['Allow'] = allowed Raise 405 if request.method not in method.
raise cherrypy.HTTPError(405, method + " not allowed; use " + allowed)
cherrypy.tools.allow_methods = cherrypy.Tool('before_handler', allow_methods) It is similar to cherrypy.tools.allow, with the CORS stuff added.
"""
request = cherrypy.request.headers
response = cherrypy.response.headers
if not isinstance(methods, (tuple, list)): # pragma: no cover
methods = [ methods ]
methods = [ m.upper() for m in methods if m ]
if not methods: # pragma: no cover
methods = [ 'GET', 'HEAD' ]
elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover
methods.append('HEAD')
response['Allow'] = ', '.join(methods)
# Allow all origins
if 'Origin' in request:
response['Access-Control-Allow-Origin'] = request['Origin']
# If it's a CORS request, send response.
request_method = request.get("Access-Control-Request-Method", None)
request_headers = request.get("Access-Control-Request-Headers", None)
if (cherrypy.request.method == "OPTIONS" and
request_method and request_headers):
response['Access-Control-Allow-Headers'] = request_headers
response['Access-Control-Allow-Methods'] = ', '.join(methods)
# Try to stop further processing and return a 200 OK
cherrypy.response.status = "200 OK"
cherrypy.response.body = ""
cherrypy.request.handler = lambda: ""
return
# Reject methods that were not explicitly allowed
if cherrypy.request.method not in methods:
raise cherrypy.HTTPError(405)
cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
# CherryPy apps # CherryPy apps
class Root(NilmApp): class Root(NilmApp):
@@ -142,7 +177,7 @@ class Stream(NilmApp):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror(NilmDBError, ValueError) @exception_to_httperror(NilmDBError, ValueError)
@cherrypy.tools.allow_methods(methods = ["POST"]) @cherrypy.tools.CORS_allow(methods = ["POST"])
def create(self, path, layout): def create(self, path, layout):
"""Create a new stream in the database. Provide path """Create a new stream in the database. Provide path
and one of the nilmdb.layout.layouts keys. and one of the nilmdb.layout.layouts keys.
@@ -153,7 +188,7 @@ class Stream(NilmApp):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror(NilmDBError) @exception_to_httperror(NilmDBError)
@cherrypy.tools.allow_methods(methods = ["POST"]) @cherrypy.tools.CORS_allow(methods = ["POST"])
def destroy(self, path): def destroy(self, path):
"""Delete a stream and its associated data.""" """Delete a stream and its associated data."""
return self.db.stream_destroy(path) return self.db.stream_destroy(path)
@@ -186,7 +221,7 @@ class Stream(NilmApp):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror(NilmDBError, LookupError, TypeError) @exception_to_httperror(NilmDBError, LookupError, TypeError)
@cherrypy.tools.allow_methods(methods = ["POST"]) @cherrypy.tools.CORS_allow(methods = ["POST"])
def set_metadata(self, path, data): def set_metadata(self, path, data):
"""Set metadata for the named stream, replacing any """Set metadata for the named stream, replacing any
existing metadata. Data should be a json-encoded existing metadata. Data should be a json-encoded
@@ -198,7 +233,7 @@ class Stream(NilmApp):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror(NilmDBError, LookupError, TypeError) @exception_to_httperror(NilmDBError, LookupError, TypeError)
@cherrypy.tools.allow_methods(methods = ["POST"]) @cherrypy.tools.CORS_allow(methods = ["POST"])
def update_metadata(self, path, data): def update_metadata(self, path, data):
"""Update metadata for the named stream. Data """Update metadata for the named stream. Data
should be a json-encoded dictionary""" should be a json-encoded dictionary"""
@@ -208,7 +243,7 @@ class Stream(NilmApp):
# /stream/insert?path=/newton/prep # /stream/insert?path=/newton/prep
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@cherrypy.tools.allow_methods(methods = ["PUT"]) @cherrypy.tools.CORS_allow(methods = ["PUT"])
def insert(self, path, start, end): def insert(self, path, start, end):
""" """
Insert new data into the database. Provide textual data Insert new data into the database. Provide textual data
@@ -264,7 +299,7 @@ class Stream(NilmApp):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror(NilmDBError) @exception_to_httperror(NilmDBError)
@cherrypy.tools.allow_methods(methods = ["POST"]) @cherrypy.tools.CORS_allow(methods = ["POST"])
def remove(self, path, start = None, end = None): def remove(self, path, start = None, end = None):
""" """
Remove data from the backend database. Removes all data in Remove data from the backend database. Removes all data in
@@ -419,16 +454,14 @@ class Server(object):
'error_page.default': self.json_error_page, 'error_page.default': self.json_error_page,
}) })
# Send a permissive Access-Control-Allow-Origin (CORS) header # Some default headers to just help identify that things are working
# with all responses so that browsers can send cross-domain app_config.update({ 'response.headers.X-Jim-Is-Awesome': 'yeah' })
# requests to this server.
app_config.update({ 'response.headers.Access-Control-Allow-Origin':
'*' })
# Only allow GET and HEAD by default. Individual handlers # Set up Cross-Origin Resource Sharing (CORS) handler so we
# can override. # can correctly respond to browsers' CORS preflight requests.
app_config.update({ 'tools.allow_methods.on': True, # This also limits verbs to GET and HEAD by default.
'tools.allow_methods.methods': ['GET', 'HEAD'] }) app_config.update({ 'tools.CORS_allow.on': True,
'tools.CORS_allow.methods': ['GET', 'HEAD'] })
# Send tracebacks in error responses. They're hidden by the # Send tracebacks in error responses. They're hidden by the
# error_page function for client errors (code 400-499). # error_page function for client errors (code 400-499).

View File

@@ -55,12 +55,6 @@ class TestClient(object):
client.version() client.version()
client.close() client.close()
# Trigger same error with a PUT request
client = nilmdb.Client(url = "http://localhost:1/")
with assert_raises(nilmdb.client.ServerError):
client.version()
client.close()
# Then a fake URL on a real host # Then a fake URL on a real host
client = nilmdb.Client(url = "http://localhost:32180/fake/") client = nilmdb.Client(url = "http://localhost:32180/fake/")
with assert_raises(nilmdb.client.ClientError): with assert_raises(nilmdb.client.ClientError):
@@ -360,12 +354,6 @@ class TestClient(object):
raise AssertionError("/stream/extract is not text/plain:\n" + raise AssertionError("/stream/extract is not text/plain:\n" +
headers()) headers())
# Make sure Access-Control-Allow-Origin gets set
if "access-control-allow-origin: " not in headers():
raise AssertionError("No Access-Control-Allow-Origin (CORS) "
"header in /stream/extract response:\n" +
headers())
client.close() client.close()
def test_client_08_unicode(self): def test_client_08_unicode(self):

View File

@@ -11,12 +11,7 @@ from nose.tools import assert_raises
import itertools import itertools
import os import os
import re import re
import shutil
import sys import sys
import threading
import urllib2
from urllib2 import urlopen, HTTPError
import Queue
import StringIO import StringIO
import shlex import shlex

View File

@@ -9,14 +9,7 @@ from nose.tools import assert_raises
import distutils.version import distutils.version
import itertools import itertools
import os import os
import shutil
import sys import sys
import cherrypy
import threading
import urllib2
from urllib2 import urlopen, HTTPError
import Queue
import cStringIO
import random import random
import unittest import unittest

View File

@@ -6,15 +6,13 @@ import distutils.version
import simplejson as json import simplejson as json
import itertools import itertools
import os import os
import shutil
import sys import sys
import cherrypy
import threading import threading
import urllib2 import urllib2
from urllib2 import urlopen, HTTPError from urllib2 import urlopen, HTTPError
import Queue
import cStringIO import cStringIO
import time import time
import requests
from nilmdb.utils import serializer_proxy from nilmdb.utils import serializer_proxy
@@ -208,3 +206,34 @@ class TestServer(object):
data = getjson("/stream/get_metadata?path=/newton/prep" data = getjson("/stream/get_metadata?path=/newton/prep"
"&key=foo") "&key=foo")
eq_(data, {'foo': None}) eq_(data, {'foo': None})
def test_cors_headers(self):
# Test that CORS headers are being set correctly
# Normal GET should send simple response
url = "http://127.0.0.1:32180/stream/list"
r = requests.get(url, headers = { "Origin": "http://google.com/" })
eq_(r.status_code, 200)
if "access-control-allow-origin" not in r.headers:
raise AssertionError("No Access-Control-Allow-Origin (CORS) "
"header in response:\n", r.headers)
eq_(r.headers["access-control-allow-origin"], "http://google.com/")
# OPTIONS without CORS preflight headers should result in 405
r = requests.options(url, headers = {
"Origin": "http://google.com/",
})
eq_(r.status_code, 405)
# OPTIONS with preflight headers should give preflight response
r = requests.options(url, headers = {
"Origin": "http://google.com/",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "X-Custom",
})
eq_(r.status_code, 200)
if "access-control-allow-origin" not in r.headers:
raise AssertionError("No Access-Control-Allow-Origin (CORS) "
"header in response:\n", r.headers)
eq_(r.headers["access-control-allow-methods"], "GET, HEAD")
eq_(r.headers["access-control-allow-headers"], "X-Custom")