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.URI; 017import java.time.ZonedDateTime; 018import java.util.Collections; 019import java.util.HashMap; 020import java.util.Map; 021import java.util.Objects; 022import java.util.ServiceLoader; 023 024import org.shredzone.acme4j.Login; 025import org.shredzone.acme4j.Session; 026import org.shredzone.acme4j.challenge.Challenge; 027import org.shredzone.acme4j.challenge.Dns01Challenge; 028import org.shredzone.acme4j.challenge.Http01Challenge; 029import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; 030import org.shredzone.acme4j.challenge.TokenChallenge; 031import org.shredzone.acme4j.connector.Connection; 032import org.shredzone.acme4j.connector.DefaultConnection; 033import org.shredzone.acme4j.connector.HttpConnector; 034import org.shredzone.acme4j.connector.NetworkSettings; 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 private static final int HTTP_NOT_MODIFIED = 304; 047 048 private static final Map<String, ChallengeProvider> CHALLENGES = challengeMap(); 049 050 @Override 051 public Connection connect(URI serverUri, NetworkSettings networkSettings) { 052 return new DefaultConnection(createHttpConnector(networkSettings)); 053 } 054 055 @Override 056 public JSON directory(Session session, URI serverUri) throws AcmeException { 057 var expires = session.getDirectoryExpires(); 058 if (expires != null && expires.isAfter(ZonedDateTime.now())) { 059 // The cached directory is still valid 060 return null; 061 } 062 063 try (var conn = connect(serverUri, session.networkSettings())) { 064 var lastModified = session.getDirectoryLastModified(); 065 var rc = conn.sendRequest(resolve(serverUri), session, lastModified); 066 if (lastModified != null && rc == HTTP_NOT_MODIFIED) { 067 // The server has not been modified since 068 return null; 069 } 070 071 // evaluate caching headers 072 session.setDirectoryLastModified(conn.getLastModified().orElse(null)); 073 session.setDirectoryExpires(conn.getExpiration().orElse(null)); 074 075 // use nonce header if there is one, saves a HEAD request... 076 conn.getNonce().ifPresent(session::setNonce); 077 078 return conn.readJsonResponse(); 079 } 080 } 081 082 private static Map<String, ChallengeProvider> challengeMap() { 083 var map = new HashMap<String, ChallengeProvider>(); 084 085 map.put(Dns01Challenge.TYPE, Dns01Challenge::new); 086 map.put(Http01Challenge.TYPE, Http01Challenge::new); 087 map.put(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new); 088 089 for (var provider : ServiceLoader.load(ChallengeProvider.class)) { 090 var typeAnno = provider.getClass().getAnnotation(ChallengeType.class); 091 if (typeAnno == null) { 092 throw new IllegalStateException("ChallengeProvider " 093 + provider.getClass().getName() 094 + " has no @ChallengeType annotation"); 095 } 096 var type = typeAnno.value(); 097 if (type == null || type.trim().isEmpty()) { 098 throw new IllegalStateException("ChallengeProvider " 099 + provider.getClass().getName() 100 + ": type must not be null or empty"); 101 } 102 if (map.containsKey(type)) { 103 throw new IllegalStateException("ChallengeProvider " 104 + provider.getClass().getName() 105 + ": there is already a provider for challenge type " 106 + type); 107 } 108 map.put(type, provider); 109 } 110 111 return Collections.unmodifiableMap(map); 112 } 113 114 /** 115 * {@inheritDoc} 116 * <p> 117 * This implementation handles the standard challenge types. For unknown types, 118 * generic {@link Challenge} or {@link TokenChallenge} instances are created. 119 * <p> 120 * Custom provider implementations may override this method to provide challenges that 121 * are proprietary to the provider. 122 */ 123 @Override 124 public Challenge createChallenge(Login login, JSON data) { 125 Objects.requireNonNull(login, "login"); 126 Objects.requireNonNull(data, "data"); 127 128 var type = data.get("type").asString(); 129 130 var constructor = CHALLENGES.get(type); 131 if (constructor != null) { 132 return constructor.create(login, data); 133 } 134 135 if (data.contains("token")) { 136 return new TokenChallenge(login, data); 137 } else { 138 return new Challenge(login, data); 139 } 140 } 141 142 /** 143 * Creates a {@link HttpConnector}. 144 * <p> 145 * Subclasses may override this method to configure the {@link HttpConnector}. 146 */ 147 protected HttpConnector createHttpConnector(NetworkSettings settings) { 148 return new HttpConnector(settings); 149 } 150 151}