blob: 1a3c37c6194c84807c9ecb58124f789a1867e5fe [file] [log] [blame]
// Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "entd/callback_server.h"
#include <base/memory/scoped_ptr.h>
#include "entd/entd.h"
namespace entd {
using std::string;
std::string CallbackServer::session_id_;
namespace {
// Max 1k request entity
const int kMaxRequestSize = 1024;
// libevent defines some of these but not all, so we define what we need here
const int kHttpOk = 200;
const int kHttpBadRequest = 400;
const int kHttpNotFound = 404;
const int kHttpBadMethod = 405;
const int kHttpBadEntitySize = 413;
const int kHttpBadMedia = 415;
const int kHttpServerError = 500;
const uint32_t kDefaultPort = 5199; // Atomic weight of Cr, sorta
const uint32_t kMinPort = 5000;
const uint32_t kMaxPort = 5999;
// Incoming requests must have this content type
const std::string kContentType = "application/json; charset=UTF-8";
// Callback functions must have this prefix, so we can avoid dispatching
// against default properties that aren't actually intended to be callbacks.
const std::string kCallbackPrefix = "cb:";
// Called by libevent when it accepts an http request.
void dispatch_OnRequest(struct evhttp_request* request, void* data) {
CallbackServer* cbs = reinterpret_cast<CallbackServer*>(data);
cbs->SetBusy(true);
cbs->OnRequest(request);
cbs->SetBusy(false);
}
v8::Handle<v8::Value> dispatch_Start(const v8::Arguments& args) {
CallbackServer* cs = CallbackServer::Unwrap(args.This());
if (!cs) {
utils::ThrowV8Exception("Method called on invalid object");
return v8::Undefined();
}
if (cs->IsRunning()) {
utils::ThrowV8Exception("Callback server is already running");
return v8::Undefined();
}
if (args.Length() < 1 || !args[0]->IsObject()) {
utils::ThrowV8Exception("Missing or invalid parameter: callbacks");
return v8::Undefined();
}
v8::Handle<v8::Object> callbacks = v8::Handle<v8::Object>::Cast(args[0]);
uint32_t port;
if (args.Length() > 1)
port = args[1]->Uint32Value();
else
port = kDefaultPort;
if (port < kMinPort || port > kMaxPort) {
utils::ThrowV8Exception("Invalid port");
return v8::Undefined();
}
if (!cs->Start(callbacks, port)) {
utils::ThrowV8Exception("Unable to start callback server");
return v8::Undefined();
}
return v8::Undefined();
}
v8::Handle<v8::Value> dispatch_Stop(const v8::Arguments& args) {
CallbackServer* cs = CallbackServer::Unwrap(args.This());
if (!cs) {
utils::ThrowV8Exception("Method called on invalid object");
return v8::Undefined();
}
if (!cs->IsRunning()) {
utils::ThrowV8Exception("Callback server is not running");
return v8::Undefined();
}
if (cs->IsBusy()) {
utils::ThrowV8Exception("Can't stop server while busy");
return v8::Undefined();
}
cs->Stop();
return v8::Undefined();
}
} // namespace
std::string CallbackServer::required_origin = "";
CallbackServer::CallbackServer(Entd* entd)
: busy_(false),
entd_(entd),
evhttp_(NULL)
{}
CallbackServer::~CallbackServer() {
CleanupTemplate();
Stop();
}
// static
void CallbackServer::SetTemplateBindings(
v8::Handle<v8::ObjectTemplate> template_object) {
template_object->Set(v8::String::NewSymbol("start"),
v8::FunctionTemplate::New(dispatch_Start));
template_object->Set(v8::String::NewSymbol("stop"),
v8::FunctionTemplate::New(dispatch_Stop));
}
void CallbackServer::OnRequest(struct evhttp_request* request) {
const char* uri = evhttp_request_uri(request);
// It might be nicer (certainly more RESTful) if the function were part
// of the URI, but that would be more parsing which might add to the
// exploitability of this code. Instead we only accept requests for
// "/dispatch", and leave the parsing to v8's native JSON parser.
if (strcmp("/dispatch", uri) != 0) {
LOG(ERROR) << "Not found: " << uri;
evhttp_send_error(request, kHttpNotFound, "Not found");
return;
}
// All requests must be POSTs, since it's a little harder to sneak those
// into other web pages.
if (request->type != EVHTTP_REQ_POST) {
LOG(ERROR) << "Invalid HTTP method";
evhttp_send_error(request, kHttpBadMethod, "Method not allowed");
return;
}
// Content type must be json. We won't be doing any form parsing,
// multipart or otherwise.
const char* header = evhttp_find_header(request->input_headers,
"Content-Type");
if (!header || strcmp(kContentType.c_str(), header) != 0) {
LOG(ERROR) << "Invalid Content-Type: " << header;
evhttp_send_error(request, kHttpBadMedia, "Bad Content-Type");
return;
}
// Check the session ID.
header = evhttp_find_header(request->input_headers, "X-Entd-Session-Id");
if (!header || header != session_id_) {
LOG(ERROR) << "Bad or missing X-Entd-Session-Id header: " << header;
evhttp_send_error(request, kHttpBadRequest,
"Bad or missing session id header");
return;
}
if (!CallbackServer::required_origin.empty()) {
header = evhttp_find_header(request->input_headers, "Origin");
if (!header ||
strcmp(CallbackServer::required_origin.c_str(), header) != 0) {
LOG(ERROR) << "Bad or missing Origin header: " << header;
evhttp_send_error(request, kHttpBadRequest,
"Bad or missing Origin header");
return;
}
}
// Check the length of the request entity. Libevent has already read it
// into memory, but we can at least refuse to parse it if it's too large.
int length = EVBUFFER_LENGTH(request->input_buffer);
if (length > kMaxRequestSize) {
LOG(ERROR) << "Request entity too large: " << length;
evhttp_send_error(request, kHttpBadEntitySize, "Request too large");
return;
}
scoped_array<char> body(new char[length + 1]);
if (!body.get()) {
LOG(ERROR) << "Error allocating input buffer";
evhttp_send_error(request, kHttpServerError, "Internal server error");
return;
}
// Read the request entity into body
evbuffer_remove(request->input_buffer, reinterpret_cast<void*>(body.get()),
length);
body[length] = '\0';
v8::HandleScope handle_scope;
// Parse the body as JSON.
v8::Handle<v8::Value> value;
if (!entd_->JsonParse(v8::String::New(body.get()), &value)) {
LOG(WARNING) << "Could not parse request entity as JSON.";
evhttp_send_error(request, kHttpBadRequest, "Invalid JSON syntax");
return;
}
// The JSON must have parsed into an Object. Arrays and scalars are
// rejected.
if (!value->IsObject()) {
LOG(WARNING) << "JSON did not represent an object.";
evhttp_send_error(request, kHttpBadRequest, "Invalid JSON value");
return;
}
v8::Handle<v8::Object> params = v8::Handle<v8::Object>::Cast(value);
// Pull the function name out of the parsed json. It must be a string.
value = params->Get(v8::String::NewSymbol("function"));
if (!value->IsString()) {
LOG(WARNING) << "Missing or invalid function name.";
evhttp_send_error(request, kHttpBadRequest,
"Missing or invalid function name");
return;
}
// Append the callback prefix to the specified function name.
v8::Local<v8::String> function_key = v8::String::Concat(
v8::String::NewSymbol(kCallbackPrefix.c_str()), value->ToString());
LOG(INFO) << "Dispatching: " << *v8::String::Utf8Value(function_key);
// Check the callbacks object for a matching function.
value = callbacks_->Get(function_key);
if (!value->IsFunction()) {
LOG(WARNING) << "Not found: " <<
std::string(*v8::String::Utf8Value(function_key));
evhttp_send_error(request, kHttpBadRequest, "Unknown function");
return;
}
// Pull out the optional argument from the JSON.
v8::Handle<v8::Function> function = v8::Handle<v8::Function>::Cast(value);
value = params->Get(v8::String::NewSymbol("argument"));
// Dispatch the function.
v8::TryCatch try_catch;
v8::Handle<v8::Value> return_value = function->Call(callbacks_, 1, &value);
// Any exception is reported to the client only as a generic 500 server
// error. Policies can invent their own ways to report soft failures.
if (try_catch.HasCaught()) {
utils::ReportV8Exception(&try_catch);
evhttp_send_error(request, kHttpServerError, "Internal server error");
return;
}
// Stringify any result of the callback into JSON.
if (!entd_->JsonStringify(return_value, &value)) {
LOG(WARNING) << "Could not stringify response value.";
evhttp_send_error(request, kHttpServerError, "Error stringifying result");
return;
}
// Allocate this buffer as late as possible, since we have to make sure
// to free it before returning.
struct evbuffer* output = evbuffer_new();
if (!output) {
LOG(ERROR) << "Error allocating output buffer";
evhttp_send_error(request, 500, "Internal server error");
return;
}
// Copy the stringification to the reply and send it to the client.
std::string result = *v8::String::Utf8Value(value);
evbuffer_add(output, result.c_str(), result.length());
evhttp_send_reply(request, kHttpOk, "OK", output);
evbuffer_free(output);
}
bool CallbackServer::Start(v8::Handle<v8::Object> callbacks, int port) {
if (evhttp_)
Stop();
LOG(INFO) << "Starting callback server on port " << port;
evhttp_ = evhttp_new(NULL);
if (!evhttp_) {
LOG(ERROR) << "Error creating callback server.";
return false;
}
evhttp_set_gencb(evhttp_, dispatch_OnRequest, reinterpret_cast<void*>(this));
int rv = evhttp_bind_socket(evhttp_, "127.0.0.1", port);
if (rv == -1) {
LOG(ERROR) << "Error binding to port.";
Stop();
return false;
}
callbacks_ = v8::Persistent<v8::Object>::New(callbacks);
port_ = port;
return true;
}
void CallbackServer::Stop() {
if (!evhttp_)
return;
LOG(INFO) << "Stopping callback server.";
evhttp_free(evhttp_);
evhttp_ = NULL;
port_ = 0;
callbacks_.Dispose();
}
bool CallbackServer::IsRunning() {
return evhttp_ != NULL;
}
} // namespace entd