All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.erudika.para.rest.Signer Maven / Gradle / Ivy

There is a newer version: 1.50.3
Show newest version
/*
 * Copyright 2013-2017 Erudika. https://erudika.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * For issues and patches go to: https://github.com/erudika
 */
package com.erudika.para.rest;

import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.DefaultRequest;
import com.amazonaws.Request;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.http.HttpMethodName;
import com.amazonaws.util.SdkHttpUtils;
import com.erudika.para.core.utils.ParaObjectUtils;
import com.erudika.para.utils.Config;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class extends {@code AWS4Signer} implementing the AWS Signature Version 4 algorithm.
 * Also contains a method for signature validation. The signatures that this class produces are
 * compatible with the original AWS SDK implementation.
 * @author Alex Bogdanovski [[email protected]]
 */
public final class Signer extends AWS4Signer {

	private static final Logger logger = LoggerFactory.getLogger(Signer.class);
	private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.
			forPattern("yyyyMMdd'T'HHmmss'Z'").withZoneUTC();

	/**
	 * No-args constructor.
	 */
	public Signer() {
		super(false);
		super.setServiceName(Config.PARA);
	}

	/**
	 * Signs a request using AWS signature V4.
	 * @param httpeMethod GET/POST/PUT... etc.
	 * @param endpoint the hostname of the API server
	 * @param resourcePath the path of the resource (starting from root e.g. "/path/to/res")
	 * @param headers the headers map
	 * @param params the params map
	 * @param entity the entity object or null
	 * @param accessKey the app's access key
	 * @param secretKey the app's secret key
	 * @return a signed request. The actual signature is inside the {@code Authorization} header.
	 */
	public Map sign(String httpeMethod, String endpoint, String resourcePath,
			Map headers, Map params, InputStream entity,
			String accessKey, String secretKey) {

		Request req = buildAWSRequest(httpeMethod, endpoint, resourcePath, headers, params, entity);
		sign(req, accessKey, secretKey);
		return req.getHeaders();
	}

	/**
	 * Signs a request using AWS signature V4.
	 * @param request the request instance
	 * @param accessKey the app's access key
	 * @param secretKey the app's secret key
	 */
	public void sign(Request request, String accessKey, String secretKey) {
		super.sign(request, new BasicAWSCredentials(accessKey, secretKey));
		resetDate();
	}

	/**
	 * Validates the signature of the request.
	 * @param incoming the incoming HTTP request containing a signature
	 * @param secretKey the app's secret key
	 * @return true if the signature is valid
	 */
	public boolean isValidSignature(HttpServletRequest incoming, String secretKey) {
		if (incoming == null || StringUtils.isBlank(secretKey)) {
			return false;
		}
		String auth = incoming.getHeader(HttpHeaders.AUTHORIZATION);
		String givenSig = StringUtils.substringAfter(auth, "Signature=");
		String sigHeaders = StringUtils.substringBetween(auth, "SignedHeaders=", ",");
		String credential = StringUtils.substringBetween(auth, "Credential=", ",");
		String accessKey = StringUtils.substringBefore(credential, "/");

		if (StringUtils.isBlank(auth)) {
			givenSig = incoming.getParameter("X-Amz-Signature");
			sigHeaders = incoming.getParameter("X-Amz-SignedHeaders");
			credential = incoming.getParameter("X-Amz-Credential");
			accessKey = StringUtils.substringBefore(credential, "/");
		}

		Request awsReq = buildAWSRequest(incoming, new HashSet(Arrays.asList(sigHeaders.split(";"))));
		sign(awsReq, accessKey, secretKey);

		String auth2 = awsReq.getHeaders().get(HttpHeaders.AUTHORIZATION);
		String recreatedSig = StringUtils.substringAfter(auth2, "Signature=");

		return StringUtils.equals(givenSig, recreatedSig);
	}

	@Override
	public void setRegionName(String regionName) {
	}

	/**
	 * The region name.
	 * @return {@link Config#AWS_REGION}
	 */
	@Override
	public String getRegionName() {
		return Config.AWS_REGION;
	}

	private void resetDate() {
		overriddenDate = null;
	}

	private Request buildAWSRequest(String httpMethod, String endpoint, String resourcePath,
			Map headers, Map params, InputStream entity) {
		Request r = new DefaultRequest(Config.PARA);

		if (!StringUtils.isBlank(httpMethod)) {
			r.setHttpMethod(HttpMethodName.valueOf(httpMethod));
		}
		if (!StringUtils.isBlank(endpoint)) {
			if (!endpoint.startsWith("http")) {
				endpoint = "https://" + endpoint;
			}
			r.setEndpoint(URI.create(endpoint));
		}
		if (!StringUtils.isBlank(resourcePath)) {
			r.setResourcePath(SdkHttpUtils.urlEncode(resourcePath, true));
		}
		if (headers != null) {
			if (headers.containsKey("x-amz-date")) {
				overriddenDate = parseAWSDate(headers.get("x-amz-date"));
			}
			// we don't need these here, added by default
			headers.remove("host");
			headers.remove("x-amz-date");
			r.setHeaders(headers);
		}
		if (params != null) {
			for (Map.Entry param : params.entrySet()) {
				r.addParameter(param.getKey(), param.getValue());
			}
		}
		if (entity != null) {
			r.setContent(entity);
		}
		return r;
	}

	private Request buildAWSRequest(HttpServletRequest req, Set headersUsed) {
		Map headers = new HashMap();
		for (Enumeration e = req.getHeaderNames(); e.hasMoreElements();) {
			String head = e.nextElement().toLowerCase();
			if (headersUsed.contains(head)) {
				headers.put(head, req.getHeader(head));
			}
		}

		Map params = new HashMap();
		for (Map.Entry param : req.getParameterMap().entrySet()) {
			params.put(param.getKey(), param.getValue()[0]);
		}

		String path = req.getRequestURI();
		String endpoint = StringUtils.removeEndIgnoreCase(req.getRequestURL().toString(), path);
		String httpMethod = req.getMethod();
		InputStream entity;
		try {
			entity = new BufferedInputStream(req.getInputStream());
			if (entity.available() <= 0) {
				entity = null;
			}
		} catch (IOException ex) {
			logger.error(null, ex);
			entity = null;
		}

		return buildAWSRequest(httpMethod, endpoint, path, headers, params, entity);
	}

	/**
	 * Returns a parsed Date.
	 * @param date a date in the AWS format yyyyMMdd'T'HHmmss'Z'
	 * @return a date
	 */
	public static Date parseAWSDate(String date) {
		if (date == null) {
			return null;
		}
		return TIME_FORMATTER.parseDateTime(date).toDate();
	}

	/**
	 * Builds, signs and executes a request to an API endpoint using the provided credentials.
	 * Signs the request using the Amazon Signature 4 algorithm and returns the response.
	 * @param apiClient Jersey Client object
	 * @param accessKey access key
	 * @param secretKey secret key
	 * @param httpMethod the method (GET, POST...)
	 * @param endpointURL protocol://host:port
	 * @param reqPath the API resource path relative to the endpointURL
	 * @param headers headers map
	 * @param params parameters map
	 * @param entity an entity containing any Java object (payload), could be null
	 * @return a response object
	 */
	public Response invokeSignedRequest(Client apiClient, String accessKey, String secretKey,
			String httpMethod, String endpointURL, String reqPath, Map headers,
			MultivaluedMap params, Entity entity) {
		byte[] jsonEntity = null;
		if (entity != null) {
			try {
				jsonEntity = ParaObjectUtils.getJsonWriter().writeValueAsBytes(entity.getEntity());
			} catch (JsonProcessingException ex) {
				jsonEntity = null;
				logger.error(null, ex);
			}
		}
		return invokeSignedRequest(apiClient, accessKey, secretKey, httpMethod,
				endpointURL, reqPath, headers, params, jsonEntity);
	}

	/**
	 * Builds, signs and executes a request to an API endpoint using the provided credentials.
	 * Signs the request using the Amazon Signature 4 algorithm and returns the response.
	 * @param apiClient Jersey Client object
	 * @param accessKey access key
	 * @param secretKey secret key
	 * @param httpMethod the method (GET, POST...)
	 * @param endpointURL protocol://host:port
	 * @param reqPath the API resource path relative to the endpointURL
	 * @param headers headers map
	 * @param params parameters map
	 * @param jsonEntity an object serialized to JSON byte array (payload), could be null
	 * @return a response object
	 */
	public Response invokeSignedRequest(Client apiClient, String accessKey, String secretKey,
			String httpMethod, String endpointURL, String reqPath,
			Map headers, MultivaluedMap params, byte[] jsonEntity) {

		boolean isJWT = StringUtils.startsWithIgnoreCase(secretKey, "Bearer");

		WebTarget target = apiClient.target(endpointURL).path(reqPath);
		Map signedHeaders = new HashMap();
		if (!isJWT) {
			signedHeaders = signRequest(accessKey, secretKey, httpMethod, endpointURL, reqPath,
					headers, params, jsonEntity);
		}

		if (params != null) {
			for (Map.Entry> param : params.entrySet()) {
				String key = param.getKey();
				List value = param.getValue();
				if (value != null && !value.isEmpty() && value.get(0) != null) {
					target = target.queryParam(key, value.toArray());
				}
			}
		}

		Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON);

		if (headers != null) {
			for (Map.Entry header : headers.entrySet()) {
				builder.header(header.getKey(), header.getValue());
			}
		}

		Entity jsonPayload = null;
		if (jsonEntity != null && jsonEntity.length > 0) {
			try {
				jsonPayload = Entity.json(new String(jsonEntity, Config.DEFAULT_ENCODING));
			} catch (IOException ex) {
				logger.error(null, ex);
			}
		}

		if (isJWT) {
			builder.header(HttpHeaders.AUTHORIZATION, secretKey);
		} else {
			builder.header(HttpHeaders.AUTHORIZATION, signedHeaders.get(HttpHeaders.AUTHORIZATION)).
					header("X-Amz-Date", signedHeaders.get("X-Amz-Date"));
		}

		if (jsonPayload != null) {
			return builder.method(httpMethod, jsonPayload);
		} else {
			return builder.method(httpMethod);
		}
	}

	/**
	 * Builds and signs a request to an API endpoint using the provided credentials.
	 * @param accessKey access key
	 * @param secretKey secret key
	 * @param httpMethod the method (GET, POST...)
	 * @param endpointURL protocol://host:port
	 * @param reqPath the API resource path relative to the endpointURL
	 * @param headers headers map
	 * @param params parameters map
	 * @param jsonEntity an object serialized to JSON byte array (payload), could be null
	 * @return a map containing the "Authorization" header
	 */
	public Map signRequest(String accessKey, String secretKey,
			String httpMethod, String endpointURL, String reqPath,
			Map headers, MultivaluedMap params, byte[] jsonEntity) {

		if (StringUtils.isBlank(accessKey)) {
			logger.error("Blank access key: {} {}", httpMethod, reqPath);
			return headers;
		}

		if (StringUtils.isBlank(secretKey)) {
			logger.debug("Anonymous request: {} {}", httpMethod, reqPath);
			if (headers == null) {
				headers = new HashMap();
			}
			headers.put(HttpHeaders.AUTHORIZATION, "Anonymous " + accessKey);
			return headers;
		}

		if (httpMethod == null) {
			httpMethod = HttpMethod.GET;
		}

		InputStream in = null;
		Map sigParams = new HashMap();

		if (params != null) {
			for (Map.Entry> param : params.entrySet()) {
				String key = param.getKey();
				List value = param.getValue();
				if (value != null && !value.isEmpty() && value.get(0) != null) {
					sigParams.put(key, value.get(0));
				}
			}
		}

		if (jsonEntity != null && jsonEntity.length > 0) {
			in = new BufferedInputStream(new ByteArrayInputStream(jsonEntity));
		}

		return sign(httpMethod, endpointURL, reqPath, headers, sigParams, in, accessKey, secretKey);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy