2011-01-24 18:00:29 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Copyright 2011 Facebook, Inc.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2011-07-04 21:03:36 +02:00
|
|
|
/**
|
|
|
|
* @group conduit
|
|
|
|
*/
|
2011-01-24 18:00:29 +01:00
|
|
|
class PhabricatorConduitAPIController
|
|
|
|
extends PhabricatorConduitController {
|
|
|
|
|
2011-01-26 02:17:19 +01:00
|
|
|
public function shouldRequireLogin() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2011-01-24 18:00:29 +01:00
|
|
|
private $method;
|
|
|
|
|
|
|
|
public function willProcessRequest(array $data) {
|
|
|
|
$this->method = $data['method'];
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function processRequest() {
|
|
|
|
$time_start = microtime(true);
|
|
|
|
$request = $this->getRequest();
|
|
|
|
|
|
|
|
$method = $this->method;
|
|
|
|
|
|
|
|
$method_class = ConduitAPIMethod::getClassNameFromAPIMethodName($method);
|
|
|
|
$api_request = null;
|
|
|
|
|
|
|
|
$log = new PhabricatorConduitMethodCallLog();
|
|
|
|
$log->setMethod($method);
|
|
|
|
$metadata = array();
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (!class_exists($method_class)) {
|
|
|
|
throw new Exception(
|
|
|
|
"Unable to load the implementation class for method '{$method}'. ".
|
|
|
|
"You may have misspelled the method, need to define ".
|
|
|
|
"'{$method_class}', or need to run 'arc build'.");
|
|
|
|
}
|
|
|
|
|
2011-07-30 03:31:14 +02:00
|
|
|
$class_info = new ReflectionClass($method_class);
|
|
|
|
if ($class_info->isAbstract()) {
|
|
|
|
throw new Exception(
|
|
|
|
"Method '{$method}' is not valid; the implementation is an abstract ".
|
|
|
|
"base class.");
|
|
|
|
}
|
|
|
|
|
2011-01-24 18:00:29 +01:00
|
|
|
$method_handler = newv($method_class, array());
|
|
|
|
|
|
|
|
if (isset($_REQUEST['params']) && is_array($_REQUEST['params'])) {
|
|
|
|
$params_post = $request->getArr('params');
|
|
|
|
foreach ($params_post as $key => $value) {
|
2011-01-24 20:30:10 +01:00
|
|
|
$params_post[$key] = json_decode($value, true);
|
2011-01-24 18:00:29 +01:00
|
|
|
}
|
|
|
|
$params = $params_post;
|
|
|
|
} else {
|
|
|
|
$params_json = $request->getStr('params');
|
|
|
|
if (!strlen($params_json)) {
|
|
|
|
$params = array();
|
|
|
|
} else {
|
2011-01-24 20:30:10 +01:00
|
|
|
$params = json_decode($params_json, true);
|
2011-01-24 18:00:29 +01:00
|
|
|
if (!is_array($params)) {
|
|
|
|
throw new Exception(
|
|
|
|
"Invalid parameter information was passed to method ".
|
|
|
|
"'{$method}', could not decode JSON serialization.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$metadata = idx($params, '__conduit__', array());
|
|
|
|
unset($params['__conduit__']);
|
|
|
|
|
2011-04-08 03:27:39 +02:00
|
|
|
$result = null;
|
|
|
|
|
2011-01-24 18:00:29 +01:00
|
|
|
$api_request = new ConduitAPIRequest($params);
|
|
|
|
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
$allow_unguarded_writes = false;
|
2011-04-08 03:27:39 +02:00
|
|
|
$auth_error = null;
|
|
|
|
if ($method_handler->shouldRequireAuthentication()) {
|
|
|
|
$auth_error = $this->authenticateUser($api_request, $metadata);
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
// If we've explicitly authenticated the user here and either done
|
|
|
|
// CSRF validation or are using a non-web authentication mechanism.
|
|
|
|
$allow_unguarded_writes = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($method_handler->shouldAllowUnguardedWrites()) {
|
|
|
|
$allow_unguarded_writes = true;
|
2011-02-06 07:36:21 +01:00
|
|
|
}
|
|
|
|
|
2011-04-08 03:27:39 +02:00
|
|
|
if ($auth_error === null) {
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
if ($allow_unguarded_writes) {
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
}
|
2011-02-06 07:36:21 +01:00
|
|
|
try {
|
|
|
|
$result = $method_handler->executeMethod($api_request);
|
|
|
|
$error_code = null;
|
|
|
|
$error_info = null;
|
|
|
|
} catch (ConduitException $ex) {
|
|
|
|
$result = null;
|
|
|
|
$error_code = $ex->getMessage();
|
|
|
|
$error_info = $method_handler->getErrorDescription($error_code);
|
|
|
|
}
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
if ($allow_unguarded_writes) {
|
|
|
|
unset($unguarded);
|
|
|
|
}
|
2011-04-08 03:27:39 +02:00
|
|
|
} else {
|
|
|
|
list($error_code, $error_info) = $auth_error;
|
2011-01-24 18:00:29 +01:00
|
|
|
}
|
|
|
|
} catch (Exception $ex) {
|
2011-08-16 22:05:12 +02:00
|
|
|
phlog($ex);
|
2011-01-24 18:00:29 +01:00
|
|
|
$result = null;
|
|
|
|
$error_code = 'ERR-CONDUIT-CORE';
|
|
|
|
$error_info = $ex->getMessage();
|
|
|
|
}
|
|
|
|
|
|
|
|
$time_end = microtime(true);
|
|
|
|
|
|
|
|
$connection_id = null;
|
|
|
|
if (idx($metadata, 'connectionID')) {
|
|
|
|
$connection_id = $metadata['connectionID'];
|
|
|
|
} else if (($method == 'conduit.connect') && $result) {
|
|
|
|
$connection_id = idx($result, 'connectionID');
|
|
|
|
}
|
|
|
|
|
|
|
|
$log->setConnectionID($connection_id);
|
|
|
|
$log->setError((string)$error_code);
|
|
|
|
$log->setDuration(1000000 * ($time_end - $time_start));
|
2011-04-13 04:00:54 +02:00
|
|
|
|
|
|
|
// TODO: This is a hack, but the insert is comparatively expensive and
|
|
|
|
// we only really care about having these logs for real CLI clients, if
|
|
|
|
// even that.
|
|
|
|
if (empty($metadata['authToken'])) {
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
2011-04-13 04:00:54 +02:00
|
|
|
$log->save();
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
unset($unguarded);
|
2011-04-13 04:00:54 +02:00
|
|
|
}
|
2011-01-24 18:00:29 +01:00
|
|
|
|
|
|
|
$result = array(
|
|
|
|
'result' => $result,
|
|
|
|
'error_code' => $error_code,
|
|
|
|
'error_info' => $error_info,
|
|
|
|
);
|
|
|
|
|
|
|
|
switch ($request->getStr('output')) {
|
|
|
|
case 'human':
|
|
|
|
return $this->buildHumanReadableResponse(
|
|
|
|
$method,
|
|
|
|
$api_request,
|
|
|
|
$result);
|
|
|
|
case 'json':
|
|
|
|
default:
|
|
|
|
return id(new AphrontFileResponse())
|
|
|
|
->setMimeType('application/json')
|
2011-02-19 22:02:25 +01:00
|
|
|
->setContent('for(;;);'.json_encode($result));
|
2011-01-24 18:00:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-04-08 03:27:39 +02:00
|
|
|
/**
|
|
|
|
* Authenticate the client making the request to a Phabricator user account.
|
|
|
|
*
|
|
|
|
* @param ConduitAPIRequest Request being executed.
|
|
|
|
* @param dict Request metadata.
|
|
|
|
* @return null|pair Null to indicate successful authentication, or
|
|
|
|
* an error code and error message pair.
|
|
|
|
*/
|
|
|
|
private function authenticateUser(
|
|
|
|
ConduitAPIRequest $api_request,
|
|
|
|
array $metadata) {
|
|
|
|
|
|
|
|
$request = $this->getRequest();
|
|
|
|
|
|
|
|
if ($request->getUser()->getPHID()) {
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
$request->validateCSRF();
|
2011-04-08 03:27:39 +02:00
|
|
|
$api_request->setUser($request->getUser());
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2011-04-13 03:06:25 +02:00
|
|
|
// Handle sessionless auth. TOOD: This is super messy.
|
|
|
|
if (isset($metadata['authUser'])) {
|
|
|
|
$user = id(new PhabricatorUser())->loadOneWhere(
|
|
|
|
'userName = %s',
|
|
|
|
$metadata['authUser']);
|
|
|
|
if (!$user) {
|
|
|
|
return array(
|
|
|
|
'ERR-INVALID-AUTH',
|
|
|
|
'Authentication is invalid.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$token = idx($metadata, 'authToken');
|
|
|
|
$signature = idx($metadata, 'authSignature');
|
|
|
|
$certificate = $user->getConduitCertificate();
|
|
|
|
if (sha1($token.$certificate) !== $signature) {
|
|
|
|
return array(
|
|
|
|
'ERR-INVALID-AUTH',
|
|
|
|
'Authentication is invalid.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$api_request->setUser($user);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2011-04-08 03:27:39 +02:00
|
|
|
$session_key = idx($metadata, 'sessionKey');
|
|
|
|
if (!$session_key) {
|
|
|
|
return array(
|
2011-04-10 22:08:47 +02:00
|
|
|
'ERR-INVALID-SESSION',
|
|
|
|
'Session key is not present.'
|
2011-04-08 03:27:39 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$session = queryfx_one(
|
|
|
|
id(new PhabricatorUser())->establishConnection('r'),
|
|
|
|
'SELECT * FROM %T WHERE sessionKey = %s',
|
|
|
|
PhabricatorUser::SESSION_TABLE,
|
|
|
|
$session_key);
|
|
|
|
if (!$session) {
|
|
|
|
return array(
|
|
|
|
'ERR-INVALID-SESSION',
|
|
|
|
'Session key is invalid.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Make sessions timeout.
|
|
|
|
// TODO: When we pull a session, read connectionID from the session table.
|
|
|
|
|
|
|
|
$user = id(new PhabricatorUser())->loadOneWhere(
|
|
|
|
'phid = %s',
|
|
|
|
$session['userPHID']);
|
|
|
|
if (!$user) {
|
|
|
|
return array(
|
|
|
|
'ERR-INVALID-SESSION',
|
|
|
|
'Session is for nonexistent user.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$api_request->setUser($user);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2011-01-24 18:00:29 +01:00
|
|
|
private function buildHumanReadableResponse(
|
|
|
|
$method,
|
|
|
|
ConduitAPIRequest $request = null,
|
|
|
|
$result = null) {
|
|
|
|
|
|
|
|
$param_rows = array();
|
2011-07-29 06:32:11 +02:00
|
|
|
$param_rows[] = array('Method', $this->renderAPIValue($method));
|
2011-01-24 18:00:29 +01:00
|
|
|
if ($request) {
|
|
|
|
foreach ($request->getAllParameters() as $key => $value) {
|
|
|
|
$param_rows[] = array(
|
|
|
|
phutil_escape_html($key),
|
2011-07-29 06:32:11 +02:00
|
|
|
$this->renderAPIValue($value),
|
2011-01-24 18:00:29 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$param_table = new AphrontTableView($param_rows);
|
|
|
|
$param_table->setColumnClasses(
|
|
|
|
array(
|
|
|
|
'header',
|
|
|
|
'wide',
|
|
|
|
));
|
|
|
|
|
|
|
|
$result_rows = array();
|
|
|
|
foreach ($result as $key => $value) {
|
|
|
|
$result_rows[] = array(
|
|
|
|
phutil_escape_html($key),
|
2011-07-29 06:32:11 +02:00
|
|
|
$this->renderAPIValue($value),
|
2011-01-24 18:00:29 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$result_table = new AphrontTableView($result_rows);
|
|
|
|
$result_table->setColumnClasses(
|
|
|
|
array(
|
|
|
|
'header',
|
|
|
|
'wide',
|
|
|
|
));
|
|
|
|
|
|
|
|
$param_panel = new AphrontPanelView();
|
|
|
|
$param_panel->setHeader('Method Parameters');
|
|
|
|
$param_panel->appendChild($param_table);
|
|
|
|
|
|
|
|
$result_panel = new AphrontPanelView();
|
|
|
|
$result_panel->setHeader('Method Result');
|
|
|
|
$result_panel->appendChild($result_table);
|
|
|
|
|
|
|
|
return $this->buildStandardPageResponse(
|
|
|
|
array(
|
|
|
|
$param_panel,
|
|
|
|
$result_panel,
|
|
|
|
),
|
|
|
|
array(
|
|
|
|
'title' => 'Method Call Result',
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2011-07-29 06:32:11 +02:00
|
|
|
private function renderAPIValue($value) {
|
|
|
|
$json = new PhutilJSON();
|
|
|
|
if (is_array($value)) {
|
|
|
|
$value = $json->encodeFormatted($value);
|
|
|
|
$value = phutil_escape_html($value);
|
|
|
|
} else {
|
|
|
|
$value = phutil_escape_html($value);
|
|
|
|
}
|
|
|
|
|
|
|
|
$value = '<pre style="white-space: pre-wrap;">'.$value.'</pre>';
|
|
|
|
|
|
|
|
return $value;
|
|
|
|
}
|
|
|
|
|
2011-01-24 18:00:29 +01:00
|
|
|
}
|