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