001/* 002 * #%L 003 * Netarchivesuite - common 004 * %% 005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023package dk.netarkivet.common.distribute; 024 025import java.io.File; 026import java.io.IOException; 027import java.net.MalformedURLException; 028import java.net.URL; 029import java.net.URLConnection; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.Map; 033import java.util.Random; 034 035import javax.servlet.ServletException; 036import javax.servlet.http.HttpServletRequest; 037import javax.servlet.http.HttpServletResponse; 038 039import org.eclipse.jetty.server.Request; 040import org.eclipse.jetty.server.Server; 041import org.eclipse.jetty.server.ServerConnector; 042import org.eclipse.jetty.server.handler.AbstractHandler; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046import dk.netarkivet.common.exceptions.ArgumentNotValid; 047import dk.netarkivet.common.exceptions.IOFailure; 048import dk.netarkivet.common.utils.CleanupHook; 049import dk.netarkivet.common.utils.CleanupIF; 050import dk.netarkivet.common.utils.FileUtils; 051import dk.netarkivet.common.utils.Settings; 052import dk.netarkivet.common.utils.SystemUtils; 053 054/** 055 * This is a registry for HTTP remote file, meant for serving registered files to remote hosts. The embedded webserver 056 * handling remote files for HTTPRemoteFile point-to-point communication. Optimised to use direct transfer on local 057 * machine. 058 */ 059public class HTTPRemoteFileRegistry implements CleanupIF { 060 061 /** Logger for this class. */ 062 private static final Logger log = LoggerFactory.getLogger(HTTPRemoteFileRegistry.class); 063 064 /** The unique instance. */ 065 protected static HTTPRemoteFileRegistry instance; 066 067 /** Protocol for URLs. */ 068 private static final String PROTOCOL = "http"; 069 070 /** Port number for generating URLs. Read from settings. */ 071 protected final int port; 072 073 /** Name of this host. Used for generating URLs. */ 074 private final String localHostName; 075 076 /** Files to serve. */ 077 private final Map<URL, FileInfo> registeredFiles; 078 079 /** Instance to create random URLs. */ 080 private final Random random; 081 082 /** 083 * Postfix to add to an URL to get cleanup URL. We are not using query string, because it behaves strangely in 084 * HttpServletRequest. 085 */ 086 private static final String UNREGISTER_URL_POSTFIX = "/unregister"; 087 088 /** The embedded webserver. */ 089 protected Server server; 090 /** The shutdown hook. */ 091 private CleanupHook cleanupHook; 092 093 /** 094 * Initialise the registry. This includes registering an HTTP server for getting the files from this machine. 095 * 096 * @throws IOFailure if it cannot be initialised. 097 */ 098 protected HTTPRemoteFileRegistry() { 099 port = Settings.getInt(HTTPRemoteFile.HTTPREMOTEFILE_PORT_NUMBER); 100 localHostName = SystemUtils.getLocalHostName(); 101 registeredFiles = Collections.synchronizedMap(new HashMap<URL, FileInfo>()); 102 random = new Random(); 103 startServer(); 104 cleanupHook = new CleanupHook(this); 105 Runtime.getRuntime().addShutdownHook(cleanupHook); 106 } 107 108 /** 109 * Start the server, including a handler that responds with registered files, removes registered files on request, 110 * and gives 404 otherwise. 111 * 112 * @throws IOFailure if it cannot be initialised. 113 */ 114 protected void startServer() { 115 server = new Server(); 116 ServerConnector connector = new ServerConnector(server); 117 connector.setPort(port); 118 server.addConnector(connector); 119 server.setHandler(new HTTPRemoteFileRegistryHandler()); 120 try { 121 server.start(); 122 } catch (Exception e) { 123 throw new IOFailure("Cannot start HTTPRemoteFile registry on port " + port, e); 124 } 125 } 126 127 /** 128 * Get the protocol part of URLs, that is HTTP. 129 * 130 * @return "http", the protocol. 131 */ 132 protected String getProtocol() { 133 return PROTOCOL; 134 } 135 136 /** 137 * Get the unique instance. 138 * 139 * @return The unique instance. 140 */ 141 public static synchronized HTTPRemoteFileRegistry getInstance() { 142 if (instance == null) { 143 instance = new HTTPRemoteFileRegistry(); 144 } 145 return instance; 146 } 147 148 /** 149 * Register a file for serving to an endpoint. 150 * 151 * @param file The file to register. 152 * @param deletable Whether it should be deleted on cleanup. 153 * @return The URL it will be served as. It will be uniquely generated. 154 * @throws ArgumentNotValid on null or unreadable file. 155 * @throws IOFailure on any trouble registerring the file 156 */ 157 public URL registerFile(File file, boolean deletable) { 158 ArgumentNotValid.checkNotNull(file, "File file"); 159 if (!file.isFile() && file.canRead()) { 160 throw new ArgumentNotValid("File '" + file + "' is not a readable file"); 161 } 162 String path; 163 URL url; 164 // ensure we get a random and unique URL. 165 do { 166 path = "/" + Integer.toHexString(random.nextInt()); 167 try { 168 url = new URL(getProtocol(), localHostName, port, path); 169 } catch (MalformedURLException e) { 170 throw new IOFailure("Unable to create URL for file '" + file + "'." + " '" + getProtocol() + "', '" 171 + localHostName + "', '" + port + "', '" + path + "''", e); 172 } 173 } while (registeredFiles.containsKey(url)); 174 registeredFiles.put(url, new FileInfo(file, deletable)); 175 log.debug("Registered file '{}' with URL '{}'", file.getPath(), url); 176 return url; 177 } 178 179 /** 180 * Get the url for cleaning up after a remote file registered under some URL. 181 * 182 * @param url some URL 183 * @return the cleanup url. 184 * @throws MalformedURLException If unable to construct the cleanup url 185 */ 186 URL getCleanupUrl(URL url) throws MalformedURLException { 187 return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath() + UNREGISTER_URL_POSTFIX); 188 } 189 190 /** 191 * Open a connection to an URL in a registry. 192 * 193 * @param url The URL to open connection to. 194 * @return a connection to an URL in a registry 195 * @throws IOException If unable to open the connection. 196 */ 197 protected URLConnection openConnection(URL url) throws IOException { 198 return url.openConnection(); 199 } 200 201 /** Pair of information registered. */ 202 private class FileInfo { 203 /** The file. */ 204 final File file; 205 /** Whether it should be deleted on cleanup. */ 206 final boolean deletable; 207 208 /** 209 * Initialise pair. 210 * 211 * @param file The file. 212 * @param deletable Whether it should be deleted on cleanup. 213 */ 214 FileInfo(File file, boolean deletable) { 215 this.file = file; 216 this.deletable = deletable; 217 } 218 } 219 220 /** Stops the server and nulls the instance. */ 221 public void cleanup() { 222 synchronized (HTTPRemoteFileRegistry.class) { 223 try { 224 server.stop(); 225 } catch (Exception e) { 226 log.warn("Unable to stop HTTPRemoteFile registry"); 227 } 228 try { 229 Runtime.getRuntime().removeShutdownHook(cleanupHook); 230 } catch (Exception e) { 231 // ignore 232 } 233 instance = null; 234 } 235 } 236 237 /** 238 * A handler for the registry. 239 * <p> 240 * It has three ways to behave: Serve registered files, return 404 on unknown files, and unregister registered 241 * files, depending on the URL. 242 */ 243 protected class HTTPRemoteFileRegistryHandler extends AbstractHandler { 244 /** 245 * A method for handling Jetty requests. 246 * 247 * @param string Unused domain. 248 * @param httpServletRequest request object. 249 * @param httpServletResponse the response to write to. 250 * @throws IOException On trouble in communication. 251 * @throws ServletException On servlet trouble. 252 * @see AbstractHandler#handle(String, org.eclipse.jetty.server.Request, HttpServletRequest, 253 * HttpServletResponse), HttpServletResponse, int) 254 */ 255 @Override 256 public void handle(String string, Request baseRequest, HttpServletRequest httpServletRequest, 257 HttpServletResponse httpServletResponse) throws IOException, ServletException { 258 // since this is a jetty handle method, we know it is a Jetty 259 // request object. 260 Request request = ((Request) httpServletRequest); 261 String urlString = httpServletRequest.getRequestURL().toString(); 262 if (urlString.endsWith(UNREGISTER_URL_POSTFIX)) { 263 URL url = new URL(urlString.substring(0, urlString.length() - UNREGISTER_URL_POSTFIX.length())); 264 FileInfo fileInfo = registeredFiles.remove(url); 265 if (fileInfo != null && fileInfo.deletable && fileInfo.file.exists()) { 266 FileUtils.remove(fileInfo.file); 267 log.debug("Removed file '{}' with URL '{}'", fileInfo.file.getPath(), url); 268 } 269 httpServletResponse.setStatus(200); 270 request.setHandled(true); 271 } else { 272 URL url = new URL(urlString); 273 FileInfo fileInfo = registeredFiles.get(url); 274 if (fileInfo != null) { 275 httpServletResponse.setStatus(200); 276 FileUtils.writeFileToStream(fileInfo.file, httpServletResponse.getOutputStream()); 277 request.setHandled(true); 278 log.debug("Served file '{}' with URL '{}'", fileInfo.file.getPath(), url); 279 } else { 280 httpServletResponse.sendError(404); 281 log.debug("File not found for URL '{}'", url); 282 } 283 } 284 } 285 } 286 287}