/* * Copyright 2016 ThoughtWorks, 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. */ package com.thoughtworks.go.agent.service; import com.thoughtworks.go.agent.common.ssl.GoAgentServerClientBuilder; import com.thoughtworks.go.agent.common.ssl.GoAgentServerHttpClient; import com.thoughtworks.go.agent.common.ssl.GoAgentServerHttpClientBuilder; import com.thoughtworks.go.config.AgentAutoRegistrationProperties; import com.thoughtworks.go.config.AgentRegistry; import com.thoughtworks.go.config.GuidService; import com.thoughtworks.go.security.KeyStoreManager; import com.thoughtworks.go.security.Registration; import com.thoughtworks.go.security.RegistrationJSONizer; import com.thoughtworks.go.server.service.AgentRuntimeInfo; import com.thoughtworks.go.util.SystemEnvironment; import com.thoughtworks.go.util.SystemUtil; import com.thoughtworks.go.util.URLService; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.NullInputStream; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.RequestBuilder; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import static com.thoughtworks.go.security.CertificateUtil.md5Fingerprint; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static org.apache.http.HttpStatus.SC_ACCEPTED; @Service public class SslInfrastructureService { private static final String CHAIN_ALIAS = "agent"; private static final Logger LOGGER = Logger.getLogger(SslInfrastructureService.class); private static final int REGISTER_RETRY_INTERVAL = 5000; private final RemoteRegistrationRequester remoteRegistrationRequester; private final KeyStoreManager keyStoreManager; private final GoAgentServerHttpClient httpClient; private transient boolean registered = false; @Autowired public SslInfrastructureService(URLService urlService, GoAgentServerHttpClient httpClient, AgentRegistry agentRegistry) throws Exception { this(new RemoteRegistrationRequester(urlService.getAgentRegistrationURL(), agentRegistry, httpClient), httpClient); } // For mocking out remote call SslInfrastructureService(RemoteRegistrationRequester requester, GoAgentServerHttpClient httpClient) throws Exception { this.remoteRegistrationRequester = requester; this.httpClient = httpClient; this.keyStoreManager = new KeyStoreManager(); this.keyStoreManager.preload(GoAgentServerClientBuilder.AGENT_CERTIFICATE_FILE, httpClientBuilder().keystorePassword()); } private GoAgentServerHttpClientBuilder httpClientBuilder() { return new GoAgentServerHttpClientBuilder(new SystemEnvironment()); } public void createSslInfrastructure() throws IOException { httpClientBuilder().initialize(); httpClient.reset(); } public void registerIfNecessary(AgentAutoRegistrationProperties agentAutoRegistrationProperties) throws Exception { registered = keyStoreManager.hasCertificates(CHAIN_ALIAS, GoAgentServerClientBuilder.AGENT_CERTIFICATE_FILE, httpClientBuilder().keystorePassword()) && GuidService.guidPresent(); if (!registered) { LOGGER.info("[Agent Registration] Starting to register agent."); register(agentAutoRegistrationProperties); createSslInfrastructure(); registered = true; LOGGER.info("[Agent Registration] Successfully registered agent."); } } public boolean isRegistered() { return registered; } private void register(AgentAutoRegistrationProperties agentAutoRegistrationProperties) throws Exception { String hostName = SystemUtil.getLocalhostNameOrRandomNameIfNotFound(); Registration keyEntry = Registration.createNullPrivateKeyEntry(); while (!keyEntry.isValid()) { try { keyEntry = remoteRegistrationRequester.requestRegistration(hostName, agentAutoRegistrationProperties); } catch (Exception e) { LOGGER.error("[Agent Registration] There was a problem registering with the go server.", e); throw e; } if ((!keyEntry.isValid())) { try { LOGGER.debug("[Agent Registration] Retrieved agent key from Go server is not valid."); Thread.sleep(REGISTER_RETRY_INTERVAL); } catch (InterruptedException e) { // Ok } } } LOGGER.info("[Agent Registration] Retrieved registration from Go server."); storeChainIntoAgentStore(keyEntry); agentAutoRegistrationProperties.scrubRegistrationProperties(); } private void storeChainIntoAgentStore(Registration keyEntry) { try { keyStoreManager.storeCertificate(CHAIN_ALIAS, GoAgentServerClientBuilder.AGENT_CERTIFICATE_FILE, httpClientBuilder().keystorePassword(), keyEntry); LOGGER.info(String.format("[Agent Registration] Stored registration for cert with hash code: %s not valid before: %s", md5Fingerprint(keyEntry.getFirstCertificate()), keyEntry.getCertificateNotBeforeDate())); } catch (Exception e) { throw bomb("Couldn't save agent key into store", e); } } public void invalidateAgentCertificate() { try { httpClient.reset(); keyStoreManager.deleteEntry(CHAIN_ALIAS, GoAgentServerClientBuilder.AGENT_CERTIFICATE_FILE, httpClientBuilder().keystorePassword()); } catch (Exception e) { LOGGER.fatal("[Agent Registration] Error while deleting key from key store", e); deleteKeyStores(); } } private void deleteKeyStores() { FileUtils.deleteQuietly(GoAgentServerClientBuilder.AGENT_CERTIFICATE_FILE); } public static class RemoteRegistrationRequester { private final AgentRegistry agentRegistry; private String serverUrl; private GoAgentServerHttpClient httpClient; public RemoteRegistrationRequester(String serverUrl, AgentRegistry agentRegistry, GoAgentServerHttpClient httpClient) { this.serverUrl = serverUrl; this.httpClient = httpClient; this.agentRegistry = agentRegistry; } protected Registration requestRegistration(String agentHostName, AgentAutoRegistrationProperties agentAutoRegisterProperties) throws IOException, ClassNotFoundException { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("[Agent Registration] Using URL %s to register.", serverUrl)); } HttpRequestBase postMethod = (HttpRequestBase) RequestBuilder.post(serverUrl) .addParameter("hostname", agentHostName) .addParameter("uuid", agentRegistry.uuid()) .addParameter("location", SystemUtil.currentWorkingDirectory()) .addParameter("usablespace", String.valueOf(AgentRuntimeInfo.usableSpace(SystemUtil.currentWorkingDirectory()))) .addParameter("operatingSystem", new SystemEnvironment().getOperatingSystemCompleteName()) .addParameter("agentAutoRegisterKey", agentAutoRegisterProperties.agentAutoRegisterKey()) .addParameter("agentAutoRegisterResources", agentAutoRegisterProperties.agentAutoRegisterResources()) .addParameter("agentAutoRegisterEnvironments", agentAutoRegisterProperties.agentAutoRegisterEnvironments()) .addParameter("agentAutoRegisterHostname", agentAutoRegisterProperties.agentAutoRegisterHostname()) .addParameter("elasticAgentId", agentAutoRegisterProperties.agentAutoRegisterElasticAgentId()) .addParameter("elasticPluginId", agentAutoRegisterProperties.agentAutoRegisterElasticPluginId()) .build(); try { CloseableHttpResponse response = httpClient.execute(postMethod); if (getStatusCode(response) == SC_ACCEPTED) { LOGGER.debug("The server has accepted the registration request."); return Registration.createNullPrivateKeyEntry(); } try (InputStream is = response.getEntity() == null ? new NullInputStream(0) : response.getEntity().getContent()) { String responseBody = IOUtils.toString(is, StandardCharsets.UTF_8); if (getStatusCode(response) == 200) { LOGGER.info("This agent is now approved by the server."); return readResponse(responseBody); } else { LOGGER.warn(String.format("The server sent a response that we could not understand. The HTTP status was %s. The response body was:\n%s", response.getStatusLine(), responseBody)); return Registration.createNullPrivateKeyEntry(); } } } finally { postMethod.releaseConnection(); } } protected Registration readResponse(String responseBody) { return RegistrationJSONizer.fromJson(responseBody); } protected int getStatusCode(CloseableHttpResponse response) { return response.getStatusLine().getStatusCode(); } } }