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.challenge; 015 016import java.time.Instant; 017import java.util.Optional; 018 019import org.shredzone.acme4j.AcmeJsonResource; 020import org.shredzone.acme4j.Login; 021import org.shredzone.acme4j.Problem; 022import org.shredzone.acme4j.Status; 023import org.shredzone.acme4j.exception.AcmeException; 024import org.shredzone.acme4j.exception.AcmeProtocolException; 025import org.shredzone.acme4j.toolbox.JSON; 026import org.shredzone.acme4j.toolbox.JSON.Value; 027import org.shredzone.acme4j.toolbox.JSONBuilder; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031/** 032 * A generic challenge. It can be used as a base class for actual challenge 033 * implementations, but it is also used if the ACME server offers a proprietary challenge 034 * that is unknown to acme4j. 035 * <p> 036 * Subclasses must override {@link Challenge#acceptable(String)} so it only accepts its 037 * own type. {@link Challenge#prepareResponse(JSONBuilder)} can be overridden to put all 038 * required data to the challenge response. 039 */ 040public class Challenge extends AcmeJsonResource { 041 private static final long serialVersionUID = 2338794776848388099L; 042 private static final Logger LOG = LoggerFactory.getLogger(Challenge.class); 043 044 protected static final String KEY_TYPE = "type"; 045 protected static final String KEY_URL = "url"; 046 protected static final String KEY_STATUS = "status"; 047 protected static final String KEY_VALIDATED = "validated"; 048 protected static final String KEY_ERROR = "error"; 049 050 /** 051 * Creates a new generic {@link Challenge} object. 052 * 053 * @param login 054 * {@link Login} the resource is bound with 055 * @param data 056 * {@link JSON} challenge data 057 */ 058 public Challenge(Login login, JSON data) { 059 super(login, data.get(KEY_URL).asURL()); 060 setJSON(data); 061 } 062 063 /** 064 * Returns the challenge type by name (e.g. "http-01"). 065 */ 066 public String getType() { 067 return getJSON().get(KEY_TYPE).asString(); 068 } 069 070 /** 071 * Returns the current status of the challenge. 072 * <p> 073 * Possible values are: {@link Status#PENDING}, {@link Status#PROCESSING}, 074 * {@link Status#VALID}, {@link Status#INVALID}. 075 * <p> 076 * A challenge is only completed when it reaches either status {@link Status#VALID} or 077 * {@link Status#INVALID}. 078 */ 079 public Status getStatus() { 080 return getJSON().get(KEY_STATUS).asStatus(); 081 } 082 083 /** 084 * Returns the validation date, if returned by the server. 085 */ 086 public Optional<Instant> getValidated() { 087 return getJSON().get(KEY_VALIDATED).map(Value::asInstant); 088 } 089 090 /** 091 * Returns a reason why the challenge has failed in the past, if returned by the 092 * server. If there are multiple errors, they can be found in 093 * {@link Problem#getSubProblems()}. 094 */ 095 public Optional<Problem> getError() { 096 return getJSON().get(KEY_ERROR).map(it -> it.asProblem(getLocation())); 097 } 098 099 /** 100 * Prepares the response message for triggering the challenge. Subclasses can add 101 * fields to the {@link JSONBuilder} as required by the challenge. Implementations of 102 * subclasses should make sure that {@link #prepareResponse(JSONBuilder)} of the 103 * superclass is invoked. 104 * 105 * @param response 106 * {@link JSONBuilder} to write the response to 107 */ 108 protected void prepareResponse(JSONBuilder response) { 109 // Do nothing here... 110 } 111 112 /** 113 * Checks if the type is acceptable to this challenge. This generic class only checks 114 * if the type is not blank. Subclasses should instead check if the given type matches 115 * expected challenge type. 116 * 117 * @param type 118 * Type to check 119 * @return {@code true} if acceptable, {@code false} if not 120 */ 121 protected boolean acceptable(String type) { 122 return type != null && !type.trim().isEmpty(); 123 } 124 125 @Override 126 protected void setJSON(JSON json) { 127 var type = json.get(KEY_TYPE).asString(); 128 129 if (!acceptable(type)) { 130 throw new AcmeProtocolException("incompatible type " + type + " for this challenge"); 131 } 132 133 var loc = json.get(KEY_URL).asString(); 134 if (!loc.equals(getLocation().toString())) { 135 throw new AcmeProtocolException("challenge has changed its location"); 136 } 137 138 super.setJSON(json); 139 } 140 141 /** 142 * Triggers this {@link Challenge}. The ACME server is requested to validate the 143 * response. Note that the validation is performed asynchronously by the ACME server. 144 * <p> 145 * After a challenge is triggered, it changes to {@link Status#PENDING}. As soon as 146 * validation takes place, it changes to {@link Status#PROCESSING}. After validation 147 * the status changes to {@link Status#VALID} or {@link Status#INVALID}, depending on 148 * the outcome of the validation. 149 * <p> 150 * If the challenge requires a resource to be set on your side (e.g. a DNS record or 151 * an HTTP file), it <em>must</em> be reachable from public before {@link #trigger()} 152 * is invoked, and <em>must not</em> be taken down until the challenge has reached 153 * {@link Status#VALID} or {@link Status#INVALID}. 154 * <p> 155 * If this method is invoked a second time, the ACME server is requested to retry the 156 * validation. This can be useful if the client state has changed, for example after a 157 * firewall rule has been updated. 158 */ 159 public void trigger() throws AcmeException { 160 LOG.debug("trigger"); 161 try (var conn = getSession().connect()) { 162 var claims = new JSONBuilder(); 163 prepareResponse(claims); 164 165 conn.sendSignedRequest(getLocation(), claims, getLogin()); 166 setJSON(conn.readJsonResponse()); 167 } 168 } 169 170}