1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.server.handlers.resource;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.net.JarURLConnection;
25 import java.net.URISyntaxException;
26 import java.net.URL;
27 import java.net.URLConnection;
28 import java.nio.ByteBuffer;
29 import java.nio.file.DirectoryStream;
30 import java.nio.file.Files;
31 import java.nio.file.Path;
32 import java.nio.file.Paths;
33 import java.util.Date;
34 import java.util.LinkedList;
35 import java.util.List;
36
37 import org.xnio.IoUtils;
38 import io.undertow.UndertowLogger;
39 import io.undertow.io.IoCallback;
40 import io.undertow.io.Sender;
41 import io.undertow.server.HttpServerExchange;
42 import io.undertow.util.DateUtils;
43 import io.undertow.util.ETag;
44 import io.undertow.util.MimeMappings;
45 import io.undertow.util.StatusCodes;
46
47 /**
48  * @author Stuart Douglas
49  */

50 public class URLResource implements Resource, RangeAwareResource {
51
52     private final URL url;
53     private final String path;
54
55     private boolean connectionOpened = false;
56     private Date lastModified;
57     private Long contentLength;
58
59     @Deprecated
60     public URLResource(final URL url, URLConnection connection, String path) {
61         this(url, path);
62     }
63
64     public URLResource(final URL url, String path) {
65         this.url = url;
66         this.path = path;
67     }
68
69     @Override
70     public String getPath() {
71         return path;
72     }
73
74     @Override
75     public Date getLastModified() {
76         openConnection();
77         return lastModified;
78     }
79
80     private void openConnection() {
81         if (!connectionOpened) {
82             connectionOpened = true;
83             URLConnection connection = null;
84             try {
85                 try {
86                     connection = url.openConnection();
87                 } catch (IOException e) {
88                     lastModified = null;
89                     contentLength = null;
90                     return;
91                 }
92                 if (url.getProtocol().equals("jar")) {
93                     connection.setUseCaches(false);
94                     URL jar = ((JarURLConnection) connection).getJarFileURL();
95                     lastModified = new Date(new File(jar.getFile()).lastModified());
96                 } else {
97                     lastModified = new Date(connection.getLastModified());
98                 }
99                 contentLength = connection.getContentLengthLong();
100             } finally {
101                 if (connection != null) {
102                     try {
103                         IoUtils.safeClose(connection.getInputStream());
104                     } catch (IOException e) {
105                         //ignore
106                     }
107                 }
108             }
109         }
110     }
111
112     @Override
113     public String getLastModifiedString() {
114         return DateUtils.toDateString(getLastModified());
115     }
116
117     @Override
118     public ETag getETag() {
119         return null;
120     }
121
122     @Override
123     public String getName() {
124         String path = url.getPath();
125         if (path.endsWith("/")) {
126             path = path.substring(0, path.length() - 1);
127         }
128         int sepIndex = path.lastIndexOf("/");
129         if (sepIndex != -1) {
130             path = path.substring(sepIndex + 1);
131         }
132         return path;
133     }
134
135     @Override
136     public boolean isDirectory() {
137         Path file = getFilePath();
138         if (file != null) {
139             return Files.isDirectory(file);
140         } else if (url.getPath().endsWith("/")) {
141             return true;
142         }
143         return false;
144     }
145
146     @Override
147     public List<Resource> list() {
148         List<Resource> result = new LinkedList<>();
149         Path file = getFilePath();
150         try {
151             if (file != null) {
152                 try (DirectoryStream<Path> stream = Files.newDirectoryStream(file)) {
153                     for (Path child : stream) {
154                         result.add(new URLResource(child.toUri().toURL(), child.toString()));
155                     }
156                 }
157             }
158         } catch (IOException e) {
159             throw new RuntimeException(e);
160         }
161         return result;
162     }
163
164     @Override
165     public String getContentType(final MimeMappings mimeMappings) {
166         final String fileName = getName();
167         int index = fileName.lastIndexOf('.');
168         if (index != -1 && index != fileName.length() - 1) {
169             return mimeMappings.getMimeType(fileName.substring(index + 1));
170         }
171         return null;
172     }
173
174     @Override
175     public void serve(Sender sender, HttpServerExchange exchange, IoCallback completionCallback) {
176         serveImpl(sender, exchange, -1, -1, false, completionCallback);
177     }
178
179     public void serveImpl(final Sender sender, final HttpServerExchange exchange, final long start, final long end, final boolean range, final IoCallback completionCallback) {
180
181         class ServerTask implements Runnable, IoCallback {
182
183             private InputStream inputStream;
184             private byte[] buffer;
185
186             long toSkip = start;
187             long remaining = end - start + 1;
188
189             @Override
190             public void run() {
191                 if (range && remaining == 0) {
192                     //we are done, just return
193                     IoUtils.safeClose(inputStream);
194                     completionCallback.onComplete(exchange, sender);
195                     return;
196                 }
197                 if (inputStream == null) {
198                     try {
199                         inputStream = url.openStream();
200                     } catch (IOException e) {
201                         exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
202                         return;
203                     }
204                     buffer = new byte[1024];//TODO: we should be pooling these
205                 }
206                 try {
207                     int res = inputStream.read(buffer);
208                     if (res == -1) {
209                         //we are done, just return
210                         IoUtils.safeClose(inputStream);
211                         completionCallback.onComplete(exchange, sender);
212                         return;
213                     }
214                     int bufferStart = 0;
215                     int length = res;
216                     if (range && toSkip > 0) {
217                         //skip to the start of the requested range
218                         //not super efficient, but what can you do
219                         while (toSkip > res) {
220                             toSkip -= res;
221                             res = inputStream.read(buffer);
222                             if (res == -1) {
223                                 //we are done, just return
224                                 IoUtils.safeClose(inputStream);
225                                 completionCallback.onComplete(exchange, sender);
226                                 return;
227                             }
228                         }
229                         bufferStart = (int) toSkip;
230                         length -= toSkip;
231                         toSkip = 0;
232                     }
233                     if (range && length > remaining) {
234                         length = (int) remaining;
235                     }
236                     sender.send(ByteBuffer.wrap(buffer, bufferStart, length), this);
237                 } catch (IOException e) {
238                     onException(exchange, sender, e);
239                 }
240
241             }
242
243             @Override
244             public void onComplete(final HttpServerExchange exchange, final Sender sender) {
245                 if (exchange.isInIoThread()) {
246                     exchange.dispatch(this);
247                 } else {
248                     run();
249                 }
250             }
251
252             @Override
253             public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) {
254                 UndertowLogger.REQUEST_IO_LOGGER.ioException(exception);
255                 IoUtils.safeClose(inputStream);
256                 if (!exchange.isResponseStarted()) {
257                     exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
258                 }
259                 completionCallback.onException(exchange, sender, exception);
260             }
261         }
262
263         ServerTask serveTask = new ServerTask();
264         if (exchange.isInIoThread()) {
265             exchange.dispatch(serveTask);
266         } else {
267             serveTask.run();
268         }
269     }
270
271     @Override
272     public Long getContentLength() {
273         openConnection();
274         return contentLength;
275     }
276
277     @Override
278     public String getCacheKey() {
279         return url.toString();
280     }
281
282     @Override
283     public File getFile() {
284         Path path = getFilePath();
285         return path != null ? path.toFile() : null;
286     }
287
288     @Override
289     public Path getFilePath() {
290         if (url.getProtocol().equals("file")) {
291             try {
292                 return Paths.get(url.toURI());
293             } catch (URISyntaxException e) {
294                 return null;
295             }
296         }
297         return null;
298     }
299
300     @Override
301     public File getResourceManagerRoot() {
302         return null;
303     }
304
305     @Override
306     public Path getResourceManagerRootPath() {
307         return null;
308     }
309
310     @Override
311     public URL getUrl() {
312         return url;
313     }
314
315     @Override
316     public void serveRange(Sender sender, HttpServerExchange exchange, long start, long end, IoCallback completionCallback) {
317         serveImpl(sender, exchange, start, end, true, completionCallback);
318     }
319
320     @Override
321     public boolean isRangeSupported() {
322         return true;
323     }
324 }
325