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
if not args.quiet:
print "Version: %s" % nilmdb.__version__
print "Database: %s" % (os.path.realpath(args.database))
if args.address == '0.0.0.0' or args.address == '::':
host = socket.getfqdn()

View File

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

View File

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

View File

@@ -55,12 +55,6 @@ class TestClient(object):
client.version()
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
client = nilmdb.Client(url = "http://localhost:32180/fake/")
with assert_raises(nilmdb.client.ClientError):
@@ -360,12 +354,6 @@ class TestClient(object):
raise AssertionError("/stream/extract is not text/plain:\n" +
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()
def test_client_08_unicode(self):

View File

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

View File

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

View File

@@ -6,15 +6,13 @@ import distutils.version
import simplejson as json
import itertools
import os
import shutil
import sys
import cherrypy
import threading
import urllib2
from urllib2 import urlopen, HTTPError
import Queue
import cStringIO
import time
import requests
from nilmdb.utils import serializer_proxy
@@ -208,3 +206,34 @@ class TestServer(object):
data = getjson("/stream/get_metadata?path=/newton/prep"
"&key=foo")
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")