Repository: mesos Updated Branches: refs/heads/master 5c8065e9a -> b72fb052b
Added TokenManager for Docker provisioner remote store. Review: https://reviews.apache.org/r/37427 Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/b72fb052 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/b72fb052 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/b72fb052 Branch: refs/heads/master Commit: b72fb052bb2ca9f754b9d2c40e7f76df97dc55ae Parents: 5c8065e Author: Jojy Varghese <[email protected]> Authored: Wed Sep 9 10:29:28 2015 -0700 Committer: Timothy Chen <[email protected]> Committed: Wed Sep 9 11:10:04 2015 -0700 ---------------------------------------------------------------------- src/Makefile.am | 9 +- .../provisioners/docker/token_manager.cpp | 361 +++++++++++++++++++ .../provisioners/docker/token_manager.hpp | 179 +++++++++ .../provisioners/docker_provisioner_tests.cpp | 354 ++++++++++++++++++ 4 files changed, 900 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/Makefile.am ---------------------------------------------------------------------- diff --git a/src/Makefile.am b/src/Makefile.am index 0a8ef6d..4ef58cd 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -483,12 +483,13 @@ libmesos_no_3rdparty_la_SOURCES = \ slave/containerizer/mesos/launch.cpp \ slave/containerizer/provisioner.cpp \ slave/containerizer/provisioners/appc.cpp \ - slave/containerizer/provisioners/paths.cpp \ + slave/containerizer/provisioners/paths.cpp \ slave/containerizer/provisioners/appc/paths.cpp \ slave/containerizer/provisioners/appc/spec.cpp \ slave/containerizer/provisioners/appc/store.cpp \ slave/containerizer/provisioners/backend.cpp \ - slave/containerizer/provisioners/backends/copy.cpp \ + slave/containerizer/provisioners/backends/copy.cpp \ + slave/containerizer/provisioners/docker/token_manager.cpp \ slave/resource_estimators/noop.cpp \ usage/usage.cpp \ v1/attributes.cpp \ @@ -763,13 +764,14 @@ libmesos_no_3rdparty_la_SOURCES += \ slave/containerizer/linux_launcher.hpp \ slave/containerizer/provisioner.hpp \ slave/containerizer/provisioners/appc.hpp \ - slave/containerizer/provisioners/paths.hpp \ + slave/containerizer/provisioners/paths.hpp \ slave/containerizer/provisioners/appc/paths.hpp \ slave/containerizer/provisioners/appc/spec.hpp \ slave/containerizer/provisioners/appc/store.hpp \ slave/containerizer/provisioners/backend.hpp \ slave/containerizer/provisioners/backends/bind.hpp \ slave/containerizer/provisioners/backends/copy.hpp \ + slave/containerizer/provisioners/docker/token_manager.hpp \ slave/containerizer/isolators/posix.hpp \ slave/containerizer/isolators/posix/disk.hpp \ slave/containerizer/isolators/cgroups/constants.hpp \ @@ -1659,6 +1661,7 @@ mesos_tests_SOURCES = \ tests/paths_tests.cpp \ tests/persistent_volume_tests.cpp \ tests/protobuf_io_tests.cpp \ + tests/provisioners/docker_provisioner_tests.cpp \ tests/rate_limiting_tests.cpp \ tests/reconciliation_tests.cpp \ tests/registrar_tests.cpp \ http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/slave/containerizer/provisioners/docker/token_manager.cpp ---------------------------------------------------------------------- diff --git a/src/slave/containerizer/provisioners/docker/token_manager.cpp b/src/slave/containerizer/provisioners/docker/token_manager.cpp new file mode 100644 index 0000000..aec915f --- /dev/null +++ b/src/slave/containerizer/provisioners/docker/token_manager.cpp @@ -0,0 +1,361 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +#include <process/defer.hpp> +#include <process/dispatch.hpp> + +#include "slave/containerizer/provisioners/docker/token_manager.hpp" + +using std::hash; +using std::string; +using std::vector; + +using process::Clock; +using process::Failure; +using process::Future; +using process::Owned; +using process::Process; +using process::Time; + +using process::http::Request; +using process::http::Response; +using process::http::URL; + +namespace mesos { +namespace internal { +namespace slave { +namespace docker { +namespace registry { + +class TokenManagerProcess : public Process<TokenManagerProcess> +{ +public: + static Try<Owned<TokenManagerProcess>> create(const URL& realm); + + Future<Token> getToken( + const string& service, + const string& scope, + const Option<string>& account); + +private: + static const string TOKEN_PATH_PREFIX; + static const Duration RESPONSE_TIMEOUT; + + TokenManagerProcess(const URL& realm) + : realm_(realm) {} + + Try<Token> getTokenFromResponse(const Response& response) const; + + /** + * Key for the token cache. + */ + struct TokenCacheKey + { + string service; + string scope; + }; + + struct TokenCacheKeyHash + { + size_t operator()(const TokenCacheKey& key) const + { + hash<string> hashFn; + + return (hashFn(key.service) ^ + (hashFn(key.scope) << 1)); + } + }; + + struct TokenCacheKeyEqual + { + bool operator()( + const TokenCacheKey& left, + const TokenCacheKey& right) const + { + return ((left.service == right.service) && + (left.scope == right.scope)); + } + }; + + typedef hashmap< + const TokenCacheKey, + Token, + TokenCacheKeyHash, + TokenCacheKeyEqual> TokenCacheType; + + const URL realm_; + TokenCacheType tokenCache_; + + TokenManagerProcess(const TokenManagerProcess&) = delete; + TokenManagerProcess& operator=(const TokenManagerProcess&) = delete; +}; + +const Duration TokenManagerProcess::RESPONSE_TIMEOUT = Seconds(10); +const string TokenManagerProcess::TOKEN_PATH_PREFIX = "/v2/token/"; + + +Token::Token( + const string& _raw, + const JSON::Object& _header, + const JSON::Object& _claims, + const Option<Time>& _expiration, + const Option<Time>& _notBefore) + : raw(_raw), + header(_header), + claims(_claims), + expiration(_expiration), + notBefore(_notBefore) {} + + +Try<Token> Token::create(const string& raw) +{ + auto decode = []( + const string& segment) -> Try<JSON::Object> { + const auto padding = segment.length() % 4; + string paddedSegment(segment); + + if (padding) { + paddedSegment.append(padding, '='); + } + + Try<string> decoded = base64::decode(paddedSegment); + if (decoded.isError()) { + return Error(decoded.error()); + } + + return JSON::parse<JSON::Object>(decoded.get()); + }; + + const vector<string> tokens = strings::tokenize(raw, "."); + + if (tokens.size() != 3) { + return Error("Invalid raw token string"); + } + + Try<JSON::Object> header = decode(tokens[0]); + if (header.isError()) { + return Error("Failed to decode 'header' segment: " + header.error()); + } + + Try<JSON::Object> claims = decode(tokens[1]); + if (claims.isError()) { + return Error("Failed to decode 'claims' segment: " + claims.error()); + } + + Result<Time> expirationTime = getTimeValue(claims.get(), "exp"); + if (expirationTime.isError()) { + return Error("Failed to decode expiration time: " + expirationTime.error()); + } + + Option<Time> expiration; + if (expirationTime.isSome()) { + expiration = expirationTime.get(); + } + + Result<Time> notBeforeTime = getTimeValue(claims.get(), "nbf"); + if (notBeforeTime.isError()) { + return Error("Failed to decode not-before time: " + notBeforeTime.error()); + } + + Option<Time> notBefore; + if (notBeforeTime.isSome()) { + notBefore = notBeforeTime.get(); + } + + Token token(raw, header.get(), claims.get(), expiration, notBefore); + + if (token.isExpired()) { + return Error("Token has expired"); + } + + // TODO(jojy): Add signature validation. + return token; +} + + +Result<Time> Token::getTimeValue(const JSON::Object& object, const string& key) +{ + Result<JSON::Number> jsonValue = object.find<JSON::Number>(key); + + Option<Time> timeValue; + + // If expiration is provided, we will process it for future validations. + if (jsonValue.isSome()) { + Try<Time> time = Time::create(jsonValue.get().value); + if (time.isError()) { + return Error("Failed to decode time: " + time.error()); + } + + timeValue = time.get(); + } + + return timeValue; +} + + +bool Token::isExpired() const +{ + if (expiration.isSome()) { + return (Clock::now() >= expiration.get()); + } + + return false; +} + + +bool Token::isValid() const +{ + if (!isExpired()) { + if (notBefore.isSome()) { + return (Clock::now() >= notBefore.get()); + } + + return true; + } + + // TODO(jojy): Add signature validation. + return false; +} + + +Try<Owned<TokenManager>> TokenManager::create( + const URL& realm) +{ + Try<Owned<TokenManagerProcess>> process = TokenManagerProcess::create(realm); + if (process.isError()) { + return Error(process.error()); + } + + return Owned<TokenManager>(new TokenManager(process.get())); +} + + +TokenManager::TokenManager(Owned<TokenManagerProcess>& process) + : process_(process) +{ + spawn(CHECK_NOTNULL(process_.get())); +} + + +TokenManager::~TokenManager() +{ + terminate(process_.get()); + process::wait(process_.get()); +} + + +Future<Token> TokenManager::getToken( + const string& service, + const string& scope, + const Option<string>& account) +{ + return dispatch( + process_.get(), + &TokenManagerProcess::getToken, + service, + scope, + account); +} + + +Try<Owned<TokenManagerProcess>> TokenManagerProcess::create(const URL& realm) +{ + return Owned<TokenManagerProcess>(new TokenManagerProcess(realm)); +} + + +Try<Token> TokenManagerProcess::getTokenFromResponse( + const Response& response) const +{ + Try<JSON::Object> tokenJSON = JSON::parse<JSON::Object>(response.body); + if (tokenJSON.isError()) { + return Error(tokenJSON.error()); + } + + Result<JSON::String> tokenString = + tokenJSON.get().find<JSON::String>("token"); + + if (tokenString.isError()) { + return Error(tokenString.error()); + } + + Try<Token> result = Token::create(tokenString.get().value); + if (result.isError()) { + return Error(result.error()); + } + + return result.get();; +} + + +Future<Token> TokenManagerProcess::getToken( + const string& service, + const string& scope, + const Option<string>& account) +{ + const TokenCacheKey tokenKey = {service, scope}; + + if (tokenCache_.contains(tokenKey)) { + Token token = tokenCache_.at(tokenKey); + + if (token.isValid()) { + return token; + } else { + LOG(WARNING) << "Cached token was invalid. Will fetch once again"; + } + } + + URL tokenUrl = realm_; + tokenUrl.path = TOKEN_PATH_PREFIX; + + tokenUrl.query = { + {"service", service}, + {"scope", scope}, + }; + + if (account.isSome()) { + tokenUrl.query.insert({"account", account.get()}); + } + + return process::http::get(tokenUrl, None()) + .after(RESPONSE_TIMEOUT, [] (Future<Response> resp) -> Future<Response> { + resp.discard(); + return Failure("Timeout waiting for response to token request"); + }) + .then(defer(self(), [this, tokenKey]( + const Future<Response>& response) -> Future<Token> { + Try<Token> token = getTokenFromResponse(response.get()); + if (token.isError()) { + return Failure( + "Failed to parse JSON Web Token object from response: " + + token.error()); + } + + tokenCache_.insert({tokenKey, token.get()}); + + return token.get(); + })); +} + +// TODO(jojy): Add implementation for basic authentication based getToken API. + +} // namespace registry { +} // namespace docker { +} // namespace slave { +} // namespace internal { +} // namespace mesos { http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/slave/containerizer/provisioners/docker/token_manager.hpp ---------------------------------------------------------------------- diff --git a/src/slave/containerizer/provisioners/docker/token_manager.hpp b/src/slave/containerizer/provisioners/docker/token_manager.hpp new file mode 100644 index 0000000..879269d --- /dev/null +++ b/src/slave/containerizer/provisioners/docker/token_manager.hpp @@ -0,0 +1,179 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +#ifndef __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__ +#define __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__ + +#include <functional> +#include <string> + +#include <stout/base64.hpp> +#include <stout/duration.hpp> +#include <stout/hashmap.hpp> +#include <stout/strings.hpp> + +#include <process/future.hpp> +#include <process/http.hpp> +#include <process/process.hpp> +#include <process/time.hpp> + +namespace mesos { +namespace internal { +namespace slave { +namespace docker { +namespace registry { + + +/** + * Encapsulates JSON Web Token. + * + * Reference: https://tools.ietf.org/html/rfc7519. + */ +struct Token +{ + /** + * Factory method for Token object. + * + * Parses the raw token string and validates for token's expiration. + * + * @returns Token if parsing and validation succeeds. + * Error if parsing or validation fails. + */ + static Try<Token> create(const std::string& rawString); + + /** + * Compares token's expiration time(expressed in seconds) with current time. + * + * @returns True if token's expiration time is greater than current time. + * False if token's expiration time is less than or equal to current + * time. + */ + bool isExpired() const; + + /** + * Validates the token if its "exp" "nbf" values are in range. + * + * @returns True if current time is within token's "exp" and "nbf" values. + * False if current time is not within token's "exp" and "nbf" + * values. + */ + bool isValid() const; + + const std::string raw; + const JSON::Object header; + const JSON::Object claims; + // TODO(jojy): Add signature information. + +private: + Token( + const std::string& raw, + const JSON::Object& headerJson, + const JSON::Object& claimsJson, + const Option<process::Time>& expireTime, + const Option<process::Time>& notBeforeTime); + + static Result<process::Time> getTimeValue( + const JSON::Object& object, + const std::string& key); + + const Option<process::Time> expiration; + const Option<process::Time> notBefore; +}; + + +// Forward declaration. +class TokenManagerProcess; + + +/** + * Acquires and manages docker registry tokens. It keeps the tokens in its + * cache to server any future request for the same token. + * The cache grows unbounded. + * TODO(jojy): The cache can be optimized to prune based on the expiry time of + * the token and server's issue time. + */ +class TokenManager +{ +public: + /** + * Factory method for creating TokenManager object. + * + * TokenManager and registry authorization realm has a 1:1 relationship. + * + * @param realm URL of the authorization server from where token will be + * requested by this TokenManager. + * @returns Owned<TokenManager> if success. + * Error on failure. + */ + static Try<process::Owned<TokenManager>> create( + const process::http::URL& realm); + + /** + * Returns JSON Web Token from cache or from remote server using "Basic + * authorization". + * + * @param service Name of the service that hosts the resource for which + * token is being requested. + * @param scope unique scope returned by the 401 Unauthorized response + * from the registry. + * @param account Name of the account which the client is acting as. + * @param user base64 encoded userid for basic authorization. + * @param password base64 encoded password for basic authorization. + * @returns Token struct that encapsulates JSON Web Token. + */ + process::Future<Token> getToken( + const std::string& service, + const std::string& scope, + const Option<std::string>& account, + const std::string& user, + const Option<std::string>& password); + + /** + * Returns JSON Web Token from cache or from remote server using "TLS/Cert" + * based authorization. + * + * @param service Name of the service that hosts the resource for which + * token is being requested. + * @param scope unique scope returned by the 401 Unauthorized response + * from the registry. + * @param account Name of the account which the client is acting as. + * @returns Token struct that encapsulates JSON Web Token. + */ + process::Future<Token> getToken( + const std::string& service, + const std::string& scope, + const Option<std::string>& account); + + ~TokenManager(); + +private: + TokenManager(process::Owned<TokenManagerProcess>& process); + + TokenManager(const TokenManager&) = delete; + TokenManager& operator=(const TokenManager&) = delete; + + process::Owned<TokenManagerProcess> process_; +}; + +} // namespace registry { +} // namespace docker { +} // namespace slave { +} // namespace internal { +} // namespace mesos { + +#endif // __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__ http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/tests/provisioners/docker_provisioner_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/provisioners/docker_provisioner_tests.cpp b/src/tests/provisioners/docker_provisioner_tests.cpp new file mode 100644 index 0000000..ff29d56 --- /dev/null +++ b/src/tests/provisioners/docker_provisioner_tests.cpp @@ -0,0 +1,354 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <stout/duration.hpp> + +#include <process/address.hpp> +#include <process/clock.hpp> +#include <process/future.hpp> +#include <process/gmock.hpp> +#include <process/owned.hpp> +#include <process/socket.hpp> +#include <process/subprocess.hpp> + +#include <process/ssl/gtest.hpp> + +#include "slave/containerizer/provisioners/docker/token_manager.hpp" + +#include "tests/mesos.hpp" + +using std::map; +using std::string; +using std::vector; + +using namespace mesos::internal::slave::docker::registry; +using namespace process; + +namespace mesos { +namespace internal { +namespace tests { + + +/** + * Provides token operations and defaults. + */ +class TokenHelper { +protected: + const string hdrBase64 = base64::encode( + "{ \ + \"alg\":\"ES256\", \ + \"typ\":\"JWT\", \ + \"x5c\":[\"test\"] \ + }"); + + string getClaimsBase64() const + { + return base64::encode(claimsJsonString); + } + + string getTokenString() const + { + return hdrBase64 + "." + getClaimsBase64() + "." + signBase64; + } + + const string signBase64 = base64::encode("{\"\"}"); + string claimsJsonString; +}; + + +/** + * Fixture for testing TokenManager component. + */ +class DockerRegistryTokenTest : public TokenHelper, public ::testing::Test +{}; + + +// Tests JSON Web Token parsing for a valid token string. +TEST_F(DockerRegistryTokenTest, ValidToken) +{ + const double expirySecs = Clock::now().secs() + Days(365).secs(); + + claimsJsonString = + "{\"access\" \ + :[ \ + { \ + \"type\":\"repository\", \ + \"name\":\"library/busybox\", \ + \"actions\":[\"pull\"]}], \ + \"aud\":\"registry.docker.io\", \ + \"exp\":" + stringify(expirySecs) + ", \ + \"iat\":1438887168, \ + \"iss\":\"auth.docker.io\", \ + \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \ + \"nbf\":1438887166, \ + \"sub\":\"\" \ + }"; + + Try<Token> token = Token::create(getTokenString()); + + ASSERT_SOME(token); +} + + +// Tests JSON Web Token parsing for a token string with expiration date in the +// past. +TEST_F(DockerRegistryTokenTest, ExpiredToken) +{ + const double expirySecs = Clock::now().secs() - Days(365).secs(); + + claimsJsonString = + "{\"access\" \ + :[ \ + { \ + \"type\":\"repository\", \ + \"name\":\"library/busybox\", \ + \"actions\":[\"pull\"]}], \ + \"aud\":\"registry.docker.io\", \ + \"exp\":" + stringify(expirySecs) + ", \ + \"iat\":1438887166, \ + \"iss\":\"auth.docker.io\", \ + \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \ + \"nbf\":1438887166, \ + \"sub\":\"\" \ + }"; + + Try<Token> token = Token::create(getTokenString()); + + EXPECT_ERROR(token); +} + + +// Tests JSON Web Token parsing for a token string with no expiration date. +TEST_F(DockerRegistryTokenTest, NoExpiration) +{ + claimsJsonString = + "{\"access\" \ + :[ \ + { \ + \"type\":\"repository\", \ + \"name\":\"library/busybox\", \ + \"actions\":[\"pull\"]}], \ + \"aud\":\"registry.docker.io\", \ + \"iat\":1438887166, \ + \"iss\":\"auth.docker.io\", \ + \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \ + \"nbf\":1438887166, \ + \"sub\":\"\" \ + }"; + + const Try<Token> token = Token::create(getTokenString()); + + ASSERT_SOME(token); +} + + +// Tests JSON Web Token parsing for a token string with not-before date in the +// future. +TEST_F(DockerRegistryTokenTest, NotBeforeInFuture) +{ + const double expirySecs = Clock::now().secs() + Days(365).secs(); + const double nbfSecs = Clock::now().secs() + Days(7).secs(); + + claimsJsonString = + "{\"access\" \ + :[ \ + { \ + \"type\":\"repository\", \ + \"name\":\"library/busybox\", \ + \"actions\":[\"pull\"]}], \ + \"aud\":\"registry.docker.io\", \ + \"exp\":" + stringify(expirySecs) + ", \ + \"iat\":1438887166, \ + \"iss\":\"auth.docker.io\", \ + \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \ + \"nbf\":" + stringify(nbfSecs) + ", \ + \"sub\":\"\" \ + }"; + + const Try<Token> token = Token::create(getTokenString()); + + ASSERT_SOME(token); + ASSERT_EQ(token.get().isValid(), false); +} + + +#ifdef USE_SSL_SOCKET + +// Test suite for docker registry tests. +class DockerRegistryClientTest : public virtual SSLTest, public TokenHelper +{ +protected: + DockerRegistryClientTest() {} + + static void SetUpTestCase() + { + SSLTest::SetUpTestCase(); + // TODO(jojy): Add registry specific directory setup. Will be added in the + // next patch when docker registry client tests are added. + } + + static void TearDownTestCase() + { + SSLTest::TearDownTestCase(); + // TODO(jojy): Add registry specific directory cleanup. Will be added in the + // next patch when docker registry client tests are added. + } +}; + + +// Tests TokenManager for a simple token request. +TEST_F(DockerRegistryClientTest, SimpleGetToken) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}}); + + ASSERT_SOME(server); + ASSERT_SOME(server.get().address()); + ASSERT_SOME(server.get().address().get().hostname()); + + Future<Socket> socket = server.get().accept(); + + // Create URL from server hostname and port. + const http::URL url( + "https", + server.get().address().get().hostname().get(), + server.get().address().get().port); + + Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url); + ASSERT_SOME(tokenMgr); + + Future<Token> token = + tokenMgr.get()->getToken( + "registry.docker.io", + "repository:library/busybox:pull", + None()); + + AWAIT_ASSERT_READY(socket); + + // Construct response and send(server side). + const double expirySecs = Clock::now().secs() + Days(365).secs(); + + claimsJsonString = + "{\"access\" \ + :[ \ + { \ + \"type\":\"repository\", \ + \"name\":\"library/busybox\", \ + \"actions\":[\"pull\"]}], \ + \"aud\":\"registry.docker.io\", \ + \"exp\":" + stringify(expirySecs) + ", \ + \"iat\":1438887168, \ + \"iss\":\"auth.docker.io\", \ + \"jti\":\"l2PJDFkzwvoL7-TajJF7\", \ + \"nbf\":1438887166, \ + \"sub\":\"\" \ + }"; + + const string tokenString(getTokenString()); + const string tokenResponse = "{\"token\":\"" + tokenString + "\"}"; + + const string buffer = + string("HTTP/1.1 200 OK\r\n") + + "Content-Length : " + + stringify(tokenResponse.length()) + "\r\n" + + "\r\n" + + tokenResponse; + + AWAIT_ASSERT_READY(Socket(socket.get()).send(buffer)); + + AWAIT_ASSERT_READY(token); + ASSERT_EQ(token.get().raw, tokenString); +} + + +// Tests TokenManager for bad token response from server. +TEST_F(DockerRegistryClientTest, BadTokenResponse) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}}); + + ASSERT_SOME(server); + ASSERT_SOME(server.get().address()); + ASSERT_SOME(server.get().address().get().hostname()); + + Future<Socket> socket = server.get().accept(); + + // Create URL from server hostname and port. + const http::URL url( + "https", + server.get().address().get().hostname().get(), + server.get().address().get().port); + + Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url); + ASSERT_SOME(tokenMgr); + + Future<Token> token = + tokenMgr.get()->getToken( + "registry.docker.io", + "repository:library/busybox:pull", + None()); + + AWAIT_ASSERT_READY(socket); + + const string tokenString("bad token"); + const string tokenResponse = "{\"token\":\"" + tokenString + "\"}"; + + const string buffer = + string("HTTP/1.1 200 OK\r\n") + + "Content-Length : " + + stringify(tokenResponse.length()) + "\r\n" + + "\r\n" + + tokenResponse; + + AWAIT_ASSERT_READY(Socket(socket.get()).send(buffer)); + + AWAIT_FAILED(token); +} + + +// Tests TokenManager for request to invalid server. +TEST_F(DockerRegistryClientTest, BadTokenServerAddress) +{ + // Create an invalid URL with current time. + const http::URL url("https", stringify(Clock::now().secs()), 0); + + Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url); + ASSERT_SOME(tokenMgr); + + Future<Token> token = + tokenMgr.get()->getToken( + "registry.docker.io", + "repository:library/busybox:pull", + None()); + + AWAIT_FAILED(token); +} + +#endif // USE_SSL_SOCKET + + +} // namespace tests { +} // namespace internal { +} // namespace mesos {
