| // 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 |