/*
*
* Copyright (c) 2013 - 2017 Lijun Liao
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation with the addition of the
* following permission added to Section 15 as permitted in Section 7(a):
*
* FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
* THE AUTHOR LIJUN LIAO. LIJUN LIAO DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
* OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the XiPKI software without
* disclosing the source code of your own applications.
*
* For more information, please contact Lijun Liao at this
* address: lijun.liao@gmail.com
*/
package org.xipki.pki.ocsp.server.impl;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.ASN1StreamParser;
import org.bouncycastle.asn1.ocsp.OCSPRequest;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.commons.audit.AuditEvent;
import org.xipki.commons.audit.AuditLevel;
import org.xipki.commons.audit.AuditService;
import org.xipki.commons.audit.AuditServiceRegister;
import org.xipki.commons.audit.AuditStatus;
import org.xipki.commons.common.util.LogUtil;
import org.xipki.commons.common.util.ParamUtil;
import org.xipki.commons.common.util.StringUtil;
import org.xipki.commons.security.HashAlgoType;
import org.xipki.pki.ocsp.server.impl.OcspRespWithCacheInfo.ResponseCacheInfo;
/**
* @author Lijun Liao
* @since 2.0.0
*/
public class HttpOcspServlet extends HttpServlet {
private static final Logger LOG = LoggerFactory.getLogger(HttpOcspServlet.class);
private static final long serialVersionUID = 1L;
private static final String CT_REQUEST = "application/ocsp-request";
private static final String CT_RESPONSE = "application/ocsp-response";
private AuditServiceRegister auditServiceRegister;
private OcspServer server;
public HttpOcspServlet() {
}
public void setServer(final OcspServer server) {
this.server = server;
}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
ResponderAndRelativeUri respAndUri = server.getResponderAndRelativeUri(request);
if (respAndUri == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Responder responder = respAndUri.getResponder();
if (responder.getRequestOption().supportsHttpGet()) {
processRequest(request, response, respAndUri, true);
} else {
super.doGet(request, response);
}
}
@Override
protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
ResponderAndRelativeUri respAndUri = server.getResponderAndRelativeUri(request);
if (respAndUri == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (StringUtil.isNotBlank(respAndUri.getRelativeUri())) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
processRequest(request, response, respAndUri, false);
}
private void processRequest(final HttpServletRequest request,
final HttpServletResponse response, final ResponderAndRelativeUri respAndUri,
final boolean getMethod) throws ServletException, IOException {
Responder responder = respAndUri.getResponder();
AuditEvent event = null;
AuditLevel auditLevel = AuditLevel.INFO;
AuditStatus auditStatus = AuditStatus.SUCCESSFUL;
String auditMessage = null;
AuditService auditService = (auditServiceRegister == null) ? null
: auditServiceRegister.getAuditService();
if (responder.getAuditOption() != null) {
event = new AuditEvent(new Date());
event.setApplicationName(OcspAuditConstants.APPNAME);
event.setName(OcspAuditConstants.NAME_PERF);
}
try {
if (server == null) {
String message = "responder in servlet not configured";
LOG.error(message);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentLength(0);
auditLevel = AuditLevel.ERROR;
auditStatus = AuditStatus.FAILED;
auditMessage = message;
return;
}
InputStream requestStream;
if (getMethod) {
String relativeUri = respAndUri.getRelativeUri();
// RFC2560 A.1.1 specifies that request longer than 255 bytes SHOULD be sent by
// POST, we support GET for longer requests anyway.
if (relativeUri.length() > responder.getRequestOption().getMaxRequestSize()) {
response.setContentLength(0);
response.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
auditStatus = AuditStatus.FAILED;
auditMessage = "request too large";
return;
}
requestStream = new ByteArrayInputStream(Base64.decode(relativeUri));
} else {
// accept only "application/ocsp-request" as content type
if (!CT_REQUEST.equalsIgnoreCase(request.getContentType())) {
response.setContentLength(0);
response.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
auditStatus = AuditStatus.FAILED;
auditMessage = "unsupported media type " + request.getContentType();
return;
}
// request too long
if (request.getContentLength() > responder.getRequestOption().getMaxRequestSize()) {
response.setContentLength(0);
response.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
auditStatus = AuditStatus.FAILED;
auditMessage = "request too large";
return;
} // if (CT_REQUEST)
requestStream = request.getInputStream();
} // end if (getMethod)
OCSPRequest ocspRequest;
try {
ASN1StreamParser parser = new ASN1StreamParser(requestStream);
ocspRequest = OCSPRequest.getInstance(parser.readObject());
} catch (Exception ex) {
response.setContentLength(0);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
auditStatus = AuditStatus.FAILED;
auditMessage = "bad request";
LogUtil.error(LOG, ex, "could not parse the request (OCSPRequest)");
return;
}
OCSPReq ocspReq = new OCSPReq(ocspRequest);
response.setContentType(HttpOcspServlet.CT_RESPONSE);
OcspRespWithCacheInfo ocspRespWithCacheInfo =
server.answer(responder, ocspReq, getMethod, event);
if (ocspRespWithCacheInfo == null) {
auditMessage = "processRequest returned null, this should not happen";
LOG.error(auditMessage);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentLength(0);
auditLevel = AuditLevel.ERROR;
auditStatus = AuditStatus.FAILED;
} else {
OCSPResp resp = ocspRespWithCacheInfo.getResponse();
byte[] encodedOcspResp = null;
response.setStatus(HttpServletResponse.SC_OK);
ResponseCacheInfo cacheInfo = ocspRespWithCacheInfo.getCacheInfo();
if (getMethod && cacheInfo != null) {
encodedOcspResp = resp.getEncoded();
long now = System.currentTimeMillis();
// RFC 5019 6.2: Date: The date and time at which the OCSP server generated
// the HTTP response.
response.setDateHeader("Date", now);
// RFC 5019 6.2: Last-Modified: date and time at which the OCSP responder
// last modified the response.
response.setDateHeader("Last-Modified", cacheInfo.getThisUpdate());
// RFC 5019 6.2: Expires: This date and time will be the same as the
// nextUpdate time-stamp in the OCSP
// response itself.
// This is overridden by max-age on HTTP/1.1 compatible components
if (cacheInfo.getNextUpdate() != null) {
response.setDateHeader("Expires", cacheInfo.getNextUpdate());
}
// RFC 5019 6.2: This profile RECOMMENDS that the ETag value be the ASCII
// HEX representation of the SHA1 hash of the OCSPResponse structure.
response.setHeader("ETag",
new StringBuilder(42).append('\\')
.append(HashAlgoType.SHA1.hexHash(encodedOcspResp))
.append('\\')
.toString());
// Max age must be in seconds in the cache-control header
long maxAge;
if (responder.getResponseOption().getCacheMaxAge() != null) {
maxAge = responder.getResponseOption().getCacheMaxAge().longValue();
} else {
maxAge = OcspServer.DFLT_CACHE_MAX_AGE;
}
if (cacheInfo.getNextUpdate() != null) {
maxAge = Math.min(maxAge,
(cacheInfo.getNextUpdate() - cacheInfo.getThisUpdate()) / 1000);
}
response.setHeader("Cache-Control",
new StringBuilder(55).append("max-age=").append(maxAge)
.append(",public,no-transform,must-revalidate").toString());
} // end if (getMethod && cacheInfo != null)
if (encodedOcspResp != null) {
response.getOutputStream().write(encodedOcspResp);
} else {
ASN1OutputStream asn1Out = new ASN1OutputStream(response.getOutputStream());
asn1Out.writeObject(resp.toASN1Structure());
asn1Out.flush();
}
} // end if (ocspRespWithCacheInfo)
} catch (EOFException ex) {
LogUtil.warn(LOG, ex, "Connection reset by peer");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentLength(0);
} catch (Throwable th) {
final String message = "Throwable thrown, this should not happen!";
LogUtil.error(LOG, th, message);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentLength(0);
auditLevel = AuditLevel.ERROR;
auditStatus = AuditStatus.FAILED;
auditMessage = "internal error";
} finally {
try {
response.flushBuffer();
} catch (IOException ex) {
final String message = "error while calling responsse.flushBuffer";
LogUtil.error(LOG, ex, message);
auditLevel = AuditLevel.ERROR;
auditStatus = AuditStatus.FAILED;
auditMessage = "internal error";
} finally {
if (event != null) {
if (auditLevel != null) {
event.setLevel(auditLevel);
}
if (auditStatus != null) {
event.setStatus(auditStatus);
}
if (auditMessage != null) {
event.addEventData(OcspAuditConstants.NAME_message, auditMessage);
}
event.finish();
auditService.logEvent(event);
}
} // end internal try
} // end external try
} // method processRequest
public void setAuditServiceRegister(final AuditServiceRegister auditServiceRegister) {
this.auditServiceRegister = ParamUtil.requireNonNull("auditServiceRegister",
auditServiceRegister);
}
}