001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2015 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.provider;
015
016import java.net.HttpURLConnection;
017import java.net.URI;
018import java.time.ZonedDateTime;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Objects;
023import java.util.function.BiFunction;
024
025import org.shredzone.acme4j.Login;
026import org.shredzone.acme4j.Session;
027import org.shredzone.acme4j.challenge.Challenge;
028import org.shredzone.acme4j.challenge.Dns01Challenge;
029import org.shredzone.acme4j.challenge.Http01Challenge;
030import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
031import org.shredzone.acme4j.challenge.TokenChallenge;
032import org.shredzone.acme4j.connector.Connection;
033import org.shredzone.acme4j.connector.DefaultConnection;
034import org.shredzone.acme4j.connector.HttpConnector;
035import org.shredzone.acme4j.exception.AcmeException;
036import org.shredzone.acme4j.toolbox.JSON;
037
038/**
039 * Abstract implementation of {@link AcmeProvider}. It consists of a challenge
040 * registry and a standard {@link HttpConnector}.
041 * <p>
042 * Implementing classes must implement at least {@link AcmeProvider#accepts(URI)}
043 * and {@link AbstractAcmeProvider#resolve(URI)}.
044 */
045public abstract class AbstractAcmeProvider implements AcmeProvider {
046
047    private static final Map<String, BiFunction<Login, JSON, Challenge>> CHALLENGES = challengeMap();
048
049    @Override
050    public Connection connect(URI serverUri) {
051        return new DefaultConnection(createHttpConnector());
052    }
053
054    @Override
055    public JSON directory(Session session, URI serverUri) throws AcmeException {
056        ZonedDateTime expires = session.getDirectoryExpires();
057        if (expires != null && expires.isAfter(ZonedDateTime.now())) {
058            // The cached directory is still valid
059            return null;
060        }
061
062        try (Connection conn = connect(serverUri)) {
063            ZonedDateTime lastModified = session.getDirectoryLastModified();
064            int rc = conn.sendRequest(resolve(serverUri), session, lastModified);
065            if (lastModified != null && rc == HttpURLConnection.HTTP_NOT_MODIFIED) {
066                // The server has not been modified since
067                return null;
068            }
069
070            // evaluate caching headers
071            session.setDirectoryLastModified(conn.getLastModified().orElse(null));
072            session.setDirectoryExpires(conn.getExpiration().orElse(null));
073
074            // use nonce header if there is one, saves a HEAD request...
075            String nonce = conn.getNonce();
076            if (nonce != null) {
077                session.setNonce(nonce);
078            }
079
080            return conn.readJsonResponse();
081        }
082    }
083
084    private static Map<String, BiFunction<Login, JSON, Challenge>> challengeMap() {
085        Map<String, BiFunction<Login, JSON, Challenge>> map = new HashMap<>();
086
087        map.put(Dns01Challenge.TYPE, Dns01Challenge::new);
088        map.put(Http01Challenge.TYPE, Http01Challenge::new);
089        map.put(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);
090
091        return Collections.unmodifiableMap(map);
092    }
093
094    /**
095     * {@inheritDoc}
096     * <p>
097     * This implementation handles the standard challenge types. For unknown types,
098     * generic {@link Challenge} or {@link TokenChallenge} instances are created.
099     * <p>
100     * Custom provider implementations may override this method to provide challenges that
101     * are unique to the provider.
102     */
103    @Override
104    public Challenge createChallenge(Login login, JSON data) {
105        Objects.requireNonNull(login, "login");
106        Objects.requireNonNull(data, "data");
107
108        String type = data.get("type").asString();
109
110        BiFunction<Login, JSON, Challenge> constructor = CHALLENGES.get(type);
111        if (constructor != null) {
112            return constructor.apply(login, data);
113        }
114
115        if (data.contains("token")) {
116            return new TokenChallenge(login, data);
117        } else {
118            return new Challenge(login, data);
119        }
120    }
121
122    /**
123     * Creates a {@link HttpConnector}.
124     * <p>
125     * Subclasses may override this method to configure the {@link HttpConnector}.
126     */
127    protected HttpConnector createHttpConnector() {
128        return new HttpConnector();
129    }
130
131}