package jenkins.plugins.nodejs.configfiles;
import static jenkins.plugins.nodejs.NodeJSConstants.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.codec.binary.Base64;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.HostnameRequirement;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Util;
import hudson.model.Run;
import hudson.util.Secret;
/**
* Helper to fill properly credentials in the the user configuration file.
*
* @author Nikolas Falco
* @since 1.0
*/
public final class RegistryHelper {
private final Collection<NPMRegistry> registries;
public RegistryHelper(@CheckForNull Collection<NPMRegistry> registries) {
this.registries = registries;
}
/**
* Resolves all registry credentials and returns a map paring registry URL
* to credential.
*
* @param build a build being run
* @return map of registry URL - credential
*/
public Map<String, StandardUsernameCredentials> resolveCredentials(Run<?, ?> build) {
Map<String, StandardUsernameCredentials> registry2credential = new HashMap<>();
for (NPMRegistry registry : registries) {
String credentialsId = registry.getCredentialsId();
if (credentialsId != null) {
// create a domain filter based on registry URL
final URL registryURL = toURL(registry.getUrl());
List<DomainRequirement> domainRequirements = Collections.emptyList();
if (registryURL != null) {
domainRequirements = Collections.<DomainRequirement> singletonList(new HostnameRequirement(registryURL.getHost()));
}
StandardUsernameCredentials c = CredentialsProvider.findCredentialById(credentialsId, StandardUsernameCredentials.class, build, domainRequirements);
if (c != null) {
registry2credential.put(registry.getUrl(), c);
}
}
}
return registry2credential;
}
/**
* Fill the npmpc user config with the given registries.
*
* @param npmrcContent .npmrc user config
* @param registry2Credentials the credentials to be inserted into the user
* config (key: registry URL, value: Jenkins credentials)
* @return the updated version of the {@code npmrcContent} with the registry
* credentials added
*/
@SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "npm auth_token could not support base64 UTF-8 char encoding")
public String fillRegistry(String npmrcContent, Map<String, StandardUsernameCredentials> registry2Credentials) {
Npmrc npmrc = new Npmrc();
npmrc.from(npmrcContent);
for (NPMRegistry registry : registries) {
StandardUsernamePasswordCredentials credentials = null;
if (registry2Credentials.containsKey(registry.getUrl())) {
credentials = (StandardUsernamePasswordCredentials) registry2Credentials.get(registry.getUrl());
}
if (registry.isHasScopes()) {
for (String scope : registry.getScopesAsList()) {
// remove protocol from the registry URL
String registryPrefix = calculatePrefix(registry.getUrl());
// ensure that URL ends with the / or will not match with scoped entries
String registryURL = fixURL(registry.getUrl());
// add scoped values to the user config file
npmrc.set(compose('@' + scope, NPM_SETTINGS_REGISTRY), registryURL);
npmrc.set(compose(registryPrefix, NPM_SETTINGS_ALWAYS_AUTH), credentials != null);
if (credentials != null) { // NOSONAR
// the _auth directive seems not be considered for scoped registry
// only authToken or username/password works
String passwordValue = Base64.encodeBase64String(Secret.toString(credentials.getPassword()).getBytes());
npmrc.set(compose(registryPrefix, NPM_SETTINGS_USER), credentials.getUsername());
npmrc.set(compose(registryPrefix, NPM_SETTINGS_PASSWORD), passwordValue);
}
}
} else {
// add values to the user config file
npmrc.set(NPM_SETTINGS_REGISTRY, registry.getUrl());
npmrc.set(NPM_SETTINGS_ALWAYS_AUTH, credentials != null);
if (credentials != null) {
String authValue = credentials.getUsername() + ':' + Secret.toString(credentials.getPassword());
authValue = Base64.encodeBase64String(authValue.getBytes());
npmrc.set(NPM_SETTINGS_AUTH, authValue);
}
}
}
return npmrc.toString();
}
@Nonnull
private String fixURL(@Nonnull final String registryURL) {
String url = registryURL;
if (!url.endsWith("/")) {
url += "/";
}
return url;
}
@Nonnull
public String calculatePrefix(@Nonnull final String registryURL) {
String trimmedURL = trimSlash(registryURL);
URL url = toURL(trimmedURL);
if (url == null) {
throw new IllegalArgumentException("Invalid url " + registryURL);
}
return "//" + trimmedURL.substring((url.getProtocol() + "://").length()) + '/';
}
@Nonnull
public String compose(@Nonnull final String registryPrefix, @Nonnull final String setting) {
return registryPrefix + ":" + setting;
}
@Nonnull
private String trimSlash(@Nonnull final String url) {
if (url != null && url.endsWith("/")) {
return url.substring(0, url.length() - 1);
}
return url;
}
@CheckForNull
private static URL toURL(@Nullable final String url) {
URL result = null;
String fixedURL = Util.fixEmptyAndTrim(url);
if (fixedURL != null) {
try {
return new URL(fixedURL);
} catch (MalformedURLException e) {
// no filter based on hostname
}
}
return result;
}
}