/******************************************************************************* * Copyright (c) 2011 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.orion.internal.server.hosting; import java.net.URI; import java.net.URISyntaxException; import java.util.ConcurrentModificationException; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.eclipse.orion.server.core.LogHelper; import org.eclipse.orion.server.core.metastore.UserInfo; /** * Provides a same-server implementation of ISiteHostingService. Maintains a table of * hosted sites for this purpose. This table is not persisted, so site launches are only * active until the server stops. */ public class SiteHostingService implements ISiteHostingService { private final SiteHostingConfig config; /** * Key: Host, in the form <code>hostname:port</code>.<br> * Value: The hosted site associated with the host.<p> * * Updates to this map occur serially and are done by {@link #start(SiteInfo, UserInfo, String)} * and {@link #stop(SiteInfo, UserInfo)}.<p> * * Reads may occur concurrently with updates and other reads, and are done by {@link #get(String)} * and {@link #get(SiteInfo, UserInfo)}. This should be OK since map operations on ConcurrentMap * are thread-safe. */ private ConcurrentMap<String, IHostedSite> sites; /** * Creates the site hosting service. * @param config The site hosting configuration */ public SiteHostingService(SiteHostingConfig config) { this.config = config; this.sites = new ConcurrentHashMap<String, IHostedSite>(); } /* * (non-Javadoc) * @see org.eclipse.orion.internal.server.servlets.hosting.ISiteHostingService#start(org.eclipse.orion.internal.server.servlets.site.SiteConfiguration, org.eclipse.orion.internal.server.servlets.workspace.WebUser, java.lang.String) */ @Override public void start(SiteInfo siteConfig, UserInfo user, String editServer, URI requestURI) throws SiteHostingException { synchronized (sites) { if (get(siteConfig, user) != null) { return; // Already started; nothing to do } String host = null; try { URI url = acquireURL(siteConfig.getHostHint(), requestURI); host = url.getHost(); IHostedSite result = sites.putIfAbsent(host, new HostedSite(siteConfig, user, host, editServer, url.toString())); if (result != null) { // Should never happen, since writes are done serially by start()/stop() throw new ConcurrentModificationException("Table was modified concurrently"); } } catch (Exception e) { if (host != null) sites.remove(host); throw new SiteHostingException(e.getMessage(), e); } } } /* * (non-Javadoc) * @see org.eclipse.orion.internal.server.servlets.hosting.ISiteHostingService#stop(org.eclipse.orion.internal.server.servlets.site.SiteConfiguration, org.eclipse.orion.internal.server.servlets.workspace.WebUser) */ @Override public void stop(SiteInfo siteConfig, UserInfo user) throws SiteHostingException { synchronized (sites) { IHostedSite site = get(siteConfig, user); if (site == null) { return; // Already stopped; nothing to do } if (!sites.remove(site.getHost(), site)) { throw new ConcurrentModificationException("Table was modified concurrently"); } } } /* * (non-Javadoc) * @see org.eclipse.orion.internal.server.servlets.hosting.ISiteHostingService#get(org.eclipse.orion.internal.server.servlets.site.SiteConfiguration, org.eclipse.orion.internal.server.servlets.workspace.WebUser) */ @Override public IHostedSite get(SiteInfo siteConfig, UserInfo user) { // Note this may overlap with a concurrent start()/stop() call that modifies the map String id = siteConfig.getId(); String userId = user.getUniqueId(); for (IHostedSite site : sites.values()) { if (site.getSiteConfigurationId().equals(id) && site.getUserId().equals(userId)) { return site; } } return null; } /* * (non-Javadoc) * @see org.eclipse.orion.internal.server.servlets.hosting.ISiteHostingService#isHosted(java.lang.String) */ @Override public boolean isHosted(String host) { // Note this may overlap with a concurrent start()/stop() call that modifies the map return get(host) != null; } /* * (non-Javadoc) * @see org.eclipse.orion.internal.server.servlets.hosting.ISiteHostingService#matchesVirtualHost(java.lang.String) */ @Override public boolean matchesVirtualHost(String host) { List<String> hosts = config.getHosts(); for (String h : hosts) { if (h.equals(host)) { return true; } else { // Request URI does not matter here since we only care about the HostPattern's host, not scheme/port. HostPattern pattern; try { pattern = getHostPattern(h, new URI("http", null, host, -1, null, null, null)); String configHost = pattern.getHost(); if (configHost != null && host.endsWith(configHost.replace("*", ""))) { //$NON-NLS-1$ //$NON-NLS-2$ return true; } } catch (URISyntaxException e) { // Should not happen } } } return false; } /** * @param host A host in the form <code>hostname:port</code>. * @return The hosted site running at <code>host</code>, or null if <code>host</code> * is not a running hosted site. */ public IHostedSite get(String host) { // Note this may overlap with a concurrent start()/stop() call that modifies the map return sites.get(host); } /** * Gets the next available URL where a site may be hosted. * * @param hint A hint to use for determining the hostname when subdomains are available. May be <code>null</code>. * @param requestURI The incoming request URI * @return The host, which will have the form <code>hostname:port</code>. * @throws NoMoreHostsException If no more hosts are available (meaning all IPs and domains * from the hosting configuration have been allocated). * @throws BadHostnameException If a host pattern or hint led to an invalid URL being generated. */ private URI acquireURL(String hint, URI requestURI) throws SiteHostingException { hint = hint == null || hint.equals("") ? "site" : hint; //$NON-NLS-1$ //$NON-NLS-2$ synchronized (sites) { URI result = null; for (String value : config.getHosts()) { try { HostPattern pattern = getHostPattern(value, requestURI); String host = pattern.getHost(); if (pattern.isWildcard()) { // It's a domain wildcard final String rest = "." + pattern.getWildcardDomain(); //$NON-NLS-1$ // Append digits if necessary to get a unique hostname String candidate = hint + rest; for (int i = 0; isHosted(candidate); i++) { candidate = hint + (Integer.toString(i)) + rest; } result = new URI(pattern.getScheme(), null, candidate, pattern.getPort(), null, null, null); break; } else { if (!isHosted(host)) { result = new URI(pattern.getScheme(), null, host, pattern.getPort(), null, null, null); break; } } } catch (URISyntaxException e) { // URI wasn't valid, either because a bad hint was provided or the server was configured with a bad HostPattern. LogHelper.log(e); if (isHintValid(hint)) throw new BadHostnameException("Invalid virtual host suffix was provided. Contact your administrator.", e); throw new BadHostnameException("Invalid host hint. Only URI hostname characters are permitted.", e); } } if (result == null) { throw new NoMoreHostsException("No more hosts available"); } return result; } } private boolean isHintValid(String hint) { try { new URI("http", null, hint, -1, null, null, null); return true; } catch (URISyntaxException e) { return false; } } /** * @param pattern * @param requestURI Used as a fallback to assign scheme and port when the pattern does not specify them. * @return */ private HostPattern getHostPattern(String pattern, URI requestURI) { String scheme = null; int port = -1; // Parse scheme if (pattern.startsWith("http://") || pattern.startsWith("https://")) { int schemeEnd = pattern.indexOf("://"); scheme = pattern.substring(0, schemeEnd); pattern = pattern.substring(schemeEnd + "://".length(), pattern.length()); if ("https".equals(scheme)) port = 443; else if ("http".equals(scheme)) port = 80; } // Parse host and port (if present) String hostPart = pattern; int pos; if ((pos = pattern.lastIndexOf(":")) != -1 && pos < pattern.length() - 1) { //$NON-NLS-1$ hostPart = pattern.substring(0, pos); try { port = Integer.parseInt(pattern.substring(pos + 1, pattern.length())); } catch (NumberFormatException e) { } } if (scheme == null) { scheme = requestURI.getScheme(); } if (port == -1) { port = requestURI.getPort(); } return new HostPattern(scheme, hostPart, defaultPort(scheme, port)); } private int defaultPort(String scheme, int port) { if (("https".equals(scheme) && port == 443) || ("http".equals(scheme) && port == 80)) return -1; return port; } /** * Represents a parsed entry from the orion.core.virtualHosts setting. */ private class HostPattern { private String scheme; private String host; private int port; private boolean isWildcard = false; private String wildcardDomain = null; public HostPattern(String scheme, String host, int port) { this.scheme = scheme; this.host = host; this.port = port; if (host != null) { int pos = host.lastIndexOf("*"); isWildcard = (pos >= 0 && pos < host.length() - 2 && host.charAt(pos + 1) == '.'); if (isWildcard) { wildcardDomain = host.substring(pos + 2, host.length()); } } } String getScheme() { return scheme; } String getHost() { return host; } String getWildcardDomain() { return wildcardDomain; } int getPort() { return port; } boolean isWildcard() { return isWildcard; } } }