From b71797c7a3dd454ddf53ee6c14af5c5a22be9272 Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Mon, 14 Sep 2015 13:46:40 +0200 Subject: API 2.0: Remove private fields from file/history Since this is a breaking change bump the api version to 2. The private fields are user_id and multipaste_id which where leaked via the multipaste_items field. This commit also adds a test case to both api versions that checks the returned fields. NOTE: Most of this commit is copied from the files of api v1 so when viewing the diff use --find-copies-harder for an easy to read diff. Signed-off-by: Florian Pritz --- application/controllers/api/v2/api_info.php | 16 + application/controllers/api/v2/file.php | 88 +++++ application/controllers/api/v2/user.php | 12 + application/service/files.php | 7 +- application/test/tests/test_api_v1.php | 22 ++ application/test/tests/test_api_v2.php | 476 ++++++++++++++++++++++++++++ 6 files changed, 620 insertions(+), 1 deletion(-) create mode 100644 application/controllers/api/v2/api_info.php create mode 100644 application/controllers/api/v2/file.php create mode 100644 application/controllers/api/v2/user.php create mode 100644 application/test/tests/test_api_v2.php diff --git a/application/controllers/api/v2/api_info.php b/application/controllers/api/v2/api_info.php new file mode 100644 index 000000000..f07086a1a --- /dev/null +++ b/application/controllers/api/v2/api_info.php @@ -0,0 +1,16 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace controllers\api\v2; + +class api_info extends \controllers\api\api_controller { + static public function get_version() + { + return "2.0.0"; + } +} diff --git a/application/controllers/api/v2/file.php b/application/controllers/api/v2/file.php new file mode 100644 index 000000000..ba80ae309 --- /dev/null +++ b/application/controllers/api/v2/file.php @@ -0,0 +1,88 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace controllers\api\v2; + +class file extends \controllers\api\api_controller { + public function __construct() + { + parent::__construct(); + + $this->load->model('mfile'); + $this->load->model('mmultipaste'); + } + + public function upload() + { + $this->muser->require_access("basic"); + + $files = getNormalizedFILES(); + + if (empty($files)) { + throw new \exceptions\PublicApiException("file/no-file", "No file was uploaded or unknown error occurred."); + } + + \service\files::verify_uploaded_files($files); + + $limits = $this->muser->get_upload_id_limits(); + $urls = array(); + + foreach ($files as $file) { + $id = $this->mfile->new_id($limits[0], $limits[1]); + \service\files::add_uploaded_file($id, $file["tmp_name"], $file["name"]); + $ids[] = $id; + $urls[] = site_url($id).'/'; + } + + return array( + "ids" => $ids, + "urls" => $urls, + ); + } + + public function get_config() + { + return array( + "upload_max_size" => $this->config->item("upload_max_size"), + "max_files_per_request" => intval(ini_get("max_file_uploads")), + "max_input_vars" => intval(ini_get("max_input_vars")), + "request_max_size" => return_bytes(ini_get("post_max_size")), + ); + } + + public function history() + { + $this->muser->require_access("apikey"); + $history = \service\files::history($this->muser->get_userid()); + # APIv1-cleanup: Remove this + foreach ($history['multipaste_items'] as $key => $item) { + unset($history['multipaste_items'][$key]['user_id']); + unset($history['multipaste_items'][$key]['multipaste_id']); + } + return $history; + } + + public function delete() + { + $this->muser->require_access("apikey"); + $ids = $this->input->post("ids"); + return \service\files::delete($ids); + } + + public function create_multipaste() + { + $this->muser->require_access("basic"); + $ids = $this->input->post("ids"); + $userid = $this->muser->get_userid(); + $limits = $this->muser->get_upload_id_limits(); + + return \service\files::create_multipaste($ids, $userid, $limits); + } + +} +# vim: set noet: diff --git a/application/controllers/api/v2/user.php b/application/controllers/api/v2/user.php new file mode 100644 index 000000000..2a233fe52 --- /dev/null +++ b/application/controllers/api/v2/user.php @@ -0,0 +1,12 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace controllers\api\v2; + +class user extends \controllers\api\v1\user { +} diff --git a/application/service/files.php b/application/service/files.php index 5e0dd140b..7cef73d97 100644 --- a/application/service/files.php +++ b/application/service/files.php @@ -51,7 +51,12 @@ class files { $multipaste_items_grouped = array(); $multipaste_items = array(); - $query = $CI->db->get_where("multipaste", array("user_id" => $user))->result_array(); + # APIv1-cleanup: Remove multipaste_id and user_id + $query = $CI->db + ->select('m.url_id, m.multipaste_id, m.user_id, m.date') + ->from("multipaste m") + ->where("user_id", $user) + ->get()->result_array(); $multipaste_info = array(); foreach ($query as $item) { $multipaste_info[$item["url_id"]] = $item; diff --git a/application/test/tests/test_api_v1.php b/application/test/tests/test_api_v1.php index bc311f8e5..f0a2096d6 100644 --- a/application/test/tests/test_api_v1.php +++ b/application/test/tests/test_api_v1.php @@ -298,6 +298,28 @@ class test_api_v1 extends \test\Test { $this->t->is($ret["data"]["total_size"], $expected_filesize, "total_size == uploaded files"); } + public function test_history_notEmptyAfterMultipaste() + { + $apikey = $this->createUserAndApikey(); + $uploadid = $this->uploadFile($apikey, "data/tests/small-file")['data']['ids'][0]; + $multipasteid = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + 'ids[1]' => $uploadid, + ))['data']['url_id']; + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("history not empty after multipaste", $ret); + + $this->t->ok(!empty($ret["data"]["items"]), "history not empty after multipaste (items)"); + $this->t->is($ret['data']["multipaste_items"][$multipasteid]['items'][$uploadid]['id'], $uploadid, "multipaste contains correct id"); + $this->t->is_deeply(array( + 'url_id', 'multipaste_id', 'user_id', 'date', 'items' + ), array_keys($ret['data']["multipaste_items"][$multipasteid]), "multipaste info only lists correct keys"); + $this->t->is_deeply(array('id'), array_keys($ret['data']["multipaste_items"][$multipasteid]['items'][$uploadid]), "multipaste item info only lists correct keys"); + } + public function test_history_notEmptyAfterUpload() { $apikey = $this->createUserAndApikey(); diff --git a/application/test/tests/test_api_v2.php b/application/test/tests/test_api_v2.php new file mode 100644 index 000000000..1886cdf7c --- /dev/null +++ b/application/test/tests/test_api_v2.php @@ -0,0 +1,476 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_api_v2 extends \test\Test { + + public function __construct() + { + parent::__construct(); + + $CI =& get_instance(); + $CI->load->model("muser"); + $CI->load->model("mfile"); + + } + + private function uploadFile($apikey, $file) + { + $ret = $this->CallAPI("POST", "$this->server/api/v2.0.0/file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create($file), + )); + $this->expectSuccess("upload file", $ret); + return $ret; + } + + private function createUser($counter) + { + $CI =& get_instance(); + $CI->db->insert("users", array( + 'username' => "testuser-api_v2-$counter", + 'password' => $CI->muser->hash_password("testpass$counter"), + 'email' => "testuser$counter@localhost.invalid", + 'referrer' => NULL + )); + + return $CI->db->insert_id(); + } + + private function createApikey($userid, $access_level = "apikey") + { + return \service\user::create_apikey($userid, "", $access_level); + } + + private function createUserAndApikey($access_level = "apikey") + { + static $counter = 100; + $counter++; + $userid = $this->createUser($counter); + return $this->createApikey($userid, $access_level); + } + + private function callEndpoint($verb, $endpoint, $data) + { + return $this->CallAPI($verb, "$this->server/api/v2.0.0/$endpoint", $data); + } + + public function test_callPrivateEndpointsWithoutApikey() + { + $endpoints = array( + "file/upload", + "file/history", + "file/delete", + "file/create_multipaste", + "user/apikeys", + "user/create_apikey", + "user/delete_apikey", + ); + foreach ($endpoints as $endpoint) { + $ret = $this->CallEndpoint("POST", $endpoint, array( + )); + $this->expectError("call $endpoint without apikey", $ret); + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'api/not-authenticated', + 'message' => 'Not authenticated. FileBin requires you to have an account, please go to the homepage for more information.', + ), $ret, "expected error"); + } + } + + public function test_callEndpointsWithoutEnoughPermissions() + { + $testconfig = array( + array( + "apikey" => $this->createUserAndApikey('basic'), + "endpoints" => array( + "file/delete", + "file/history", + ), + ), + array( + "apikey" => $this->createUserAndApikey(), + "endpoints" => array( + "user/apikeys", + "user/create_apikey", + "user/delete_apikey", + ), + ), + ); + foreach ($testconfig as $test) { + foreach ($test['endpoints'] as $endpoint) { + $ret = $this->CallEndpoint("POST", $endpoint, array( + "apikey" => $test['apikey'], + )); + $this->expectError("call $endpoint without enough permissions", $ret); + $this->t->is_deeply(array( + 'status' => "error", + 'error_id' => "api/insufficient-permissions", + 'message' => "Access denied: Access level too low", + ), $ret, "expected permission error"); + } + } + } + + public function test_create_apikey_createNewKey() + { + $this->createUser(1); + $ret = $this->CallEndpoint("POST", "user/create_apikey", array( + "username" => "testuser-api_v2-1", + "password" => "testpass1", + "access_level" => "apikey", + "comment" => "main api key", + )); + $this->expectSuccess("create-apikey", $ret); + + $this->t->isnt($ret["data"]["new_key"], "", "apikey not empty"); + } + + public function test_apikeys_getApikey() + { + $userid = $this->createUser(2); + $apikey = $this->createApikey($userid); + $ret = $this->CallEndpoint("POST", "user/apikeys", array( + "username" => "testuser-api_v2-2", + "password" => "testpass2", + )); + $this->expectSuccess("get apikeys", $ret); + + $this->t->is($ret["data"]["apikeys"][$apikey]["key"], $apikey, "expected key 1"); + $this->t->is($ret["data"]["apikeys"][$apikey]["access_level"], "apikey", "expected key 1 acces_level"); + $this->t->is($ret["data"]["apikeys"][$apikey]["comment"], "", "expected key 1 comment"); + $this->t->ok(is_int($ret["data"]["apikeys"][$apikey]["created"]) , "expected key 1 creation time is int"); + } + + public function test_delete_apikey_deleteOwnKey() + { + $apikey = $this->createUserAndApikey("full"); + $ret = $this->CallEndpoint("POST", "user/delete_apikey", array( + "apikey" => $apikey, + "delete_key" => $apikey, + )); + $this->expectSuccess("delete apikey", $ret); + + $this->t->is($ret["data"]["deleted_keys"][$apikey]["key"], $apikey, "expected key"); + } + + public function test_delete_apikey_errorDeleteOtherUserKey() + { + $apikey = $this->createUserAndApikey("full"); + $apikey2 = $this->createUserAndApikey("full"); + $ret = $this->CallEndpoint("POST", "user/delete_apikey", array( + "apikey" => $apikey, + "delete_key" => $apikey2, + )); + $this->expectError("delete apikey of other user", $ret); + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'user/delete_apikey/failed', + 'message' => 'Apikey deletion failed. Possibly wrong owner.', + ), $ret, "expected error"); + } + + public function test_authentication_invalidPassword() + { + $userid = $this->createUser(3); + $ret = $this->CallEndpoint("POST", "user/apikeys", array( + "username" => "testuser-api_v2-3", + "password" => "wrongpass", + )); + $this->expectError("invalid password", $ret); + + $this->t->is_deeply(array ( + 'status' => 'error', + 'error_id' => 'user/login-failed', + 'message' => 'Login failed', + ), $ret, "expected error"); + } + + public function test_authentication_invalidUser() + { + $userid = $this->createUser(4); + $ret = $this->CallEndpoint("POST", "user/apikeys", array( + "username" => "testuser-api_v2-invalid", + "password" => "testpass4", + )); + $this->expectError("invalid username", $ret); + + $this->t->is_deeply(array ( + 'status' => 'error', + 'error_id' => 'user/login-failed', + 'message' => 'Login failed', + ), $ret, "expected error"); + } + + public function test_history_empty() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("get history", $ret); + + $this->t->ok(empty($ret["data"]["items"]), "items key exists and empty"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "multipaste_items key exists and empty"); + $this->t->is($ret["data"]["total_size"], 0, "total_size = 0 since no uploads"); + } + + public function test_get_config() + { + $ret = $this->CallEndpoint("GET", "file/get_config", array( + )); + $this->expectSuccess("get_config", $ret); + + $this->t->like($ret["data"]["upload_max_size"], '/[0-9]+/', "upload_max_size is int"); + $this->t->like($ret["data"]["max_files_per_request"], '/[0-9]+/', "max_files_per_request is int"); + } + + public function test_upload_uploadFile() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create("data/tests/small-file"), + )); + $this->expectSuccess("upload file", $ret); + + $this->t->ok(!empty($ret["data"]["ids"]), "got IDs"); + $this->t->ok(!empty($ret["data"]["urls"]), "got URLs"); + } + + public function test_upload_uploadFileSameMD5() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create("data/tests/message1.bin"), + "file[2]" => curl_file_create("data/tests/message2.bin"), + )); + $this->expectSuccess("upload file", $ret); + + $this->t->ok(!empty($ret["data"]["ids"]), "got IDs"); + $this->t->ok(!empty($ret["data"]["urls"]), "got URLs"); + + foreach ($ret["data"]["urls"] as $url) { + $data[] = $this->SendHTTPRequest("GET", $url, ''); + } + $this->t->ok($data[0] !== $data[1], 'Returned file contents should differ'); + } + + public function test_upload_uploadNothing() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + )); + $this->expectError("upload no file", $ret); + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'file/no-file', + 'message' => 'No file was uploaded or unknown error occurred.', + ), $ret, "expected reply"); + } + + public function test_history_notEmptyAfterUploadSameMD5() + { + $apikey = $this->createUserAndApikey(); + $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create("data/tests/message1.bin"), + "file[2]" => curl_file_create("data/tests/message2.bin"), + )); + $expected_filesize = filesize("data/tests/message1.bin") + filesize("data/tests/message2.bin"); + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("history not empty after upload", $ret); + + $this->t->ok(!empty($ret["data"]["items"]), "history not empty after upload (items)"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "didn't upload multipaste"); + $this->t->is($ret["data"]["total_size"], $expected_filesize, "total_size == uploaded files"); + } + + public function test_history_notEmptyAfterMultipaste() + { + $apikey = $this->createUserAndApikey(); + $uploadid = $this->uploadFile($apikey, "data/tests/small-file")['data']['ids'][0]; + $multipasteid = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + 'ids[1]' => $uploadid, + ))['data']['url_id']; + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("history not empty after multipaste", $ret); + + $this->t->ok(!empty($ret["data"]["items"]), "history not empty after multipaste (items)"); + $this->t->is($ret['data']["multipaste_items"][$multipasteid]['items'][$uploadid]['id'], $uploadid, "multipaste contains correct id"); + $this->t->is_deeply(array( + 'url_id', 'date', 'items' + ), array_keys($ret['data']["multipaste_items"][$multipasteid]), "multipaste info only lists correct keys"); + $this->t->is_deeply(array('id'), array_keys($ret['data']["multipaste_items"][$multipasteid]['items'][$uploadid]), "multipaste item info only lists correct keys"); + } + + public function test_history_notEmptyAfterUpload() + { + $apikey = $this->createUserAndApikey(); + $this->uploadFile($apikey, "data/tests/small-file"); + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("history not empty after upload", $ret); + + $this->t->ok(!empty($ret["data"]["items"]), "history not empty after upload (items)"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "didn't upload multipaste"); + $this->t->is($ret["data"]["total_size"], filesize("data/tests/small-file"), "total_size == uploaded file"); + } + + public function test_history_notSharedBetweenUsers() + { + $apikey = $this->createUserAndApikey(); + $apikey2 = $this->createUserAndApikey(); + $this->uploadFile($apikey, "data/tests/small-file"); + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey2, + )); + $this->expectSuccess("get history", $ret); + + $this->t->ok(empty($ret["data"]["items"]), "items key exists and empty"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "multipaste_items key exists and empty"); + $this->t->is($ret["data"]["total_size"], 0, "total_size = 0 since no uploads"); + } + + public function test_delete_canDeleteUploaded() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/delete", array( + "apikey" => $apikey, + "ids[1]" => $id, + )); + $this->expectSuccess("delete uploaded file", $ret); + + $this->t->ok(empty($ret["data"]["errors"]), "no errors"); + $this->t->is_deeply(array( + $id => array( + "id" => $id + ) + ), $ret["data"]["deleted"], "deleted wanted ID"); + $this->t->is($ret["data"]["total_count"], 1, "total_count correct"); + $this->t->is($ret["data"]["deleted_count"], 1, "deleted_count correct"); + } + + public function test_delete_errorIfNotOwner() + { + $apikey = $this->createUserAndApikey(); + $apikey2 = $this->createUserAndApikey(); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/delete", array( + "apikey" => $apikey2, + "ids[1]" => $id, + )); + $this->expectSuccess("delete file of someone else", $ret); + + $this->t->ok(empty($ret["data"]["deleted"]), "not deleted"); + $this->t->is_deeply(array( + $id => array( + "id" => $id, + "reason" => "wrong owner" + ) + ), $ret["data"]["errors"], "error wanted ID"); + $this->t->is($ret["data"]["total_count"], 1, "total_count correct"); + $this->t->is($ret["data"]["deleted_count"], 0, "deleted_count correct"); + } + + public function test_create_multipaste_canCreate() + { + $apikey = $this->createUserAndApikey("basic"); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id2 = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + "ids[1]" => $id, + "ids[2]" => $id2, + )); + $this->expectSuccess("create multipaste", $ret); + + $this->t->isnt($ret["data"]["url_id"], "", "got a multipaste ID"); + $this->t->isnt($ret["data"]["url"], "", "got a multipaste URL"); + } + + public function test_create_multipaste_errorOnWrongID() + { + $apikey = $this->createUserAndApikey("basic"); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $id2 = $id."invalid"; + $ret = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + "ids[1]" => $id, + "ids[2]" => $id2, + )); + $this->expectError("create multipaste with wrong ID", $ret); + + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'file/create_multipaste/verify-failed', + 'message' => 'Failed to verify ID(s)', + 'data' => + array ( + $id2 => + array ( + 'id' => $id2, + 'reason' => 'doesn\'t exist', + ), + ), + ), $ret, "expected error response"); + } + + public function test_create_multipaste_errorOnWrongOwner() + { + $apikey = $this->createUserAndApikey("basic"); + $apikey2 = $this->createUserAndApikey("basic"); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey2, + "ids[1]" => $id, + )); + $this->expectError("create multipaste with wrong owner", $ret); + + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'file/create_multipaste/verify-failed', + 'message' => 'Failed to verify ID(s)', + 'data' => + array ( + $id => + array ( + 'id' => $id, + 'reason' => 'not owned by you', + ), + ), + ), $ret, "expected error response"); + } +} -- cgit v1.2.3-24-g4f1b