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.Objects.requireNonNull;
017import static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;
018
019import java.net.MalformedURLException;
020import java.net.URL;
021import java.security.KeyPair;
022import java.security.cert.X509Certificate;
023import java.util.Objects;
024
025import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
026import org.shredzone.acme4j.challenge.Challenge;
027import org.shredzone.acme4j.connector.Resource;
028import org.shredzone.acme4j.exception.AcmeException;
029import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
030import org.shredzone.acme4j.exception.AcmeProtocolException;
031import org.shredzone.acme4j.toolbox.JSON;
032
033/**
034 * A {@link Login} into an account.
035 * <p>
036 * A login is bound to a {@link Session}. However, a {@link Session} can handle multiple
037 * logins in parallel.
038 * <p>
039 * To create a login, you need to specify the location URI of the {@link Account}, and
040 * need to provide the {@link KeyPair} the account was created with. If the account's
041 * location URL is unknown, the account can be re-registered with the
042 * {@link AccountBuilder}, using {@link AccountBuilder#onlyExisting()} to make sure that
043 * no new account will be created. If the key pair was lost though, there is no automatic
044 * way to regain access to your account, and you have to contact your CA's support hotline
045 * for assistance.
046 * <p>
047 * Note that {@link Login} objects are intentionally not serializable, as they contain a
048 * keypair and volatile data. On distributed systems, you can create a {@link Login} to
049 * the same account for every service instance.
050 */
051public class Login {
052
053    private final Session session;
054    private final URL accountLocation;
055    private final Account account;
056    private KeyPair keyPair;
057
058    /**
059     * Creates a new {@link Login}.
060     *
061     * @param accountLocation
062     *            Account location {@link URL}
063     * @param keyPair
064     *            {@link KeyPair} of the account
065     * @param session
066     *            {@link Session} to be used
067     */
068    public Login(URL accountLocation, KeyPair keyPair, Session session) {
069        this.accountLocation = Objects.requireNonNull(accountLocation, "accountLocation");
070        this.keyPair = Objects.requireNonNull(keyPair, "keyPair");
071        this.session = Objects.requireNonNull(session, "session");
072        this.account = new Account(this);
073    }
074
075    /**
076     * Gets the {@link Session} that is used.
077     */
078    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
079    public Session getSession() {
080        return session;
081    }
082
083    /**
084     * Gets the {@link KeyPair} of the ACME account.
085     */
086    public KeyPair getKeyPair() {
087        return keyPair;
088    }
089
090    /**
091     * Gets the location {@link URL} of the account.
092     */
093    public URL getAccountLocation() {
094        return accountLocation;
095    }
096
097    /**
098     * Gets the {@link Account} that is bound to this login.
099     *
100     * @return {@link Account} bound to the login
101     */
102    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
103    public Account getAccount() {
104        return account;
105    }
106
107    /**
108     * Creates a new instance of an existing {@link Authorization} and binds it to this
109     * login.
110     *
111     * @param location
112     *         Location of the Authorization
113     * @return {@link Authorization} bound to the login
114     */
115    public Authorization bindAuthorization(URL location) {
116        return new Authorization(this, requireNonNull(location, "location"));
117    }
118
119    /**
120     * Creates a new instance of an existing {@link Certificate} and binds it to this
121     * login.
122     *
123     * @param location
124     *         Location of the Certificate
125     * @return {@link Certificate} bound to the login
126     */
127    public Certificate bindCertificate(URL location) {
128        return new Certificate(this, requireNonNull(location, "location"));
129    }
130
131    /**
132     * Creates a new instance of an existing {@link Order} and binds it to this login.
133     *
134     * @param location
135     *         Location URL of the order
136     * @return {@link Order} bound to the login
137     */
138    public Order bindOrder(URL location) {
139        return new Order(this, requireNonNull(location, "location"));
140    }
141
142    /**
143     * Creates a new instance of an existing {@link RenewalInfo} and binds it to this
144     * login.
145     *
146     * @param location
147     *         Location URL of the renewal info
148     * @return {@link RenewalInfo} bound to the login
149     * @since 3.0.0
150     */
151    public RenewalInfo bindRenewalInfo(URL location) {
152        return new RenewalInfo(this, requireNonNull(location, "location"));
153    }
154
155    /**
156     * Creates a new instance of an existing {@link RenewalInfo} and binds it to this
157     * login.
158     *
159     * @param certificate
160     *         {@link X509Certificate} to get the {@link RenewalInfo} for
161     * @return {@link RenewalInfo} bound to the login
162     * @draft This method is currently based on an RFC draft. It may be changed or removed
163     * without notice to reflect future changes to the draft. SemVer rules do not apply
164     * here.
165     * @since 3.2.0
166     */
167    public RenewalInfo bindRenewalInfo(X509Certificate certificate) throws AcmeException {
168        try {
169            var url = getSession().resourceUrl(Resource.RENEWAL_INFO).toExternalForm();
170            if (!url.endsWith("/")) {
171                url += '/';
172            }
173            url += getRenewalUniqueIdentifier(certificate);
174            return bindRenewalInfo(new URL(url));
175        } catch (MalformedURLException ex) {
176            throw new AcmeProtocolException("Invalid RenewalInfo URL", ex);
177        }
178    }
179
180    /**
181     * Creates a new instance of an existing {@link Challenge} and binds it to this
182     * login. Use this method only if the resulting challenge type is unknown.
183     *
184     * @param location
185     *         Location URL of the challenge
186     * @return {@link Challenge} bound to the login
187     * @since 2.8
188     * @see #bindChallenge(URL, Class)
189     */
190    public Challenge bindChallenge(URL location) {
191        try (var connect = session.connect()) {
192            connect.sendSignedPostAsGetRequest(location, this);
193            return createChallenge(connect.readJsonResponse());
194        } catch (AcmeException ex) {
195            throw new AcmeLazyLoadingException(Challenge.class, location, ex);
196        }
197    }
198
199    /**
200     * Creates a new instance of an existing {@link Challenge} and binds it to this
201     * login. Use this method if the resulting challenge type is known.
202     *
203     * @param location
204     *         Location URL of the challenge
205     * @param type
206     *         Expected challenge type
207     * @return Challenge bound to the login
208     * @throws AcmeProtocolException
209     *         if the challenge found at the location does not match the expected
210     *         challenge type.
211     * @since 2.12
212     */
213    public <C extends Challenge> C bindChallenge(URL location, Class<C> type) {
214        var challenge = bindChallenge(location);
215        if (!type.isInstance(challenge)) {
216            throw new AcmeProtocolException("Challenge type " + challenge.getType()
217                    + " does not match requested class " + type);
218        }
219        return type.cast(challenge);
220    }
221
222    /**
223     * Creates a {@link Challenge} instance for the given challenge data.
224     *
225     * @param data
226     *            Challenge JSON data
227     * @return {@link Challenge} instance
228     */
229    public Challenge createChallenge(JSON data) {
230        var challenge = session.provider().createChallenge(this, data);
231        if (challenge == null) {
232            throw new AcmeProtocolException("Could not create challenge for: " + data);
233        }
234        return challenge;
235    }
236
237    /**
238     * Creates a builder for a new {@link Order}.
239     *
240     * @return {@link OrderBuilder} object
241     * @since 3.0.0
242     */
243    public OrderBuilder newOrder() {
244        return new OrderBuilder(this);
245    }
246
247    /**
248     * Sets a different {@link KeyPair}. The new key pair is only used locally in this
249     * instance, but is not set on server side!
250     */
251    protected void setKeyPair(KeyPair keyPair) {
252        this.keyPair = Objects.requireNonNull(keyPair, "keyPair");
253    }
254
255}