/*
* Copyright 2015 LINE Corporation
*
* LINE Corporation 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 com.linecorp.armeria.server;
import static java.util.Objects.requireNonNull;
import java.net.IDN;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.base.Ascii;
import com.linecorp.armeria.common.Request;
import com.linecorp.armeria.common.Response;
import io.netty.handler.ssl.SslContext;
import io.netty.util.DomainNameMapping;
import io.netty.util.DomainNameMappingBuilder;
/**
* A <a href="https://en.wikipedia.org/wiki/Virtual_hosting#Name-based">name-based virtual host</a>.
* A {@link VirtualHost} contains the following information:
* <ul>
* <li>the hostname pattern, as defined in
* <a href="http://tools.ietf.org/html/rfc2818#section-3.1">the section 3.1 of RFC2818</a></li>
* <li>{@link SslContext} if TLS is enabled</li>
* <li>the list of available {@link Service}s and their {@link PathMapping}s</li>
* </ul>
*
* @see VirtualHostBuilder
*/
public final class VirtualHost {
private static final Pattern HOSTNAME_PATTERN = Pattern.compile(
"^(?:[-_a-zA-Z0-9]|[-_a-zA-Z0-9][-_.a-zA-Z0-9]*[-_a-zA-Z0-9])$");
/**
* Initialized later by {@link ServerConfig} via {@link #setServerConfig(ServerConfig)}.
*/
private ServerConfig serverConfig;
private final String defaultHostname;
private final String hostnamePattern;
private final SslContext sslContext;
private final List<ServiceConfig> services;
private final PathMappings<ServiceConfig> serviceMapping = new PathMappings<>();
private String strVal;
VirtualHost(String defaultHostname, String hostnamePattern,
SslContext sslContext, Iterable<ServiceConfig> serviceConfigs) {
defaultHostname = normalizeDefaultHostname(defaultHostname);
hostnamePattern = normalizeHostnamePattern(hostnamePattern);
ensureHostnamePatternMatchesDefaultHostname(hostnamePattern, defaultHostname);
this.defaultHostname = defaultHostname;
this.hostnamePattern = hostnamePattern;
this.sslContext = validateSslContext(sslContext);
requireNonNull(serviceConfigs, "serviceConfigs");
final List<ServiceConfig> servicesCopy = new ArrayList<>();
for (ServiceConfig c : serviceConfigs) {
c = c.build(this);
servicesCopy.add(c);
serviceMapping.add(c.pathMapping(), c);
}
services = Collections.unmodifiableList(servicesCopy);
serviceMapping.freeze();
}
/**
* IDNA ASCII conversion, case normalization and validation.
*/
static String normalizeDefaultHostname(String defaultHostname) {
requireNonNull(defaultHostname, "defaultHostname");
if (needsNormalization(defaultHostname)) {
defaultHostname = IDN.toASCII(defaultHostname, IDN.ALLOW_UNASSIGNED);
}
if (!HOSTNAME_PATTERN.matcher(defaultHostname).matches()) {
throw new IllegalArgumentException("defaultHostname: " + defaultHostname);
}
return Ascii.toLowerCase(defaultHostname);
}
/**
* IDNA ASCII conversion, case normalization and validation.
*/
static String normalizeHostnamePattern(String hostnamePattern) {
requireNonNull(hostnamePattern, "hostnamePattern");
if (needsNormalization(hostnamePattern)) {
hostnamePattern = IDN.toASCII(hostnamePattern, IDN.ALLOW_UNASSIGNED);
}
if (!"*".equals(hostnamePattern) &&
!HOSTNAME_PATTERN.matcher(hostnamePattern.startsWith("*.") ? hostnamePattern.substring(2)
: hostnamePattern).matches()) {
throw new IllegalArgumentException("hostnamePattern: " + hostnamePattern);
}
return Ascii.toLowerCase(hostnamePattern);
}
/**
* Ensure that 'hostnamePattern' matches 'defaultHostname'.
*/
static void ensureHostnamePatternMatchesDefaultHostname(String hostnamePattern, String defaultHostname) {
if ("*".equals(hostnamePattern)) {
return;
}
// Pretty convoluted way to validate but it's done only once and
// we don't need to duplicate the pattern matching logic.
final DomainNameMapping<Boolean> mapping =
new DomainNameMappingBuilder<>(Boolean.FALSE).add(hostnamePattern, Boolean.TRUE).build();
if (!mapping.map(defaultHostname)) {
throw new IllegalArgumentException(
"defaultHostname: " + defaultHostname +
" (must be matched by hostnamePattern: " + hostnamePattern + ')');
}
}
private static boolean needsNormalization(String hostnamePattern) {
final int length = hostnamePattern.length();
for (int i = 0; i < length; i++) {
int c = hostnamePattern.charAt(i);
if (c > 0x7F) {
return true;
}
}
return false;
}
static SslContext validateSslContext(SslContext sslContext) {
if (sslContext != null && !sslContext.isServer()) {
throw new IllegalArgumentException("sslContext: " + sslContext + " (expected: server context)");
}
return sslContext;
}
/**
* Returns the {@link Server} where this {@link VirtualHost} belongs to.
*/
public Server server() {
if (serverConfig == null) {
throw new IllegalStateException("server is not configured yet.");
}
return serverConfig.server();
}
void setServerConfig(ServerConfig serverConfig) {
if (this.serverConfig != null) {
throw new IllegalStateException("VirtualHost cannot be added to more than one Server.");
}
this.serverConfig = requireNonNull(serverConfig, "serverConfig");
}
/**
* Returns the default hostname of this virtual host.
*/
public String defaultHostname() {
return defaultHostname;
}
/**
* Returns the hostname pattern of this virtual host, as defined in
* <a href="http://tools.ietf.org/html/rfc2818#section-3.1">the section 3.1 of RFC2818</a>
*/
public String hostnamePattern() {
return hostnamePattern;
}
/**
* Returns the {@link SslContext} of this virtual host.
*/
public SslContext sslContext() {
return sslContext;
}
/**
* Returns the information about the {@link Service}s bound to this virtual host.
*/
public List<ServiceConfig> serviceConfigs() {
return services;
}
/**
* Finds the {@link Service} whose {@link PathMapping} matches the {@code path}.
*
* @return the {@link Service} wrapped by {@link PathMapped} if there's a match.
* {@link PathMapped#empty()} if there's no match.
*/
public PathMapped<ServiceConfig> findServiceConfig(String path) {
return serviceMapping.apply(path);
}
VirtualHost decorate(@Nullable Function<Service<Request, Response>, Service<Request, Response>> decorator) {
if (decorator == null) {
return this;
}
final List<ServiceConfig> services =
this.services.stream().map(cfg -> {
final PathMapping pathMapping = cfg.pathMapping();
final Service<Request, Response> service = decorator.apply(cfg.service());
final String loggerName = cfg.loggerName().orElse(null);
return new ServiceConfig(pathMapping, service, loggerName);
}).collect(Collectors.toList());
return new VirtualHost(defaultHostname(), hostnamePattern(), sslContext(), services);
}
@Override
public String toString() {
String strVal = this.strVal;
if (strVal == null) {
this.strVal = strVal = toString(
getClass(), defaultHostname(), hostnamePattern(), sslContext(), serviceConfigs());
}
return strVal;
}
static String toString(Class<?> type, String defaultHostname, String hostnamePattern,
SslContext sslContext, List<?> services) {
StringBuilder buf = new StringBuilder();
if (type != null) {
buf.append(type.getSimpleName());
}
buf.append('(');
buf.append(defaultHostname);
buf.append('/');
buf.append(hostnamePattern);
buf.append(", ssl: ");
buf.append(sslContext != null);
buf.append(", services: ");
buf.append(services);
buf.append(')');
return buf.toString();
}
}