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