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 transfer.
81   */
82  public final class NHttpFileServer {
83  
84      private static class HttpFileHandler implements AsyncServerRequestHandler<Message<HttpRequest, Void>> {
85  
86          private final File docRoot;
87  
88          HttpFileHandler(final File docRoot) {
89              this.docRoot = docRoot;
90          }
91  
92          @Override
93          public void handle(final Message<HttpRequest, Void> message, final ResponseTrigger responseTrigger, final HttpContext context)
94                  throws HttpException, IOException {
95              println("Handling " + message + " in " + context);
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(
116                         AsyncResponseBuilder.create(HttpStatus.SC_NOT_FOUND).setEntity("<html><body><h1>" + msg + "</h1></body></html>", mimeType).build(),
117                         context);
118 
119             } else if (!file.canRead()) {
120                 final String msg = "Cannot read file " + file.getPath();
121                 println(msg);
122                 responseTrigger.submitResponse(
123                         AsyncResponseBuilder.create(HttpStatus.SC_FORBIDDEN).setEntity("<html><body><h1>" + msg + "</h1></body></html>", mimeType).build(),
124                         context);
125 
126             } else {
127 
128                 ContentType contentType;
129                 final String fileName = file.getName().toLowerCase(Locale.ROOT);
130 // The following causes a failure on Linux and macOS in HttpProviderTestCase:
131 // org.apache.commons.vfs2.FileSystemException: GET method failed for "http://localhost:37637/read-tests/file1.txt" range "10" with HTTP status 200.
132 //                at org.apache.commons.vfs2.provider.http.HttpRandomAccessContent.getDataInputStream(HttpRandomAccessContent.java:80)
133 //                if (fileName.endsWith(".txt")) {
134 //                    contentType = ContentType.TEXT_PLAIN;
135 //                } else if (fileName.endsWith(".html") || fileName.endsWith(".htm") || file.isDirectory()) {
136 //                    contentType = ContentType.TEXT_HTML;
137 //                } else if (fileName.endsWith(".xml")) {
138 //                    contentType = ContentType.TEXT_XML;
139 //                } else {
140 //                    contentType = ContentType.DEFAULT_BINARY;
141 //                }
142                 contentType = ContentType.TEXT_HTML;
143                 final HttpCoreContext coreContext = HttpCoreContext.adapt(context);
144                 final EndpointDetails endpoint = coreContext.getEndpointDetails();
145 
146                 println(endpoint + " | serving file " + file.getPath());
147 
148                 // @formatter:off
149                 responseTrigger.submitResponse(
150                     AsyncResponseBuilder.create(HttpStatus.SC_OK)
151                         .setEntity(file.isDirectory()
152                             ? AsyncEntityProducers.create(file.toString(), contentType)
153                             : AsyncEntityProducers.create(file, contentType))
154                         .addHeader(HttpHeaders.LAST_MODIFIED, DateUtils.formatDate(new Date(file.lastModified())))
155                     .build(), context);
156                 // @formatter:on
157             }
158         }
159 
160         @Override
161         public AsyncRequestConsumer<Message<HttpRequest, Void>> prepare(final HttpRequest request, final EntityDetails entityDetails, final HttpContext context)
162                 throws HttpException {
163             return new BasicRequestConsumer<>(entityDetails != null ? new NoopEntityConsumer() : null);
164         }
165 
166     }
167 
168     public static final boolean DEBUG = Boolean.getBoolean(NHttpFileServer.class.getSimpleName() + ".debug");
169 
170     public static void main(final String[] args) throws Exception {
171         if (args.length < 1) {
172             System.err.println("Please specify document root directory");
173             System.exit(1);
174         }
175         // Document root directory
176         final File docRoot = new File(args[0]);
177         int port = 8080;
178         if (args.length >= 2) {
179             port = Integer.parseInt(args[1]);
180         }
181         start(port, docRoot, 0).awaitTermination();
182     }
183 
184     static void println(final String msg) {
185         if (DEBUG) {
186             System.out.println(HttpDateGenerator.INSTANCE.getCurrentDate() + " | " + msg);
187         }
188     }
189 
190     public static NHttpFileServer start(final int port, final File docRoot, final long waitMillis) throws KeyManagementException, UnrecoverableKeyException,
191             NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, InterruptedException, ExecutionException {
192         return new NHttpFileServer(port, docRoot).start();
193     }
194 
195     private final File docRoot;
196 
197     private ListenerEndpoint listenerEndpoint;
198 
199     private final int port;
200 
201     private HttpAsyncServer server;
202 
203     private NHttpFileServer(final int port, final File docRoot) {
204         this.port = port;
205         this.docRoot = docRoot;
206     }
207 
208     private void awaitTermination() throws InterruptedException {
209         server.awaitShutdown(TimeValue.MAX_VALUE);
210     }
211 
212     public void close() {
213         if (server.getStatus() == IOReactorStatus.ACTIVE) {
214             final CloseMode closeMode = CloseMode.GRACEFUL;
215             println("HTTP server shutting down (closeMode=" + closeMode + ")...");
216             server.close(closeMode);
217             println("HTTP server shut down.");
218         }
219     }
220 
221     public int getPort() {
222         if (server == null) {
223             return port;
224         }
225         return ((InetSocketAddress) listenerEndpoint.getAddress()).getPort();
226     }
227 
228     public void shutdown(final long gracePeriod, final TimeUnit timeUnit) throws InterruptedException {
229         if (server != null) {
230             server.initiateShutdown();
231             server.awaitShutdown(TimeValue.of(gracePeriod, timeUnit));
232         }
233 
234     }
235 
236     private NHttpFileServer start() throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, CertificateException,
237             IOException, InterruptedException, ExecutionException {
238         final AsyncServerBootstrap bootstrap = AsyncServerBootstrap.bootstrap();
239         SSLContext sslContext = null;
240         if (port == 8443 || port == 443) {
241             // Initialize SSL context
242             final URL url = NHttpFileServer.class.getResource("/test.keystore");
243             if (url == null) {
244                 println("Keystore not found");
245                 System.exit(1);
246             }
247             println("Loading keystore " + url);
248             sslContext = SSLContexts.custom().loadKeyMaterial(url, "nopassword".toCharArray(), "nopassword".toCharArray()).build();
249             bootstrap.setTlsStrategy(new BasicServerTlsStrategy(sslContext, new FixedPortStrategy(port)));
250         }
251 
252         // @formatter:off
253         final IOReactorConfig config = IOReactorConfig.custom()
254                 .setSoTimeout(15, TimeUnit.SECONDS)
255                 .setTcpNoDelay(true)
256                 .build();
257         // @formatter:on
258 
259         server = bootstrap
260                 .setExceptionCallback(Exception::printStackTrace)
261                 .setIOReactorConfig(config)
262                 .register("*", new HttpFileHandler(docRoot)).create();
263 
264         Runtime.getRuntime().addShutdownHook(new Thread(this::close));
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 }