org.yamcs.http.HttpServer Maven / Gradle / Ivy
package org.yamcs.http;
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.net.ssl.SSLException;
import org.yamcs.AbstractYamcsService;
import org.yamcs.InitException;
import org.yamcs.Spec;
import org.yamcs.Spec.OptionType;
import org.yamcs.YConfiguration;
import org.yamcs.api.Api;
import org.yamcs.api.HttpRoute;
import org.yamcs.api.WebSocketTopic;
import org.yamcs.http.api.ActivitiesApi;
import org.yamcs.http.api.AlarmsApi;
import org.yamcs.http.api.AuditApi;
import org.yamcs.http.api.BucketsApi;
import org.yamcs.http.api.ClearanceApi;
import org.yamcs.http.api.CommandsApi;
import org.yamcs.http.api.Cop1Api;
import org.yamcs.http.api.DatabaseApi;
import org.yamcs.http.api.EventsApi;
import org.yamcs.http.api.FileTransferApi;
import org.yamcs.http.api.IamApi;
import org.yamcs.http.api.IndexesApi;
import org.yamcs.http.api.InstancesApi;
import org.yamcs.http.api.LinksApi;
import org.yamcs.http.api.MdbApi;
import org.yamcs.http.api.MdbOverrideApi;
import org.yamcs.http.api.PacketsApi;
import org.yamcs.http.api.ParameterArchiveApi;
import org.yamcs.http.api.ParameterListsApi;
import org.yamcs.http.api.ParameterValuesApi;
import org.yamcs.http.api.ProcessingApi;
import org.yamcs.http.api.QueuesApi;
import org.yamcs.http.api.ReplicationApi;
import org.yamcs.http.api.RocksDbApi;
import org.yamcs.http.api.ServerApi;
import org.yamcs.http.api.ServicesApi;
import org.yamcs.http.api.SessionsApi;
import org.yamcs.http.api.StreamArchiveApi;
import org.yamcs.http.api.TableApi;
import org.yamcs.http.api.TimeApi;
import org.yamcs.http.api.TimeCorrelationApi;
import org.yamcs.http.api.TimelineApi;
import org.yamcs.http.audit.AuditLog;
import org.yamcs.http.auth.AuthHandler;
import org.yamcs.http.auth.TokenStore;
import org.yamcs.protobuf.CancelOptions;
import org.yamcs.protobuf.Reply;
import org.yamcs.utils.ExceptionUtil;
import com.codahale.metrics.MetricRegistry;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ServiceManager;
import com.google.protobuf.Descriptors.MethodDescriptor;
import com.google.protobuf.util.JsonFormat;
import com.google.protobuf.util.JsonFormat.TypeRegistry;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.cors.CorsConfig;
import io.netty.handler.codec.http.cors.CorsConfigBuilder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.GlobalEventExecutor;
import io.netty.util.concurrent.ThreadPerTaskExecutor;
/**
* Server-wide HTTP server based on Netty that provides a number of Yamcs web services:
*
*
* - REST API
*
- WebSocket API
*
- Static file serving
*
*/
public class HttpServer extends AbstractYamcsService {
public static final HttpRoute WEBSOCKET_ROUTE = HttpRoute.newBuilder().setGet("/api/websocket").build();
// Protobuf weirdness. When unspecified it defaults to "type.googleapis.com" ...
public static final String TYPE_URL_PREFIX = "";
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ChannelGroup clientChannels;
private GlobalTrafficShapingHandler globalTrafficHandler;
private List> apis = new ArrayList<>();
private List routes = new ArrayList<>();
private List topics = new ArrayList<>();
private MetricRegistry metricRegistry = new MetricRegistry();
private List bindings = new ArrayList<>(2);
private String contextPath;
private boolean reverseLookup;
private int nThreads;
// Cross-origin Resource Sharing (CORS) enables use of the HTTP API in non-official client web applications
private CorsConfig corsConfig;
private ProtobufRegistry protobufRegistry = new ProtobufRegistry();
private JsonFormat.Parser jsonParser;
private JsonFormat.Printer jsonPrinter;
// Services (may participate in start-stop events)
private TokenStore tokenStore;
private AuditLog auditLog;
// Guava manager for sub-services
private ServiceManager serviceManager;
// Handlers at root level. Wrapped in a Supplier because
// we want to give the possiblity to make request-scoped instances
private Map> httpHandlers = new HashMap<>();
// Extra handlers at root level. Wrapped in a Supplier because
// we want to give the possiblity to make request-scoped instances
private Map> extraHandlers = new HashMap<>();
@Override
public Spec getSpec() {
Spec corsSpec = new Spec();
corsSpec.addOption("allowOrigin", OptionType.STRING).withRequired(true);
corsSpec.addOption("allowCredentials", OptionType.BOOLEAN).withRequired(true);
Spec websiteSpec = new Spec();
websiteSpec.addOption("tag", OptionType.STRING);
Spec lohiSpec = new Spec();
lohiSpec.addOption("low", OptionType.INTEGER).withDefault(32 * 1024);
lohiSpec.addOption("high", OptionType.INTEGER).withDefault(128 * 1024);
Spec websocketSpec = new Spec();
websocketSpec.addOption("writeBufferWaterMark", OptionType.MAP).withSpec(lohiSpec).withApplySpecDefaults(true);
websocketSpec.addOption("maxFrameLength", OptionType.INTEGER).withDefault(65536);
// Value in seconds. Both nginx and apache have a default timeout of 60 seconds before
// they will close an idle WebSocket connection, therefore we choose a value well below that.
websocketSpec.addOption("pingWhenIdleFor", OptionType.INTEGER).withDefault(40);
Spec bindingSpec = new Spec();
bindingSpec.addOption("address", OptionType.STRING);
bindingSpec.addOption("port", OptionType.INTEGER).withRequired(true);
bindingSpec.addOption("tlsCert", OptionType.LIST_OR_ELEMENT).withElementType(OptionType.STRING);
bindingSpec.addOption("tlsKey", OptionType.STRING);
bindingSpec.requireTogether("tlsCert", "tlsKey");
Spec spec = new Spec();
spec.addOption("address", OptionType.STRING);
spec.addOption("port", OptionType.INTEGER).withDefault(8090);
spec.addOption("tlsCert", OptionType.LIST_OR_ELEMENT).withElementType(OptionType.STRING);
spec.addOption("tlsKey", OptionType.STRING);
spec.addOption("contextPath", OptionType.STRING).withDefault("" /* NOT null */);
spec.addOption("zeroCopyEnabled", OptionType.BOOLEAN).withDefault(true)
.withDeprecationMessage("This optimization is automatically enabled where possible");
spec.addOption("maxInitialLineLength", OptionType.INTEGER).withDefault(8192);
spec.addOption("maxHeaderSize", OptionType.INTEGER).withDefault(8192);
spec.addOption("maxContentLength", OptionType.INTEGER).withDefault(65536);
spec.addOption("maxPageSize", OptionType.INTEGER).withDefault(1000);
spec.addOption("cors", OptionType.MAP).withSpec(corsSpec);
spec.addOption("webSocket", OptionType.MAP).withSpec(websocketSpec).withApplySpecDefaults(true);
spec.addOption("bindings", OptionType.LIST)
.withElementType(OptionType.MAP)
.withSpec(bindingSpec);
spec.addOption("nThreads", OptionType.INTEGER).withDefault(0);
spec.addOption("reverseLookup", OptionType.BOOLEAN).withDefault(false);
// When using multiple bindings, best to avoid confusion and disable the top-level properties
spec.mutuallyExclusive("address", "bindings");
spec.mutuallyExclusive("port", "bindings");
spec.mutuallyExclusive("tlsCert", "bindings");
spec.mutuallyExclusive("tlsKey", "bindings");
spec.requireTogether("tlsCert", "tlsKey");
return spec;
}
@Override
public void init(String yamcsInstance, String serviceName, YConfiguration config) throws InitException {
super.init(yamcsInstance, serviceName, config);
tokenStore = new TokenStore();
auditLog = new AuditLog();
tokenStore.init(this);
auditLog.init(this);
clientChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
if (config.containsKey("bindings")) {
for (YConfiguration bindingConfig : config.getConfigList("bindings")) {
try {
Binding binding = Binding.fromConfig(bindingConfig);
bindings.add(binding);
} catch (UnknownHostException e) {
throw new InitException("Cannot determine IP address for binding " + bindingConfig, e);
}
}
} else {
try {
Binding binding = Binding.fromConfig(config);
bindings.add(binding);
} catch (UnknownHostException e) {
throw new InitException("Cannot determine IP address for binding " + config, e);
}
}
contextPath = config.getString("contextPath");
if (!contextPath.isEmpty()) {
if (!contextPath.startsWith("/")) {
throw new InitException("contextPath must start with a slash token");
}
if (contextPath.endsWith("/")) {
throw new InitException("contextPath may not end with a slash token");
}
}
reverseLookup = config.getBoolean("reverseLookup");
if (config.containsKey("cors")) {
YConfiguration ycors = config.getConfig("cors");
String[] origins = ycors.getString("allowOrigin").split(",");
CorsConfigBuilder corsb = null;
if (origins.length == 1) {
corsb = CorsConfigBuilder.forOrigin(origins[0]);
} else {
corsb = CorsConfigBuilder.forOrigins(origins);
}
if (ycors.getBoolean("allowCredentials")) {
corsb.allowCredentials();
}
corsb.allowedRequestMethods(
HttpMethod.GET,
HttpMethod.POST,
HttpMethod.PATCH,
HttpMethod.PUT,
HttpMethod.DELETE);
corsb.allowedRequestHeaders(
HttpHeaderNames.CONTENT_TYPE,
HttpHeaderNames.ACCEPT,
HttpHeaderNames.AUTHORIZATION,
HttpHeaderNames.ORIGIN);
corsConfig = corsb.build();
}
nThreads = config.getInt("nThreads");
addApi(new ActivitiesApi());
addApi(new AlarmsApi(auditLog));
addApi(new AuditApi(auditLog));
addApi(new BucketsApi());
addApi(new FileTransferApi(auditLog));
addApi(new ClearanceApi(auditLog));
addApi(new CommandsApi());
addApi(new Cop1Api());
addApi(new DatabaseApi());
addApi(new EventsApi());
addApi(new IamApi(auditLog, tokenStore));
addApi(new IndexesApi());
addApi(new InstancesApi());
addApi(new LinksApi(auditLog));
addApi(new MdbApi());
addApi(new MdbOverrideApi());
addApi(new PacketsApi());
addApi(new ParameterArchiveApi());
addApi(new ParameterListsApi());
addApi(new ParameterValuesApi());
addApi(new ProcessingApi());
addApi(new QueuesApi(auditLog));
addApi(new ReplicationApi());
addApi(new RocksDbApi(auditLog));
addApi(new ServerApi(this));
addApi(new ServicesApi());
addApi(new SessionsApi());
addApi(new StreamArchiveApi());
addApi(new TableApi());
addApi(new TimeApi());
addApi(new TimeCorrelationApi());
addApi(new TimelineApi());
var wellKnownHandler = new WellKnownHandler();
addRoute(".well-known", () -> wellKnownHandler);
var authHandler = new AuthHandler(this);
addRoute("auth", () -> authHandler);
var faviconHandler = new FaviconHandler();
for (var path : FaviconHandler.HANDLED_PATHS) {
addRoute(path, () -> faviconHandler);
}
var robotsTxtHandler = new RobotsTxtHandler();
for (var path : RobotsTxtHandler.HANDLED_PATHS) {
addRoute(path, () -> robotsTxtHandler);
}
var apiHandler = new ApiHandler(this);
addRoute("api", () -> apiHandler);
}
public void addRoute(String pathSegment, Supplier handler) {
httpHandlers.put(pathSegment, handler);
}
public void addApi(Api api) {
apis.add(api);
for (MethodDescriptor method : api.getDescriptorForType().getMethods()) {
RpcDescriptor descriptor = protobufRegistry.getRpc(method.getFullName());
if (descriptor == null) {
throw new UnsupportedOperationException("Unable to find rpc definition: " + method.getFullName());
}
if (WEBSOCKET_ROUTE.equals(descriptor.getHttpRoute())) {
topics.add(new Topic(api, descriptor.getWebSocketTopic(), descriptor));
for (WebSocketTopic topic : descriptor.getAdditionalWebSocketTopics()) {
topics.add(new Topic(api, topic, descriptor));
}
} else {
routes.add(new Route(api, descriptor.getHttpRoute(), descriptor, metricRegistry));
for (HttpRoute route : descriptor.getAdditionalHttpRoutes()) {
routes.add(new Route(api, route, descriptor, metricRegistry));
}
}
}
// Regenerate JSON converters with type support (needed for the "Any" type)
TypeRegistry.Builder typeRegistryb = TypeRegistry.newBuilder();
typeRegistryb.add(CancelOptions.getDescriptor());
typeRegistryb.add(Reply.getDescriptor());
apis.forEach(a -> typeRegistryb.add(a.getDescriptorForType().getFile().getMessageTypes()));
TypeRegistry typeRegistry = typeRegistryb.build();
jsonParser = JsonFormat.parser().usingTypeRegistry(typeRegistry);
jsonPrinter = JsonFormat.printer().usingTypeRegistry(typeRegistry);
// Sort in a way that increases chances of a good URI match
Collections.sort(routes);
}
@Override
protected void doStart() {
try {
startServer();
notifyStarted();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
notifyFailed(e);
} catch (Exception e) {
notifyFailed(e);
}
}
public void startServer() throws Exception {
serviceManager = new ServiceManager(Arrays.asList(
tokenStore, auditLog));
serviceManager.startAsync().awaitHealthy(10, TimeUnit.SECONDS);
bossGroup = new NioEventLoopGroup(1);
// Note that by default (i.e. with nThreads = 0), Netty will limit the number
// of worker threads to 2*number of CPU cores
workerGroup = new NioEventLoopGroup(nThreads,
new ThreadPerTaskExecutor(new DefaultThreadFactory("YamcsHttpServer")));
// Measure global traffic, we also add a channel-specific measurer in channel-init.
globalTrafficHandler = new GlobalTrafficShapingHandler(workerGroup, 5000);
for (var binding : bindings) {
createAndBindBootstrap(workerGroup, binding, globalTrafficHandler);
log.debug("Serving from {}{}", binding, contextPath);
}
}
private void createAndBindBootstrap(EventLoopGroup workerGroup, Binding binding,
GlobalTrafficShapingHandler globalTrafficHandler)
throws InterruptedException, SSLException, IOException {
SslContext sslContext = null;
if (binding.isTLS()) {
sslContext = binding.createSslContext();
}
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(HttpServer.class, LogLevel.DEBUG))
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new HttpServerChannelInitializer(this, sslContext, globalTrafficHandler));
// Bind and start to accept incoming connections.
InetAddress address = binding.getAddress();
int port = binding.getPort();
if (address == null) {
bootstrap.bind(new InetSocketAddress(port)).sync();
} else {
bootstrap.bind(new InetSocketAddress(address, port)).sync();
}
}
HttpHandler createHttpHandler(String pathSegment) {
Supplier supplier = httpHandlers.get(pathSegment);
return supplier != null ? supplier.get() : null;
}
Handler createHandler(String pathSegment) {
Supplier supplier = extraHandlers.get(pathSegment);
return supplier != null ? supplier.get() : null;
}
public TokenStore getTokenStore() {
return tokenStore;
}
public AuditLog getAuditLog() {
return auditLog;
}
public List getBindings() {
return bindings;
}
public String getContextPath() {
return contextPath;
}
public List getRoutes() {
return routes;
}
public List getTopics() {
return topics;
}
public ProtobufRegistry getProtobufRegistry() {
return protobufRegistry;
}
public GlobalTrafficShapingHandler getGlobalTrafficShapingHandler() {
return globalTrafficHandler;
}
public JsonFormat.Parser getJsonParser() {
return jsonParser;
}
public JsonFormat.Printer getJsonPrinter() {
return jsonPrinter;
}
public boolean getReverseLookup() {
return reverseLookup;
}
public CorsConfig getCorsConfig() {
return corsConfig;
}
public MetricRegistry getMetricRegistry() {
return metricRegistry;
}
void trackClientChannel(Channel channel) {
clientChannels.add(channel);
}
public List getClientChannels() {
return new ArrayList<>(clientChannels);
}
public void closeChannel(String id) {
clientChannels.close(ch -> ch.id().asShortText().equals(id));
}
@Override
protected void doStop() {
globalTrafficHandler.release();
var closers = listeningDecorator(Executors.newCachedThreadPool());
var future1 = closers.submit(() -> {
return workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).get();
});
var future2 = closers.submit(() -> {
return bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).get();
});
var future3 = closers.submit(() -> {
serviceManager.stopAsync();
serviceManager.awaitStopped(5, TimeUnit.SECONDS);
return true; // Force use of Callable interface, instead of Runnable
});
closers.shutdown();
Futures.addCallback(Futures.allAsList(future1, future2, future3), new FutureCallback<>() {
@Override
public void onSuccess(List
© 2015 - 2024 Weber Informatics LLC | Privacy Policy