/*
* Copyright (C) 2011-2012 Intel Corporation
* All rights reserved.
*/
package com.intel.mtwilson.security.http.jaxrs;
//import com.intel.mountwilson.http.security.adapter.*;
import javax.ws.rs.client.ClientRequestFilter;
import javax.annotation.Priority;
import javax.ws.rs.client.ClientRequestContext;
import com.intel.mtwilson.security.http.RsaAuthorization;
import com.intel.dcsg.cpg.crypto.RsaCredentialX509;
import java.io.ByteArrayOutputStream;
import org.glassfish.jersey.message.MessageBodyWorkers;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.HashMap;
import javax.ws.rs.Priorities;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
/**
* This is a HTTP CLIENT filter to handle OUTGOING requests.
*
* Sample usage:
*
clientConfig = new ClientConfig();
RsaCredentialX509 credential = new RsaCredentialX509(privateKey, x509Certificate);
clientConfig.register(new X509AuthorizationFilter(credential));
*
* Example output header:
* Authorization: X509 fingerprint="7Rt5AyFkoqGekveyBb53nG3EVaiPy2ZRdTgQ59tcI00=", headers="X-Nonce,Date", algorithm="SHA256withRSA", signature="PdEREUSh6y/Y8H+QMBTg3hJVkS7eb+/9f7/RdaWRbAq+yycNBhjq2iYn3wFs7pZjJtlwK/KbpzU7ZKoyKHx70f/ivqhjyJhUMWCWFD/qZcQToevaosDwAXJH0uXiJQZPP16n0D7ZFqJ435vj9MujR3kVDb+lFGb+YyRUIOAmIKf2AaCXHATDi3cMYpACN/FxFvszoAmNvWmocR41aLdwD/RAMOmZH60qlT3vWW1/76BYyRaG8L/5VIImP79fWOdsmujN6hktabohXdo2Pr9udtHTRreaUslFI5/hnowmQvUQAEUePCPF2QnkOc6iwrwKSzcdlwoqaumu2XK9EqxzWg=="
*
* Because this filter creates an Authorization header with a signature over the http method, URL, and entity body (if provided),
* it should be the LAST filter that is applied so that it can sign the final form of the entity body. The only exception to that
* would be if a server filter decodes the entity body BEFORE the security filter, for example gzip compression. In any such case,
* you must take care to match the order in which the filters are applied on the client and server.
*
* @author jbuhacoff
* @since 2.0
*/
@Priority(Priorities.AUTHORIZATION)
public class X509AuthorizationFilter implements ClientRequestFilter {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(X509AuthorizationFilter.class);
@Context
protected MessageBodyWorkers workers;
private RsaAuthorization auth;
public X509AuthorizationFilter(RsaCredentialX509 credential) {
auth = new RsaAuthorization(credential);
}
/**
* This method assumes that the entity body of the request is either null or a String or
* has a toString() method that returns the String that should be signed.
*
* @param cr
* @return
* @throws ClientHandlerException
*/
@Override
public void filter(ClientRequestContext requestContext)
throws IOException {
// Modify the request
try {
// first convert the headers we want to sign into to a map<string,string>
MultivaluedMap<String,Object> headers = requestContext.getHeaders();
HashMap<String,String> map = new HashMap<>();
if( headers.containsKey("Date")) {
map.put("Date", headers.getFirst("Date").toString());
}
log.debug("Request context URL: {}", requestContext.getUri().toURL().toString());
String header;
if( requestContext.getEntity() == null ) {
header = auth.getAuthorization(requestContext.getMethod(), requestContext.getUri().toURL().toString(), map);
}
else {
// find the message body writer that jaxrs will use to serialize the requset, and use it to get a preview of the message body for calculating the signature
// when the request is sent, the framework will use the same message body writer to write the entity to requestContext.getEntityStream().
// it may be better to handle the entity case as an interceptor and the non-entity case as a filter (because interceptors don't get called when there is no entity), because the interceptor will have access to the serialized entity and we won't need to have a jersey-specific @Context for the MessageBodyWriter
log.debug("entity class 1: {}", requestContext.getEntityClass());
log.debug("entity class 2: {}", requestContext.getEntity().getClass());
log.debug("entity media type: {}", requestContext.getMediaType().toString());
final MessageBodyWriter messageBodyWriter =
workers.getMessageBodyWriter(requestContext.getEntityClass(), requestContext.getEntityClass(),
new Annotation[]{}, requestContext.getMediaType());
//public void writeTo(T t, Class<?> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, Object> mm, OutputStream out) throws IOException, WebApplicationException;
ByteArrayOutputStream out = new ByteArrayOutputStream();
messageBodyWriter.writeTo(requestContext.getEntity(), requestContext.getEntityClass(), requestContext.getEntityType(), requestContext.getEntity().getClass().getAnnotations(), requestContext.getMediaType(), headers, out);
header = auth.getAuthorization(requestContext.getMethod(), requestContext.getUri().toURL().toString(), map, new String(out.toByteArray(),Charset.forName("UTF-8")));
//header = auth.getAuthorization(requestContext.getMethod(), requestContext.getUri().toURL().toString(), map, requestContext.getEntity().toString());
}
// the authorization class adds Date header if we don't already have it, and it adds an X-Nonce header which we need to include in our request
for(String headerName : map.keySet()) {
headers.putSingle(headerName, map.get(headerName));
}
log.debug("Authorization: {}", header);
requestContext.getHeaders().add("Authorization", header);
}
catch(NoSuchAlgorithmException | InvalidKeyException | IOException | SignatureException e) {
throw new IOException(e);
}
}
}