/*************************************************************************
* Copyright 2009-2012 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.objectstorage.pipeline.handlers;
import com.eucalyptus.http.MappingHttpRequest;
import com.eucalyptus.objectstorage.exceptions.s3.*;
import com.eucalyptus.objectstorage.pipeline.auth.S3Authentication;
import com.eucalyptus.objectstorage.pipeline.auth.S3Authentication.S3Authenticator;
import com.eucalyptus.objectstorage.pipeline.handlers.AwsChunkStream.AwsChunk;
import com.eucalyptus.objectstorage.pipeline.handlers.AwsChunkStream.StreamingHttpRequest;
import com.eucalyptus.ws.handlers.MessageStackHandler;
import com.eucalyptus.ws.server.MessageStatistics;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import org.apache.log4j.Logger;
import org.jboss.netty.channel.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Callable;
public class ObjectStorageAuthenticationHandler extends MessageStackHandler {
private static final Logger LOG = Logger.getLogger(ObjectStorageAuthenticationHandler.class);
private boolean authenticatedInitialRequest;
/**
* Note: Overriding to ensure that the message is passed to the next stage in the pipeline only if it
* passes authentication.
*/
@Override
public void handleUpstream(final ChannelHandlerContext ctx, final ChannelEvent channelEvent) throws Exception {
if (channelEvent instanceof MessageEvent) {
Callable<Long> stat = MessageStatistics.startUpstream(ctx.getChannel(), this);
try {
MessageEvent event = (MessageEvent) channelEvent;
// Handle V4 streaming requests
if (event.getMessage() instanceof StreamingHttpRequest) {
StreamingHttpRequest request = (StreamingHttpRequest) event.getMessage();
// Authenticate initial request if necessary
if (!authenticatedInitialRequest) {
authenticate(request.initialRequest);
ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), request.initialRequest, event.getRemoteAddress()));
}
// Authenticate chunks
authenticate(request);
for (AwsChunk chunk : request.awsChunks) {
ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), chunk.toHttpChunk(), event.getRemoteAddress()));
}
} else {
if (event.getMessage() instanceof MappingHttpRequest)
authenticate((MappingHttpRequest) event.getMessage());
ctx.sendUpstream(channelEvent);
}
} catch (AccessDeniedException | SignatureDoesNotMatchException | InvalidAccessKeyIdException | MissingSecurityHeaderException ex) {
LOG.debug(ex.getMessage());
Channels.fireExceptionCaught(ctx, ex);
} catch (Throwable e) {
LOG.error(e, e);
Channels.fireExceptionCaught(ctx, e);
} finally {
stat.call();
}
} else {
ctx.sendUpstream(channelEvent);
}
}
/**
* Authentication Handler for ObjectStorage REST requests (POST method and SOAP are processed using different
* handlers).
*
* @throws AccessDeniedException if the auth header is invalid
* @throws SignatureDoesNotMatchException if the signature is invalid
* @throws InvalidAccessKeyIdException if the contextual AWS key is invalid
* @throws InternalErrorException if something unexpected occurs
* @throws MissingSecurityHeaderException is the auth header is invalid
*/
private void authenticate(MappingHttpRequest request) throws S3Exception {
removeDuplicateHeaderValues(request);
joinDuplicateHeaders(request);
Map<String, String> lowercaseParams = lowercaseKeys(request.getParameters());
S3Authenticator.of(request, lowercaseParams).authenticate(request, lowercaseParams);
authenticatedInitialRequest = true;
}
/**
* Authenticate streaming V4 chunks.
*/
private void authenticate(StreamingHttpRequest request) throws S3Exception {
removeDuplicateHeaderValues(request.initialRequest);
joinDuplicateHeaders(request.initialRequest);
Map<String, String> lowercaseParams = lowercaseKeys(request.initialRequest.getParameters());
if (request.awsChunks.isEmpty())
S3Authenticator.of(request.initialRequest, lowercaseParams).authenticate(request.initialRequest, lowercaseParams);
else
S3Authentication.authenticateV4Streaming(request.initialRequest, request.awsChunks);
}
/**
* This method exists to clean up a problem encountered periodically where the HTTP headers are identically duplicated.
* <p>
* TODO Move to somewhere common outside of object-storage
*/
private static void removeDuplicateHeaderValues(MappingHttpRequest request) {
List<String> hdrList;
Map<String, List<String>> fixedHeaders = new HashMap<>();
boolean foundDup = false;
for (String header : request.getHeaderNames()) {
hdrList = request.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: " + request.logMessage());
request.clearHeaders();
for (Map.Entry<String, List<String>> e : fixedHeaders.entrySet()) {
for (String v : e.getValue()) {
request.addHeader(e.getKey(), v);
}
}
}
}
/**
* 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.
* <p>
* TODO Move to somewhere common outside of object-storage
*/
private static void joinDuplicateHeaders(MappingHttpRequest request) {
// Join headers
Map<String, String> joined = new TreeMap<>();
for (String header : request.getHeaderNames())
joined.put(header, Joiner.on(',').join(request.getHeaders(header)));
// Remove all headers
request.clearHeaders();
// Add joined headers
for (Map.Entry<String, String> entry : joined.entrySet())
request.addHeader(entry.getKey(), entry.getValue());
}
/**
* Returns a representation of the {@code map} with lowercase keys.
*/
static Map<String, String> lowercaseKeys(Map<String, String> map) {
Map<String, String> result = new HashMap<>();
map.entrySet().forEach(e -> result.put(e.getKey().toLowerCase(), e.getValue()));
return result;
}
}