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.ServiceLoader; 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, ChallengeProvider> 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, ChallengeProvider> challengeMap() { 085 Map<String, ChallengeProvider> 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 for (ChallengeProvider provider : ServiceLoader.load(ChallengeProvider.class)) { 092 ChallengeType typeAnno = provider.getClass().getAnnotation(ChallengeType.class); 093 if (typeAnno == null) { 094 throw new IllegalStateException("ChallengeProvider " 095 + provider.getClass().getName() 096 + " has no @ChallengeType annotation"); 097 } 098 String type = typeAnno.value(); 099 if (type == null || type.trim().isEmpty()) { 100 throw new IllegalStateException("ChallengeProvider " 101 + provider.getClass().getName() 102 + ": type must not be null or empty"); 103 } 104 if (map.containsKey(type)) { 105 throw new IllegalStateException("ChallengeProvider " 106 + provider.getClass().getName() 107 + ": there is already a provider for challenge type " 108 + type); 109 } 110 map.put(type, provider); 111 } 112 113 return Collections.unmodifiableMap(map); 114 } 115 116 /** 117 * {@inheritDoc} 118 * <p> 119 * This implementation handles the standard challenge types. For unknown types, 120 * generic {@link Challenge} or {@link TokenChallenge} instances are created. 121 * <p> 122 * Custom provider implementations may override this method to provide challenges that 123 * are unique to the provider. 124 */ 125 @Override 126 public Challenge createChallenge(Login login, JSON data) { 127 Objects.requireNonNull(login, "login"); 128 Objects.requireNonNull(data, "data"); 129 130 String type = data.get("type").asString(); 131 132 ChallengeProvider constructor = CHALLENGES.get(type); 133 if (constructor != null) { 134 return constructor.create(login, data); 135 } 136 137 if (data.contains("token")) { 138 return new TokenChallenge(login, data); 139 } else { 140 return new Challenge(login, data); 141 } 142 } 143 144 /** 145 * Creates a {@link HttpConnector}. 146 * <p> 147 * Subclasses may override this method to configure the {@link HttpConnector}. 148 */ 149 protected HttpConnector createHttpConnector() { 150 return new HttpConnector(); 151 } 152 153}