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}