001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2018 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j;
015
016import static java.util.Collections.unmodifiableMap;
017import static java.util.Objects.requireNonNull;
018import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
019
020import java.io.Serializable;
021import java.net.InetAddress;
022import java.net.UnknownHostException;
023import java.util.Map;
024import java.util.TreeMap;
025
026import org.shredzone.acme4j.exception.AcmeProtocolException;
027import org.shredzone.acme4j.toolbox.JSON;
028
029/**
030 * Represents an identifier.
031 * <p>
032 * The ACME protocol only defines the DNS identifier, which identifies a domain name.
033 * acme4j also supports IP identifiers.
034 * <p>
035 * CAs, and other acme4j modules, may define further, proprietary identifier types.
036 *
037 * @since 2.3
038 */
039public class Identifier implements Serializable {
040    private static final long serialVersionUID = -7777851842076362412L;
041
042    /**
043     * Type constant for DNS identifiers.
044     */
045    public static final String TYPE_DNS = "dns";
046
047    /**
048     * Type constant for IP identifiers.
049     *
050     * @see <a href="https://tools.ietf.org/html/rfc8738">RFC 8738</a>
051     */
052    public static final String TYPE_IP = "ip";
053
054    static final String KEY_TYPE = "type";
055    static final String KEY_VALUE = "value";
056    static final String KEY_ANCESTOR_DOMAIN = "ancestorDomain";
057    static final String KEY_SUBDOMAIN_AUTH_ALLOWED = "subdomainAuthAllowed";
058
059    private final Map<String, Object> content = new TreeMap<>();
060
061    /**
062     * Creates a new {@link Identifier}.
063     * <p>
064     * This is a generic constructor for identifiers. Refer to the documentation of your
065     * CA to find out about the accepted identifier types and values.
066     * <p>
067     * Note that for DNS identifiers, no ASCII encoding of unicode domain takes place
068     * here. Use {@link #dns(String)} instead.
069     *
070     * @param type
071     *            Identifier type
072     * @param value
073     *            Identifier value
074     */
075    public Identifier(String type, String value) {
076        content.put(KEY_TYPE, requireNonNull(type, KEY_TYPE));
077        content.put(KEY_VALUE, requireNonNull(value, KEY_VALUE));
078    }
079
080    /**
081     * Creates a new {@link Identifier} from the given {@link JSON} structure.
082     *
083     * @param json
084     *            {@link JSON} containing the identifier data
085     */
086    public Identifier(JSON json) {
087        if (!json.contains(KEY_TYPE)) {
088            throw new AcmeProtocolException("Required key " + KEY_TYPE + " is missing");
089        }
090        if (!json.contains(KEY_VALUE)) {
091            throw new AcmeProtocolException("Required key " + KEY_VALUE + " is missing");
092        }
093        content.putAll(json.toMap());
094    }
095
096    /**
097     * Makes a copy of the given Identifier.
098     */
099    private Identifier(Identifier identifier) {
100        content.putAll(identifier.content);
101    }
102
103    /**
104     * Creates a new DNS identifier for the given domain name.
105     *
106     * @param domain
107     *            Domain name. Unicode domains are automatically ASCII encoded.
108     * @return New {@link Identifier}
109     */
110    public static Identifier dns(String domain) {
111        return new Identifier(TYPE_DNS, toAce(domain));
112    }
113
114    /**
115     * Creates a new IP identifier for the given {@link InetAddress}.
116     *
117     * @param ip
118     *            {@link InetAddress}
119     * @return New {@link Identifier}
120     */
121    public static Identifier ip(InetAddress ip) {
122        return new Identifier(TYPE_IP, ip.getHostAddress());
123    }
124
125    /**
126     * Creates a new IP identifier for the given {@link InetAddress}.
127     *
128     * @param ip
129     *            IP address as {@link String}
130     * @return New {@link Identifier}
131     * @since 2.7
132     */
133    public static Identifier ip(String ip) {
134        try {
135            return ip(InetAddress.getByName(ip));
136        } catch (UnknownHostException ex) {
137            throw new IllegalArgumentException("Bad IP: " + ip, ex);
138        }
139    }
140
141    /**
142     * Sets an ancestor domain, as required in RFC-9444.
143     *
144     * @param domain
145     *         The ancestor domain to be set. Unicode domains are automatically ASCII
146     *         encoded.
147     * @return An {@link Identifier} that contains the ancestor domain.
148     * @since 3.3.0
149     */
150    public Identifier withAncestorDomain(String domain) {
151        expectType(TYPE_DNS);
152
153        var result = new Identifier(this);
154        result.content.put(KEY_ANCESTOR_DOMAIN, toAce(domain));
155        return result;
156    }
157
158    /**
159     * Gives the permission to authorize subdomains of this domain, as required in
160     * RFC-9444.
161     *
162     * @return An {@link Identifier} that allows subdomain auths.
163     * @since 3.3.0
164     */
165    public Identifier allowSubdomainAuth() {
166        expectType(TYPE_DNS);
167
168        var result = new Identifier(this);
169        result.content.put(KEY_SUBDOMAIN_AUTH_ALLOWED, true);
170        return result;
171    }
172
173    /**
174     * Returns the identifier type.
175     */
176    public String getType() {
177        return content.get(KEY_TYPE).toString();
178    }
179
180    /**
181     * Returns the identifier value.
182     */
183    public String getValue() {
184        return content.get(KEY_VALUE).toString();
185    }
186
187    /**
188     * Returns the domain name if this is a DNS identifier.
189     *
190     * @return Domain name. Unicode domains are ASCII encoded.
191     * @throws AcmeProtocolException
192     *             if this is not a DNS identifier.
193     */
194    public String getDomain() {
195        expectType(TYPE_DNS);
196        return getValue();
197    }
198
199    /**
200     * Returns the IP address if this is an IP identifier.
201     *
202     * @return {@link InetAddress}
203     * @throws AcmeProtocolException
204     *             if this is not a DNS identifier.
205     */
206    public InetAddress getIP() {
207        expectType(TYPE_IP);
208        try {
209            return InetAddress.getByName(getValue());
210        } catch (UnknownHostException ex) {
211            throw new AcmeProtocolException("bad ip identifier value", ex);
212        }
213    }
214
215    /**
216     * Returns the identifier as JSON map.
217     */
218    public Map<String, Object> toMap() {
219        return unmodifiableMap(content);
220    }
221
222    /**
223     * Makes sure this identifier is of the given type.
224     *
225     * @param type
226     *         Expected type
227     * @throws AcmeProtocolException
228     *         if this identifier is of a different type
229     */
230    private void expectType(String type) {
231        if (!type.equals(getType())) {
232            throw new AcmeProtocolException("expected '" + type + "' identifier, but found '" + getType() + "'");
233        }
234    }
235
236    @Override
237    public String toString() {
238        if (content.size() == 2) {
239            return getType() + '=' + getValue();
240        }
241        return content.toString();
242    }
243
244    @Override
245    public boolean equals(Object obj) {
246        if (!(obj instanceof Identifier)) {
247            return false;
248        }
249
250        var i = (Identifier) obj;
251        return content.equals(i.content);
252    }
253
254    @Override
255    public int hashCode() {
256        return content.hashCode();
257    }
258
259    @Override
260    protected final void finalize() {
261        // CT_CONSTRUCTOR_THROW: Prevents finalizer attack
262    }
263
264}