001/*
002 *
003 * Copyright (c) 2015 The Regents of the University of California.
004 * All rights reserved.
005 *
006 * '$Author: crawl $'
007 * '$Date: 2017-08-23 22:42:39 -0700 (Wed, 23 Aug 2017) $' 
008 * '$Revision: 1375 $'
009 * 
010 * Permission is hereby granted, without written agreement and without
011 * license or royalty fees, to use, copy, modify, and distribute this
012 * software and its documentation for any purpose, provided that the above
013 * copyright notice and the following two paragraphs appear in all copies
014 * of this software.
015 *
016 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
017 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
018 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
019 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
020 * SUCH DAMAGE.
021 *
022 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
023 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
024 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
025 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
026 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
027 * ENHANCEMENTS, OR MODIFICATIONS.
028 *
029 */
030package org.kepler.webview.server.handler;
031
032import java.io.File;
033import java.net.HttpURLConnection;
034import java.text.SimpleDateFormat;
035import java.util.Collections;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Set;
039
040import org.kepler.webview.server.WebViewConfiguration;
041import org.kepler.webview.server.WebViewServer;
042
043import io.vertx.core.file.FileProps;
044import io.vertx.core.file.FileSystem;
045import io.vertx.core.http.HttpServerRequest;
046import io.vertx.core.json.JsonArray;
047import io.vertx.core.json.JsonObject;
048import io.vertx.ext.web.RoutingContext;
049
050public class NoMatchHandler extends BaseHandler {
051    
052    public NoMatchHandler(WebViewServer server) {
053        super(server);
054        
055        _allowWorkflowDownloads = WebViewConfiguration.getHttpServerAllowWorkflowDownloads();
056        
057        _dirsToIndex = WebViewConfiguration.getHttpServerDirectoriesToIndex();
058        
059        for(String dir: new HashSet<String>(_dirsToIndex)) {
060            if(!dir.startsWith(File.separator)) {
061                _dirsToIndex.remove(dir);
062                System.err.println("WARNING: directory to index will be ignored since it is not an absolute path: " + dir);
063            }
064        }
065        
066        _fileSystem = WebViewServer.vertx().fileSystem();
067        
068        _rootDir = WebViewConfiguration.getHttpServerRootDir();
069        if(_rootDir == null) {
070            _rootDir = "";
071        }
072
073    }
074
075    @Override
076    public void handle(RoutingContext context) {
077        
078        long timestamp = System.currentTimeMillis();
079        
080        HttpServerRequest req = context.request();
081        String normalizedPath = context.normalisedPath();
082        
083        String path = WebViewServer.findFile(normalizedPath);
084        boolean isDir = false;
085        if(path != null) {
086            isDir = new File(path).isDirectory();
087            if(!isDir && (_allowWorkflowDownloads || !path.endsWith(".kar"))) {
088                context.response().sendFile(path);
089                _server.log(req, context.user(), HttpURLConnection.HTTP_OK, timestamp, new File(path).length());
090                return;
091            }
092        }
093        
094        int error;
095        String message;
096        if(isDir) {
097            File indexFile = new File(path, "index.html");
098            if(indexFile.exists()) {
099                context.response().sendFile(indexFile.getAbsolutePath());
100                _server.log(req, context.user(), HttpURLConnection.HTTP_OK, timestamp, new File(path).length());
101                return;
102            } else {
103                // remove trailing / from path
104                while(path.endsWith("/")) {
105                    path.substring(0, path.length()-1);
106                }
107                for(String dir : _dirsToIndex) {
108                    if(path.substring(_rootDir.length() + 1).startsWith(dir)) {
109                        _sendDirectoryIndex(context, path, timestamp);
110                        return;
111                    }
112                }
113            }
114            System.err.println("Unhandled http request (directory) for: " + normalizedPath);
115            error = HttpURLConnection.HTTP_FORBIDDEN;
116            message = "File " + normalizedPath + ": permission denied.";                
117        } else if(path != null && path.endsWith(".kar") && !_allowWorkflowDownloads) {
118            System.err.println("Unhandled http request (workflow permission denied) for: " + normalizedPath);
119            error = HttpURLConnection.HTTP_FORBIDDEN;            
120            message = "File " + normalizedPath + ": permission denied.";
121        } else {
122            System.err.println("Unhandled http request (file not found) for: " + normalizedPath);
123            error = HttpURLConnection.HTTP_NOT_FOUND;
124            message = "File " + normalizedPath + " not found.";
125        }
126        
127        context.response().headers().set("Content-Type", "text/html");
128        // NOTE: always return not found since we do not want to disclose
129        // the presence of directories.
130        context.response().setChunked(true)
131            .write("<html>\n<body>\n<h2>" + message + "</h2>\n</body>\n</html>")
132            .setStatusCode(HttpURLConnection.HTTP_NOT_FOUND)
133            .end();
134        _server.log(req, context.user(), error, timestamp);            
135    }
136
137    /** Send a directory index.
138     * @param context The routing context
139     * @param path The directory index to send
140     * @param timestamp The request timestamp 
141     */
142    private void _sendDirectoryIndex(RoutingContext context, String path, long timestamp) {
143        HttpServerRequest req = context.request();
144        String normalizedPath = context.normalisedPath();
145        
146        String accept = req.headers().get("accept");
147        if(accept.contains("application/json") || accept.contains("text/javascript")) {
148            _fileSystem.readDir(path, readResult -> {
149                if(readResult.failed()) {
150                    System.err.println("Error reading directory " + path + ": " + readResult.cause());
151                    _sendResponseWithError(req, readResult.cause().toString());
152                    _server.log(req, context.user(), HttpURLConnection.HTTP_INTERNAL_ERROR, timestamp, new File(path).length());
153                    return;
154                }
155                
156                JsonArray array = new JsonArray();
157                for(String file: readResult.result()) {
158                    JsonObject json = new JsonObject()
159                        .put("name", file.substring(_rootDir.length() + 1));
160                    FileProps props = _fileSystem.lpropsBlocking(file);
161                    if(!props.isDirectory()) {
162                        json.put("lastModified", props.lastModifiedTime());
163                    }
164                    array.add(json);
165                }
166                
167                _sendResponseWithSuccessJson(req, new JsonObject().put(normalizedPath.substring(1), array));
168                _server.log(req, context.user(), HttpURLConnection.HTTP_OK, timestamp, new File(path).length());
169            });
170        } else {
171            _fileSystem.readDir(path, readResult -> {
172                if(readResult.failed()) {
173                    System.err.println("Error reading directory " + path + ": " + readResult.cause());
174                    context.response()
175                        .putHeader("Content-Type", "text/html")
176                        .write("<html>\n<body>\n<h2>Error reading directory.</h2>\n</body>\n</html>")
177                        .setStatusCode(HttpURLConnection.HTTP_INTERNAL_ERROR)
178                        .end();
179                    _server.log(req, context.user(), HttpURLConnection.HTTP_INTERNAL_ERROR, timestamp, new File(path).length());
180                    return;
181                }
182                StringBuilder buf = new StringBuilder();
183                buf.append("<meta content='text/html;charset=utf-8' http-equiv='Content-Type'>");
184                buf.append("<html><body><h2>Index of ");
185                buf.append(normalizedPath);
186                buf.append("</h2><ul>");
187                
188                List<String> files = readResult.result();
189                Collections.sort(files);
190                
191                for(String file: files) {
192                    String f = file.substring(_rootDir.length() + 1 + normalizedPath.length());
193                    buf.append("<li><a href='");
194                    buf.append(normalizedPath);
195                    buf.append("/");
196                    buf.append(f);
197                    buf.append("'>");
198                    buf.append(f);
199                    FileProps props = _fileSystem.propsBlocking(file);
200                    if(props.isDirectory()) {
201                        buf.append("/");
202                    } else {
203                        buf.append(", ");
204                        buf.append(_timestampFormat.format(props.lastModifiedTime()));
205                    }
206                    buf.append("</a></li>");
207                }
208
209                buf.append("<li><a href='");
210                buf.append(normalizedPath.substring(0, normalizedPath.lastIndexOf('/')));
211                buf.append("'>Parent directory");
212                buf.append("</a></li>");
213                
214                buf.append("</ul></body></html>");
215                _sendResponseWithSuccessText(req, "text/html", buf.toString());
216                _server.log(req, context.user(), HttpURLConnection.HTTP_OK, timestamp, new File(path).length());
217             });            
218        }
219        
220    }
221    
222    /** If true, allow downloads for workflow files. */
223    private boolean _allowWorkflowDownloads;
224    
225    /** Set of directories that can be indexed. */
226    private Set<String> _dirsToIndex;    
227    
228    /** Vertx file system. */
229    private FileSystem _fileSystem;
230    
231    /** Root directory of http server. */
232    private String _rootDir;
233    
234    /** Timestamp format for last modified times. */
235    private SimpleDateFormat _timestampFormat = new SimpleDateFormat("HH:mm:ss z yyyy.MM.dd");
236}