/*
* dCache - http://www.dcache.org/
*
* Copyright (C) 2016 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.dcache.gplazma.util;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.globus.gsi.gssapi.jaas.GlobusPrincipal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static com.google.common.base.Preconditions.checkState;
import static org.dcache.gplazma.util.IGTFInfo.Type.POLICY;
import static org.dcache.gplazma.util.IGTFInfo.Type.TRUST_ANCHOR;
/**
* Represents the information contains within an IGTF .info file; either
* a profile or a trust-anchor file. The semantics of this class' fields
* are defined here:
*
* https://wiki.eugridpma.org/Main/IGTFInfoFile
*/
public class IGTFInfo
{
public enum Type {
/**
* Information about a policy. This is typically used to identify
* CAs that have been accepted by some grid infrastructure.
*/
POLICY,
/** Information about a specific Certificate Authority. */
TRUST_ANCHOR
}
/**
* The current status of a trust anchor.
*/
public static enum Status
{
DISCONTINUED(false),
EXPERIMENTAL(false),
UNACCREDITED(false),
ACCREDITED_CLASSIC(true),
ACCREDITED_MICS(true),
ACCREDITED_SLCS(true),
ACCREDITED_IOTA(true);
private final boolean isAccredited;
Status(boolean isAccredited)
{
this.isAccredited = isAccredited;
}
public boolean isAccredited()
{
return isAccredited;
}
}
private static final Map<String,Status> TO_STATUS;
static {
ImmutableMap.Builder<String,Status> mapping = ImmutableMap.builder();
mapping.put("discontinued", Status.DISCONTINUED);
mapping.put("experimental", Status.EXPERIMENTAL);
mapping.put("unaccredited", Status.UNACCREDITED);
mapping.put("accredited:classic", Status.ACCREDITED_CLASSIC);
mapping.put("accredited:mics", Status.ACCREDITED_MICS);
mapping.put("accredited:slcs", Status.ACCREDITED_SLCS);
mapping.put("accredited:iota", Status.ACCREDITED_IOTA);
TO_STATUS = mapping.build();
}
private static final CharMatcher VALID_HEX = CharMatcher.inRange('0', '9').or(CharMatcher.inRange('A', 'F'));
private final Type type;
private boolean immutable;
private String name;
private String alias;
private Version version;
private URI caUrl;
private List<URI> crlUrl = ImmutableList.of();
private URI policyUrl;
private URI email;
private Status status;
private URI url;
private BigInteger sha1fp0;
private ImmutableList<GlobusPrincipal> dns = ImmutableList.of();
private Map<String,String> policyRequires = ImmutableMap.of();
private List<String> trustAnchorRequires = ImmutableList.of();
private List<String> obsoletes;
private List<String> problems = null;
private IGTFInfo(Type type)
{
this.type = type;
}
public static IGTFInfo.Builder builder(Type type)
{
return new IGTFInfo(type).new Builder();
}
public Type getType()
{
return type;
}
/**
* The name is some non-null string that represents this TrustAnchor
* or Policy. The alias is used, if available, otherwise it is a name
* derived from the filename.
*/
public String getName()
{
if (alias != null) {
return alias;
} else if (name != null) {
return name;
}
throw new IllegalStateException("info file has no alias and filename was not specified");
}
public String getAlias()
{
return alias;
}
public Version getVersion()
{
return version;
}
public URI getCAUrl()
{
return caUrl;
}
public List<URI> getCRLUrls()
{
return crlUrl;
}
public URI getPolicyUrl()
{
return policyUrl;
}
public URI getEmail()
{
return email;
}
public Status getStatus()
{
return status;
}
public URI getUrl()
{
return url;
}
public BigInteger getSHA1FP0()
{
return sha1fp0;
}
public GlobusPrincipal getSubjectDN()
{
return dns.isEmpty() ? null : dns.get(0);
}
public List<GlobusPrincipal> getSubjectDNs()
{
return dns;
}
public Map<String,String> getPolicyRequires()
{
return policyRequires;
}
public List<String> getTrustAnchorRequires()
{
return trustAnchorRequires;
}
public List<String> getObsoletes()
{
return obsoletes;
}
private void require(boolean isDefined, String name, Type... types)
{
for (Type type : types) {
if (this.type == type && !isDefined) {
if (problems == null) {
problems = new ArrayList<>();
}
problems.add("missing '" + name + "'");
}
}
}
private void checkValid() throws ParserException
{
require(version != null, "version", POLICY, TRUST_ANCHOR);
require(!dns.isEmpty(), "subjectdn", POLICY, TRUST_ANCHOR);
require(!policyRequires.isEmpty(), "requires", POLICY);
require(alias != null, "alias", TRUST_ANCHOR);
require(crlUrl != null, "crl_url", TRUST_ANCHOR);
require(email != null, "email", TRUST_ANCHOR);
require(status != null, "status", TRUST_ANCHOR);
if (problems != null) {
String description = problems.size() == 1 ? problems.get(0) : problems.toString();
throw new ParserException("bad info file: " + description);
}
}
public class Builder
{
private void checkMutable()
{
checkState(!IGTFInfo.this.immutable, "IGTFPolicy.Builder#build has been called");
}
public void setAlias(String alias)
{
checkMutable();
IGTFInfo.this.alias = alias;
}
public void setFilename(String name)
{
checkMutable();
if (name.startsWith("policy-")) {
name = name.substring(7);
}
if (name.endsWith(".info")) {
name = name.substring(0, name.length()-5);
}
IGTFInfo.this.name = name;
}
public void setVersion(String version) throws ParserException
{
checkMutable();
IGTFInfo.this.version = new Version(version);
}
public void setCAUrl(String url) throws ParserException
{
checkMutable();
try {
IGTFInfo.this.caUrl = new URI(url);
} catch (URISyntaxException e) {
throw new ParserException(e);
}
}
public void setCRLUrl(String urlList) throws ParserException
{
checkMutable();
try {
ImmutableList.Builder<URI> urls = ImmutableList.builder();
for (String url : Splitter.on(';').trimResults().split(urlList)) {
urls.add(new URI(url));
}
IGTFInfo.this.crlUrl = urls.build();
} catch (URISyntaxException e) {
throw new ParserException(e);
}
}
public void setPolicyUrl(String url) throws ParserException
{
checkMutable();
try {
IGTFInfo.this.policyUrl = new URI(url);
} catch (URISyntaxException e) {
throw new ParserException(e);
}
}
public void setEmail(String address) throws ParserException
{
checkMutable();
try {
IGTFInfo.this.email = new URI("mailto:" + address);
} catch (URISyntaxException e) {
throw new ParserException(e);
}
}
public void setStatus(String status) throws ParserException
{
checkMutable();
IGTFInfo.this.status = TO_STATUS.get(status);
if (IGTFInfo.this.status == null) {
throw new ParserException("Unknown value '" + status + "'");
}
}
public void setUrl(String url) throws ParserException
{
checkMutable();
try {
IGTFInfo.this.url = new URI(url);
} catch (URISyntaxException e) {
throw new ParserException(e);
}
}
public void setSHA1FP0(String value) throws ParserException
{
checkMutable();
StringBuilder onlyHex = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (VALID_HEX.matches(c)) {
onlyHex.append(c);
} else if (c != ':') {
throw new ParserException("Invalid character '" + c + "'");
}
}
try {
IGTFInfo.this.sha1fp0 = new BigInteger(onlyHex.toString(), 16);
} catch (NumberFormatException e) {
throw new ParserException("Invalid value: " + e.getMessage(), e);
}
}
public void setSubjectDN(String value) throws ParserException
{
checkMutable();
ImmutableList.Builder<GlobusPrincipal> dns = ImmutableList.builder();
if (type == TRUST_ANCHOR) {
GlobusPrincipal p = new GlobusPrincipal(checkValidQuotedDn(value));
dns.add(p);
IGTFInfo.this.dns = dns.build();
} else {
// REVISIT: how to handle DNs with a double-quote?
for (String dn : Splitter.on(',').omitEmptyStrings().trimResults().split(value)) {
GlobusPrincipal p = new GlobusPrincipal(checkValidQuotedDn(dn));
dns.add(p);
}
ImmutableList<GlobusPrincipal> list = dns.build();
checkValid(!list.isEmpty(), "no Distinguished Names");
IGTFInfo.this.dns = list;
}
}
private String checkValidQuotedDn(String value) throws ParserException
{
checkMutable();
checkValid(value.startsWith("\""), "value does not start with '\"'");
checkValid(value.endsWith("\""), "value does not end with '\"'");
checkValid(value.length() > 2, "missing quoted content");
return value.substring(1, value.length()-1);
}
public void setRequires(String value) throws ParserException
{
checkMutable();
switch (type) {
case POLICY:
Map<String,String> pr = Splitter.on(',').trimResults().
withKeyValueSeparator(Splitter.on('=').trimResults()).split(value);
IGTFInfo.this.policyRequires = ImmutableMap.copyOf(pr);
break;
case TRUST_ANCHOR:
IGTFInfo.this.trustAnchorRequires =
ImmutableList.copyOf(Splitter.on(',').trimResults().split(value));
break;
}
}
public void setObsoletes(String value)
{
checkMutable();
ImmutableList.Builder<String> obsoletes = ImmutableList.builder();
for (String item : Splitter.on(',').trimResults().split(value)) {
obsoletes.add(item);
}
IGTFInfo.this.obsoletes = obsoletes.build();
}
public IGTFInfo build() throws ParserException
{
IGTFInfo.this.immutable = true;
IGTFInfo.this.checkValid();
return IGTFInfo.this;
}
}
public static class ParserException extends Exception
{
public ParserException(String message) {
super(message);
}
public ParserException(String message, Throwable t) {
super(message, t);
}
public ParserException(Throwable t) {
super(t.getMessage(), t);
}
}
public static class Version
{
private final int major;
private final int minor;
private final String pkg;
private final String value;
public Version(String value) throws ParserException
{
this.value = value;
int dot = value.indexOf('.');
if (dot == -1) {
throw new ParserException("Malformed version: missing '.'");
}
try {
major = Integer.parseInt(value.substring(0, dot));
} catch (NumberFormatException e) {
throw new ParserException("Malformed major version: " + e.getMessage(), e);
}
int dash = value.indexOf('-');
String minorValue;
if (dash == -1) {
minorValue = value.substring(dot+1);
pkg = null;
} else {
minorValue = value.substring(dot+1, dash);
pkg = value.substring(dash+1);
}
try {
this.minor = Integer.parseInt(minorValue);
} catch (NumberFormatException e) {
throw new ParserException("Malformed minor version: " + e.getMessage(), e);
}
}
public String getVersion()
{
return value;
}
public int getMajor()
{
return major;
}
public int getMinor()
{
return minor;
}
public String getPackage()
{
return pkg;
}
@Override
public int hashCode()
{
return value.hashCode();
}
@Override
public boolean equals(Object other)
{
if (other == this) {
return true;
}
if (!(other instanceof Version)) {
return false;
}
Version v = (Version) other;
return v.major == this.major &&
v.minor == this.minor &&
Objects.equals(v.pkg, this.pkg);
}
}
public static void checkValid(boolean isOK, String message) throws ParserException
{
if (!isOK) {
throw new ParserException(message);
}
}
}