summaryrefslogtreecommitdiff
path: root/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java
diff options
context:
space:
mode:
authorRobert Gemmell <robbie@apache.org>2015-06-25 10:22:51 +0000
committerRobert Gemmell <robbie@apache.org>2015-06-25 10:22:51 +0000
commit32ae758bc2e8fd962b66a4ab6341b14009f1907e (patch)
tree2f4d8174813284a6ea58bb6b7f6520aa92287476 /qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java
parent116d91ad7825a98af36a869fc751206fbce0c59f (diff)
parentf7e896076143de4572b4f1f67ef0765125f2498d (diff)
downloadqpid-python-32ae758bc2e8fd962b66a4ab6341b14009f1907e.tar.gz
NO-JIRA: create branch for qpid-cpp 0.34 RC process
git-svn-id: https://svn.apache.org/repos/asf/qpid/branches/qpid-cpp-0.34-rc@1687469 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java')
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java369
1 files changed, 369 insertions, 0 deletions
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java
new file mode 100644
index 0000000000..2cbb1140b1
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java
@@ -0,0 +1,369 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.restapi;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_PARTIAL;
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+
+/**
+ * FileServer is a fairly simple HTTP File Server that can be used to serve files from the root directory specified
+ * during construction.
+ * <p>
+ * Although this is a relatively simple File Server it is still able to serve large files as it uses streaming, in
+ * addition it uses the HTTP Range/Content-Range/Content-Length Headers to allow resuming of partial downloads
+ * from clients that support it.
+ *
+ * @author Fraser Adams
+ */
+public class FileServer implements Server
+{
+ /**
+ * HashMap mapping file extension to MIME type for common types.
+ * TODO make this set of mappings configurable via properties or similar mechanism.
+ */
+ private static Map<String, String> _mimeTypes = new HashMap<String, String>();
+ static
+ {
+ _mimeTypes.put("htm", "text/html");
+ _mimeTypes.put("html", "text/html");
+ _mimeTypes.put("txt", "text/plain");
+ _mimeTypes.put("asc", "text/plain");
+ _mimeTypes.put("xml", "text/xml");
+ _mimeTypes.put("css", "text/css");
+ _mimeTypes.put("htc", "text/x-component");
+ _mimeTypes.put("gif", "image/gif");
+ _mimeTypes.put("jpg", "image/jpeg");
+ _mimeTypes.put("jpeg", "image/jpeg");
+ _mimeTypes.put("png", "image/png");
+ _mimeTypes.put("ico", "image/x-icon");
+ _mimeTypes.put("mp3", "audio/mpeg");
+ _mimeTypes.put("m3u", "audio/mpeg-url");
+ _mimeTypes.put("js", "application/x-javascript");
+ _mimeTypes.put("pdf", "application/pdf");
+ _mimeTypes.put("doc", "application/msword");
+ _mimeTypes.put("ppt", "application/mspowerpoint");
+ _mimeTypes.put("xls", "application/excel");
+ _mimeTypes.put("ogg", "application/x-ogg");
+ _mimeTypes.put("zip", "application/octet-stream");
+ _mimeTypes.put("exe", "application/octet-stream");
+ _mimeTypes.put("class", "application/octet-stream");
+ }
+
+ private final File _home;
+ private final boolean _allowDirectoryListing;
+
+ /**
+ * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'.
+ *
+ * @param uri the uri to be encoded.
+ * @return the encoded uri as a String.
+ */
+ private String encodeUri(final String uri)
+ {
+ StringBuilder encodedUri = new StringBuilder();
+ StringTokenizer st = new StringTokenizer(uri, "/ ", true);
+ while (st.hasMoreTokens())
+ {
+ String tok = st.nextToken();
+ if (tok.equals("/"))
+ {
+ encodedUri.append("/");
+ }
+ else if (tok.equals(" "))
+ {
+ encodedUri.append("%20");
+ }
+ else
+ {
+ try
+ {
+ encodedUri.append(URLEncoder.encode(tok, "UTF-8"));
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ }
+ }
+ }
+ return encodedUri.toString();
+ }
+
+ /**
+ * Renders a number in a more "human readable" format providing a bytes/KB/MB/GB format depending on the size.
+ *
+ * @param number the number to be rendered.
+ * @return a String representation of the number in a more human readable form.
+ */
+ private String renderNumber(float number)
+ {
+ if (number < 1000)
+ {
+ return String.format("%.0f", number) + " bytes";
+ }
+ else if (number < 1000000)
+ {
+ number /= 1000.0f;
+ return String.format("%.1f", number) + " KB";
+ }
+ else if (number < 1000000000)
+ {
+ number /= 1000000.0f;
+ return String.format("%.1f", number) + " MB";
+ }
+ else
+ {
+ number /= 1000000000.0f;
+ return String.format("%.1f", number) + " GB";
+ }
+ }
+
+ /**
+ * Construct an instance of FileServer.
+ *
+ * @param home the path name of the root directory that we wish this FieServer to serve via HTTP.
+ * @param allowDirectoryListing a flag that if set will serve a directory listing to the client and thus enable
+ * browsing to sub-directories of the home directory. N.B. protection has been put in place to mitigate
+ * against the possibility of serving directories that may be parents of the root directory.
+ */
+ public FileServer(final String home, final boolean allowDirectoryListing)
+ {
+ _home = new File(home);
+ _allowDirectoryListing = allowDirectoryListing;
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a GET request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doGet(final HttpTransaction tx) throws IOException
+ {
+ String user = tx.getPrincipal() != null ? tx.getPrincipal() : "none";
+ String path = tx.getRequestURI();
+
+ //System.out.println();
+ //System.out.println("FileServer doGet " + path + ", user: " + user);
+ //System.out.println("thread = " + Thread.currentThread().getId());
+ //tx.logRequest();
+
+ // If the _home filesystem that we use as a root to serve files from is not a directory then
+ // we sent an error response and return.
+ if (!_home.isDirectory())
+ {
+ tx.sendResponse(HTTP_INTERNAL_ERROR, "text/plain",
+ "500 Internal Server Error: given document root is not a directory.");
+ return;
+ }
+
+ //String path = tx.getRequestURI();
+
+ // Prohibit getting out of _home directory
+ if (path.startsWith("..") || path.endsWith("..") || path.indexOf("../") >= 0)
+ {
+ tx.sendResponse(HTTP_FORBIDDEN, "text/plain", "403 Forbidden: Won't serve ../ for security reasons.");
+ return;
+ }
+
+ File file = new File(_home, path);
+ if (!file.exists())
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found: File " + path + " not found.");
+ return;
+ }
+
+ // List the directory, if necessary
+ if (file.isDirectory())
+ {
+ File directory = file;
+ // Browsers get confused without '/' after the directory, send a redirect.
+ if (!path.endsWith("/"))
+ {
+ path += "/";
+ tx.setHeader("Location", path);
+ tx.sendResponse(HTTP_MOVED_PERM, "text/html",
+ "<html><body>Redirected: <a href=\"" + path + "\">" + path + "</a></body></html>");
+ return;
+ }
+
+ // First try index.html and index.htm
+ if (new File(directory, "index.html").exists())
+ {
+ file = new File(_home, path + "/index.html");
+ }
+ else if (new File(directory, "index.htm").exists())
+ {
+ file = new File(_home, path + "/index.htm");
+ }
+ else if (_allowDirectoryListing)
+ { // No index file, list the directory
+ StringBuilder response = new StringBuilder("<html><body><h1>Directory " + path + "</h1><br/>");
+
+ if (path.length() > 1)
+ {
+ String u = path.substring(0, path.length() - 1);
+ int slash = u.lastIndexOf('/');
+ if (slash >= 0 && slash < u.length())
+ {
+ response.append("<b><a href=\"" + path.substring(0, slash + 1) + "\">..</a></b><br/>");
+ }
+ }
+
+ String[] files = directory.list();
+ for (String name : files)
+ {
+ File current = new File(directory, name);
+ boolean isDir = current.isDirectory();
+ boolean isFile = current.isFile();
+ if (isDir)
+ {
+ response.append("<b>");
+ name += "/";
+ }
+
+ response.append("<a href=\"" + encodeUri(path + name) + "\">" + name + "</a>");
+
+ if (isFile)
+ { // If it's a file show the file size
+ response.append(" &nbsp;<font size=2>(" + renderNumber(current.length()) + ")</font>");
+ }
+ response.append("<br/>");
+ if (isDir)
+ {
+ response.append("</b>");
+ }
+ }
+ tx.sendResponse(HTTP_OK, "text/html", response.toString());
+ return;
+ }
+ else
+ {
+ tx.sendResponse(HTTP_FORBIDDEN, "text/plain", "403 Forbidden: No directory listing.");
+ return;
+ }
+ }
+
+ try
+ {
+ // Get MIME type from file name extension, if possible
+ String fileName = file.getCanonicalPath();
+ String mime = null;
+ int dot = fileName.lastIndexOf('.');
+ if (dot >= 0)
+ {
+ String fileExtension = fileName.substring(dot + 1).toLowerCase();
+ mime = _mimeTypes.get(fileExtension);
+ }
+
+ if (mime == null)
+ {
+ mime = "application/octet-stream";
+ }
+
+ // Use Range header allow download resuming.
+ long startFrom = 0;
+ long length = file.length();
+
+ String range = tx.getHeader("Range");
+ if (range != null)
+ {
+ if (range.startsWith("bytes="))
+ {
+ range = range.substring("bytes=".length());
+
+ int minus = range.indexOf('-');
+ if (minus > 0)
+ {
+ range = range.substring(0, minus);
+ }
+
+ try
+ {
+ startFrom = Long.parseLong(range);
+ }
+ catch (NumberFormatException nfe)
+ {
+ }
+ }
+ }
+
+ FileInputStream is = new FileInputStream(file);
+ is.skip(startFrom);
+
+ int status = (startFrom == 0) ? HTTP_OK : HTTP_PARTIAL;
+ tx.setHeader("Content-Length", "" + (length - startFrom));
+ tx.setHeader("Content-Range", "" + startFrom + "-" + (length - 1) + "/" + length);
+
+ tx.sendResponse(status, mime, is);
+ }
+ catch (IOException ioe)
+ {
+ tx.sendResponse(HTTP_FORBIDDEN, "text/plain", "403 Forbidden: Reading file failed.");
+ }
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a POST request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doPost(final HttpTransaction tx) throws IOException
+ {
+ tx.sendResponse(HTTP_BAD_METHOD, "text/plain", "405 Bad Method.");
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a PUT request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doPut(final HttpTransaction tx) throws IOException
+ {
+ tx.sendResponse(HTTP_BAD_METHOD, "text/plain", "405 Bad Method.");
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a DELETE request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doDelete(final HttpTransaction tx) throws IOException
+ {
+ tx.sendResponse(HTTP_BAD_METHOD, "text/plain", "405 Bad Method.");
+ }
+}
+