/************************************************************************* * Copyright 2009-2015 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. * * This file may incorporate work covered under the following copyright * and permission notice: * * Software License Agreement (BSD License) * * Copyright (c) 2008, Regents of the University of California * All rights reserved. * * Redistribution and use of this software in source and binary forms, * with or without modification, are permitted provided that the * following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE * THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL, * COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE, * AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING * IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA, * SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY, * WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION, * REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO * IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT * NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS. ************************************************************************/ package com.eucalyptus.walrus.pipeline; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.eucalyptus.auth.login.AuthenticationException; import com.eucalyptus.auth.login.SecurityContext; import com.eucalyptus.component.ComponentIds; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.walrus.WalrusBackend; import com.eucalyptus.walrus.auth.WalrusWrappedCredentials; import com.eucalyptus.walrus.exceptions.AccessDeniedException; import com.eucalyptus.walrus.exceptions.MethodNotAllowedException; import com.eucalyptus.walrus.util.WalrusProperties; import com.eucalyptus.walrus.util.WalrusUtil; import com.eucalyptus.ws.handlers.MessageStackHandler; import org.apache.commons.httpclient.util.DateUtil; import org.apache.log4j.Logger; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.DownstreamMessageEvent; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.HttpVersion; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; public class WalrusAuthenticationHandler extends MessageStackHandler { private static Logger LOG = Logger.getLogger(WalrusAuthenticationHandler.class); private static final String AWS_AUTH_TYPE = "AWS"; private static final String EUCA_AUTH_TYPE = "EUCA2-RSA-SHA256"; private static final String EUCA_OLD_AUTH_TYPE = "Euca"; protected static final String ISO_8601_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; //Use the ISO8601 format public static enum SecurityParameter { AWSAccessKeyId, Timestamp, Expires, Signature, Authorization, Date, Content_MD5, Content_Type, SecurityToken, } /* The possible fields in an authorization header */ private static enum AuthorizationField { Type, AccessKeyId, CertFingerPrint, SignedHeaders, Signature } /** * Ensure that only one header for each name exists (i.e. not 2 Authorization headers) Accomplish this by comma-delimited concatenating any * duplicates found as per HTTP 1.1 RFC 2616 section 4.2 * * TODO: Also, should convert all headers to lower-case for consistent processing later. This is okay since headers are case-insensitive. * * in HTTP */ private static void canonicalizeHeaders(MappingHttpRequest httpRequest) { //Iterate through headers and find duplicates, concatenate their values together and remove from // request as we find them. TreeMap<String, String> headerMap = new TreeMap<String, String>(); String value = null; //Construct a map of the normalized headers, cannot modify in-place since // conconcurrent-modify exception may result for (String header : httpRequest.getHeaderNames()) { //TODO: zhill, put in the map in lower-case form. headerMap.put(header, Joiner.on(',').join(httpRequest.getHeaders(header))); } //Remove *all* headers httpRequest.clearHeaders(); //Add the normalized headers back into the request for (String foundHeader : headerMap.keySet()) { httpRequest.addHeader(foundHeader, headerMap.get(foundHeader).toString()); } } /** * This method exists to clean up a problem encountered periodically where the HTTP headers are duplicated */ private static void removeDuplicateHeaderValues(MappingHttpRequest httpRequest) { List<String> hdrList = null; HashMap<String, List<String>> fixedHeaders = new HashMap<String, List<String>>(); boolean foundDup = false; for (String header : httpRequest.getHeaderNames()) { hdrList = httpRequest.getHeaders(header); //Only address the specific case where there is exactly one identical copy of the header if (hdrList != null && hdrList.size() == 2 && hdrList.get(0).equals(hdrList.get(1))) { foundDup = true; fixedHeaders.put(header, Lists.newArrayList(hdrList.get(0))); } else { fixedHeaders.put(header, hdrList); } } if (foundDup) { LOG.debug("Found duplicate headers in: " + httpRequest.logMessage()); httpRequest.clearHeaders(); for (Map.Entry<String, List<String>> e : fixedHeaders.entrySet()) { for (String v : e.getValue()) { httpRequest.addHeader(e.getKey(), v); } } } } @Override public void incomingMessage(ChannelHandlerContext ctx, MessageEvent event) throws Exception { if (event.getMessage() instanceof MappingHttpRequest) { try { MappingHttpRequest httpRequest = (MappingHttpRequest) event.getMessage(); removeDuplicateHeaderValues(httpRequest); //Consolidate duplicates, etc. canonicalizeHeaders(httpRequest); handle(httpRequest); } catch (Exception ex) { Channels.fireExceptionCaught(ctx, ex); } } } /** * Process the authorization header */ public static Map<AuthorizationField, String> processAuthorizationHeader(String authorization) throws AccessDeniedException { if (Strings.isNullOrEmpty(authorization)) { return null; } HashMap<AuthorizationField, String> authMap = new HashMap<AuthorizationField, String>(); String[] components = authorization.split(" "); if (components.length < 2) { throw new AccessDeniedException("Invalid authoriztion header"); } if (AWS_AUTH_TYPE.equals(components[0]) && (components.length == 2 || (components.length == 3 && components[2].startsWith(":")))) { //Expect: components[1] = <AccessKeyId>:<Signature> authMap.put(AuthorizationField.Type, AWS_AUTH_TYPE); if (components.length == 2) { String[] signatureElements = components[1].split(":"); authMap.put(AuthorizationField.AccessKeyId, signatureElements[0]); authMap.put(AuthorizationField.Signature, signatureElements[1]); } else { authMap.put(AuthorizationField.AccessKeyId, components[1].trim()); authMap.put(AuthorizationField.Signature, components[2].substring(1).trim()); } } else if (EUCA_AUTH_TYPE.equals(components[0]) && components.length == 4) { //Expect: components[0] = EUCA2-RSA-SHA256 components[1] = <fingerprint of signing certificate> components[2] = <list of signed headers> components[3] = <Signature> authMap.put(AuthorizationField.Type, EUCA_AUTH_TYPE); authMap.put(AuthorizationField.CertFingerPrint, components[1].trim()); authMap.put(AuthorizationField.SignedHeaders, components[2].trim()); authMap.put(AuthorizationField.Signature, components[3].trim()); } else if (EUCA_OLD_AUTH_TYPE.equals(components[0]) && components.length == 1) { authMap.put(AuthorizationField.Type, EUCA_OLD_AUTH_TYPE); } else { throw new AccessDeniedException("Invalid authorization header"); } return authMap; } private static class S3Authentication { /** * Authenticate using S3-spec REST authentication */ private static void authenticate(MappingHttpRequest httpRequest, Map<AuthorizationField, String> authMap) throws AccessDeniedException { if (!authMap.get(AuthorizationField.Type).equals(AWS_AUTH_TYPE)) { throw new AccessDeniedException("Mismatch between expected and found authentication types"); } //Standard S3 authentication signed by SecretKeyID String verb = httpRequest.getMethod().getName(); String date = getDate(httpRequest); String addrString = getS3AddressString(httpRequest); String content_md5 = httpRequest.getHeader("Content-MD5"); content_md5 = content_md5 == null ? "" : content_md5; String content_type = httpRequest.getHeader(WalrusProperties.CONTENT_TYPE); content_type = content_type == null ? "" : content_type; String securityToken = httpRequest.getHeader(WalrusProperties.X_AMZ_SECURITY_TOKEN); String canonicalizedAmzHeaders = getCanonicalizedAmzHeaders(httpRequest); String data = verb + "\n" + content_md5 + "\n" + content_type + "\n" + date + "\n" + canonicalizedAmzHeaders + addrString; String accessKeyId = authMap.get(AuthorizationField.AccessKeyId); String signature = authMap.get(AuthorizationField.Signature); try { SecurityContext.getLoginContext(new WalrusWrappedCredentials(httpRequest.getCorrelationId(), data, accessKeyId, signature, securityToken)) .login(); } catch (Exception ex) { //Try stripping of the '/services/Walrus' portion of the addrString and retry the signature calc String servicePath = ComponentIds.lookup(WalrusBackend.class).getServicePath(); if (addrString.startsWith(servicePath)) { try { String modifiedAddrString = addrString.replaceFirst(servicePath, ""); data = verb + "\n" + content_md5 + "\n" + content_type + "\n" + date + "\n" + canonicalizedAmzHeaders + modifiedAddrString; SecurityContext.getLoginContext(new WalrusWrappedCredentials(httpRequest.getCorrelationId(), data, accessKeyId, signature, securityToken)) .login(); } catch (Exception ex2) { LOG.error(ex2); throw new AccessDeniedException(ex2.getMessage()); } } else { LOG.error(ex); throw new AccessDeniedException(ex.getMessage()); } } } /** * See if the expires string indicates the message is expired. */ private static boolean checkExpires(String expires) { Long expireTime = Long.parseLong(expires); Long currentTime = new Date().getTime() / 1000; if (currentTime > expireTime) { return false; } return true; } /** * Gets the date for S3-spec authentication */ private static String getDate(MappingHttpRequest httpRequest) throws AccessDeniedException { String date; String verifyDate; if (httpRequest.containsHeader("x-amz-date")) { date = ""; verifyDate = httpRequest.getHeader("x-amz-date"); } else { date = httpRequest.getAndRemoveHeader(SecurityParameter.Date.toString()); verifyDate = date; if (date == null || date.length() <= 0) { throw new AccessDeniedException("User authentication failed. Date must be specified."); } } try { Date dateToVerify = DateUtil.parseDate(verifyDate); Date currentDate = new Date(); if (Math.abs(currentDate.getTime() - dateToVerify.getTime()) > WalrusProperties.EXPIRATION_LIMIT) { LOG.error("Incoming WalrusBackend message is expired. Current date: " + currentDate.toString() + " Message's Verification Date: " + dateToVerify.toString()); throw new AccessDeniedException("Message expired. Sorry."); } } catch (Exception ex) { LOG.error("Cannot parse date: " + verifyDate); throw new AccessDeniedException("Unable to parse date."); } return date; } private static String getCanonicalizedAmzHeaders(MappingHttpRequest httpRequest) { String result = ""; Set<String> headerNames = httpRequest.getHeaderNames(); TreeMap<String, String> amzHeaders = new TreeMap<String, String>(); for (String headerName : headerNames) { String headerNameString = headerName.toLowerCase().trim(); if (headerNameString.startsWith("x-amz-")) { String value = httpRequest.getHeader(headerName).trim(); String[] parts = value.split("\n"); value = ""; for (String part : parts) { part = part.trim(); value += part + " "; } value = value.trim(); if (amzHeaders.containsKey(headerNameString)) { String oldValue = (String) amzHeaders.remove(headerNameString); oldValue += "," + value; amzHeaders.put(headerNameString, oldValue); } else { amzHeaders.put(headerNameString, value); } } } Iterator<String> iterator = amzHeaders.keySet().iterator(); while (iterator.hasNext()) { String key = iterator.next(); String value = (String) amzHeaders.get(key); result += key + ":" + value + "\n"; } return result; } //Old method for getting signature info from Auth header private static String[] getSigInfo(String auth_part) { int index = auth_part.lastIndexOf(" "); String sigString = auth_part.substring(index + 1); return sigString.split(":"); } /** * AWS S3-spec address string, which includes the query parameters */ private static String getS3AddressString(MappingHttpRequest httpRequest) throws AccessDeniedException { String addr = httpRequest.getUri(); String targetHost = httpRequest.getHeader(HttpHeaders.Names.HOST); if (targetHost.contains(".walrus")) { String bucket = targetHost.substring(0, targetHost.indexOf(".walrus")); addr = "/" + bucket + addr; } String[] addrStrings = addr.split("\\?"); StringBuilder addrString = new StringBuilder(addrStrings[0]); if (addrStrings.length > 1) { //Split into individual parameter=value strings String[] params = addrStrings[1].split("&"); //Sort the query parameters before adding them to the canonical string Arrays.sort(params); String[] pair = null; boolean first = true; try { for (String qparam : params) { pair = qparam.split("="); //pair[0] = param name, pair[1] = param value if it is present for (WalrusProperties.SubResource subResource : WalrusProperties.SubResource.values()) { if (pair[0].equals(subResource.toString())) { if (first) { addrString.append("?"); first = false; } else { addrString.append("&"); } addrString.append(subResource.toString()).append((pair.length > 1 ? "=" + WalrusUtil.URLdecode(pair[1]) : "")); } } } } catch (UnsupportedEncodingException e) { throw new AccessDeniedException("Could not verify request. Failed url decoding query parameters: " + e.getMessage()); } } return addrString.toString(); } } //End class S3Authentication /** * Authentication Handler for Walrus REST requests (POST method and SOAP are processed using different handlers) */ public void handle(MappingHttpRequest httpRequest) throws AccessDeniedException, MethodNotAllowedException { //Clean up the headers such that no duplicates may exist etc. //sanitizeHeaders(httpRequest); Map<String, String> parameters = httpRequest.getParameters(); if (httpRequest.containsHeader(SecurityParameter.Authorization.toString())) { String authHeader = httpRequest.getAndRemoveHeader(SecurityParameter.Authorization.toString()); Map<AuthorizationField, String> authMap = processAuthorizationHeader(authHeader); if (AWS_AUTH_TYPE.equals(authMap.get(AuthorizationField.Type))) { //Normally signed request using AccessKeyId/SecretKeyId pair S3Authentication.authenticate(httpRequest, authMap); return; } } // Added to handle EUCA-11882 and EUCA-11496 String servicePath = ComponentIds.lookup(WalrusBackend.class).getServicePath(); if (HttpMethod.HEAD == httpRequest.getMethod() && servicePath.equals(httpRequest.getServicePath())) { throw new MethodNotAllowedException(); } throw new AccessDeniedException("Invalid Authentication Scheme"); } public void exceptionCaught(final ChannelHandlerContext ctx, final ExceptionEvent exceptionEvent) throws Exception { LOG.info("[exception " + exceptionEvent + "]"); final HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR); DownstreamMessageEvent newEvent = new DownstreamMessageEvent(ctx.getChannel(), ctx.getChannel().getCloseFuture(), response, null); ctx.sendDownstream(newEvent); newEvent.getFuture().addListener(ChannelFutureListener.CLOSE); } }