Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.vmware.xenon.common.http.netty.NettyWebSocketRequestHandler Maven / Gradle / Ivy
/*
* Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
*
* Licensed 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.vmware.xenon.common.http.netty;
import java.net.URI;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.logging.Level;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.Service.Action;
import com.vmware.xenon.common.ServiceHost;
import com.vmware.xenon.common.ServiceSubscriptionState;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.common.WebSocketService;
import com.vmware.xenon.services.common.ServiceUriPaths;
import com.vmware.xenon.services.common.authn.AuthenticationConstants;
public class NettyWebSocketRequestHandler extends SimpleChannelInboundHandler {
public static class CreateServiceResponse {
public String uri;
}
private WebSocketServerHandshaker handshaker;
private final ConcurrentMap> serviceSubscriptions = new ConcurrentHashMap<>();
private final ConcurrentMap webSocketServices = new ConcurrentHashMap<>();
private ServiceHost host;
private String handshakePath;
private String servicePrefix;
private String authToken;
/**
* {@code true} means that handshake is at least started and we can process any subsequent {@link WebSocketFrame}
* request objects. Otherwise if we see a {@link WebSocketFrame} object - it belongs to some other handler.
*/
private volatile boolean handshakeAccepted;
public NettyWebSocketRequestHandler(ServiceHost host, String socketHandshakePath,
String servicePrefix) {
this.host = host;
this.handshakePath = socketHandshakePath;
this.servicePrefix = servicePrefix;
}
@Override
public boolean acceptInboundMessage(Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest nettyRequest = (FullHttpRequest) msg;
return nettyRequest.uri().contentEquals(this.handshakePath);
}
if (msg instanceof WebSocketFrame) {
return this.handshakeAccepted;
}
return false;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg)
throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest nettyRequest = (FullHttpRequest) msg;
this.handshakeAccepted = true;
performWebsocketHandshake(ctx, nettyRequest);
return;
}
if (msg instanceof WebSocketFrame) {
WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof CloseWebSocketFrame) {
this.handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
if (frame instanceof PingWebSocketFrame) {
ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
return;
}
if (!(frame instanceof TextWebSocketFrame)) {
this.handshaker.close(
ctx.channel(),
new CloseWebSocketFrame(1003, String.format(
"%s frame types not supported", frame.getClass()
.getName())));
return;
}
String frameText = ((TextWebSocketFrame) frame).text();
this.host.run(() -> {
if (this.authToken != null) {
this.host.populateAuthorizationContext(this.authToken, ServiceUriPaths.CORE_WEB_SOCKET_ENDPOINT, authCtx -> {
processWebSocketFrame(ctx, frameText);
});
return;
}
processWebSocketFrame(ctx, frameText);
});
return;
}
}
private void performWebsocketHandshake(final ChannelHandlerContext ctx,
FullHttpRequest nettyRequest) {
WebSocketServerHandshakerFactory factory =
new WebSocketServerHandshakerFactory(this.handshakePath, null, false);
this.handshaker = factory.newHandshaker(nettyRequest);
if (this.handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
ChannelPromise promise = new DefaultChannelPromise(ctx.channel());
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
ctx.channel().close();
}
ctx.channel()
.closeFuture()
.addListener(f -> {
for (java.util.Map.Entry> e :
NettyWebSocketRequestHandler.this.serviceSubscriptions
.entrySet()) {
WebSocketService svc = NettyWebSocketRequestHandler.this.webSocketServices
.get(e.getKey());
if (svc != null) {
deleteServiceSubscriptions(svc);
}
NettyWebSocketRequestHandler.this.host.stopService(svc);
}
});
}
});
DefaultHttpHeaders responseHeaders = new DefaultHttpHeaders();
CharSequence token = nettyRequest.headers().get(Operation.REQUEST_AUTH_TOKEN_HEADER, null);
if (token == null) {
String cookie = nettyRequest.headers().get(HttpHeaderNames.COOKIE);
if (cookie != null) {
token = CookieJar.decodeCookies(cookie)
.get(AuthenticationConstants.REQUEST_AUTH_TOKEN_COOKIE);
}
}
this.authToken = token == null ? null : token.toString();
this.handshaker.handshake(ctx.channel(), nettyRequest, responseHeaders, promise);
}
}
private void deleteServiceSubscriptions(WebSocketService service) {
Set subscriptions = this.serviceSubscriptions.remove(service.getUri());
ServiceSubscriptionState.ServiceSubscriber body =
new ServiceSubscriptionState.ServiceSubscriber();
body.reference = service.getUri();
for (String unsubscribeFrom : subscriptions) {
this.host.sendRequest(Operation
.createDelete(service, unsubscribeFrom)
.setBody(body).setReferer(service.getUri()));
}
}
/**
* Processes incoming web socket frame. {@link PingWebSocketFrame} and {@link CloseWebSocketFrame} frames are
* processed in a usual way. {@link io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame} frames are not
* supported. {@link TextWebSocketFrame} frames are processed as described below.
Whenever invalid frame is
* encountered - the underlying connection is closed.
Incoming frame format Incoming frame format is
* the same for all frames:
*
* REQUEST_ID
* METHOD URI
* {jsonPayload} (Optional and may be multiline)
*
* REQUEST_ID is an arbitrary string generated on client which is used to correlate server response with
* original client request. This string is included into response frames (described below). REQUEST_ID should
* be unique within the same web socket connection.
*
* METHOD is one of [POST, DELETE, REPLY] and URI is service path, such as {@code /core/ws-service}.
*
* Web socket connection is not a proxy, so arbitrary methods and request URIs are not supported.
*
* Line breaks are always CRLF similar to HTTP
* (RFC 2616 ).
*
* Outgoing frame format
* {@link io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame} frames are never sent to client.
* Text frames have the following format:
*
* (RESULT_CODE REQUEST_ID (optional line))|(HTTP_METHOD SERVICE_URI)
* {jsonPayload}
*
* RESULT_CODE is one of 200, 404, 500 with similar meanings to corresponding HTTP codes (OK, NOT_FOUND, ISE).
*
* REQUEST_ID is the same string which was passed by client in initial request.
*
* SERVICE_URI - a URI assigned to the client-side service.
*
* Json payload is either server response on request (in case when first line is RESULT_CODE REQUEST_ID) or
* {@link com.vmware.xenon.common.Operation.SerializedOperation} in case when this frame is an operation to be
* complete by a client service and first line is HTTP_METHOD SERVICE_URI. Method name corresponds to
* {@link com.vmware.xenon.common.Operation.SerializedOperation#action} specified in the serialized operation.
* Possible incoming request
* All below requests should conform spec above. REQUEST_ID is omitted everywhere below to simplify the doc.
*
* POST /core/ws-service with no body - requests a new service to be created. Response body is
* {"uri": "http://some-ip-addr/core/ws-service/some-uuid"} where uri value is assigned temporary link to the
* service. ToDo: make node IP addresses invisible to client
* DELETE /core/ws-service/some-uuid with no body - removes previously created web socket service.
* Service should be defined in the same web socket connection.
* POST /someservicepath/subscriptions with standard subscription body - subscribes the specified
* service to the specified target service. Observer must be a web socket-based serviced created within
* current connection. Subscription is removed automatically whenever web socket connection is broken.
* DELETE /someservicepath/subscriptions with standard subscription body - removes previously created
* subscription. Subscription should be made via request specified above within the same connection
* REPLY /core/ws-service with serialized operation as body - should be issued in response for an
* incoming request. No response from the server is assumed and REQUEST_ID field is ignored.
*
*
*
* @param ctx Netty channel context handler
* @param text Incoming websocket frame text
*/
private void processWebSocketFrame(ChannelHandlerContext ctx, String text) {
int requestIdSep = text.indexOf(Operation.CR_LF);
if (requestIdSep < 0) {
this.handshaker.close(ctx.channel(),
new CloseWebSocketFrame(1003, "Malformed frame"));
return;
}
String requestId = text.substring(0, requestIdSep);
int requestLineSep = text.indexOf(Operation.CR_LF, requestIdSep + Operation.CR_LF.length());
String body;
if (requestLineSep < 0) {
requestLineSep = text.length();
body = "";
} else {
body = text.substring(requestLineSep + Operation.CR_LF.length());
}
String requestLine = text
.substring(requestIdSep + Operation.CR_LF.length(), requestLineSep);
int methodSep = requestLine.indexOf(" ");
if (methodSep < 0) {
this.handshaker.close(ctx.channel(),
new CloseWebSocketFrame(1003, "Malformed frame"));
return;
}
String method = requestLine.substring(0, methodSep);
String path = requestLine.substring(methodSep + 1);
try {
if (method.equals("DELETE")) {
if (path.startsWith(this.servicePrefix)) {
// Shutdown service permanently and delete all known service subscriptions
URI serviceToDelete = UriUtils.buildPublicUri(this.host, path);
WebSocketService removed = this.webSocketServices.remove(serviceToDelete);
if (removed != null) {
deleteServiceSubscriptions(removed);
this.host.stopService(removed);
ctx.writeAndFlush(new TextWebSocketFrame("200 " + requestId));
} else {
ctx.writeAndFlush(new TextWebSocketFrame("404 " + requestId));
}
return;
}
if (path.endsWith(ServiceHost.SERVICE_URI_SUFFIX_SUBSCRIPTIONS)) {
// Delete a single subscription
ServiceSubscriptionState.ServiceSubscriber state = Utils.fromJson(body,
ServiceSubscriptionState.ServiceSubscriber.class);
WebSocketService service = this.webSocketServices.get(state.reference);
this.host.sendRequest(Operation
.createDelete(service, path)
.setBody(body)
.setReferer(service.getUri())
.setCompletion(
(completedOp, failure) -> {
ctx.writeAndFlush(new TextWebSocketFrame(completedOp
.getStatusCode() + " " + requestId));
getSubscriptions(service).remove(path);
}));
return;
}
ctx.writeAndFlush(new TextWebSocketFrame(Integer
.toString(Operation.STATUS_CODE_NOT_FOUND) + " " + requestId));
return;
}
if (method.equals(Action.POST.toString())) {
if (path.equals(this.servicePrefix)) {
// Create a new ephemeral service
URI wsServiceUri = buildWsServiceUri(java.util.UUID.randomUUID().toString());
CreateServiceResponse response = new CreateServiceResponse();
response.uri = wsServiceUri.toString();
WebSocketService webSocketService = new WebSocketService(ctx, wsServiceUri);
this.host
.startService(
Operation
.createPost(wsServiceUri)
.setCompletion(
(o, t) -> {
if (t != null) {
ctx.writeAndFlush(new TextWebSocketFrame(
Integer.toString(Operation.STATUS_CODE_SERVER_FAILURE_THRESHOLD)
+ " "
+ requestId
+ Operation.CR_LF
+ Utils.toJson(t)));
} else {
ctx.writeAndFlush(new TextWebSocketFrame(
Integer.toString(Operation.STATUS_CODE_ACCEPTED)
+ " "
+ requestId
+ Operation.CR_LF
+ Utils.toJson(response)));
}
}), webSocketService);
this.webSocketServices.put(wsServiceUri, webSocketService);
return;
}
if (path.endsWith(ServiceHost.SERVICE_URI_SUFFIX_SUBSCRIPTIONS)) {
// Subscribe for service updates with auto-unsubscribe
ServiceSubscriptionState.ServiceSubscriber state = Utils.fromJson(body,
ServiceSubscriptionState.ServiceSubscriber.class);
WebSocketService service = this.webSocketServices.get(state.reference);
this.host.sendRequest(Operation
.createPost(service, path)
.setBody(body)
.setReferer(service.getUri())
.setCompletion(
(completedOp, failure) -> {
ctx.writeAndFlush(new TextWebSocketFrame(completedOp
.getStatusCode() + " " + requestId));
if (completedOp.getStatusCode() >= 200
&& completedOp.getStatusCode() < 300) {
getSubscriptions(service).add(path);
}
}));
return;
}
}
if (method.equals("REPLY")) {
if (path.startsWith(this.servicePrefix)
&& path.length() > this.servicePrefix.length()) {
// Forward ephemeral service response to the caller
String serviceId = path.substring(this.servicePrefix.length() + 1);
URI serviceUri = buildWsServiceUri(serviceId);
WebSocketService service = this.webSocketServices.get(serviceUri);
if (service != null) {
service.handleWebSocketMessage(body);
}
return;
}
}
ctx.writeAndFlush(new TextWebSocketFrame("404 " + requestId));
this.host
.log(Level.FINE, "Unsupported websocket request: %s %s %s", method, path, body);
} catch (Exception e) {
ctx.writeAndFlush("500 " + requestId);
}
}
private Set getSubscriptions(WebSocketService service) {
return this.serviceSubscriptions.computeIfAbsent(service.getUri(),
k -> new ConcurrentSkipListSet<>());
}
/**
* Builds public ephemeral web socket-based service URI based on service id.
*
* @param serviceId Service ID.
* @return Service URI.
*/
private URI buildWsServiceUri(String serviceId) {
return UriUtils.buildPublicUri(this.host,
UriUtils.buildUriPath(this.servicePrefix, serviceId));
}
}