1 /*
2  * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3  * Copyright (C) 2010  Mickael Guessant
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  */
19 
20 package davmail.http.request;
21 
22 import davmail.exchange.XMLStreamUtil;
23 import org.apache.http.Header;
24 import org.apache.http.HttpResponse;
25 import org.apache.http.HttpStatus;
26 import org.apache.http.client.HttpResponseException;
27 import org.apache.http.client.ResponseHandler;
28 import org.apache.http.client.methods.HttpPost;
29 import org.apache.http.entity.AbstractHttpEntity;
30 import org.apache.jackrabbit.webdav.MultiStatusResponse;
31 import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
32 import org.apache.jackrabbit.webdav.xml.Namespace;
33 import org.apache.log4j.Logger;
34 
35 import javax.xml.stream.XMLStreamConstants;
36 import javax.xml.stream.XMLStreamException;
37 import javax.xml.stream.XMLStreamReader;
38 import java.io.ByteArrayInputStream;
39 import java.io.FilterInputStream;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.OutputStream;
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 public abstract class ExchangeDavRequest extends HttpPost implements ResponseHandler<List<MultiStatusResponse>> {
47     protected static final Logger LOGGER = Logger.getLogger(ExchangeDavRequest.class);
48     private static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8";
49 
50     private HttpResponse response;
51     private List<MultiStatusResponse> responses;
52 
53     /**
54      * Create PROPPATCH method.
55      *
56      * @param path path
57      */
ExchangeDavRequest(String path)58     public ExchangeDavRequest(String path) {
59         super(path);
60         AbstractHttpEntity httpEntity = new AbstractHttpEntity() {
61             byte[] content;
62 
63             @Override
64             public boolean isRepeatable() {
65                 return true;
66             }
67 
68             @Override
69             public long getContentLength() {
70                 if (content == null) {
71                     content = generateRequestContent();
72                 }
73                 return content.length;
74             }
75 
76             @Override
77             public InputStream getContent() throws UnsupportedOperationException {
78                 if (content == null) {
79                     content = generateRequestContent();
80                 }
81                 return new ByteArrayInputStream(content);
82             }
83 
84             @Override
85             public void writeTo(OutputStream outputStream) throws IOException {
86                 if (content == null) {
87                     content = generateRequestContent();
88                 }
89                 outputStream.write(content);
90             }
91 
92             @Override
93             public boolean isStreaming() {
94                 return false;
95             }
96         };
97 
98         httpEntity.setContentType(XML_CONTENT_TYPE);
99         setEntity(httpEntity);
100     }
101 
102     /**
103      * Generate request content from property values.
104      *
105      * @return request content as byte array
106      */
generateRequestContent()107     protected abstract byte[] generateRequestContent();
108 
109     @Override
handleResponse(HttpResponse response)110     public List<MultiStatusResponse> handleResponse(HttpResponse response) {
111         this.response = response;
112         Header contentTypeHeader = response.getFirstHeader("Content-Type");
113         if (contentTypeHeader != null && "text/xml".equals(contentTypeHeader.getValue())) {
114             responses = new ArrayList<>();
115             XMLStreamReader reader;
116             try {
117                 reader = XMLStreamUtil.createXMLStreamReader(new FilterInputStream(response.getEntity().getContent()) {
118                     final byte[] lastbytes = new byte[3];
119 
120                     @Override
121                     public int read(byte[] bytes, int off, int len) throws IOException {
122                         int count = in.read(bytes, off, len);
123                         // patch invalid element name
124                         for (int i = 0; i < count; i++) {
125                             byte currentByte = bytes[off + i];
126                             if ((lastbytes[0] == '<') && (currentByte >= '0' && currentByte <= '9')) {
127                                 // move invalid first tag char to valid range
128                                 bytes[off + i] = (byte) (currentByte + 49);
129                             }
130                             lastbytes[0] = lastbytes[1];
131                             lastbytes[1] = lastbytes[2];
132                             lastbytes[2] = currentByte;
133                         }
134                         return count;
135                     }
136 
137                 });
138                 while (reader.hasNext()) {
139                     reader.next();
140                     if (XMLStreamUtil.isStartTag(reader, "response")) {
141                         handleResponse(reader);
142                     }
143                 }
144 
145             } catch (IOException | XMLStreamException e) {
146                 LOGGER.error("Error while parsing soap response: " + e, e);
147             }
148         }
149         return responses;
150     }
151 
handleResponse(XMLStreamReader reader)152     protected void handleResponse(XMLStreamReader reader) throws XMLStreamException {
153         MultiStatusResponse multiStatusResponse = null;
154         String href = null;
155         String responseStatus = "";
156         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "response")) {
157             reader.next();
158             if (XMLStreamUtil.isStartTag(reader)) {
159                 String tagLocalName = reader.getLocalName();
160                 if ("href".equals(tagLocalName)) {
161                     href = reader.getElementText();
162                 } else if ("status".equals(tagLocalName)) {
163                     responseStatus = reader.getElementText();
164                 } else if ("propstat".equals(tagLocalName)) {
165                     if (multiStatusResponse == null) {
166                         multiStatusResponse = new MultiStatusResponse(href, responseStatus);
167                     }
168                     handlePropstat(reader, multiStatusResponse);
169                 }
170             }
171         }
172         if (multiStatusResponse != null) {
173             responses.add(multiStatusResponse);
174         }
175     }
176 
handlePropstat(XMLStreamReader reader, MultiStatusResponse multiStatusResponse)177     protected void handlePropstat(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
178         int propstatStatus = 0;
179         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "propstat")) {
180             reader.next();
181             if (XMLStreamUtil.isStartTag(reader)) {
182                 String tagLocalName = reader.getLocalName();
183                 if ("status".equals(tagLocalName)) {
184                     if ("HTTP/1.1 200 OK".equals(reader.getElementText())) {
185                         propstatStatus = HttpStatus.SC_OK;
186                     } else {
187                         propstatStatus = 0;
188                     }
189                 } else if ("prop".equals(tagLocalName) && propstatStatus == HttpStatus.SC_OK) {
190                     handleProperty(reader, multiStatusResponse);
191                 }
192             }
193         }
194 
195     }
196 
handleProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse)197     protected void handleProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
198         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) {
199             reader.next();
200             if (XMLStreamUtil.isStartTag(reader)) {
201                 Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI());
202                 String tagLocalName = reader.getLocalName();
203                 if (reader.getAttributeCount() > 0 && "mv.string".equals(reader.getAttributeValue(0))) {
204                     handleMultiValuedProperty(reader, multiStatusResponse);
205                 } else {
206                     String tagContent = getTagContent(reader);
207                     if (tagContent != null) {
208                         multiStatusResponse.add(new DefaultDavProperty<>(tagLocalName, tagContent, namespace));
209                     }
210                 }
211             }
212         }
213     }
214 
handleMultiValuedProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse)215     protected void handleMultiValuedProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
216         String tagLocalName = reader.getLocalName();
217         Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI());
218         ArrayList<String> values = new ArrayList<>();
219         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, tagLocalName)) {
220             reader.next();
221             if (XMLStreamUtil.isStartTag(reader)) {
222                 String tagContent = getTagContent(reader);
223                 if (tagContent != null) {
224                     values.add(tagContent);
225                 }
226             }
227         }
228         multiStatusResponse.add(new DefaultDavProperty<>(tagLocalName, values, namespace));
229     }
230 
getTagContent(XMLStreamReader reader)231     protected String getTagContent(XMLStreamReader reader) throws XMLStreamException {
232         String value = null;
233         String tagLocalName = reader.getLocalName();
234         while (reader.hasNext() &&
235                 !((reader.getEventType() == XMLStreamConstants.END_ELEMENT) && tagLocalName.equals(reader.getLocalName()))) {
236             reader.next();
237             if (reader.getEventType() == XMLStreamConstants.CHARACTERS) {
238                 value = reader.getText();
239             }
240         }
241         // empty tag
242         if (!reader.hasNext()) {
243             throw new XMLStreamException("End element for " + tagLocalName + " not found");
244         }
245         return value;
246     }
247 
248     /**
249      * Get Multistatus responses.
250      *
251      * @return responses
252      * @throws HttpResponseException on error
253      */
getResponses()254     public MultiStatusResponse[] getResponses() throws HttpResponseException {
255         if (responses == null) {
256             // TODO: compare with native HttpClient error handling
257             throw new HttpResponseException(response.getStatusLine().getStatusCode(),
258                     response.getStatusLine().getReasonPhrase());
259         }
260         return responses.toArray(new MultiStatusResponse[0]);
261     }
262 
263     /**
264      * Get single Multistatus response.
265      *
266      * @return response
267      * @throws HttpResponseException on error
268      */
getResponse()269     public MultiStatusResponse getResponse() throws HttpResponseException {
270         if (responses == null || responses.size() != 1) {
271             throw new HttpResponseException(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
272         }
273         return responses.get(0);
274     }
275 
276     /**
277      * Return method http status code.
278      *
279      * @return http status code
280      * @throws HttpResponseException on error
281      */
getResponseStatusCode()282     public int getResponseStatusCode() throws HttpResponseException {
283         String responseDescription = getResponse().getResponseDescription();
284         if ("HTTP/1.1 201 Created".equals(responseDescription)) {
285             return HttpStatus.SC_CREATED;
286         } else {
287             return HttpStatus.SC_OK;
288         }
289     }
290 
291 }
292