diff --git a/include/cgimap/rate_limiter.hpp b/include/cgimap/rate_limiter.hpp index 6c32f26de..5a7a12cae 100644 --- a/include/cgimap/rate_limiter.hpp +++ b/include/cgimap/rate_limiter.hpp @@ -5,13 +5,32 @@ #include #include -class rate_limiter { +struct rate_limiter { + virtual ~rate_limiter(); + + // check if the key is below the rate limit. return true to indicate that it + // is. + virtual bool check(const std::string &key) = 0; + + // update the limit for the key to say it has consumed this number of bytes. + virtual void update(const std::string &key, int bytes) = 0; +}; + +struct null_rate_limiter + : public rate_limiter { + ~null_rate_limiter(); + bool check(const std::string &key); + void update(const std::string &key, int bytes); +}; + +class memcached_rate_limiter + : public rate_limiter { public: /** * Methods. */ - rate_limiter(const boost::program_options::variables_map &options); - ~rate_limiter(void); + memcached_rate_limiter(const boost::program_options::variables_map &options); + ~memcached_rate_limiter(void); bool check(const std::string &key); void update(const std::string &key, int bytes); diff --git a/src/Makefile.am b/src/Makefile.am index 4e58f8a7c..2142f5b48 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -51,7 +51,8 @@ ___openstreetmap_cgimap_SOURCES=\ main.cpp ___test_test_core_SOURCES=\ - ../test/test_core.cpp + ../test/test_core.cpp \ + ../test/test_request.cpp ___test_test_parse_id_list_SOURCES=\ ../test/test_parse_id_list.cpp @@ -69,7 +70,8 @@ if ENABLE_APIDB ___test_test_apidb_backend_SOURCES=\ ../test/test_apidb_backend.cpp \ ../test/test_formatter.cpp \ - ../test/test_database.cpp + ../test/test_database.cpp \ + ../test/test_request.cpp endif libcgimap_fcgi_la_SOURCES=\ diff --git a/src/backend/apidb/oauth_store.cpp b/src/backend/apidb/oauth_store.cpp index 420e4c56b..07c30c721 100644 --- a/src/backend/apidb/oauth_store.cpp +++ b/src/backend/apidb/oauth_store.cpp @@ -90,6 +90,18 @@ oauth_store::oauth_store(const po::variables_map &opts) "WHERE token=$1") PREPARE_ARGS(("character varying")); + // return a row with the consumer secret for a given consumer key. + m_connection.prepare("consumer_secret_for_key", + "SELECT secret FROM client_applications " + "WHERE key=$1") + PREPARE_ARGS(("character varying")); + + // return a row with the token secret given the token ID. + m_connection.prepare("token_secret_for_id", + "SELECT secret FROM oauth_tokens " + "WHERE token=$1") + PREPARE_ARGS(("character varying")); + // clang-format on } @@ -97,14 +109,28 @@ oauth_store::~oauth_store() {} boost::optional oauth_store::consumer_secret(const std::string &consumer_key) { - // TODO: implement me! - return boost::none; + pqxx::work w(m_connection, "oauth_get_consumer_secret_for_key"); + pqxx::result res = w.prepared("consumer_secret_for_key")(consumer_key).exec(); + + if (res.affected_rows() > 0) { + return res[0][0].as(); + + } else { + return boost::none; + } } boost::optional oauth_store::token_secret(const std::string &token_id) { - // TODO: implement me! - return boost::none; + pqxx::work w(m_connection, "oauth_get_token_secret_for_id"); + pqxx::result res = w.prepared("token_secret_for_id")(token_id).exec(); + + if (res.affected_rows() > 0) { + return res[0][0].as(); + + } else { + return boost::none; + } } bool diff --git a/src/main.cpp b/src/main.cpp index 6b0e75d7c..cf5362504 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -151,7 +151,7 @@ static void process_requests(int socket, const po::variables_map &options) { } // create the rate limiter - rate_limiter limiter(options); + memcached_rate_limiter limiter(options); // create the routes map (from URIs to handlers) routes route; diff --git a/src/rate_limiter.cpp b/src/rate_limiter.cpp index 0c368089e..335a3d252 100644 --- a/src/rate_limiter.cpp +++ b/src/rate_limiter.cpp @@ -3,12 +3,25 @@ #include "cgimap/rate_limiter.hpp" -struct rate_limiter::state { +rate_limiter::~rate_limiter() { +} + +null_rate_limiter::~null_rate_limiter() { +} + +bool null_rate_limiter::check(const std::string &key) { + return true; +} + +void null_rate_limiter::update(const std::string &key, int bytes) { +} + +struct memcached_rate_limiter::state { time_t last_update; int bytes_served; }; -rate_limiter::rate_limiter( +memcached_rate_limiter::memcached_rate_limiter( const boost::program_options::variables_map &options) { if (options.count("memcache") && (ptr = memcached_create(NULL)) != NULL) { memcached_server_st *server_list; @@ -40,12 +53,12 @@ rate_limiter::rate_limiter( } } -rate_limiter::~rate_limiter(void) { +memcached_rate_limiter::~memcached_rate_limiter(void) { if (ptr) memcached_free(ptr); } -bool rate_limiter::check(const std::string &key) { +bool memcached_rate_limiter::check(const std::string &key) { int bytes_served = 0; std::string mc_key; state *sp; @@ -72,7 +85,7 @@ bool rate_limiter::check(const std::string &key) { return bytes_served < max_bytes; } -void rate_limiter::update(const std::string &key, int bytes) { +void memcached_rate_limiter::update(const std::string &key, int bytes) { if (ptr) { time_t now = time(NULL); std::string mc_key; diff --git a/test/test_apidb_backend.cpp b/test/test_apidb_backend.cpp index 95f8131d5..81bd83ec6 100644 --- a/test/test_apidb_backend.cpp +++ b/test/test_apidb_backend.cpp @@ -4,15 +4,22 @@ #include #include #include +#include +#include #include #include #include "cgimap/config.hpp" #include "cgimap/time.hpp" +#include "cgimap/oauth.hpp" +#include "cgimap/rate_limiter.hpp" +#include "cgimap/routes.hpp" +#include "cgimap/process_request.hpp" + #include "test_formatter.hpp" #include "test_database.hpp" -#include "cgimap/oauth.hpp" +#include "test_request.hpp" namespace { @@ -395,6 +402,127 @@ void test_changeset_with_comments_including_discussions(boost::shared_ptr &) { return 0; } + int select_ways(const std::vector &) { return 0; } + int select_relations(const std::vector &) { return 0; } + int select_nodes_from_bbox(const bbox &bounds, int max_nodes) { return 0; } + void select_nodes_from_relations() {} + void select_ways_from_nodes() {} + void select_ways_from_relations() {} + void select_relations_from_ways() {} + void select_nodes_from_way_nodes() {} + void select_relations_from_nodes() {} + void select_relations_from_relations() {} + void select_relations_members_of_relations() {} + bool supports_changesets() { return false; } + int select_changesets(const std::vector &) { return 0; } + void select_changeset_discussions() {} + + struct factory + : public data_selection::factory { + virtual ~factory() {} + virtual boost::shared_ptr make_selection() { + return boost::make_shared(); + } + }; +}; + +struct recording_rate_limiter + : public rate_limiter { + ~recording_rate_limiter() {} + + bool check(const std::string &key) { + m_keys_seen.insert(key); + return true; + } + + void update(const std::string &key, int bytes) { + m_keys_seen.insert(key); + } + + bool saw_key(const std::string &key) { + return m_keys_seen.count(key) > 0; + } + +private: + std::set m_keys_seen; +}; + + +void test_oauth_end_to_end(boost::shared_ptr store) { + recording_rate_limiter limiter; + std::string generator("test_apidb_backend.cpp"); + routes route; + boost::shared_ptr factory = + boost::make_shared(); + + test_request req; + req.set_header("SCRIPT_URL", "/api/0.6/relation/165475/full"); + req.set_header("SCRIPT_URI", + "http://www.openstreetmap.org/api/0.6/relation/165475/full"); + req.set_header("HTTP_HOST", "www.openstreetmap.org"); + req.set_header("HTTP_ACCEPT_ENCODING", + "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"); + req.set_header("HTTP_ACCEPT", "*/*"); + req.set_header("HTTP_USER_AGENT", "OAuth gem v0.4.7"); + req.set_header("HTTP_AUTHORIZATION", + "OAuth oauth_consumer_key=\"x3tHSMbotPe5fBlItMbg\", " + "oauth_nonce=\"dvu3eTk8i1uvj8zQ8Wef91UF6ngQdlTA3xQ2vEf7xU\", " + "oauth_signature=\"ewKFprItE5uaDHKFu3IVzuEHbno%3D\", " + "oauth_signature_method=\"HMAC-SHA1\", " + "oauth_timestamp=\"1475844649\", " + "oauth_token=\"15zpwgGjdjBu1DD65X7kcHzaWqfQpvqmMtqa3ZIO\", " + "oauth_version=\"1.0\""); + req.set_header("HTTP_X_REQUEST_ID", "V-eaKX8AAQEAAF4UzHwAAAHt"); + req.set_header("HTTP_X_FORWARDED_HOST", "www.openstreetmap.org"); + req.set_header("HTTP_X_FORWARDED_SERVER", "www.openstreetmap.org"); + req.set_header("HTTP_CONNECTION", "Keep-Alive"); + req.set_header("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"); + req.set_header("SERVER_SIGNATURE", "
Apache/2.4.18 (Ubuntu) Server " + "at www.openstreetmap.org Port 80
"); + req.set_header("SERVER_SOFTWARE", "Apache/2.4.18 (Ubuntu)"); + req.set_header("SERVER_NAME", "www.openstreetmap.org"); + req.set_header("SERVER_ADDR", "127.0.0.1"); + req.set_header("SERVER_PORT", "80"); + req.set_header("REMOTE_ADDR", "127.0.0.1"); + req.set_header("DOCUMENT_ROOT", "/srv/www.openstreetmap.org/rails/public"); + req.set_header("REQUEST_SCHEME", "http"); + req.set_header("SERVER_PROTOCOL", "HTTP/1.1"); + req.set_header("REQUEST_METHOD", "GET"); + req.set_header("QUERY_STRING", ""); + req.set_header("REQUEST_URI", "/api/0.6/relation/165475/full"); + req.set_header("SCRIPT_NAME", "/api/0.6/relation/165475/full"); + + assert_equal >( + std::string("ewKFprItE5uaDHKFu3IVzuEHbno="), + oauth::detail::hashed_signature(req, *store), + "hashed signatures"); + + process_request(req, limiter, generator, route, factory, store); + + assert_equal(404, req.response_status(), "response status"); + assert_equal(false, limiter.saw_key("addr:127.0.0.1"), + "saw addr:127.0.0.1 as a rate limit key"); + assert_equal(true, limiter.saw_key("user:1"), + "saw user:1 as a rate limit key"); +} + } // anonymous namespace int main(int, char **) { @@ -417,8 +545,6 @@ int main(int, char **) { tdb.run(boost::function)>( &test_get_user_id_for_token)); - // TODO: add tests for consumer_secret and token_secret - tdb.run(boost::function)>( &test_negative_changeset_ids)); @@ -437,7 +563,8 @@ int main(int, char **) { tdb.run(boost::function)>( &test_changeset_with_comments_including_discussions)); - // TODO: add end-to-end test for OAuth request. + tdb.run(boost::function)>( + &test_oauth_end_to_end)); } catch (const test_database::setup_error &e) { std::cout << "Unable to set up test database: " << e.what() << std::endl; diff --git a/test/test_apidb_backend.sql b/test/test_apidb_backend.sql index 410f4b466..fdf48e1af 100644 --- a/test/test_apidb_backend.sql +++ b/test/test_apidb_backend.sql @@ -42,24 +42,35 @@ VALUES (1, 0, 0, 1, true, '2013-11-14T02:10:00Z', 3221225472, 1), -- the API is publicly readable, so _all_ tokens can read the API. there is no -- "allow_read_api" setting on the token itself. but some tokens might have been -- revoked or expired. +INSERT INTO client_applications + (id, name, user_id, allow_read_prefs, key, secret) +VALUES + (1, 'test_client_application', 1, true, + 'x3tHSMbotPe5fBlItMbg', '1NZRJ0u2o7OilPDe60nfZsKJTC7RUZPrNfYwGBjATw'); + INSERT INTO oauth_tokens - (id, user_id, token, secret, created_at, authorized_at, invalidated_at) + (id, user_id, client_application_id, allow_read_prefs, token, secret, + created_at, authorized_at, invalidated_at) VALUES -- valid key - (1, 1, + (1, 1, 1, true, 'OfkxM4sSeyXjzgDTIOaJxcutsnqBoalr842NHOrA', 'fFCKdXsLxeHPlgrIPr5fZSpXKnDuLw0GvJTzeE99', '2016-07-11T19:12:00Z', '2016-07-11T19:12:00Z', NULL), -- not authorized - (2, 1, + (2, 1, 1, true, 'wpNsXPhrgWl4ELPjPbhfwjjSbNk9npsKoNrMGFlC', 'NZskvUUYlOuCsPKuMbSTz5eMpVJVI3LsyW11Z2Uq', '2016-07-11T19:12:00Z', NULL, NULL), -- invalidated - (3, 1, + (3, 1, 1, true, 'Rzcm5aDiDgqgub8j96MfDaYyAc4cRwI9CmZB7HBf', '2UxsEFziZGv64hdWN3Qa90Vb6v1aovVxaTTQIn1D', - '2016-07-11T19:12:00Z', '2016-07-11T19:12:00Z', '2016-07-11T19:12:00Z'); + '2016-07-11T19:12:00Z', '2016-07-11T19:12:00Z', '2016-07-11T19:12:00Z'), + (4, 1, 1, true, + '15zpwgGjdjBu1DD65X7kcHzaWqfQpvqmMtqa3ZIO', + 'H3Vb9Kgf4LpTyVlft5xsI9MwzknQsTu6CkHE0qK3', + '2016-10-07T00:00:00Z', '2016-10-07T00:00:00Z', NULL); -- update the number of edits UPDATE changesets diff --git a/test/test_core.cpp b/test/test_core.cpp index 7952ab346..6cfda983b 100644 --- a/test/test_core.cpp +++ b/test/test_core.cpp @@ -19,83 +19,14 @@ #include #include +#include "test_request.hpp" + namespace fs = boost::filesystem; namespace po = boost::program_options; namespace al = boost::algorithm; namespace pt = boost::property_tree; namespace bt = boost::posix_time; -/** - * Mock output buffer so that we can get back an in-memory result as a string - * backed buffer. - */ -struct test_output_buffer : public output_buffer { - explicit test_output_buffer(std::ostream &out) : m_out(out), m_written(0) {} - virtual ~test_output_buffer() {} - virtual int write(const char *buffer, int len) { - m_out.write(buffer, len); - m_written += len; - return len; - } - virtual int written() { return m_written; } - virtual int close() { return 0; } - virtual void flush() {} - -private: - std::ostream &m_out; - int m_written; -}; - -/** - * Mock request so that we can control the headers and get back the response - * body for comparison to what we expect. - */ -struct test_request : public request { - test_request() {} - - /// implementation of request interface - virtual ~test_request() {} - virtual const char *get_param(const char *key) { - std::string key_str(key); - std::map::iterator itr = m_params.find(key_str); - if (itr != m_params.end()) { - return itr->second.c_str(); - } else { - return NULL; - } - } - - virtual void dispose() {} - - /// getters and setters for the input headers and output response - void set_header(const std::string &k, const std::string &v) { - m_params.insert(std::make_pair(k, v)); - } - std::stringstream &buffer() { return m_output; } - - bt::ptime get_current_time() const { return m_now; } - void set_current_time(const bt::ptime &now) { m_now = now; } - -protected: - virtual void write_header_info(int status, const headers_t &headers) { - assert(m_output.tellp() == 0); - m_output << "Status: " << status << " " << status_message(status) << "\r\n"; - BOOST_FOREACH(const request::headers_t::value_type &header, headers) { - m_output << header.first << ": " << header.second << "\r\n"; - } - m_output << "\r\n"; - } - virtual boost::shared_ptr get_buffer_internal() { - return boost::shared_ptr(new test_output_buffer(m_output)); - } - virtual void finish_internal() {} - -private: - std::stringstream m_output; - std::map m_params; - bt::ptime m_now; -}; - std::map read_headers(std::istream &in, const std::string &separator) { std::map headers; @@ -598,7 +529,7 @@ int main(int argc, char *argv[]) { boost::shared_ptr data_backend = make_staticxml_backend(); boost::shared_ptr factory = data_backend->create(vm); - rate_limiter limiter(vm); + null_rate_limiter limiter; routes route; BOOST_FOREACH(fs::path test_case, test_cases) { diff --git a/test/test_request.cpp b/test/test_request.cpp new file mode 100644 index 000000000..d12c614fc --- /dev/null +++ b/test/test_request.cpp @@ -0,0 +1,80 @@ +#include "test_request.hpp" +#include "cgimap/request_helpers.hpp" + +#include + +namespace bt = boost::posix_time; + +test_output_buffer::test_output_buffer(std::ostream &out) + : m_out(out), m_written(0) { +} + +test_output_buffer::~test_output_buffer() {} + +int test_output_buffer::write(const char *buffer, int len) { + m_out.write(buffer, len); + m_written += len; + return len; +} + +int test_output_buffer::written() { + return m_written; +} + +int test_output_buffer::close() { + return 0; +} + +void test_output_buffer::flush() {} + +test_request::test_request() : m_status(-1) {} + +test_request::~test_request() {} + +const char *test_request::get_param(const char *key) { + std::string key_str(key); + std::map::iterator itr = m_params.find(key_str); + if (itr != m_params.end()) { + return itr->second.c_str(); + } else { + return NULL; + } +} + +void test_request::dispose() {} + +void test_request::set_header(const std::string &k, const std::string &v) { + m_params.insert(std::make_pair(k, v)); +} + +std::stringstream &test_request::buffer() { + return m_output; +} + +bt::ptime test_request::get_current_time() const { + return m_now; +} + +void test_request::set_current_time(const bt::ptime &now) { + m_now = now; +} + +void test_request::write_header_info(int status, const headers_t &headers) { + assert(m_output.tellp() == 0); + m_status = status; + m_output << "Status: " << status << " " << status_message(status) << "\r\n"; + BOOST_FOREACH(const request::headers_t::value_type &header, headers) { + m_output << header.first << ": " << header.second << "\r\n"; + } + m_output << "\r\n"; +} + +int test_request::response_status() const { + return m_status; +} + +boost::shared_ptr test_request::get_buffer_internal() { + return boost::shared_ptr(new test_output_buffer(m_output)); +} + +void test_request::finish_internal() {} diff --git a/test/test_request.hpp b/test/test_request.hpp new file mode 100644 index 000000000..ea61e26c5 --- /dev/null +++ b/test/test_request.hpp @@ -0,0 +1,58 @@ +#ifndef TEST_TEST_REQUEST_HPP +#define TEST_TEST_REQUEST_HPP + +#include "cgimap/request.hpp" +#include "cgimap/output_buffer.hpp" + +/** + * Mock output buffer so that we can get back an in-memory result as a string + * backed buffer. + */ +struct test_output_buffer : public output_buffer { + explicit test_output_buffer(std::ostream &out); + virtual ~test_output_buffer(); + virtual int write(const char *buffer, int len); + virtual int written(); + virtual int close(); + virtual void flush(); + +private: + std::ostream &m_out; + int m_written; +}; + +/** + * Mock request so that we can control the headers and get back the response + * body for comparison to what we expect. + */ +struct test_request : public request { + test_request(); + + /// implementation of request interface + virtual ~test_request(); + virtual const char *get_param(const char *key); + + virtual void dispose(); + + /// getters and setters for the input headers and output response + void set_header(const std::string &k, const std::string &v); + std::stringstream &buffer(); + + boost::posix_time::ptime get_current_time() const; + void set_current_time(const boost::posix_time::ptime &now); + + int response_status() const; + +protected: + virtual void write_header_info(int status, const headers_t &headers); + virtual boost::shared_ptr get_buffer_internal(); + virtual void finish_internal(); + +private: + int m_status; + std::stringstream m_output; + std::map m_params; + boost::posix_time::ptime m_now; +}; + +#endif /* TEST_TEST_REQUEST_HPP */