Skip to content

Commit

Permalink
Add OPTIONS responses for CORS preflight requests. This required moving
Browse files Browse the repository at this point in the history
around REST auth a bit since OPTIONS doesn't need auth.
@todo: add the admin screen to set the parameters
  • Loading branch information
shadlaws committed Jun 15, 2013
1 parent 5c134db commit e13c88e
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 24 deletions.
81 changes: 61 additions & 20 deletions modules/rest/classes/Rest/Controller/Rest.php
Expand Up @@ -47,32 +47,16 @@ abstract class Rest_Controller_Rest extends Controller {
);

/**
* Get the REST access key (if provided), attempt to login the user, and check auth.
* The only two possible results are a successful login or a 403 Forbidden. Because
* of this, the $auth variable is simply passed through without modification.
* Override Controller::check_auth() since REST doesn't have pages for login or reauth redirects.
* We check maintenance mode here, and handle REST authentication in Controller_Rest::before().
*
* NOTE: this doesn't extend Controller::check_auth(), but rather *replaces* it with
* its restful counterpart (i.e. parent::check_auth() is never called).
*
* @see Controller::check_auth(), which is replaced by this implementation
* @see Controller::auth_for_private_gallery()
* @see Controller::auth_for_maintenance_mode()
* @see Rest::set_active_user()
*/
public function check_auth($auth) {
// Get the access key (if provided)
$key = $this->request->headers("X-Gallery-Request-Key");
if (empty($key)) {
$key = ($this->request->method() == HTTP_Request::GET) ?
$this->request->query("access_key") : $this->request->post("access_key");
}

// Attempt to login the user. This will fire a 403 Forbidden if unsuccessful.
Rest::set_active_user($key);

// Check for maintenance mode or private gallery restrictions. Since there is no
// redirection to login/reauthenticate screen in REST, fire a 403 Forbidden if found.
if ($this->auth_for_maintenance_mode() || $this->auth_for_private_gallery()) {
if (Module::get_var("gallery", "maintenance_mode", 0)) {
throw Rest_Exception::factory(403);
}

Expand All @@ -94,14 +78,36 @@ public function before() {
$method = $this->request->method();
}

// If the method is not one of GET, POST, PUT, or DELETE, fire a 405 Method Not Allowed.
// If the method is not allowed, fire a 405 Method Not Allowed.
if (!in_array($method, Rest::$allowed_methods)) {
throw Rest_Exception::factory(405);
}

// Set the action as the method.
$this->request->action(strtolower($method));

// If we have an OPTIONS request, we're done here. This intentionally skips login.
if ($method == HTTP_Request::OPTIONS) {
return;
}

// Get the access key (if provided)
$key = $this->request->headers("X-Gallery-Request-Key");
if (empty($key)) {
$key = ($this->request->method() == HTTP_Request::GET) ?
$this->request->query("access_key") : $this->request->post("access_key");
}

// Attempt to login the user. This will fire a 403 Forbidden if unsuccessful.
Rest::set_active_user($key);

// Process the "Origin" header if sent (not required).
if (($method != HTTP_Request::OPTIONS) &&
($origin = $this->request->headers("Origin")) &&
Rest::approve_origin($origin)) {
$this->response->headers("Access-Control-Allow-Origin", $origin);
}

// Get the REST type and id (note: strlen("Controller_Rest_") --> 16).
$this->rest_type = Inflector::convert_class_to_module_name(substr(get_class($this), 16));
$this->rest_id = $this->request->arg_optional(0);
Expand Down Expand Up @@ -341,6 +347,41 @@ public function action_delete() {
Rest::delete($this->rest_type, $this->rest_id, $this->request->post());
}

/**
* Send an OPTIONS response for a CORS preflight request. This action should *not*
* ever be overriden in REST resource classes.
* @see http://www.w3.org/TR/cors
*/
public function action_options() {
$origin = $this->request->headers("Origin"); // required
$method = $this->request->headers("Access-Control-Request-Method"); // required
$headers = $this->request->headers("Access-Control-Request-Headers"); // optional

$allow_origin = Rest::approve_origin($origin);
$allow_method = (!$method || in_array(strtoupper($method), Rest::$allowed_methods));
$allow_headers = true;
if (!empty($headers)) {
$allowed_headers = array_map("strtolower", Rest::$allowed_headers);
$headers = explode(",", $headers);
foreach ($headers as $header) {
if (!in_array(strtolower(trim($header)), $allowed_headers)) {
$allow_headers = false;
}
}
}

if (!$allow_origin || !$allow_method || !$allow_headers) {
throw Rest_Exception::factory(403);
}

// CORS preflight passed - send response (headers only, no body).
$this->response->headers("Access-Control-Allow-Origin", $allow_origin);
$this->response->headers("Access-Control-Allow-Methods", Rest::$allowed_methods);
$this->response->headers("Access-Control-Allow-Headers", Rest::$allowed_headers);
$this->response->headers("Access-Control-Expose-Headers", Rest::$exposed_headers);
$this->response->headers("Access-Control-Max-Age", Rest::$preflight_max_age);
}

/**
* Check if a method is defined for this resource. This is called by the standard
* implementations of action_get(), action_post(), etc., and fires a 400 Bad Request
Expand Down
6 changes: 3 additions & 3 deletions modules/rest/classes/Rest/Controller/Rest/AccessKey.php
Expand Up @@ -18,7 +18,7 @@
* Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
*/
class Rest_Controller_Rest_AccessKey extends Controller_Rest {
public function check_auth($auth) {
public function before() {
if ($this->request->method() != HTTP_Request::GET) {
// Check login using "user" and "password" fields in POST. Fire a 403 Forbidden if it fails.
if (!Validation::factory($this->request->post())
Expand All @@ -31,12 +31,12 @@ public function check_auth($auth) {
$this->request->headers("X-Gallery-Request-Key", Rest::access_key());
}

return parent::check_auth($auth);
return parent::before();
}

public function action_get() {
// We want to return an empty response with either status 200 or 403, depending on if guest
// access is allowed. Since Controller_Rest::check_auth() would have already fired a 403
// access is allowed. Since Controller_Rest::before() would have already fired a 403
// if a login was required, we have nothing left to do here - this will return a 200.
}

Expand Down
52 changes: 51 additions & 1 deletion modules/rest/classes/Rest/Rest.php
Expand Up @@ -24,9 +24,23 @@ class Rest_Rest {
HTTP_Request::GET,
HTTP_Request::POST,
HTTP_Request::PUT,
HTTP_Request::DELETE
HTTP_Request::DELETE,
HTTP_Request::OPTIONS
);

static $allowed_headers = array(
"X-Gallery-Request-Key",
"X-Gallery-Request-Method",
"X-Requested-With"
);

static $exposed_headers = array(
"X-Gallery-Api-Version",
"Allow"
);

static $preflight_max_age = 604800; // one week

static function init() {
// Add the REST API version and allowed methods to the header. Since we're adding it to
// Response::$default_config, even error responses (e.g. 404) will have these headers.
Expand Down Expand Up @@ -340,6 +354,42 @@ static function registry($return_class_names=false) {
return array_unique($results);
}

/**
* Approve an "Origin" header value. This checks the REST config and returns an
* "Access-Control-Allow-Origin" header value if it passes or false if it fails.
*
* @param string origin
* @return mixed false if failed, string if passed
* @see http://www.w3.org/TR/cors
*/
static function approve_origin($origin) {
$origin = strtolower($origin);
if (empty($origin)) {
return false;
}

switch (Module::get_var("rest", "cors_embedding", "none")) {
case "all":
return "*";

case "list":
foreach (unserialize(Module::get_var("rest", "approved_domains", array())) as $domain) {
// Check the end of the sent origin against our list. So, if "example.com" is approved,
// then "foo.example.com", "http://example.com", and "https://foo.example.com" are also
// approved.
if (substr($origin, -strlen($domain)) == $domain) {
return $origin;
}
}

case "none":
default:
// No checks if "none" or invalid.
}

return false;
}

static protected function _call_rest_func($func, $type, $id, $params) {
if (is_array($type)) {
list ($type, $id, $params) = static::_split_triad($type);
Expand Down

0 comments on commit e13c88e

Please sign in to comment.