View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25  
26   */
27  package org.apache.commons.vfs2.util;
28  
29  import java.io.File;
30  import java.io.IOException;
31  import java.net.InetSocketAddress;
32  import java.net.URI;
33  import java.net.URISyntaxException;
34  import java.net.URL;
35  import java.security.KeyManagementException;
36  import java.security.KeyStoreException;
37  import java.security.NoSuchAlgorithmException;
38  import java.security.UnrecoverableKeyException;
39  import java.security.cert.CertificateException;
40  import java.util.Date;
41  import java.util.Locale;
42  import java.util.concurrent.ExecutionException;
43  import java.util.concurrent.Future;
44  import java.util.concurrent.TimeUnit;
45  
46  import javax.net.ssl.SSLContext;
47  
48  import org.apache.hc.client5.http.utils.DateUtils;
49  import org.apache.hc.core5.http.ContentType;
50  import org.apache.hc.core5.http.EndpointDetails;
51  import org.apache.hc.core5.http.EntityDetails;
52  import org.apache.hc.core5.http.HttpException;
53  import org.apache.hc.core5.http.HttpHeaders;
54  import org.apache.hc.core5.http.HttpRequest;
55  import org.apache.hc.core5.http.HttpStatus;
56  import org.apache.hc.core5.http.Message;
57  import org.apache.hc.core5.http.MethodNotSupportedException;
58  import org.apache.hc.core5.http.ProtocolException;
59  import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap;
60  import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer;
61  import org.apache.hc.core5.http.nio.AsyncRequestConsumer;
62  import org.apache.hc.core5.http.nio.AsyncServerRequestHandler;
63  import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers;
64  import org.apache.hc.core5.http.nio.entity.NoopEntityConsumer;
65  import org.apache.hc.core5.http.nio.ssl.BasicServerTlsStrategy;
66  import org.apache.hc.core5.http.nio.ssl.FixedPortStrategy;
67  import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder;
68  import org.apache.hc.core5.http.nio.support.BasicRequestConsumer;
69  import org.apache.hc.core5.http.protocol.HttpContext;
70  import org.apache.hc.core5.http.protocol.HttpCoreContext;
71  import org.apache.hc.core5.http.protocol.HttpDateGenerator;
72  import org.apache.hc.core5.io.CloseMode;
73  import org.apache.hc.core5.reactor.IOReactorConfig;
74  import org.apache.hc.core5.reactor.IOReactorStatus;
75  import org.apache.hc.core5.reactor.ListenerEndpoint;
76  import org.apache.hc.core5.ssl.SSLContexts;
77  import org.apache.hc.core5.util.TimeValue;
78  
79  /**
80   * Embedded HTTP/1.1 file server based on a non-blocking I/O model and capable of direct channel (zero copy) data
81   * transfer.
82   */
83  public class NHttpFileServer {
84  
85      private static class HttpFileHandler implements AsyncServerRequestHandler<Message<HttpRequest, Void>> {
86  
87          private final File docRoot;
88  
89          HttpFileHandler(final File docRoot) {
90              this.docRoot = docRoot;
91          }
92  
93          @Override
94          public void handle(final Message<HttpRequest, Void> message, final ResponseTrigger responseTrigger,
95              final HttpContext context) throws HttpException, IOException {
96              final HttpRequest request = message.getHead();
97              final String method = request.getMethod().toUpperCase(Locale.ROOT);
98              if (!method.equals("GET") && !method.equals("HEAD") && !method.equals("POST")) {
99                  throw new MethodNotSupportedException(method + " method not supported");
100             }
101 
102             final URI requestUri;
103             try {
104                 requestUri = request.getUri();
105             } catch (final URISyntaxException ex) {
106                 throw new ProtocolException(ex.getMessage(), ex);
107             }
108             final String path = requestUri.getPath();
109             final File file = new File(docRoot, path);
110             final ContentType mimeType = ContentType.TEXT_HTML;
111             if (!file.exists()) {
112 
113                 final String msg = "File " + file.getPath() + " not found";
114                 println(msg);
115                 responseTrigger.submitResponse(AsyncResponseBuilder.create(HttpStatus.SC_NOT_FOUND)
116                     .setEntity("<html><body><h1>" + msg + "</h1></body></html>", mimeType).build(), context);
117 
118             } else if (!file.canRead()) {
119                 final String msg = "Cannot read file " + file.getPath();
120                 println(msg);
121                 responseTrigger.submitResponse(AsyncResponseBuilder.create(HttpStatus.SC_FORBIDDEN)
122                     .setEntity("<html><body><h1>" + msg + "</h1></body></html>", mimeType).build(), context);
123 
124             } else {
125 
126                 ContentType contentType;
127                 final String filename = file.getName().toLowerCase(Locale.ROOT);
128 // The following causes a failure on Linux and Macos in HttpProviderTestCase:
129 // org.apache.commons.vfs2.FileSystemException: GET method failed for "http://localhost:37637/read-tests/file1.txt" range "10" with HTTP status 200.
130 //                at org.apache.commons.vfs2.provider.http.HttpRandomAccessContent.getDataInputStream(HttpRandomAccessContent.java:80)
131 //                if (filename.endsWith(".txt")) {
132 //                    contentType = ContentType.TEXT_PLAIN;
133 //                } else if (filename.endsWith(".html") || filename.endsWith(".htm") || file.isDirectory()) {
134 //                    contentType = ContentType.TEXT_HTML;
135 //                } else if (filename.endsWith(".xml")) {
136 //                    contentType = ContentType.TEXT_XML;
137 //                } else {
138 //                    contentType = ContentType.DEFAULT_BINARY;
139 //                }
140                 contentType = ContentType.TEXT_HTML;
141                 final HttpCoreContext coreContext = HttpCoreContext.adapt(context);
142                 final EndpointDetails endpoint = coreContext.getEndpointDetails();
143 
144                 println(endpoint + " | serving file " + file.getPath());
145 
146                 // @formatter:off
147                 responseTrigger.submitResponse(
148                     AsyncResponseBuilder.create(HttpStatus.SC_OK)
149                         .setEntity(file.isDirectory()
150                             ? AsyncEntityProducers.create(file.toString(), contentType)
151                             : AsyncEntityProducers.create(file, contentType))
152                         .addHeader(HttpHeaders.LAST_MODIFIED, DateUtils.formatDate(new Date(file.lastModified())))
153                     .build(), context);
154                 // @formatter:on
155             }
156         }
157 
158         @Override
159         public AsyncRequestConsumer<Message<HttpRequest, Void>> prepare(final HttpRequest request,
160             final EntityDetails entityDetails, final HttpContext context) throws HttpException {
161             return new BasicRequestConsumer<>(entityDetails != null ? new NoopEntityConsumer() : null);
162         }
163 
164     }
165 
166     public static boolean DEBUG = Boolean.getBoolean(NHttpFileServer.class.getSimpleName() + ".debug");
167 
168     public static void main(final String[] args) throws Exception {
169         if (args.length < 1) {
170             System.err.println("Please specify document root directory");
171             System.exit(1);
172         }
173         // Document root directory
174         final File docRoot = new File(args[0]);
175         int port = 8080;
176         if (args.length >= 2) {
177             port = Integer.parseInt(args[1]);
178         }
179         new NHttpFileServer(port, docRoot).start().awaitTermination();
180     }
181 
182     static final void println(final String msg) {
183         if (DEBUG) {
184             System.out.println(HttpDateGenerator.INSTANCE.getCurrentDate() + " | " + msg);
185         }
186     }
187 
188     public static NHttpFileServer start(final int port, final File docRoot, final long waitMillis)
189         throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException,
190         CertificateException, IOException, InterruptedException, ExecutionException {
191         return new NHttpFileServer(port, docRoot).start();
192     }
193 
194     private final File docRoot;
195     private ListenerEndpoint listenerEndpoint;
196     private final int port;
197     private HttpAsyncServer server;
198 
199     private NHttpFileServer(final int port, final File docRoot) {
200         this.port = port;
201         this.docRoot = docRoot;
202     }
203 
204     private void awaitTermination() throws InterruptedException {
205         server.awaitShutdown(TimeValue.MAX_VALUE);
206     }
207 
208     public void close() {
209         if (server.getStatus() == IOReactorStatus.ACTIVE) {
210             final CloseMode closeMode = CloseMode.GRACEFUL;
211             println("HTTP server shutting down (closeMode=" + closeMode + ")...");
212             server.close(closeMode);
213             println("HTTP server shut down.");
214         }
215     }
216 
217     public int getPort() {
218         if (server == null) {
219             return port;
220         }
221         return ((InetSocketAddress) listenerEndpoint.getAddress()).getPort();
222     }
223 
224     public void shutdown(final long gracePeriod, final TimeUnit timeUnit) throws InterruptedException {
225         if (server != null) {
226             server.initiateShutdown();
227             server.awaitShutdown(TimeValue.of(gracePeriod, timeUnit));
228         }
229 
230     }
231 
232     private NHttpFileServer start() throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException,
233         KeyStoreException, CertificateException, IOException, InterruptedException, ExecutionException {
234         final AsyncServerBootstrap bootstrap = AsyncServerBootstrap.bootstrap();
235         SSLContext sslContext = null;
236         if (port == 8443 || port == 443) {
237             // Initialize SSL context
238             final URL url = NHttpFileServer.class.getResource("/test.keystore");
239             if (url == null) {
240                 println("Keystore not found");
241                 System.exit(1);
242             }
243             println("Loading keystore " + url);
244             sslContext = SSLContexts.custom()
245                 .loadKeyMaterial(url, "nopassword".toCharArray(), "nopassword".toCharArray()).build();
246             bootstrap.setTlsStrategy(new BasicServerTlsStrategy(sslContext, new FixedPortStrategy(port)));
247         }
248 
249         // @formatter:off
250         final IOReactorConfig config = IOReactorConfig.custom()
251                 .setSoTimeout(15, TimeUnit.SECONDS)
252                 .setTcpNoDelay(true)
253                 .build();
254         // @formatter:on
255 
256         server = bootstrap.setIOReactorConfig(config).register("*", new HttpFileHandler(docRoot)).create();
257 
258         Runtime.getRuntime().addShutdownHook(new Thread() {
259             @Override
260             public void run() {
261                 close();
262             }
263 
264         });
265 
266         server.start();
267 
268         final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(port));
269         listenerEndpoint = future.get();
270         println("Serving " + docRoot + " on " + listenerEndpoint.getAddress()
271             + (sslContext == null ? "" : " with " + sslContext.getProvider() + " " + sslContext.getProtocol()));
272         return this;
273     }
274 
275 }