org.shredzone.acme4j.provider.AbstractAcmeProvider Maven / Gradle / Ivy
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*/
package org.shredzone.acme4j.provider;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
import org.shredzone.acme4j.challenge.TokenChallenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.DefaultConnection;
import org.shredzone.acme4j.connector.HttpConnector;
import org.shredzone.acme4j.connector.NetworkSettings;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.toolbox.JSON;
/**
* Abstract implementation of {@link AcmeProvider}. It consists of a challenge
* registry and a standard {@link HttpConnector}.
*
* Implementing classes must implement at least {@link AcmeProvider#accepts(URI)}
* and {@link AbstractAcmeProvider#resolve(URI)}.
*/
public abstract class AbstractAcmeProvider implements AcmeProvider {
private static final int HTTP_NOT_MODIFIED = 304;
private static final Map CHALLENGES = challengeMap();
@Override
public Connection connect(URI serverUri, NetworkSettings networkSettings) {
return new DefaultConnection(createHttpConnector(networkSettings));
}
@Override
public JSON directory(Session session, URI serverUri) throws AcmeException {
var expires = session.getDirectoryExpires();
if (expires != null && expires.isAfter(ZonedDateTime.now())) {
// The cached directory is still valid
return null;
}
try (var conn = connect(serverUri, session.networkSettings())) {
var lastModified = session.getDirectoryLastModified();
var rc = conn.sendRequest(resolve(serverUri), session, lastModified);
if (lastModified != null && rc == HTTP_NOT_MODIFIED) {
// The server has not been modified since
return null;
}
// evaluate caching headers
session.setDirectoryLastModified(conn.getLastModified().orElse(null));
session.setDirectoryExpires(conn.getExpiration().orElse(null));
// use nonce header if there is one, saves a HEAD request...
conn.getNonce().ifPresent(session::setNonce);
return conn.readJsonResponse();
}
}
private static Map challengeMap() {
var map = new HashMap();
map.put(Dns01Challenge.TYPE, Dns01Challenge::new);
map.put(Http01Challenge.TYPE, Http01Challenge::new);
map.put(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);
for (var provider : ServiceLoader.load(ChallengeProvider.class)) {
var typeAnno = provider.getClass().getAnnotation(ChallengeType.class);
if (typeAnno == null) {
throw new IllegalStateException("ChallengeProvider "
+ provider.getClass().getName()
+ " has no @ChallengeType annotation");
}
var type = typeAnno.value();
if (type == null || type.trim().isEmpty()) {
throw new IllegalStateException("ChallengeProvider "
+ provider.getClass().getName()
+ ": type must not be null or empty");
}
if (map.containsKey(type)) {
throw new IllegalStateException("ChallengeProvider "
+ provider.getClass().getName()
+ ": there is already a provider for challenge type "
+ type);
}
map.put(type, provider);
}
return Collections.unmodifiableMap(map);
}
/**
* {@inheritDoc}
*
* This implementation handles the standard challenge types. For unknown types,
* generic {@link Challenge} or {@link TokenChallenge} instances are created.
*
* Custom provider implementations may override this method to provide challenges that
* are proprietary to the provider.
*/
@Override
public Challenge createChallenge(Login login, JSON data) {
Objects.requireNonNull(login, "login");
Objects.requireNonNull(data, "data");
var type = data.get("type").asString();
var constructor = CHALLENGES.get(type);
if (constructor != null) {
return constructor.create(login, data);
}
if (data.contains("token")) {
return new TokenChallenge(login, data);
} else {
return new Challenge(login, data);
}
}
/**
* Creates a {@link HttpConnector}.
*
* Subclasses may override this method to configure the {@link HttpConnector}.
*/
protected HttpConnector createHttpConnector(NetworkSettings settings) {
return new HttpConnector(settings);
}
}