1 /*
2  * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15 package com.amazonaws.services.s3.internal;
16
17 import com.amazonaws.AmazonServiceException;
18 import com.amazonaws.ClientConfiguration;
19 import com.amazonaws.http.HttpMethodName;
20 import com.amazonaws.http.HttpResponse;
21 import com.amazonaws.http.HttpResponseHandler;
22 import com.amazonaws.services.s3.Headers;
23 import com.amazonaws.services.s3.model.AmazonS3Exception;
24 import com.amazonaws.util.IOUtils;
25
26 import com.amazonaws.util.XmlUtils;
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29
30 import java.io.ByteArrayInputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.Map;
34
35 import javax.xml.stream.XMLInputFactory;
36 import javax.xml.stream.XMLStreamConstants;
37 import javax.xml.stream.XMLStreamException;
38 import javax.xml.stream.XMLStreamReader;
39
40 import static com.amazonaws.util.StringUtils.UTF8;
41
42 /**
43  * Response handler for S3 error responses. S3 error responses are different
44  * from other AWS error responses in a few ways. Most error responses will
45  * contain an XML body, but not all (ex: error responses to HEAD requests will
46  * not), so this error handler has to account for that. The actual XML error
47  * response body is slightly different than other services like SimpleDB or EC2
48  * and some information isn't explicitly represented in the XML error response
49  * body (ex: error type/fault information) so it has to be inferred from other
50  * parts of the error response.
51  */

52 public class S3ErrorResponseHandler implements
53         HttpResponseHandler<AmazonServiceException> {
54     /** Shared logger for profiling information */
55     private static final Log log = LogFactory
56             .getLog(S3ErrorResponseHandler.class);
57
58     private enum S3ErrorTags {
59         Error, Message, Code, RequestId, HostId
60     };
61
62     private final ClientConfiguration clientConfiguration;
63
64     public S3ErrorResponseHandler(ClientConfiguration clientConfiguration) {
65         this.clientConfiguration = clientConfiguration;
66     }
67
68     @Override
69     public AmazonServiceException handle(HttpResponse httpResponse)
70             throws XMLStreamException {
71         final AmazonServiceException exception = createException(httpResponse);
72         exception.setHttpHeaders(httpResponse.getHeaders());
73         return exception;
74     }
75
76     private AmazonServiceException createException(HttpResponse httpResponse) throws
77                                                                               XMLStreamException {
78         final InputStream is = httpResponse.getContent();
79         String xmlContent = null;
80         /*
81          * We don't always get an error response body back from S3. When we send
82          * a HEAD request, we don't receive a body, so we'll have to just return
83          * what we can.
84          */

85         if (is == null
86                 || httpResponse.getRequest().getHttpMethod() == HttpMethodName.HEAD) {
87             return createExceptionFromHeaders(httpResponse, null);
88         }
89
90         String content = null;
91         try {
92             content = IOUtils.toString(is);
93         } catch (IOException ioe) {
94             if (log.isDebugEnabled())
95                 log.debug("Failed in parsing the error response : ", ioe);
96             return createExceptionFromHeaders(httpResponse, null);
97         }
98
99         XMLStreamReader reader
100             = XmlUtils.getXmlInputFactory().createXMLStreamReader(new ByteArrayInputStream(content.getBytes(UTF8)));
101
102         try {
103             /*
104              * target depth is to determine if the XML Error response from the
105              * server has any element inside <Error> tag have child tags.
106              * Parsing such tags is not supported now. target depth is
107              * incremented for every start tag and decremented after every end
108              * tag is encountered.
109              */

110             int targetDepth = 0;
111             final AmazonS3ExceptionBuilder exceptionBuilder = new AmazonS3ExceptionBuilder();
112             exceptionBuilder.setErrorResponseXml(content);
113             exceptionBuilder.setStatusCode(httpResponse.getStatusCode());
114             exceptionBuilder.setCloudFrontId(httpResponse.getHeaders().get(Headers.CLOUD_FRONT_ID));
115             String bucketRegion = httpResponse.getHeader(Headers.S3_BUCKET_REGION);
116             if (bucketRegion != null) {
117                 exceptionBuilder.addAdditionalDetail(Headers.S3_BUCKET_REGION, bucketRegion);
118             }
119
120             boolean hasErrorTagVisited = false;
121             while (reader.hasNext()) {
122                 int event = reader.next();
123
124                 switch (event) {
125                 case XMLStreamConstants.START_ELEMENT:
126                     targetDepth++;
127                     String tagName = reader.getLocalName();
128                     if (targetDepth == 1
129                             && !S3ErrorTags.Error.toString().equals(tagName))
130                         return createExceptionFromHeaders(httpResponse,
131                                 "Unable to parse error response. Error XML Not in proper format."
132                                         + content);
133                     if (S3ErrorTags.Error.toString().equals(tagName)) {
134                         hasErrorTagVisited = true;
135                     }
136                     continue;
137                 case XMLStreamConstants.CHARACTERS:
138                     xmlContent = reader.getText();
139                     if (xmlContent != null)
140                         xmlContent = xmlContent.trim();
141                     continue;
142                 case XMLStreamConstants.END_ELEMENT:
143                     tagName = reader.getLocalName();
144                     targetDepth--;
145                     if (!(hasErrorTagVisited) || targetDepth > 1) {
146                         return createExceptionFromHeaders(httpResponse,
147                                 "Unable to parse error response. Error XML Not in proper format."
148                                         + content);
149                     }
150                     if (S3ErrorTags.Message.toString().equals(tagName)) {
151                         exceptionBuilder.setErrorMessage(xmlContent);
152                     } else if (S3ErrorTags.Code.toString().equals(tagName)) {
153                         exceptionBuilder.setErrorCode(xmlContent);
154                     } else if (S3ErrorTags.RequestId.toString().equals(tagName)) {
155                         exceptionBuilder.setRequestId(xmlContent);
156                     } else if (S3ErrorTags.HostId.toString().equals(tagName)) {
157                         exceptionBuilder.setExtendedRequestId(xmlContent);
158                     } else {
159                         exceptionBuilder.addAdditionalDetail(tagName, xmlContent);
160                     }
161                     continue;
162                 case XMLStreamConstants.END_DOCUMENT:
163                     exceptionBuilder.setProxyHost(clientConfiguration.getProxyHost());
164                     return exceptionBuilder.build();
165                 }
166             }
167         } catch (Exception e) {
168             if (log.isDebugEnabled())
169                 log.debug("Failed in parsing the error response : " + content,
170                         e);
171         }
172         return createExceptionFromHeaders(httpResponse, content);
173     }
174
175     private AmazonS3Exception createExceptionFromHeaders(
176             HttpResponse errorResponse, String errorResponseXml) {
177         final Map<String, String> headers = errorResponse.getHeaders();
178         final int statusCode = errorResponse.getStatusCode();
179         final AmazonS3ExceptionBuilder exceptionBuilder = new AmazonS3ExceptionBuilder();
180         exceptionBuilder.setErrorMessage(errorResponse.getStatusText());
181         exceptionBuilder.setErrorResponseXml(errorResponseXml);
182         exceptionBuilder.setStatusCode(statusCode);
183         exceptionBuilder
184                 .setExtendedRequestId(headers.get(Headers.EXTENDED_REQUEST_ID));
185         exceptionBuilder.setRequestId(headers.get(Headers.REQUEST_ID));
186         exceptionBuilder.setCloudFrontId(headers.get(Headers.CLOUD_FRONT_ID));
187         exceptionBuilder
188                 .setErrorCode(statusCode + " " + errorResponse.getStatusText());
189         exceptionBuilder.addAdditionalDetail(Headers.S3_BUCKET_REGION,
190                 errorResponse.getHeaders().get(Headers.S3_BUCKET_REGION));
191         exceptionBuilder.setProxyHost(clientConfiguration.getProxyHost());
192         return exceptionBuilder.build();
193     }
194
195     /**
196      * Since this response handler completely consumes all the data from the
197      * underlying HTTP connection during the handle method, we don't need to
198      * keep the HTTP connection open.
199      *
200      * @see com.amazonaws.http.HttpResponseHandler#needsConnectionLeftOpen()
201      */

202     public boolean needsConnectionLeftOpen() {
203         return false;
204     }
205 }
206