All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.linecorp.armeria.server.VirtualHost Maven / Gradle / Ivy

Go to download

Asynchronous HTTP/2 RPC/REST client/server library built on top of Java 8, Netty, Thrift and GRPC (armeria-shaded)

There is a newer version: 0.75.0
Show newest version
/*
 * 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:
 *
 *   https://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.annotations.VisibleForTesting;
import com.google.common.base.Ascii;

import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.MediaTypeSet;
import com.linecorp.armeria.common.metric.MeterIdPrefix;

import io.micrometer.core.instrument.MeterRegistry;
import io.netty.handler.ssl.SslContext;
import io.netty.util.DomainNameMapping;
import io.netty.util.DomainNameMappingBuilder;

/**
 * A name-based virtual host.
 * A {@link VirtualHost} contains the following information:
 * 
    *
  • the hostname pattern, as defined in * the section 3.1 of RFC2818
  • *
  • {@link SslContext} if TLS is enabled
  • *
  • the list of available {@link Service}s and their {@link PathMapping}s
  • *
* * @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)}. */ @Nullable private ServerConfig serverConfig; private final String defaultHostname; private final String hostnamePattern; @Nullable private final SslContext sslContext; private final List services; private final Router router; private final MediaTypeSet producibleMediaTypes; @Nullable private String strVal; /** * Use this constructor when you are sure that the {@link ServiceConfig}s have no duplicate * {@link PathMapping}s or it's OK to have them. This is useful when you create a new {@link VirtualHost} * from an existing {@link VirtualHost}, because its {@link ServiceConfig}s were validated already. */ VirtualHost(String defaultHostname, String hostnamePattern, @Nullable SslContext sslContext, Iterable serviceConfigs, MediaTypeSet producibleMediaTypes) { this(defaultHostname, hostnamePattern, sslContext, serviceConfigs, producibleMediaTypes, (virtualHost, mapping, existingMapping) -> {}); } VirtualHost(String defaultHostname, String hostnamePattern, @Nullable SslContext sslContext, Iterable serviceConfigs, MediaTypeSet producibleMediaTypes, RejectedPathMappingHandler rejectionHandler) { defaultHostname = normalizeDefaultHostname(defaultHostname); hostnamePattern = normalizeHostnamePattern(hostnamePattern); ensureHostnamePatternMatchesDefaultHostname(hostnamePattern, defaultHostname); this.defaultHostname = defaultHostname; this.hostnamePattern = hostnamePattern; this.sslContext = validateSslContext(sslContext); this.producibleMediaTypes = producibleMediaTypes; requireNonNull(serviceConfigs, "serviceConfigs"); final List servicesCopy = new ArrayList<>(); for (ServiceConfig c : serviceConfigs) { c = c.build(this); servicesCopy.add(c); } services = Collections.unmodifiableList(servicesCopy); router = Routers.ofVirtualHost(this, services, rejectionHandler); } /** * 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 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++) { final int c = hostnamePattern.charAt(i); if (c > 0x7F) { return true; } } return false; } @Nullable static SslContext validateSslContext(@Nullable 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"); final MeterRegistry registry = serverConfig.meterRegistry(); final MeterIdPrefix idPrefix = new MeterIdPrefix("armeria.server.router.virtualHostCache", "hostnamePattern", hostnamePattern); router.registerMetrics(registry, idPrefix); } /** * Returns the default hostname of this virtual host. */ public String defaultHostname() { return defaultHostname; } /** * Returns the hostname pattern of this virtual host, as defined in * the section 3.1 of RFC2818. */ public String hostnamePattern() { return hostnamePattern; } /** * Returns the {@link SslContext} of this virtual host. */ @Nullable public SslContext sslContext() { return sslContext; } /** * Returns the information about the {@link Service}s bound to this virtual host. */ public List serviceConfigs() { return services; } /** * Returns {@link MediaTypeSet} that consists of media types producible by this virtual host. */ public MediaTypeSet producibleMediaTypes() { return producibleMediaTypes; } /** * Finds the {@link Service} whose {@link Router} matches the {@link PathMappingContext}. * * @param mappingCtx a context to find the {@link Service}. * * @return the {@link ServiceConfig} wrapped by a {@link PathMapped} if there's a match. * {@link PathMapped#empty()} if there's no match. */ public PathMapped findServiceConfig(PathMappingContext mappingCtx) { requireNonNull(mappingCtx, "mappingCtx"); return router.find(mappingCtx); } @VisibleForTesting Router router() { return router; } VirtualHost decorate(@Nullable Function, Service> decorator) { if (decorator == null) { return this; } final List services = this.services.stream().map(cfg -> { final PathMapping pathMapping = cfg.pathMapping(); final Service 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, producibleMediaTypes()); } @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(@Nullable Class type, String defaultHostname, String hostnamePattern, @Nullable SslContext sslContext, List services) { final 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(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy