package org.jenkinsci.plugins.github.config;
import com.cloudbees.jenkins.GitHubWebHook;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import hudson.Extension;
import hudson.XmlFile;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.util.FormValidation;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.apache.commons.codec.binary.Base64;
import org.jenkinsci.main.modules.instance_identity.InstanceIdentity;
import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.Messages;
import org.jenkinsci.plugins.github.internal.GHPluginConfigException;
import org.jenkinsci.plugins.github.migration.Migrator;
import org.kohsuke.github.GitHub;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.google.common.base.Charsets.UTF_8;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.loginToGithub;
import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.clearRedundantCaches;
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
/**
* Global configuration to store all GH Plugin settings
* such as hook managing policy, credentials etc.
*
* @author lanwen (Merkushev Kirill)
* @since 1.13.0
*/
@Extension
public class GitHubPluginConfig extends GlobalConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPluginConfig.class);
public static final String GITHUB_PLUGIN_CONFIGURATION_ID = "github-plugin-configuration";
/**
* Helps to avoid null in {@link GitHubPlugin#configuration()}
*/
public static final GitHubPluginConfig EMPTY_CONFIG =
new GitHubPluginConfig(Collections.<GitHubServerConfig>emptyList());
private List<GitHubServerConfig> configs = new ArrayList<GitHubServerConfig>();
private URL hookUrl;
private HookSecretConfig hookSecretConfig = new HookSecretConfig(null);
private transient boolean overrideHookUrl;
/**
* Used to get current instance identity.
* It compared with same value when testing hook url availability in {@link #doCheckHookUrl(String)}
*/
@Inject
@SuppressWarnings("unused")
private transient InstanceIdentity identity;
public GitHubPluginConfig() {
load();
}
public GitHubPluginConfig(List<GitHubServerConfig> configs) {
this.configs = configs;
}
@SuppressWarnings("unused")
public void setConfigs(List<GitHubServerConfig> configs) {
this.configs = configs;
}
public List<GitHubServerConfig> getConfigs() {
return configs;
}
public boolean isManageHooks() {
return from(getConfigs()).filter(allowedToManageHooks()).first().isPresent();
}
public void setHookUrl(URL hookUrl) {
if (overrideHookUrl) {
this.hookUrl = hookUrl;
} else {
this.hookUrl = null;
}
}
public void setOverrideHookUrl(boolean overrideHookUrl) {
this.overrideHookUrl = overrideHookUrl;
}
/**
* @return hook url used as endpoint to search and write auto-managed hooks in GH
* @throws GHPluginConfigException if default jenkins url is malformed
*/
public URL getHookUrl() throws GHPluginConfigException {
if (hookUrl != null) {
return hookUrl;
} else {
return constructDefaultUrl();
}
}
public boolean isOverrideHookURL() {
return hookUrl != null;
}
/**
* Filters all stored configs against given predicate then
* logs in as the given user and returns the non null connection objects
*/
public Iterable<GitHub> findGithubConfig(Predicate<GitHubServerConfig> match) {
// try all the credentials since we don't know which one would work
return from(getConfigs())
.filter(match)
.transform(loginToGithub())
.filter(Predicates.notNull());
}
public List<Descriptor> actions() {
return Collections.singletonList(Jenkins.getInstance().getDescriptor(GitHubTokenCredentialsCreator.class));
}
/**
* To avoid long class name as id in xml tag name and config file
*/
@Override
public String getId() {
return GITHUB_PLUGIN_CONFIGURATION_ID;
}
/**
* @return config file with global {@link com.thoughtworks.xstream.XStream} instance
* with enabled aliases in {@link Migrator#enableAliases()}
*/
@Override
protected XmlFile getConfigFile() {
return new XmlFile(Jenkins.XSTREAM2, super.getConfigFile().getFile());
}
@Override
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
try {
req.bindJSON(this, json);
} catch (Exception e) {
LOGGER.debug("Problem while submitting form for GitHub Plugin ({})", e.getMessage(), e);
LOGGER.trace("GH form data: {}", json.toString());
throw new FormException(
format("Malformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration");
}
save();
clearRedundantCaches(configs);
return true;
}
@Override
public String getDisplayName() {
return "GitHub";
}
@SuppressWarnings("unused")
public FormValidation doReRegister() {
if (!GitHubPlugin.configuration().isManageHooks()) {
return FormValidation.warning("Works only when Jenkins manages hooks (one or more creds specified)");
}
List<Item> registered = GitHubWebHook.get().reRegisterAllHooks();
LOGGER.info("Called registerHooks() for {} items", registered.size());
return FormValidation.ok("Called re-register hooks for %s items", registered.size());
}
@SuppressWarnings("unused")
public FormValidation doCheckHookUrl(@QueryParameter String value) {
try {
HttpURLConnection con = (HttpURLConnection) new URL(value).openConnection();
con.setRequestMethod("POST");
con.setRequestProperty(GitHubWebHook.URL_VALIDATION_HEADER, "true");
con.connect();
if (con.getResponseCode() != 200) {
return FormValidation.error("Got %d from %s", con.getResponseCode(), value);
}
String v = con.getHeaderField(GitHubWebHook.X_INSTANCE_IDENTITY);
if (v == null) {
// people might be running clever apps that's not Jenkins, and that's OK
return FormValidation.warning("It doesn't look like %s is talking to any Jenkins. "
+ "Are you running your own app?", value);
}
RSAPublicKey key = identity.getPublic();
String expected = new String(Base64.encodeBase64(key.getEncoded()), UTF_8);
if (!expected.equals(v)) {
// if it responds but with a different ID, that's more likely wrong than correct
return FormValidation.error("%s is connecting to different Jenkins instances", value);
}
return FormValidation.ok();
} catch (IOException e) {
return FormValidation.error(e, "Failed to test a connection to %s", value);
}
}
/**
* Used by default in {@link #getHookUrl()}
*
* @return url to be used in GH hooks configuration as main endpoint
* @throws GHPluginConfigException if jenkins root url empty of malformed
*/
private static URL constructDefaultUrl() {
String jenkinsUrl = Jenkins.getInstance().getRootUrl();
validateConfig(isNotEmpty(jenkinsUrl), Messages.global_config_url_is_empty());
try {
return new URL(jenkinsUrl + GitHubWebHook.get().getUrlName() + '/');
} catch (MalformedURLException e) {
throw new GHPluginConfigException(Messages.global_config_hook_url_is_malformed(e.getMessage()));
}
}
/**
* Util method just to hide one more if for better readability
*
* @param state to check. If false, then exception will be thrown
* @param message message to describe exception in case of false state
*
* @throws GHPluginConfigException if state is false
*/
private static void validateConfig(boolean state, String message) {
if (!state) {
throw new GHPluginConfigException(message);
}
}
public HookSecretConfig getHookSecretConfig() {
return hookSecretConfig;
}
public void setHookSecretConfig(HookSecretConfig hookSecretConfig) {
this.hookSecretConfig = hookSecretConfig;
}
}