From b588523aa5ac034939fe17ae0d064c29491d26a1 Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Tue, 21 Apr 2020 11:05:02 +0200 Subject: [PATCH 01/34] Initial import [broken] --- .../ext/eventbus/bridge/tcp/BridgeEvent.java | 3 +- .../tcp/JsonRPCStreamEventBusBridge.java | 44 ++ .../bridge/tcp/impl/BridgeEventImpl.java | 1 - .../impl/JsonRPCStreamEventBusBridgeImpl.java | 433 ++++++++++++ .../bridge/tcp/impl/ParserHandler.java | 23 + .../bridge/tcp/impl/ReadableBuffer.java | 213 ++++++ .../bridge/tcp/impl/StreamParser.java | 171 +++++ .../bridge/tcp/impl/codec/JsonRPC.java | 17 + .../tcp/impl/protocol/JsonRPCHelper.java | 120 ++++ .../tcp/JsonRPCStreamEventBusBridgeTest.java | 619 ++++++++++++++++++ .../eventbus/bridge/tcp/StreamParserTest.java | 48 ++ 11 files changed, 1690 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ParserHandler.java create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java create mode 100644 src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java create mode 100644 src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java index 1485c22..2eabcab 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java @@ -22,6 +22,7 @@ import io.vertx.core.Future; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetSocket; +import io.vertx.ext.bridge.BaseBridgeEvent; /** * Represents an event that occurs on the event bus bridge. @@ -31,7 +32,7 @@ * @author Tim Fox */ @VertxGen -public interface BridgeEvent extends io.vertx.ext.bridge.BaseBridgeEvent { +public interface BridgeEvent extends BaseBridgeEvent { /** * Get the raw JSON message for the event. This will be null for SOCKET_CREATED or SOCKET_CLOSED events as there is diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java new file mode 100644 index 0000000..5d0a97e --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.net.NetSocket; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.eventbus.bridge.tcp.impl.JsonRPCStreamEventBusBridgeImpl; + +/** + * JSONRPC stream EventBus bridge for Vert.x + * + * @author Paulo Lopes + */ +@VertxGen +public interface JsonRPCStreamEventBusBridge extends Handler { + + static JsonRPCStreamEventBusBridge create(Vertx vertx) { + return create(vertx, null, null); + } + + static JsonRPCStreamEventBusBridge create(Vertx vertx, BridgeOptions options) { + return create(vertx, options, null); + } + + static JsonRPCStreamEventBusBridge create(Vertx vertx, BridgeOptions options, Handler eventHandler) { + return new JsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); + } +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java index d18d11a..e117add 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java @@ -114,5 +114,4 @@ public boolean tryFail(Throwable cause) { public boolean tryFail(String failureMessage) { return promise.tryFail(failureMessage); } - } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java new file mode 100644 index 0000000..5847d69 --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -0,0 +1,433 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp.impl; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.*; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetSocket; +import io.vertx.ext.bridge.BridgeEventType; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCStreamEventBusBridge; +import io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Abstract TCP EventBus bridge. Handles all common socket operations but has no knowledge on the payload. + * + * @author Paulo Lopes + */ +public class JsonRPCStreamEventBusBridgeImpl implements JsonRPCStreamEventBusBridge { + + private static final Logger log = LoggerFactory.getLogger(JsonRPCStreamEventBusBridgeImpl.class); + private static final JsonObject EMPTY = new JsonObject(Collections.emptyMap()); + + private final EventBus eb; + + private final Map compiledREs = new HashMap<>(); + private final BridgeOptions options; + private final Handler bridgeEventHandler; + + + public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler eventHandler) { + this.eb = vertx.eventBus(); + this.options = options != null ? options : new BridgeOptions(); + this.bridgeEventHandler = eventHandler; + } + + private void dispatch(NetSocket socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { + + switch (method) { + case "send": + checkCallHook( + () -> new BridgeEventImpl(BridgeEventType.SEND, msg, socket), + () -> send(socket, id, msg, registry, replies), + () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) + ); + break; + case "publish": + checkCallHook( + () -> new BridgeEventImpl(BridgeEventType.SEND, msg, socket), + () -> publish(socket, id, msg, registry, replies), + () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) + ); + break; + case "register": + checkCallHook( + () -> new BridgeEventImpl(BridgeEventType.REGISTER, msg, socket), + () -> register(socket, id, msg, registry, replies), + () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) + ); + break; + case "unregister": + checkCallHook( + () -> new BridgeEventImpl(BridgeEventType.UNREGISTER, msg, socket), + () -> unregister(socket, id, msg, registry, replies), + () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) + ); + break; + case "ping": + JsonRPCHelper.response(id, "pong", socket); + break; + default: + JsonRPCHelper.error(id, -32601, "unknown_method", socket); + break; + } + } + + private void unregister(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + final JsonObject params = msg.getJsonObject("params", EMPTY); + final String address = params.getString("address"); + + if (address == null) { + JsonRPCHelper.error(id, -32602, "invalid_parameters", socket); + return; + } + + if (checkMatches(false, address)) { + MessageConsumer consumer = registry.remove(address); + if (consumer != null) { + consumer.unregister(); + if (id != null) { + // ack + JsonRPCHelper.response(id, EMPTY, socket); + } + } else { + JsonRPCHelper.error(id, -32044, "unknown_address", socket); + } + } else { + JsonRPCHelper.error(id, -32040, "access_denied", socket); + } + } + + private void register(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + final JsonObject params = msg.getJsonObject("params", EMPTY); + final String address = params.getString("address"); + + if (address == null) { + JsonRPCHelper.error(id, -32602, "invalid_parameters", socket); + return; + } + + if (checkMatches(false, address)) { + registry.put(address, eb.consumer(address, res1 -> { + // save a reference to the message so tcp bridged messages can be replied properly + if (res1.replyAddress() != null) { + replies.put(res1.replyAddress(), res1); + } + + final JsonObject responseHeaders = new JsonObject(); + + // clone the headers from / to + for (Map.Entry entry : res1.headers()) { + responseHeaders.put(entry.getKey(), entry.getValue()); + } + + JsonRPCHelper.response( + id, + new JsonObject() + .put("address", res1.address()) + .put("replyAddress", res1.replyAddress()) + .put("headers", responseHeaders) + .put("isSend", res1.isSend()) + .put("body", res1.body()), + socket); + })); + checkCallHook( + () -> new BridgeEventImpl(BridgeEventType.REGISTERED, msg, socket), + () -> { + if (id != null) { + // ack + JsonRPCHelper.response(id, EMPTY, socket); + } + }); + } else { + JsonRPCHelper.error(id, -32040, "access_denied", socket); + } + } + + private void publish(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + final JsonObject params = msg.getJsonObject("params", EMPTY); + final String address = params.getString("address"); + + if (address == null) { + JsonRPCHelper.error(id, -32602, "invalid_parameters", socket); + return; + } + + if (checkMatches(true, address)) { + final JsonObject body = params.getJsonObject("body"); + final DeliveryOptions deliveryOptions = parseMsgHeaders(new DeliveryOptions(), params.getJsonObject("headers")); + + eb.publish(address, body, deliveryOptions); + if (id != null) { + // ack + JsonRPCHelper.response(id, EMPTY, socket); + } + } else { + JsonRPCHelper.error(id, -32040, "access_denied", socket); + } + } + + private void send(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + final JsonObject params = msg.getJsonObject("params", EMPTY); + final String address = params.getString("address"); + + if (address == null) { + JsonRPCHelper.error(id, -32602, "invalid_parameters", socket); + return; + } + + if (checkMatches(true, address, replies)) { + final JsonObject body = params.getJsonObject("body"); + final DeliveryOptions deliveryOptions = parseMsgHeaders(new DeliveryOptions(), params.getJsonObject("headers")); + + if (id != null) { + // id is not null, it is a request from TCP endpoint that will wait for a response + eb.request(address, body, deliveryOptions, request -> { + if (request.failed()) { + JsonRPCHelper.error(id, (ReplyException) request.cause(), socket); + } else { + final Message response = request.result(); + final JsonObject responseHeaders = new JsonObject(); + + // clone the headers from / to + for (Map.Entry entry : response.headers()) { + responseHeaders.put(entry.getKey(), entry.getValue()); + } + + if (response.replyAddress() != null) { + replies.put(response.replyAddress(), response); + } + + JsonRPCHelper.response( + id, + new JsonObject() + .put("headers", responseHeaders) + .put("id", response.replyAddress()) + // TODO: why? + .put("send", true) + .put("body", response.body()), + socket); + } + }); + } else { + // no reply address it might be a response, a failure or a request that does not need a response + if (replies.containsKey(address)) { + // address is registered, it is not a request + Integer failureCode = msg.getInteger("failureCode"); + if (failureCode == null) { + //No failure code, it is a response + replies.get(address).reply(body, deliveryOptions); + } else { + //Failure code, fail the original response + replies.get(address).fail(msg.getInteger("failureCode"), msg.getString("message")); + } + } else { + // it is a request that does not expect a response + eb.send(address, body, deliveryOptions); + } + } + // replies are a one time off operation + replies.remove(address); + } else { + JsonRPCHelper.error(id, -32040, "access_denied", socket); + } + } + + @Override + public void handle(NetSocket socket) { + checkCallHook( + // process the new socket according to the event handler + () -> new BridgeEventImpl(BridgeEventType.SOCKET_CREATED, null, socket), + // on success + () -> { + final Map> registry = new ConcurrentHashMap<>(); + final Map> replies = new ConcurrentHashMap<>(); + + socket + .exceptionHandler(t -> { + log.error(t.getMessage(), t); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }) + .endHandler(v -> { + registry.values().forEach(MessageConsumer::unregister); + // normal close, trigger the event + checkCallHook(() -> new BridgeEventImpl(BridgeEventType.SOCKET_CLOSED, null, socket)); + registry.clear(); + }) + .handler( + // create a protocol parser + new StreamParser() + .exceptionHandler(t -> { + log.error(t.getMessage(), t); + }) + .handler((contentType, body) -> { + // TODO: handle content type + + // TODO: body may be an array (batching) + final JsonObject msg = new JsonObject(body); + + // validation + if (!"2.0".equals(msg.getString("jsonrpc"))) { + log.error("Invalid message: " + msg); + return; + } + + final String method = msg.getString("method"); + if (method == null) { + log.error("Invalid method: " + msg.getString("method")); + return; + } + + final Object id = msg.getValue("id"); + if (id != null) { + if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { + log.error("Invalid id: " + msg.getValue("id")); + return; + } + } + + dispatch( + socket, + method, + id, + msg, + registry, + replies); + })); + }, + // on failure + socket::close + ); + } + + private void checkCallHook(Supplier eventSupplier) { + checkCallHook(eventSupplier, null, null); + } + + private void checkCallHook(Supplier eventSupplier, Runnable okAction) { + checkCallHook(eventSupplier, okAction, null); + } + + private void checkCallHook(Supplier eventSupplier, Runnable okAction, Runnable rejectAction) { + if (bridgeEventHandler == null) { + if (okAction != null) { + okAction.run(); + } + } else { + BridgeEvent event = eventSupplier.get(); + bridgeEventHandler.handle(event); + event.future().onComplete(res -> { + if (res.succeeded()) { + if (res.result()) { + if (okAction != null) { + okAction.run(); + } + } else { + if (rejectAction != null) { + rejectAction.run(); + } else { + log.debug("Bridge handler prevented: " + event.toString()); + } + } + } else { + log.error("Failure in bridge event handler", res.cause()); + } + }); + } + } + + private boolean checkMatches(boolean inbound, String address) { + return checkMatches(inbound, address, null); + } + + private boolean checkMatches(boolean inbound, String address, Map> replies) { + // special case, when dealing with replies the addresses are not in the inbound/outbound list but on + // the replies registry + if (replies != null && inbound && replies.containsKey(address)) { + return true; + } + + List matches = inbound ? options.getInboundPermitteds() : options.getOutboundPermitteds(); + + for (PermittedOptions matchHolder : matches) { + String matchAddress = matchHolder.getAddress(); + String matchRegex; + if (matchAddress == null) { + matchRegex = matchHolder.getAddressRegex(); + } else { + matchRegex = null; + } + + boolean addressOK; + if (matchAddress == null) { + addressOK = matchRegex == null || regexMatches(matchRegex, address); + } else { + addressOK = matchAddress.equals(address); + } + + if (addressOK) { + return true; + } + } + + return false; + } + + private boolean regexMatches(String matchRegex, String address) { + Pattern pattern = compiledREs.get(matchRegex); + if (pattern == null) { + pattern = Pattern.compile(matchRegex); + compiledREs.put(matchRegex, pattern); + } + Matcher m = pattern.matcher(address); + return m.matches(); + } + + private DeliveryOptions parseMsgHeaders(DeliveryOptions options, JsonObject headers) { + if (headers == null) + return options; + + Iterator fnameIter = headers.fieldNames().iterator(); + String fname; + while (fnameIter.hasNext()) { + fname = fnameIter.next(); + if ("timeout".equals(fname)) { + options.setSendTimeout(headers.getLong(fname)); + } else if ("localOnly".equals(fname)) { + options.setLocalOnly(headers.getBoolean(fname)); + } else if ("codecName".equals(fname)) { + options.setCodecName(headers.getString(fname)); + } else { + options.addHeader(fname, headers.getString(fname)); + } + } + + return options; + } +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ParserHandler.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ParserHandler.java new file mode 100644 index 0000000..4674c9c --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ParserHandler.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 Red Hat, Inc. + *

+ * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + *

+ * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + *

+ * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + *

+ * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp.impl; + +import io.vertx.core.buffer.Buffer; + +@FunctionalInterface +public interface ParserHandler { + void handle(String contentType, Buffer body); +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java new file mode 100644 index 0000000..e04695a --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java @@ -0,0 +1,213 @@ +/* + * Copyright 2019 Red Hat, Inc. + *

+ * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + *

+ * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + *

+ * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + *

+ * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp.impl; + +import io.vertx.core.buffer.Buffer; + +import java.nio.charset.Charset; + +final class ReadableBuffer { + + // limit of integer parsing before overflowing + private static final long MAX_LONG_DIV_10 = Long.MAX_VALUE / 10; + private static final int MAX_INTEGER_DIV_10 = Integer.MAX_VALUE / 10; + private static final int MARK_WATERMARK = 4 * 1024 * 1024; + + private Buffer buffer; + private int offset; + + private int mark; + + void append(Buffer chunk) { + // either the buffer is null or all read + if (buffer == null || Math.min(mark, offset) == buffer.length()) { + buffer = chunk; + offset = 0; + return; + } + + // slice the buffer discarding the read bytes + if ( + // the offset (read operations) must be further than the last checkpoint + offset >= mark && + // there must be already read more than water mark + mark > MARK_WATERMARK && + // and there are more bytes to read already + buffer.length() > mark) { + + // clean up when there's too much data + buffer = buffer.getBuffer(mark, buffer.length()); + offset -= mark; + mark = 0; + } + + buffer.appendBuffer(chunk); + } + + int findLineEnd() { + int index = -1; + for (int i = offset; i < buffer.length(); i++) { + if (buffer.getByte(i) == '\n') { + index = i; + break; + } + } + + return (index > 0 && buffer.getByte(index - 1) == '\r') ? index + 1 : -1; + } + + long readLong(int end) { + long value = 0; + + boolean negative = buffer.getByte(this.offset) == '-'; + + int offset = negative ? this.offset + 1 : this.offset; + + while (offset < end) { + if (value > MAX_LONG_DIV_10) { + throw new ArithmeticException("Overflow"); + } + + int digit = buffer.getByte(offset++) - '0'; + + if (digit < 0 || digit > 9) { + throw new IllegalStateException("Not a digit " + (char) digit); + } + + value = value * 10 - digit; + } + if (!negative) value = -value; + this.offset = end; + return value; + } + + int readInt(int end) { + int value = 0; + + boolean negative = buffer.getByte(this.offset) == '-'; + + int offset = negative ? this.offset + 1 : this.offset; + + while (offset < end) { + if (value > MAX_INTEGER_DIV_10) { + throw new ArithmeticException("Overflow"); + } + + int digit = buffer.getByte(offset++) - '0'; + + if (digit < 0 || digit > 9) { + throw new IllegalStateException("Not a digit " + (char) digit); + } + + value = value * 10 - digit; + } + if (!negative) value = -value; + this.offset = end; + return value; + } + + Buffer readLine() { + return readLine(findLineEnd()); + } + + String readLine(Charset charset) { + return readLine(findLineEnd(), charset); + } + + private Buffer readLine(int end) { + Buffer bytes = null; + if (end >= offset) { + bytes = buffer.getBuffer(offset, end); + offset = end; + } + return bytes; + } + + String readLine(int end, Charset charset) { + byte[] bytes = null; + if (end >= offset) { + bytes = buffer.getBytes(offset, end); + offset = end; + } + if (bytes != null) { + return new String(bytes, charset); + } + return null; + } + + Buffer readBytes(int count) { + Buffer bytes = null; + if (buffer.length() - offset >= count) { + bytes = buffer.getBuffer(offset, offset + count); + offset += count; + } + return bytes; + } + + byte readByte() { + return buffer.getByte(offset++); + } + + byte getByte(int index) { + return buffer.getByte(index); + } + + int readableBytes() { + return buffer.length() - offset; + } + + void mark() { + mark = offset; + } + + void reset() { + offset = mark; + } + + int offset() { + return offset; + } + + boolean skip(int count) { + if (readableBytes() >= count) { + offset += count; + return true; + } + + return false; + } + + @Override + public String toString() { + return buffer != null ? buffer.toString() : "null"; + } + + public boolean startsWith(byte[] prefix, int toffset) { + final int len = prefix.length; + + if (len > toffset - offset) { + return false; + } + + for (int i = 0; i < len; i++) { + if (Character.toLowerCase(getByte(offset + i)) != prefix[i]) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java new file mode 100644 index 0000000..28d380e --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java @@ -0,0 +1,171 @@ +/* + * Copyright 2019 Red Hat, Inc. + *

+ * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + *

+ * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + *

+ * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + *

+ * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp.impl; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +public final class StreamParser implements Handler { + + private static final Logger log = LoggerFactory.getLogger(StreamParser.class); + + private static final Buffer EMPTY = Buffer.buffer(0); + private static final byte[] CONTENT_TYPE = "Content-Type:".toLowerCase().getBytes(); + private static final byte[] CONTENT_LENGTH = "Content-Length:".toLowerCase().getBytes(); + + // the callback when a full response message has been decoded + private ParserHandler handler; + private Handler exceptionHandler; + + // a composite buffer to allow buffer concatenation as if it was + // a long stream + private final ReadableBuffer buffer = new ReadableBuffer(); + + public StreamParser handler(ParserHandler handler) { + this.handler = handler; + return this; + } + + public StreamParser exceptionHandler(Handler handler) { + this.exceptionHandler = handler; + return this; + } + + // parser state machine state + private boolean eol = true; + private int contentLength = 0; + private String contentType = null; + + @Override + public void handle(Buffer chunk) { + // add the chunk to the buffer + buffer.append(chunk); + + while (buffer.readableBytes() >= (eol ? 3 : contentLength)) { + // setup a rollback point + buffer.mark(); + + // we need to locate the eol + if (eol) { + // locate the eol and handle as a C string + final int start = buffer.offset(); + final int eol = buffer.findLineEnd(); // including \r\n + final int end = eol - 2; + + // not found at all + if (eol == -1) { + buffer.reset(); + break; + } + + final int length = eol - start - 2; + + if (length == 0) { + // switch from headers to content + this.eol = false; + } else { + // process line + if ( + ('{' == buffer.getByte(start) && '}' == buffer.getByte(end - 1)) || + ('[' == buffer.getByte(start) && ']' == buffer.getByte(end - 1)) + ) { + // special case, when no headers were used + Buffer payload = buffer.readBytes(length); + if (handler != null) { + try { + handler.handle(contentType, payload); + } catch (RuntimeException e) { + log.error("Cannot handle message", e); + } + } else { + log.info("No handler expecting: " + payload); + } + // reset as this was a message without headers + contentType = null; + contentLength = 0; + } + else if (buffer.startsWith(CONTENT_TYPE, end)) { + // guaranteed to be possible as the startswith passed + buffer.skip(13); + // skip white space + while (buffer.getByte(buffer.offset()) == ' ') { + buffer.skip(1); + } + contentType = buffer.readLine(end, StandardCharsets.UTF_8); + } + else if (buffer.startsWith(CONTENT_LENGTH, end)) { + // guaranteed to be possible as the startswith passed + buffer.skip(15); + // skip white space + while (buffer.getByte(buffer.offset()) == ' ') { + buffer.skip(1); + } + + try { + // set the expected content length + contentLength = buffer.readInt(end); + } catch (RuntimeException e) { + if (exceptionHandler != null) { + exceptionHandler.handle(e); + } else { + log.error("Cannot parse Content-Length", e); + } + return; + } + } else { + log.warn("Unhandled header: " + buffer.readLine(end, StandardCharsets.UTF_8)); + } + } + // skip \r\n + buffer.skip(2); + } else { + // empty string + if (contentLength == 0) { + // special case as we don't need to allocate objects for this + if (handler != null) { + try { + handler.handle(contentType, EMPTY); + } catch (RuntimeException e) { + log.error("Cannot handle message", e); + } + } else { + log.info("No handler expecting: \"\""); + } + } else { + // fixed length parsing && read the required bytes + Buffer b = buffer.readBytes(contentLength); + if (handler != null) { + try { + handler.handle(contentType, b); + } catch (RuntimeException e) { + log.error("Cannot handle message", e); + } + } else { + log.info("No handler expecting: " + b); + } + } + // switch back to eol parsing + eol = true; + contentType = null; + contentLength = 0; + } + } + } +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java new file mode 100644 index 0000000..2c45195 --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java @@ -0,0 +1,17 @@ +package io.vertx.ext.eventbus.bridge.tcp.impl.codec; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.eventbus.bridge.tcp.impl.ParserHandler; + +public class JsonRPC implements ParserHandler { + @Override + public void handle(String contentType, Buffer body) { + // TODO: handle content type + try { + JsonObject json = new JsonObject(body); + } catch (RuntimeException e) { + + } + } +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java new file mode 100644 index 0000000..1b34570 --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp.impl.protocol; + +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.eventbus.ReplyException; +import io.vertx.core.json.JsonObject; +import io.vertx.core.streams.WriteStream; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Helper class to format and send frames over a socket + * + * @author Paulo Lopes + */ +public class JsonRPCHelper { + + private JsonRPCHelper() { + } + + public static void request(String method, Object id, JsonObject params, MultiMap headers, WriteStream handler) { + + final JsonObject payload = new JsonObject().put("jsonrpc", "2.0"); + + if (method == null) { + throw new IllegalStateException("method cannot be null"); + } + + payload.put("method", method); + + if (id != null) { + payload.put("id", id); + } + + if (params != null) { + payload.put("params", params.copy()); + } + + // write + if (headers != null) { + headers.forEach(entry -> { + handler.write( + Buffer.buffer(entry.getKey()).appendString(": ").appendString(entry.getValue()).appendString("\r\n") + ); + }); + // end of headers + handler.write(Buffer.buffer("\r\n")); + } + + handler.write(payload.toBuffer().appendString("\r\n")); + } + + public static void request(String method, Object id, JsonObject params, WriteStream handler) { + request(method, id, params, null, handler); + } + + public static void request(String method, Object id, WriteStream handler) { + request(method, id, null, null, handler); + } + + public static void request(String method, WriteStream handler) { + request(method, null, null, null, handler); + } + + public static void request(String method, JsonObject params, WriteStream handler) { + request(method, null, params, null, handler); + } + + public static void response(Object id, Object result, WriteStream handler) { + final JsonObject payload = new JsonObject() + .put("jsonrpc", "2.0") + .put("id", id) + .put("result", result); + + handler.write(payload.toBuffer().appendString("\r\n")); + } + + public static void error(Object id, Number code, String message, WriteStream handler) { + final JsonObject payload = new JsonObject() + .put("jsonrpc", "2.0") + .put("id", id); + + final JsonObject error = new JsonObject(); + payload.put("error", error); + + if (code != null) { + error.put("code", code); + } + + if (message != null) { + error.put("message", message); + } + + handler.write(payload.toBuffer().appendString("\r\n")); + } + + public static void error(Object id, ReplyException failure, WriteStream handler) { + error(id, failure.failureCode(), failure.getMessage(), handler); + } + + public static void error(Object id, String message, WriteStream handler) { + error(id, -32000, message, handler); + } +} diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java new file mode 100644 index 0000000..b2edc67 --- /dev/null +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -0,0 +1,619 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetClient; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.impl.StreamParser; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.RunTestOnContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.*; + +@RunWith(VertxUnitRunner.class) +public class JsonRPCStreamEventBusBridgeTest { + + String id() { + return UUID.randomUUID().toString(); + } + + @Rule + public RunTestOnContext rule = new RunTestOnContext(); + + private volatile Handler eventHandler = event -> event.complete(true); + + @Before + public void before(TestContext should) { + final Async test = should.async(); + final Vertx vertx = rule.vertx(); + + vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + + vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); + + vertx.setPeriodic(1000, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); + + vertx.createNetServer() + .connectHandler(JsonRPCStreamEventBusBridge.create( + vertx, + new BridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("hello")) + .addInboundPermitted(new PermittedOptions().setAddress("echo")) + .addInboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("echo")) + .addOutboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("ping")), + event -> eventHandler.handle(event))) + .listen(7000, res -> { + should.assertTrue(res.succeeded()); + test.complete(); + }); + } + + @Test(timeout = 10_000L) + public void testSendVoidMessage(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + vertx.eventBus().consumer("test", (Message msg) -> { + client.close(); + test.complete(); + }); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + request("send", new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), socket); + })); + } + + @Test(timeout = 10_000L) + public void testNoHandlers(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .handler((contentType, body) -> { + JsonObject frame = new JsonObject(body); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + client.close(); + test.complete(); + }).exceptionHandler(should::fail); + + socket.handler(parser); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("body", new JsonObject().put("value", "vert.x")), + socket); + })); + } + + @Test(timeout = 10_000L) + public void testErrorReply(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + vertx.eventBus().consumer("test", (Message msg) -> { + msg.fail(0, "oops!"); + }); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + client.close(); + test.complete(); + }); + + socket.handler(parser); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("body", new JsonObject().put("value", "vert.x")), + socket); + })); + } + + @Test(timeout = 10_000L) + public void testSendsFromOtherSideOfBridge(TestContext should) { + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + final AtomicBoolean ack = new AtomicBoolean(false); + + // 2 replies will arrive: + // 1). acknowledge register + // 2). greeting + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + + if (!ack.getAndSet(true)) { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + } else { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertNotEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals(true, result.getBoolean("send")); + should.assertEquals("hi", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", "ping"), + socket); + })); + + } + + @Test(timeout = 10_000L) + public void testSendMessageWithReplyBacktrack(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals(true, result.getBoolean("send")); + should.assertEquals("Hello vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + }); + + socket.handler(parser); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "hello") + .put("body", new JsonObject().put("value", "vert.x")), + socket); + })); + } + + @Test(timeout = 10_000L) + public void testSendMessageWithReplyBacktrackTimeout(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + // This does not reply and will provoke a timeout + vertx.eventBus().consumer("test", (Message msg) -> { /* Nothing! */ }); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject error = frame.getJsonObject("error"); + + should.assertEquals("Timed out after waiting 100(ms) for a reply. address: __vertx.reply.1, repliedAddress: test", error.getString("message")); + should.assertEquals(-1, error.getInteger("code")); + + client.close(); + test.complete(); + }); + + socket.handler(parser); + + JsonObject headers = new JsonObject().put("timeout", 100L); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("headers", headers) + .put("body", new JsonObject().put("value", "vert.x")), + socket); + })); + } + + @Test(timeout = 10_000L) + public void testSendMessageWithDuplicateReplyID(TestContext should) { + // replies must always return to the same origin + + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + vertx.eventBus().consumer("third-party-receiver", msg -> should.fail()); + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + client.close(); + test.complete(); + }); + + socket.handler(parser); + + + request( + "send", + "third-party-receiver", + new JsonObject() + .put("address", "hello") + .put("body", new JsonObject().put("value", "vert.x")), + socket); + })); + } + + @Test(timeout = 10_000L) + public void testRegister(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + // 1 reply will arrive + // MESSAGE for echo + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals(false, result.getBoolean("send")); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + }); + + socket.handler(parser); + + request( + "register", + id(), + new JsonObject() + .put("address", "echo"), + socket); + + // now try to publish a message so it gets delivered both to the consumer registred on the startup and to this + // remote consumer + + request( + "publish", + id(), + new JsonObject() + .put("address", "echo") + .put("body", new JsonObject().put("value", "vert.x")), + socket); + })); + + } + + @Test(timeout = 10_000L) + public void testUnRegister(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + + final String address = "test"; + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + // 2 replies will arrive: + // 1). message published to test + // 2). err of NO_HANDLERS because of consumer for 'test' is unregistered. + final AtomicBoolean unregistered = new AtomicBoolean(false); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + if (unregistered.get()) { + // consumer on 'test' has been unregistered, send message will fail. + should.assertEquals("err", frame.getString("type")); + should.assertEquals("#backtrack", frame.getString("address")); + should.assertEquals("NO_HANDLERS", frame.getString("failureType")); + should.assertEquals("No handlers for address test", frame.getString("message")); + client.close(); + test.complete(); + } else { + // got message, then unregister the handler + should.assertNotEquals("err", frame.getString("type")); + should.assertEquals(false, frame.getBoolean("send")); + should.assertEquals("Vert.x", frame.getJsonObject("body").getString("value")); + unregistered.compareAndSet(false, true); + + request( + "unregister", + "#backtrack", + new JsonObject() + .put("address", address), + socket); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", address) + .put("body", new JsonObject().put("value", "This will fail anyway!")), + socket); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", address), + socket); + + request( + "publish", + "#backtrack", + new JsonObject() + .put("address", address) + .put("body", new JsonObject().put("value", "vert.x")), + socket); + })); + } + + @Test(timeout = 10_000L) + public void testReplyFromClient(TestContext should) { + // Send a request from java and get a response from the client + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + final String address = "test"; + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + if ("message".equals(frame.getString("type"))) { + should.assertEquals(true, frame.getBoolean("send")); + should.assertEquals("Vert.x", frame.getJsonObject("body").getString("value")); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", frame.getString("replyAddress")) + .put("body", new JsonObject().put("value", "You got it")), + socket); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", address), + socket); + + // There is now way to know that the register actually happened, wait a bit before sending. + vertx.setTimer(500L, timerId -> { + vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { + should.assertTrue(respMessage.succeeded()); + should.assertEquals("You got it", respMessage.result().body().getString("value")); + client.close(); + test.complete(); + }); + }); + + })); + + } + + @Test(timeout = 10_000L) + public void testFailFromClient(TestContext should) { + // Send a request from java and get a response from the client + final Vertx vertx = rule.vertx(); + + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + final String address = "test"; + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + if ("message".equals(frame.getString("type"))) { + should.assertEquals(true, frame.getBoolean("send")); + should.assertEquals("Vert.x", frame.getJsonObject("body").getString("value")); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", "echo"), + socket); + + //FrameHelper.writeFrame(new JsonObject().put("type", "send").put("address", frame.getString("replyAddress")).put("failureCode", 1234).put("message", "ooops!"), socket); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", address), + socket); + + // There is now way to know that the register actually happened, wait a bit before sending. + vertx.setTimer(500L, timerId -> { + vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { + should.assertTrue(respMessage.failed()); + should.assertEquals("ooops!", respMessage.cause().getMessage()); + client.close(); + test.complete(); + }); + }); + })); + } + + @Test(timeout = 10_000L) + public void testSendPing(TestContext should) { + final Vertx vertx = rule.vertx(); + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + // MESSAGE for ping + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + should.assertEquals("pong", frame.getString("result")); + client.close(); + test.complete(); + }); + + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + socket.handler(parser); + request( + "register", + "#backtrack", + new JsonObject() + .put("address", "echo"), + socket); + + request( + "ping", + "#backtrack", + socket); + })); + } + + @Test(timeout = 10_000L) + public void testNoAddress(TestContext should) { + final Vertx vertx = rule.vertx(); + + NetClient client = vertx.createNetClient(); + final Async test = should.async(); + final AtomicBoolean errorOnce = new AtomicBoolean(false); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((mimeType, body) -> { + JsonObject frame = new JsonObject(body); + if (!errorOnce.compareAndSet(false, true)) { + should.fail("Client gets error message twice!"); + } else { + should.assertEquals("err", frame.getString("type")); + should.assertEquals("missing_address", frame.getString("message")); + vertx.setTimer(200, l -> { + client.close(); + test.complete(); + }); + } + }); + client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + socket.handler(parser); + request( + "send", + "#backtrack", + socket); + })); + } + +} diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java new file mode 100644 index 0000000..c71b0da --- /dev/null +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java @@ -0,0 +1,48 @@ +package io.vertx.ext.eventbus.bridge.tcp; + +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.eventbus.bridge.tcp.impl.StreamParser; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.RunTestOnContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class StreamParserTest { + + @Rule + public RunTestOnContext rule = new RunTestOnContext(); + + @Test(timeout = 30_000) + public void testParseSimple(TestContext should) { + final Async test = should.async(); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((contentType, body) -> { + System.out.println(body.toString()); + test.complete(); + }); + + parser.handle(Buffer.buffer( + "Content-Length: 38\r\n" + + "Content-Type: application/vscode-jsonrpc;charset=utf-8\r\n" + + "\r\n" + + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"hi\"}")); + } + + @Test(timeout = 30_000) + public void testParseSimpleHeaderless(TestContext should) { + final Async test = should.async(); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler((contentType, body) -> { + System.out.println(body.toString()); + test.complete(); + }); + + parser.handle(Buffer.buffer("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"hi\"}\r\n")); + } +} From 0e3d01ce8c4e014959258d3cd2b7190a6827c7f4 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 24 Mar 2022 15:14:43 +0530 Subject: [PATCH 02/34] Fix testSendPing for JsonRPCStreamEventBusBridge The parser in the test assumes that it will get only 1 message i.e the message of the pong. However actually, the register request's response is also received by it. Since register is tested elsewhere and not needed to test ping, do not register in this test. Signed-off-by: Lucifer Morningstar --- .../bridge/tcp/JsonRPCStreamEventBusBridgeTest.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index b2edc67..9d60dcd 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -571,13 +571,6 @@ public void testSendPing(TestContext should) { client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { socket.handler(parser); - request( - "register", - "#backtrack", - new JsonObject() - .put("address", "echo"), - socket); - request( "ping", "#backtrack", From a50cf414495d9b761404c56a011ba5b1e84fd81a Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Sat, 2 Apr 2022 23:03:38 +0530 Subject: [PATCH 03/34] Fix testRegister The parser in the test assumes that it will get only 1 message i.e the message of the pong. However actually, three messages will arrive. 1 ACK for register, 1 ACK for publish and finally the message for publish message itself. Update the test to handle 3 messages. Signed-off-by: Lucifer Morningstar --- .../tcp/JsonRPCStreamEventBusBridgeTest.java | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index 9d60dcd..41b2f17 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -34,6 +34,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.*; @@ -337,31 +338,52 @@ public void testRegister(TestContext should) { final Async test = should.async(); client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + final AtomicInteger messageCount = new AtomicInteger(0); - // 1 reply will arrive - // MESSAGE for echo + // 3 messages will arrive + // 1) ACK for register message + // 2) ACK for publish message + // 3) MESSAGE for echo final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) .handler((mimeType, body) -> { JsonObject frame = new JsonObject(body); - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); + if (messageCount.get() == 0) { + // ACK for register message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time ACK for publish is expected + should.assertTrue(messageCount.compareAndSet(0, 1)); + } + else if (messageCount.get() == 1) { + // ACK for publish message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time reply for echo message is expected + should.assertTrue(messageCount.compareAndSet(1, 2)); + } else { + // reply for echo message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); - JsonObject result = frame.getJsonObject("result"); + JsonObject result = frame.getJsonObject("result"); - should.assertEquals(false, result.getBoolean("send")); - should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); - client.close(); - test.complete(); + should.assertEquals(false, result.getBoolean("isSend")); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + } }); socket.handler(parser); request( "register", - id(), + "#backtrack", new JsonObject() .put("address", "echo"), socket); @@ -371,10 +393,10 @@ public void testRegister(TestContext should) { request( "publish", - id(), + "#backtrack", new JsonObject() .put("address", "echo") - .put("body", new JsonObject().put("value", "vert.x")), + .put("body", new JsonObject().put("value", "Vert.x")), socket); })); From 6d00160ad9df7cf46166f665924fd565fb8f1993 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Sun, 3 Apr 2022 00:11:16 +0530 Subject: [PATCH 04/34] Fix testUnregister The parser in the test was not handling messages. Add handling for 1 ACK for register, 1 ACK for publish and 1 ACK for unregister in addition to publish and send message. The error checking also looks inconsistent/broken at the moment for now fix by conforming to whatever bridge returns. Reevaluate after tests are fixed. Signed-off-by: Lucifer Morningstar --- .../tcp/JsonRPCStreamEventBusBridgeTest.java | 74 +++++++++++++------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index 41b2f17..b8fd7f7 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -18,6 +18,7 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; +import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetClient; import io.vertx.ext.bridge.BridgeOptions; @@ -412,35 +413,50 @@ public void testUnRegister(TestContext should) { final String address = "test"; client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { - // 2 replies will arrive: - // 1). message published to test - // 2). err of NO_HANDLERS because of consumer for 'test' is unregistered. - final AtomicBoolean unregistered = new AtomicBoolean(false); + // 4 replies will arrive: + // 1). ACK for register + // 2). ACK for publish + // 3). message published to test + // 4). err of NO_HANDLERS because of consumer for 'test' is unregistered. + final AtomicInteger messageCount = new AtomicInteger(0); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) .handler((mimeType, body) -> { JsonObject frame = new JsonObject(body); - if (unregistered.get()) { - // consumer on 'test' has been unregistered, send message will fail. - should.assertEquals("err", frame.getString("type")); - should.assertEquals("#backtrack", frame.getString("address")); - should.assertEquals("NO_HANDLERS", frame.getString("failureType")); - should.assertEquals("No handlers for address test", frame.getString("message")); - client.close(); - test.complete(); - } else { + + if (messageCount.get() == 0) { + // ACK for register message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time ACK for publish is expected + should.assertTrue(messageCount.compareAndSet(0, 1)); + } + else if (messageCount.get() == 1) { + // ACK for publish message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time reply for echo message is expected + should.assertTrue(messageCount.compareAndSet(1, 2)); + } else if (messageCount.get() == 2) { // got message, then unregister the handler - should.assertNotEquals("err", frame.getString("type")); - should.assertEquals(false, frame.getBoolean("send")); - should.assertEquals("Vert.x", frame.getJsonObject("body").getString("value")); - unregistered.compareAndSet(false, true); + should.assertFalse(frame.containsKey("error")); + JsonObject result = frame.getJsonObject("result"); + should.assertEquals(false, result.getBoolean("isSend")); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); - request( - "unregister", - "#backtrack", - new JsonObject() - .put("address", address), - socket); + // increment message count so that next time ACK for unregister is expected + should.assertTrue(messageCount.compareAndSet(2, 3)); + + request("unregister", "#backtrack", new JsonObject().put("address", address), socket); + } else if (messageCount.get() == 3) { + // ACK for unregister message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time error reply for send message is expected + should.assertTrue(messageCount.compareAndSet(3, 4)); request( "send", @@ -449,6 +465,16 @@ public void testUnRegister(TestContext should) { .put("address", address) .put("body", new JsonObject().put("value", "This will fail anyway!")), socket); + } else { + // TODO: Check error handling of bridge for consistency + // consumer on 'test' has been unregistered, send message will fail. + should.assertTrue(frame.containsKey("error")); + JsonObject error = frame.getJsonObject("error"); + should.assertEquals(-1, error.getInteger("code")); + should.assertEquals("No handlers for address test", error.getString("message")); + + client.close(); + test.complete(); } }); @@ -466,7 +492,7 @@ public void testUnRegister(TestContext should) { "#backtrack", new JsonObject() .put("address", address) - .put("body", new JsonObject().put("value", "vert.x")), + .put("body", new JsonObject().put("value", "Vert.x")), socket); })); } From 282ea3875abeb34ccf270358c43f926bd3611a54 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Sun, 3 Apr 2022 00:38:58 +0530 Subject: [PATCH 05/34] Fix testNoAddress Fix test assertions like message format for invalid send. Signed-off-by: Lucifer Morningstar --- .../bridge/tcp/JsonRPCStreamEventBusBridgeTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index b8fd7f7..2a8b4bd 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -184,6 +184,7 @@ public void testSendsFromOtherSideOfBridge(TestContext should) { .exceptionHandler(should::fail) .handler((mimeType, body) -> { JsonObject frame = new JsonObject(body); + System.out.println(body); if (!ack.getAndSet(true)) { should.assertFalse(frame.containsKey("error")); @@ -196,7 +197,7 @@ public void testSendsFromOtherSideOfBridge(TestContext should) { JsonObject result = frame.getJsonObject("result"); - should.assertEquals(true, result.getBoolean("send")); + should.assertEquals(true, result.getBoolean("isSend")); should.assertEquals("hi", result.getJsonObject("body").getString("value")); client.close(); test.complete(); @@ -640,8 +641,8 @@ public void testNoAddress(TestContext should) { if (!errorOnce.compareAndSet(false, true)) { should.fail("Client gets error message twice!"); } else { - should.assertEquals("err", frame.getString("type")); - should.assertEquals("missing_address", frame.getString("message")); + should.assertTrue(frame.containsKey("error")); + should.assertEquals("invalid_parameters", frame.getJsonObject("error").getString("message")); vertx.setTimer(200, l -> { client.close(); test.complete(); From d475d9cca69aab36afef57f94cdb3ff66b9c9db8 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Sun, 3 Apr 2022 13:54:20 +0530 Subject: [PATCH 06/34] Fix testSendsFromOtherSideOfBridge The JSON-RPC 2.0 spec mandates value of id in response to be same as that in the request. The broken test assertion was trying to test otherwise, hence was wrong. Signed-off-by: Lucifer Morningstar --- .../eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index 2a8b4bd..aeec6d8 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -184,7 +184,6 @@ public void testSendsFromOtherSideOfBridge(TestContext should) { .exceptionHandler(should::fail) .handler((mimeType, body) -> { JsonObject frame = new JsonObject(body); - System.out.println(body); if (!ack.getAndSet(true)) { should.assertFalse(frame.containsKey("error")); @@ -193,7 +192,7 @@ public void testSendsFromOtherSideOfBridge(TestContext should) { } else { should.assertFalse(frame.containsKey("error")); should.assertTrue(frame.containsKey("result")); - should.assertNotEquals("#backtrack", frame.getValue("id")); + should.assertEquals("#backtrack", frame.getValue("id")); JsonObject result = frame.getJsonObject("result"); From 63d0b96ea1c89b2176b849aa2f9ae12dd260f024 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Sun, 3 Apr 2022 14:42:13 +0530 Subject: [PATCH 07/34] Fix testReplyFromClient The format of message in JSON-RPC is different from TCP bridge so need to fix test assertions accordingly. Signed-off-by: Lucifer Morningstar --- .../tcp/JsonRPCStreamEventBusBridgeTest.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index aeec6d8..0ed80d7 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -506,19 +506,26 @@ public void testReplyFromClient(TestContext should) { final String address = "test"; client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { + final AtomicBoolean ack = new AtomicBoolean(false); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) .handler((mimeType, body) -> { JsonObject frame = new JsonObject(body); - if ("message".equals(frame.getString("type"))) { - should.assertEquals(true, frame.getBoolean("send")); - should.assertEquals("Vert.x", frame.getJsonObject("body").getString("value")); + + if (!ack.getAndSet(true)) { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + } else { + JsonObject result = frame.getJsonObject("result"); + should.assertTrue(result.getBoolean("isSend")); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); request( "send", "#backtrack", new JsonObject() - .put("address", frame.getString("replyAddress")) + .put("address", result.getString("replyAddress")) .put("body", new JsonObject().put("value", "You got it")), socket); } From 0a38e69ffc31eb9b7b36b3e29dee64f192c4363f Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Mon, 4 Apr 2022 00:22:59 +0530 Subject: [PATCH 08/34] Fix testFailFromClient The implementation is looking for a top level failureCode field in the message which would be right for Vertx protocol but not for the JSON-RPC implementation. For now, I have changed it to look for an error field in the params. This may need adjusting as I couldn't find the case of JSON-RPC client sending back error message to server in the spec. Signed-off-by: Lucifer Morningstar --- .../impl/JsonRPCStreamEventBusBridgeImpl.java | 10 +++---- .../tcp/JsonRPCStreamEventBusBridgeTest.java | 27 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 5847d69..3cf93bb 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -238,13 +238,13 @@ private void send(NetSocket socket, Object id, JsonObject msg, Map { + final AtomicBoolean ack = new AtomicBoolean(false); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) .handler((mimeType, body) -> { JsonObject frame = new JsonObject(body); - if ("message".equals(frame.getString("type"))) { - should.assertEquals(true, frame.getBoolean("send")); - should.assertEquals("Vert.x", frame.getJsonObject("body").getString("value")); + if (!ack.getAndSet(true)) { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + } else { + JsonObject result = frame.getJsonObject("result"); + should.assertTrue(result.getBoolean("isSend")); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); request( - "register", - "#backtrack", + "send", + null, new JsonObject() - .put("address", "echo"), + .put("address", result.getString("replyAddress")) + .put("error", new JsonObject().put("failureCode", 1234).put("message", "ooops!")), socket); - - //FrameHelper.writeFrame(new JsonObject().put("type", "send").put("address", frame.getString("replyAddress")).put("failureCode", 1234).put("message", "ooops!"), socket); } }); From 923173efae6ab30025f4e9825970eda1fbee7d58 Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Thu, 2 Jun 2022 10:03:35 +0200 Subject: [PATCH 09/34] Adding a PoC on using websockets Signed-off-by: Paulo Lopes --- pom.xml | 1 - .../ext/eventbus/bridge/tcp/BridgeEvent.java | 5 +- .../tcp/JsonRPCStreamEventBusBridge.java | 7 ++ .../impl/JsonRPCStreamEventBusBridgeImpl.java | 99 +++++++++++++++++-- .../bridge/tcp/impl/protocol/FrameParser.java | 9 +- .../tcp/impl/protocol/JsonRPCHelper.java | 3 + .../bridge/tcp/InteropWebSocketServer.java | 71 +++++++++++++ .../tcp/TcpEventBusBridgeEventTest.java | 4 +- src/test/resources/ws.html | 29 ++++++ 9 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java create mode 100644 src/test/resources/ws.html diff --git a/pom.xml b/pom.xml index 4e84d69..c4a45da 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,6 @@ io.vertx vertx-bridge-common - ${project.version} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java index 2eabcab..856324b 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java @@ -44,11 +44,14 @@ public interface BridgeEvent extends BaseBridgeEvent { @Fluent BridgeEvent setRawMessage(JsonObject message); + // TODO: this will cause problems with WebSockets as they don't share any common base interface + // this will be a breaking change to users, as the return type is now generic, is this OK? + /** * Get the SockJSSocket instance corresponding to the event * * @return the SockJSSocket instance */ @CacheReturn - NetSocket socket(); + T socket(); } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index 5d0a97e..f54fd0a 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -27,6 +27,13 @@ * * @author Paulo Lopes */ + +// TODO: "extends Handler" was a bad idea because it locks the implementation to TCP sockets. Instead we +// should have explicit methods that either handle a NetSocket or a WebSocketBase: +// handle(NetSocket socket) handle(WebSocketBase socket) +// or: return a handler, e.g.: +// Handler webSocketHandler(); +// Handler netSocketHandler(); @VertxGen public interface JsonRPCStreamEventBusBridge extends Handler { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 3cf93bb..7ce002d 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -17,11 +17,14 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.*; +import io.vertx.core.http.WebSocketBase; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetSocket; +import io.vertx.core.streams.WriteStream; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; @@ -58,33 +61,33 @@ public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handl this.bridgeEventHandler = eventHandler; } - private void dispatch(NetSocket socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { + private void dispatch(WriteStream socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { switch (method) { case "send": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.SEND, msg, socket), + () -> new BridgeEventImpl(BridgeEventType.SEND, msg, null), () -> send(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); break; case "publish": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.SEND, msg, socket), + () -> new BridgeEventImpl(BridgeEventType.SEND, msg, null), () -> publish(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); break; case "register": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.REGISTER, msg, socket), + () -> new BridgeEventImpl(BridgeEventType.REGISTER, msg, null), () -> register(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); break; case "unregister": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.UNREGISTER, msg, socket), + () -> new BridgeEventImpl(BridgeEventType.UNREGISTER, msg, null), () -> unregister(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); @@ -98,7 +101,7 @@ private void dispatch(NetSocket socket, String method, Object id, JsonObject msg } } - private void unregister(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + private void unregister(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -123,7 +126,7 @@ private void unregister(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + private void register(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -157,7 +160,7 @@ private void register(NetSocket socket, Object id, JsonObject msg, Map new BridgeEventImpl(BridgeEventType.REGISTERED, msg, socket), + () -> new BridgeEventImpl(BridgeEventType.REGISTERED, msg, null), () -> { if (id != null) { // ack @@ -169,7 +172,7 @@ private void register(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + private void publish(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -192,7 +195,7 @@ private void publish(NetSocket socket, Object id, JsonObject msg, Map> registry, Map> replies) { + private void send(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -326,6 +329,82 @@ public void handle(NetSocket socket) { ); } + public void handle(WebSocketBase socket) { + checkCallHook( + // process the new socket according to the event handler + () -> new BridgeEventImpl(BridgeEventType.SOCKET_CREATED, null, null), + // on success + () -> { + final Map> registry = new ConcurrentHashMap<>(); + final Map> replies = new ConcurrentHashMap<>(); + + socket + .exceptionHandler(t -> { + log.error(t.getMessage(), t); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }) + .endHandler(v -> { + registry.values().forEach(MessageConsumer::unregister); + // normal close, trigger the event + checkCallHook(() -> new BridgeEventImpl(BridgeEventType.SOCKET_CLOSED, null, null)); + registry.clear(); + }) + .frameHandler(frame -> { + // TODO: this could be an [], in this case, after parsing, we should loop and call for each element the + // code bellow. + + // One idea from vs-jsonrpcstream was the use of content-types, so define how the message was formated + // by default json (like in the spec) but microsoft was suggesting messagepack as alternative. I'm not + // sure if we should implement this. The TCP parser was accounting for it, but is it a good idea? maybe not? + + final JsonObject msg = new JsonObject(frame.binaryData()); + + // validation + if (!"2.0".equals(msg.getString("jsonrpc"))) { + log.error("Invalid message: " + msg); + return; + } + + final String method = msg.getString("method"); + if (method == null) { + log.error("Invalid method: " + msg.getString("method")); + return; + } + + final Object id = msg.getValue("id"); + if (id != null) { + if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { + log.error("Invalid id: " + msg.getValue("id")); + return; + } + } + + // TODO: we should wrap the socket in order to override the "write" method to write a text frame + // TODO: the current WriteStream assumes binary frames which are harder to handle on the browser + // TODO: maybe we could make this configurable (binary/text) + + // if we create a wraper, say an interface: + // interface SocketWriter { write(Buffer buff) } + // then we can create specific implementation wrappers for all kinds of sockets, netSocket, webSocket (binary or text) + + // given that the wraper is at the socket level (it's not that heavy in terms of garbage collection, 1 extra object per connection. + // And a connection is long lasting, not like HTTP + + dispatch( + socket, + method, + id, + msg, + registry, + replies); + }); + }, + // on failure + socket::close + ); + } + private void checkCallHook(Supplier eventSupplier) { checkCallHook(eventSupplier, null, null); } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/FrameParser.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/FrameParser.java index b5e1f3e..3af18c3 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/FrameParser.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/FrameParser.java @@ -23,6 +23,12 @@ import io.vertx.core.json.JsonObject; /** + * TODO: refactor this whole thing to be simpler. Avoid the header's parsing, that is probably a bad idea it was very VisualStudio specific + * once we do that, don't rely on line endings as end of message. Instead we need to locate the end of a message. + * To locate the end of the message, we need to count braces. If a message starts with "{" we increase the counter, + * every time we see "}" we decrease. If we reach 0 it's a full message. If we ever go negative we're on a broken state. + * The same for "[" as jsonrpc batches are just an array of messages + * * Simple LV parser * * @author Paulo Lopes @@ -64,6 +70,7 @@ public void handle(Buffer buffer) { if (remainingBytes - 4 >= length) { // we have a complete message try { + // TODO: this is wrong, we can have both JsonObject or JsonArray client.handle(Future.succeededFuture(new JsonObject(_buffer.getString(_offset, _offset + length)))); } catch (DecodeException e) { // bad json @@ -111,4 +118,4 @@ private void append(Buffer newBuffer) { private int bytesRemaining() { return (_buffer.length() - _offset) < 0 ? 0 : (_buffer.length() - _offset); } -} \ No newline at end of file +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java index 1b34570..3991ab4 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java @@ -34,6 +34,9 @@ public class JsonRPCHelper { private JsonRPCHelper() { } + // TODO: Should we refactor this helpers to return the buffer with the encoded message and let the caller perform + // the write? This would allow the caller to differentiate from a binary write from a text write? + // The same applies to all methods on this helper class public static void request(String method, Object id, JsonObject params, MultiMap headers, WriteStream handler) { final JsonObject payload = new JsonObject().put("jsonrpc", "2.0"); diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java new file mode 100644 index 0000000..eec823d --- /dev/null +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -0,0 +1,71 @@ +package io.vertx.ext.eventbus.bridge.tcp; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.impl.JsonRPCStreamEventBusBridgeImpl; + +public class InteropWebSocketServer extends AbstractVerticle { + + // To test just run this application from the IDE and then open the browser on http://localhost:8080 + // later we can also automate this with a vert.x web client, I'll show you next week how to bootstrap it. + public static void main(String[] args) { + Vertx.vertx().deployVerticle(new InteropWebSocketServer()); + } + + @Override + public void start(Promise start) { + // just to have some messages flowing around + vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); + vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); + + // once we fix the interface we can avoid the casts + JsonRPCStreamEventBusBridgeImpl bridge = (JsonRPCStreamEventBusBridgeImpl) JsonRPCStreamEventBusBridge.create( + vertx, + new BridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("hello")) + .addInboundPermitted(new PermittedOptions().setAddress("echo")) + .addInboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("echo")) + .addOutboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("ping"))); + + vertx + .createHttpServer() + .requestHandler(req -> { + // this is where any http request will land + + if ("/jsonrpc".equals(req.path())) { + // we switch from HTTP to WebSocket + req.toWebSocket() + .onFailure(err -> { + err.printStackTrace(); + req.response().setStatusCode(500).end(err.getMessage()); + }) + .onSuccess(bridge::handle); + } else { + // serve the base HTML application + if ("/".equals(req.path())) { + req.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") + .sendFile("ws.html"); + } else { + // 404 all the rest + req.response().setStatusCode(404).end("Not Found"); + } + } + }) + .listen(8080) + .onFailure(start::fail) + .onSuccess(server -> { + System.out.println("Server listening at http://localhost:8080"); + start.complete(); + }); + } +} diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java index 175c3e8..0cc2cd5 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java @@ -69,9 +69,9 @@ public void before(TestContext context) { .setKeyStoreOptions(sslKeyPairCerts.getServerKeyStore()), be -> { logger.info("Handled a bridge event " + be.getRawMessage()); - if (be.socket().isSsl()) { + if (be.socket().isSsl()) { try { - for (Certificate c : be.socket().peerCertificates()) { + for (Certificate c : be.socket().peerCertificates()) { logger.info(((X509Certificate)c).getSubjectDN().toString()); } } catch (SSLPeerUnverifiedException e) { diff --git a/src/test/resources/ws.html b/src/test/resources/ws.html new file mode 100644 index 0000000..5207a4d --- /dev/null +++ b/src/test/resources/ws.html @@ -0,0 +1,29 @@ + + + + + + + + From a3c6e507e29507c25644050fe959abcb547f9ec5 Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Thu, 2 Jun 2022 13:45:43 +0200 Subject: [PATCH 10/34] Update parser to not rely on line endings but message boundaries Signed-off-by: Paulo Lopes --- .../impl/JsonRPCStreamEventBusBridgeImpl.java | 4 +- .../bridge/tcp/impl/ReadableBuffer.java | 171 +++++------------- .../bridge/tcp/impl/StreamParser.java | 160 +++++----------- .../bridge/tcp/impl/codec/JsonRPC.java | 17 -- .../tcp/JsonRPCStreamEventBusBridgeTest.java | 32 ++-- .../eventbus/bridge/tcp/StreamParserTest.java | 23 ++- 6 files changed, 125 insertions(+), 282 deletions(-) delete mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 7ce002d..7e55e5a 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -289,11 +289,11 @@ public void handle(NetSocket socket) { .exceptionHandler(t -> { log.error(t.getMessage(), t); }) - .handler((contentType, body) -> { + .handler(buffer -> { // TODO: handle content type // TODO: body may be an array (batching) - final JsonObject msg = new JsonObject(body); + final JsonObject msg = new JsonObject(buffer); // validation if (!"2.0".equals(msg.getString("jsonrpc"))) { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java index e04695a..bcab1a3 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/ReadableBuffer.java @@ -17,18 +17,12 @@ import io.vertx.core.buffer.Buffer; -import java.nio.charset.Charset; - final class ReadableBuffer { - // limit of integer parsing before overflowing - private static final long MAX_LONG_DIV_10 = Long.MAX_VALUE / 10; - private static final int MAX_INTEGER_DIV_10 = Integer.MAX_VALUE / 10; - private static final int MARK_WATERMARK = 4 * 1024 * 1024; + private static final int MARK_WATERMARK = 4 * 1024; private Buffer buffer; private int offset; - private int mark; void append(Buffer chunk) { @@ -57,114 +51,77 @@ void append(Buffer chunk) { buffer.appendBuffer(chunk); } - int findLineEnd() { - int index = -1; + int findSTX() { for (int i = offset; i < buffer.length(); i++) { - if (buffer.getByte(i) == '\n') { - index = i; - break; + byte b = buffer.getByte(i); + switch (b) { + case '\r': + case '\n': + // skip new lines + continue; + case '{': + case '[': + return i; + default: + throw new IllegalStateException("Unexpected value in buffer: (int)" + ((int) b)); } } - return (index > 0 && buffer.getByte(index - 1) == '\r') ? index + 1 : -1; + return -1; } - long readLong(int end) { - long value = 0; - - boolean negative = buffer.getByte(this.offset) == '-'; - - int offset = negative ? this.offset + 1 : this.offset; - - while (offset < end) { - if (value > MAX_LONG_DIV_10) { - throw new ArithmeticException("Overflow"); - } + int findETX(int offset) { + // brace start / end + final byte bs, be; + // brace count + int bc = 0; - int digit = buffer.getByte(offset++) - '0'; - - if (digit < 0 || digit > 9) { - throw new IllegalStateException("Not a digit " + (char) digit); - } - - value = value * 10 - digit; + switch (buffer.getByte(offset)) { + case '{': + bs = '{'; + be = '}'; + break; + case '[': + bs = '['; + be = ']'; + break; + default: + throw new IllegalStateException("Message 1st byte isn't valid: " + buffer.getByte(offset)); } - if (!negative) value = -value; - this.offset = end; - return value; - } - int readInt(int end) { - int value = 0; - - boolean negative = buffer.getByte(this.offset) == '-'; - - int offset = negative ? this.offset + 1 : this.offset; - - while (offset < end) { - if (value > MAX_INTEGER_DIV_10) { - throw new ArithmeticException("Overflow"); + for (int i = offset; i < buffer.length(); i++) { + byte b = buffer.getByte(i); + if (b == bs) { + bc++; + } else + if (b == be) { + bc--; + } else { + continue; } - - int digit = buffer.getByte(offset++) - '0'; - - if (digit < 0 || digit > 9) { - throw new IllegalStateException("Not a digit " + (char) digit); + // validation + if (bc < 0) { + // unbalanced braces + throw new IllegalStateException("Message format is not valid: " + buffer.getString(offset, i) + "..."); + } + if (bc == 0) { + // complete + return i + 1; } - - value = value * 10 - digit; - } - if (!negative) value = -value; - this.offset = end; - return value; - } - - Buffer readLine() { - return readLine(findLineEnd()); - } - - String readLine(Charset charset) { - return readLine(findLineEnd(), charset); - } - - private Buffer readLine(int end) { - Buffer bytes = null; - if (end >= offset) { - bytes = buffer.getBuffer(offset, end); - offset = end; } - return bytes; - } - String readLine(int end, Charset charset) { - byte[] bytes = null; - if (end >= offset) { - bytes = buffer.getBytes(offset, end); - offset = end; - } - if (bytes != null) { - return new String(bytes, charset); - } - return null; + return -1; } - Buffer readBytes(int count) { + Buffer readBytes(int offset, int count) { Buffer bytes = null; if (buffer.length() - offset >= count) { bytes = buffer.getBuffer(offset, offset + count); - offset += count; + this.offset = offset + count; } return bytes; } - byte readByte() { - return buffer.getByte(offset++); - } - - byte getByte(int index) { - return buffer.getByte(index); - } - int readableBytes() { return buffer.length() - offset; } @@ -177,37 +134,9 @@ void reset() { offset = mark; } - int offset() { - return offset; - } - - boolean skip(int count) { - if (readableBytes() >= count) { - offset += count; - return true; - } - - return false; - } @Override public String toString() { return buffer != null ? buffer.toString() : "null"; } - - public boolean startsWith(byte[] prefix, int toffset) { - final int len = prefix.length; - - if (len > toffset - offset) { - return false; - } - - for (int i = 0; i < len; i++) { - if (Character.toLowerCase(getByte(offset + i)) != prefix[i]) { - return false; - } - } - - return true; - } } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java index 28d380e..c8f0a85 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/StreamParser.java @@ -20,25 +20,19 @@ import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; -import java.nio.charset.StandardCharsets; - public final class StreamParser implements Handler { - private static final Logger log = LoggerFactory.getLogger(StreamParser.class); - - private static final Buffer EMPTY = Buffer.buffer(0); - private static final byte[] CONTENT_TYPE = "Content-Type:".toLowerCase().getBytes(); - private static final byte[] CONTENT_LENGTH = "Content-Length:".toLowerCase().getBytes(); + private static final Logger LOGGER = LoggerFactory.getLogger(StreamParser.class); // the callback when a full response message has been decoded - private ParserHandler handler; + private Handler handler; private Handler exceptionHandler; // a composite buffer to allow buffer concatenation as if it was // a long stream private final ReadableBuffer buffer = new ReadableBuffer(); - public StreamParser handler(ParserHandler handler) { + public StreamParser handler(Handler handler) { this.handler = handler; return this; } @@ -48,123 +42,51 @@ public StreamParser exceptionHandler(Handler handler) { return this; } - // parser state machine state - private boolean eol = true; - private int contentLength = 0; - private String contentType = null; - @Override public void handle(Buffer chunk) { - // add the chunk to the buffer - buffer.append(chunk); - - while (buffer.readableBytes() >= (eol ? 3 : contentLength)) { - // setup a rollback point - buffer.mark(); - - // we need to locate the eol - if (eol) { - // locate the eol and handle as a C string - final int start = buffer.offset(); - final int eol = buffer.findLineEnd(); // including \r\n - final int end = eol - 2; - - // not found at all - if (eol == -1) { - buffer.reset(); - break; - } + if (chunk.length() > 0) { + // add the chunk to the buffer + buffer.append(chunk); - final int length = eol - start - 2; - - if (length == 0) { - // switch from headers to content - this.eol = false; - } else { - // process line - if ( - ('{' == buffer.getByte(start) && '}' == buffer.getByte(end - 1)) || - ('[' == buffer.getByte(start) && ']' == buffer.getByte(end - 1)) - ) { - // special case, when no headers were used - Buffer payload = buffer.readBytes(length); - if (handler != null) { - try { - handler.handle(contentType, payload); - } catch (RuntimeException e) { - log.error("Cannot handle message", e); - } - } else { - log.info("No handler expecting: " + payload); - } - // reset as this was a message without headers - contentType = null; - contentLength = 0; - } - else if (buffer.startsWith(CONTENT_TYPE, end)) { - // guaranteed to be possible as the startswith passed - buffer.skip(13); - // skip white space - while (buffer.getByte(buffer.offset()) == ' ') { - buffer.skip(1); - } - contentType = buffer.readLine(end, StandardCharsets.UTF_8); + // the minimum messages are "{}" or "[]" + while (buffer.readableBytes() >= 2) { + // setup a rollback point + buffer.mark(); + + final Buffer payload; + + try { + // locate the message boundaries + final int start = buffer.findSTX(); + + // no start found yet + if (start == -1) { + buffer.reset(); + break; } - else if (buffer.startsWith(CONTENT_LENGTH, end)) { - // guaranteed to be possible as the startswith passed - buffer.skip(15); - // skip white space - while (buffer.getByte(buffer.offset()) == ' ') { - buffer.skip(1); - } - - try { - // set the expected content length - contentLength = buffer.readInt(end); - } catch (RuntimeException e) { - if (exceptionHandler != null) { - exceptionHandler.handle(e); - } else { - log.error("Cannot parse Content-Length", e); - } - return; - } - } else { - log.warn("Unhandled header: " + buffer.readLine(end, StandardCharsets.UTF_8)); + + final int end = buffer.findETX(start); + + // no end found yet + if (end == -1) { + buffer.reset(); + break; } + + payload = buffer.readBytes(start, end - start); + } catch (IllegalStateException ise) { + exceptionHandler.handle(ise); + break; } - // skip \r\n - buffer.skip(2); - } else { - // empty string - if (contentLength == 0) { - // special case as we don't need to allocate objects for this - if (handler != null) { - try { - handler.handle(contentType, EMPTY); - } catch (RuntimeException e) { - log.error("Cannot handle message", e); - } - } else { - log.info("No handler expecting: \"\""); - } - } else { - // fixed length parsing && read the required bytes - Buffer b = buffer.readBytes(contentLength); - if (handler != null) { - try { - handler.handle(contentType, b); - } catch (RuntimeException e) { - log.error("Cannot handle message", e); - } - } else { - log.info("No handler expecting: " + b); - } + + // payload is found, deliver it to the handler + try { + handler.handle(payload); + } catch (RuntimeException e) { + // these are user exceptions, not protocol exceptions + // we can continue the parsing job + LOGGER.error("Failed to handle payload", e); } - // switch back to eol parsing - eol = true; - contentType = null; - contentLength = 0; } } } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java deleted file mode 100644 index 2c45195..0000000 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/codec/JsonRPC.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.vertx.ext.eventbus.bridge.tcp.impl.codec; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.eventbus.bridge.tcp.impl.ParserHandler; - -public class JsonRPC implements ParserHandler { - @Override - public void handle(String contentType, Buffer body) { - // TODO: handle content type - try { - JsonObject json = new JsonObject(body); - } catch (RuntimeException e) { - - } - } -} diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index 9f801b1..5a458ae 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -18,14 +18,11 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; -import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetClient; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.StreamParser; -import io.vertx.ext.eventbus.bridge.tcp.impl.codec.JsonRPC; -import io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.RunTestOnContext; @@ -35,7 +32,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -47,7 +43,7 @@ public class JsonRPCStreamEventBusBridgeTest { @Rule public RunTestOnContext rule = new RunTestOnContext(); - private volatile Handler eventHandler = event -> event.complete(true); + private final Handler eventHandler = event -> event.complete(true); @Before public void before(TestContext should) { @@ -70,7 +66,7 @@ public void before(TestContext should) { .addOutboundPermitted(new PermittedOptions().setAddress("echo")) .addOutboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("ping")), - event -> eventHandler.handle(event))) + eventHandler)) .listen(7000, res -> { should.assertTrue(res.succeeded()); test.complete(); @@ -104,7 +100,7 @@ public void testNoHandlers(TestContext should) { client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { final StreamParser parser = new StreamParser() - .handler((contentType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); should.assertTrue(frame.containsKey("error")); @@ -142,7 +138,7 @@ public void testErrorReply(TestContext should) { final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); should.assertTrue(frame.containsKey("error")); @@ -180,7 +176,7 @@ public void testSendsFromOtherSideOfBridge(TestContext should) { // 2). greeting final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); if (!ack.getAndSet(true)) { @@ -224,7 +220,7 @@ public void testSendMessageWithReplyBacktrack(TestContext should) { final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); should.assertFalse(frame.containsKey("error")); @@ -265,7 +261,7 @@ public void testSendMessageWithReplyBacktrackTimeout(TestContext should) { final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); should.assertTrue(frame.containsKey("error")); @@ -310,7 +306,7 @@ public void testSendMessageWithDuplicateReplyID(TestContext should) { final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); client.close(); test.complete(); @@ -345,7 +341,7 @@ public void testRegister(TestContext should) { // 3) MESSAGE for echo final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); if (messageCount.get() == 0) { @@ -419,7 +415,7 @@ public void testUnRegister(TestContext should) { final AtomicInteger messageCount = new AtomicInteger(0); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); if (messageCount.get() == 0) { @@ -507,7 +503,7 @@ public void testReplyFromClient(TestContext should) { final AtomicBoolean ack = new AtomicBoolean(false); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); if (!ack.getAndSet(true)) { @@ -565,7 +561,7 @@ public void testFailFromClient(TestContext should) { final AtomicBoolean ack = new AtomicBoolean(false); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); if (!ack.getAndSet(true)) { should.assertFalse(frame.containsKey("error")); @@ -615,7 +611,7 @@ public void testSendPing(TestContext should) { // MESSAGE for ping final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); should.assertFalse(frame.containsKey("error")); @@ -645,7 +641,7 @@ public void testNoAddress(TestContext should) { final AtomicBoolean errorOnce = new AtomicBoolean(false); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((mimeType, body) -> { + .handler(body -> { JsonObject frame = new JsonObject(body); if (!errorOnce.compareAndSet(false, true)) { should.fail("Client gets error message twice!"); diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java index c71b0da..8db0ff0 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/StreamParserTest.java @@ -21,24 +21,37 @@ public void testParseSimple(TestContext should) { final Async test = should.async(); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((contentType, body) -> { - System.out.println(body.toString()); + .handler(body -> { + // extra line feed and carriage return are ignored + should.assertEquals("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"hi\"}", body.toString()); test.complete(); }); parser.handle(Buffer.buffer( - "Content-Length: 38\r\n" + - "Content-Type: application/vscode-jsonrpc;charset=utf-8\r\n" + "\r\n" + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"hi\"}")); } + @Test(timeout = 30_000) + public void testParseSimpleWithPreambleFail(TestContext should) { + final Async test = should.async(); + final StreamParser parser = new StreamParser() + .exceptionHandler(err -> test.complete()) + .handler(body -> should.fail("There is something else than JSON in the preamble of the buffer")); + + parser.handle(Buffer.buffer( + "Content-Length: 38\r\n" + + "Content-Type: application/vscode-jsonrpc;charset=utf-8\r\n" + + "\r\n" + + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"hi\"}")); + } + @Test(timeout = 30_000) public void testParseSimpleHeaderless(TestContext should) { final Async test = should.async(); final StreamParser parser = new StreamParser() .exceptionHandler(should::fail) - .handler((contentType, body) -> { + .handler(body -> { System.out.println(body.toString()); test.complete(); }); From d150083671d21d98fcfaadfb0a8af2b4404adcbc Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 22 Jun 2022 21:15:50 +0530 Subject: [PATCH 11/34] interim checkin --- pom.xml | 2 +- src/client/nodejs/package.json | 2 +- .../ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java | 3 +++ .../bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c4a45da..6016780 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ vertx-tcp-eventbus-bridge - 4.3.2-SNAPSHOT + 999-SNAPSHOT Vert.x TCP EventBus Bridge diff --git a/src/client/nodejs/package.json b/src/client/nodejs/package.json index 13a1a99..645ea79 100644 --- a/src/client/nodejs/package.json +++ b/src/client/nodejs/package.json @@ -11,4 +11,4 @@ "scripts": { "test": "mocha ./test/index.js" } -} \ No newline at end of file +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index f54fd0a..a302f59 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -34,6 +34,9 @@ // or: return a handler, e.g.: // Handler webSocketHandler(); // Handler netSocketHandler(); + +// How about generifying this interface as in JsonRPCStreamEventBusBridge extends Handler ? +// similarly create a base class for the impl and concrete impls for each protocol. @VertxGen public interface JsonRPCStreamEventBusBridge extends Handler { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 7e55e5a..5403021 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -358,6 +358,7 @@ public void handle(WebSocketBase socket) { // by default json (like in the spec) but microsoft was suggesting messagepack as alternative. I'm not // sure if we should implement this. The TCP parser was accounting for it, but is it a good idea? maybe not? + // FIXME: seems to raise error upon tab close final JsonObject msg = new JsonObject(frame.binaryData()); // validation From aaea6ddff52f8c62f384d5fea10f28d631bbeee7 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 15 Jun 2022 00:54:40 +0530 Subject: [PATCH 12/34] try generifying on transport --- .../ext/eventbus/bridge/tcp/BridgeEvent.java | 6 +- .../tcp/JsonRPCStreamEventBusBridge.java | 29 ++- .../bridge/tcp/TcpEventBusBridge.java | 3 +- .../bridge/tcp/impl/BridgeEventImpl.java | 11 +- .../impl/JsonRPCStreamEventBusBridgeImpl.java | 200 +++--------------- .../TCPJsonRPCStreamEventBusBridgeImpl.java | 89 ++++++++ .../tcp/impl/TcpEventBusBridgeImpl.java | 12 +- ...socketJsonRPCStreamEventBusBridgeImpl.java | 101 +++++++++ .../bridge/tcp/InteropWebSocketServer.java | 6 +- .../tcp/JsonRPCStreamEventBusBridgeTest.java | 5 +- .../tcp/TcpEventBusBridgeEventTest.java | 4 +- 11 files changed, 264 insertions(+), 202 deletions(-) create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java index 856324b..3bedd21 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/BridgeEvent.java @@ -32,7 +32,7 @@ * @author Tim Fox */ @VertxGen -public interface BridgeEvent extends BaseBridgeEvent { +public interface BridgeEvent extends BaseBridgeEvent { /** * Get the raw JSON message for the event. This will be null for SOCKET_CREATED or SOCKET_CLOSED events as there is @@ -42,7 +42,7 @@ public interface BridgeEvent extends BaseBridgeEvent { * @return this reference, so it can be used fluently */ @Fluent - BridgeEvent setRawMessage(JsonObject message); + BridgeEvent setRawMessage(JsonObject message); // TODO: this will cause problems with WebSockets as they don't share any common base interface // this will be a breaking change to users, as the return type is now generic, is this OK? @@ -53,5 +53,5 @@ public interface BridgeEvent extends BaseBridgeEvent { * @return the SockJSSocket instance */ @CacheReturn - T socket(); + T socket(); } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index a302f59..2853279 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -18,9 +18,12 @@ import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.http.WebSocketBase; import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.JsonRPCStreamEventBusBridgeImpl; +import io.vertx.ext.eventbus.bridge.tcp.impl.TCPJsonRPCStreamEventBusBridgeImpl; +import io.vertx.ext.eventbus.bridge.tcp.impl.WebsocketJsonRPCStreamEventBusBridgeImpl; /** * JSONRPC stream EventBus bridge for Vert.x @@ -38,17 +41,29 @@ // How about generifying this interface as in JsonRPCStreamEventBusBridge extends Handler ? // similarly create a base class for the impl and concrete impls for each protocol. @VertxGen -public interface JsonRPCStreamEventBusBridge extends Handler { +public interface JsonRPCStreamEventBusBridge { - static JsonRPCStreamEventBusBridge create(Vertx vertx) { - return create(vertx, null, null); + static Handler netSocketHandler(Vertx vertx) { + return netSocketHandler(vertx, null, null); } - static JsonRPCStreamEventBusBridge create(Vertx vertx, BridgeOptions options) { - return create(vertx, options, null); + static Handler netSocketHandler(Vertx vertx, BridgeOptions options) { + return netSocketHandler(vertx, options, null); } - static JsonRPCStreamEventBusBridge create(Vertx vertx, BridgeOptions options, Handler eventHandler) { - return new JsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); + static Handler netSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler) { + return new TCPJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); + } + + static Handler webSocketHandler(Vertx vertx) { + return webSocketHandler(vertx, null, null); + } + + static Handler webSocketHandler(Vertx vertx, BridgeOptions options) { + return webSocketHandler(vertx, options, null); + } + + static Handler webSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler) { + return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); } } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridge.java index 5abbdda..cd10836 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridge.java @@ -22,6 +22,7 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.TcpEventBusBridgeImpl; @@ -44,7 +45,7 @@ static TcpEventBusBridge create(Vertx vertx, BridgeOptions options) { static TcpEventBusBridge create(Vertx vertx, BridgeOptions options, NetServerOptions netServerOptions) { return new TcpEventBusBridgeImpl(vertx, options, netServerOptions,null); } - static TcpEventBusBridge create(Vertx vertx, BridgeOptions options, NetServerOptions netServerOptions,Handler eventHandler) { + static TcpEventBusBridge create(Vertx vertx, BridgeOptions options, NetServerOptions netServerOptions, Handler> eventHandler) { return new TcpEventBusBridgeImpl(vertx, options, netServerOptions,eventHandler); } /** diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java index e117add..fafe1af 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/BridgeEventImpl.java @@ -20,7 +20,6 @@ import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.json.JsonObject; -import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; @@ -28,14 +27,14 @@ * @author Tim Fox * @author grant@iowntheinter.net */ -class BridgeEventImpl implements BridgeEvent { +class BridgeEventImpl implements BridgeEvent { private final BridgeEventType type; private final JsonObject rawMessage; - private final NetSocket socket; + private final T socket; private final Promise promise; - public BridgeEventImpl(BridgeEventType type, JsonObject rawMessage, NetSocket socket) { + public BridgeEventImpl(BridgeEventType type, JsonObject rawMessage, T socket) { this.type = type; this.rawMessage = rawMessage; this.socket = socket; @@ -58,7 +57,7 @@ public JsonObject getRawMessage() { } @Override - public BridgeEvent setRawMessage(JsonObject message) { + public BridgeEvent setRawMessage(JsonObject message) { if (message != rawMessage) { rawMessage.clear().mergeIn(message); } @@ -71,7 +70,7 @@ public void handle(AsyncResult asyncResult) { } @Override - public NetSocket socket() { + public T socket() { return socket; } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 5403021..0d4f938 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -19,7 +19,6 @@ import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.*; -import io.vertx.core.http.WebSocketBase; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonObject; @@ -29,7 +28,6 @@ import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; -import io.vertx.ext.eventbus.bridge.tcp.JsonRPCStreamEventBusBridge; import io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper; import java.util.*; @@ -43,51 +41,50 @@ * * @author Paulo Lopes */ -public class JsonRPCStreamEventBusBridgeImpl implements JsonRPCStreamEventBusBridge { +public abstract class JsonRPCStreamEventBusBridgeImpl implements Handler { - private static final Logger log = LoggerFactory.getLogger(JsonRPCStreamEventBusBridgeImpl.class); - private static final JsonObject EMPTY = new JsonObject(Collections.emptyMap()); + protected static final Logger log = LoggerFactory.getLogger(JsonRPCStreamEventBusBridgeImpl.class); + protected static final JsonObject EMPTY = new JsonObject(Collections.emptyMap()); - private final EventBus eb; + protected final EventBus eb; - private final Map compiledREs = new HashMap<>(); - private final BridgeOptions options; - private final Handler bridgeEventHandler; + protected final Map compiledREs = new HashMap<>(); + protected final BridgeOptions options; + protected final Handler> bridgeEventHandler; - - public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler eventHandler) { + public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> eventHandler) { this.eb = vertx.eventBus(); this.options = options != null ? options : new BridgeOptions(); this.bridgeEventHandler = eventHandler; } - private void dispatch(WriteStream socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void dispatch(WriteStream socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { switch (method) { case "send": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.SEND, msg, null), + () -> new BridgeEventImpl<>(BridgeEventType.SEND, msg, null), () -> send(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); break; case "publish": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.SEND, msg, null), + () -> new BridgeEventImpl<>(BridgeEventType.SEND, msg, null), () -> publish(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); break; case "register": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.REGISTER, msg, null), + () -> new BridgeEventImpl<>(BridgeEventType.REGISTER, msg, null), () -> register(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); break; case "unregister": checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.UNREGISTER, msg, null), + () -> new BridgeEventImpl<>(BridgeEventType.UNREGISTER, msg, null), () -> unregister(socket, id, msg, registry, replies), () -> JsonRPCHelper.error(id, -32040, "access_denied", socket) ); @@ -101,7 +98,7 @@ private void dispatch(WriteStream socket, String method, Object id, Json } } - private void unregister(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void unregister(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -126,7 +123,7 @@ private void unregister(WriteStream socket, Object id, JsonObject msg, M } } - private void register(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void register(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -160,7 +157,7 @@ private void register(WriteStream socket, Object id, JsonObject msg, Map socket); })); checkCallHook( - () -> new BridgeEventImpl(BridgeEventType.REGISTERED, msg, null), + () -> new BridgeEventImpl<>(BridgeEventType.REGISTERED, msg, null), () -> { if (id != null) { // ack @@ -172,7 +169,7 @@ private void register(WriteStream socket, Object id, JsonObject msg, Map } } - private void publish(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void publish(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -195,7 +192,7 @@ private void publish(WriteStream socket, Object id, JsonObject msg, Map< } } - private void send(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void send(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -261,166 +258,21 @@ private void send(WriteStream socket, Object id, JsonObject msg, Map new BridgeEventImpl(BridgeEventType.SOCKET_CREATED, null, socket), - // on success - () -> { - final Map> registry = new ConcurrentHashMap<>(); - final Map> replies = new ConcurrentHashMap<>(); - - socket - .exceptionHandler(t -> { - log.error(t.getMessage(), t); - registry.values().forEach(MessageConsumer::unregister); - registry.clear(); - }) - .endHandler(v -> { - registry.values().forEach(MessageConsumer::unregister); - // normal close, trigger the event - checkCallHook(() -> new BridgeEventImpl(BridgeEventType.SOCKET_CLOSED, null, socket)); - registry.clear(); - }) - .handler( - // create a protocol parser - new StreamParser() - .exceptionHandler(t -> { - log.error(t.getMessage(), t); - }) - .handler(buffer -> { - // TODO: handle content type - - // TODO: body may be an array (batching) - final JsonObject msg = new JsonObject(buffer); - - // validation - if (!"2.0".equals(msg.getString("jsonrpc"))) { - log.error("Invalid message: " + msg); - return; - } - - final String method = msg.getString("method"); - if (method == null) { - log.error("Invalid method: " + msg.getString("method")); - return; - } - - final Object id = msg.getValue("id"); - if (id != null) { - if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { - log.error("Invalid id: " + msg.getValue("id")); - return; - } - } - - dispatch( - socket, - method, - id, - msg, - registry, - replies); - })); - }, - // on failure - socket::close - ); - } - - public void handle(WebSocketBase socket) { - checkCallHook( - // process the new socket according to the event handler - () -> new BridgeEventImpl(BridgeEventType.SOCKET_CREATED, null, null), - // on success - () -> { - final Map> registry = new ConcurrentHashMap<>(); - final Map> replies = new ConcurrentHashMap<>(); - - socket - .exceptionHandler(t -> { - log.error(t.getMessage(), t); - registry.values().forEach(MessageConsumer::unregister); - registry.clear(); - }) - .endHandler(v -> { - registry.values().forEach(MessageConsumer::unregister); - // normal close, trigger the event - checkCallHook(() -> new BridgeEventImpl(BridgeEventType.SOCKET_CLOSED, null, null)); - registry.clear(); - }) - .frameHandler(frame -> { - // TODO: this could be an [], in this case, after parsing, we should loop and call for each element the - // code bellow. - - // One idea from vs-jsonrpcstream was the use of content-types, so define how the message was formated - // by default json (like in the spec) but microsoft was suggesting messagepack as alternative. I'm not - // sure if we should implement this. The TCP parser was accounting for it, but is it a good idea? maybe not? - - // FIXME: seems to raise error upon tab close - final JsonObject msg = new JsonObject(frame.binaryData()); - - // validation - if (!"2.0".equals(msg.getString("jsonrpc"))) { - log.error("Invalid message: " + msg); - return; - } - - final String method = msg.getString("method"); - if (method == null) { - log.error("Invalid method: " + msg.getString("method")); - return; - } - - final Object id = msg.getValue("id"); - if (id != null) { - if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { - log.error("Invalid id: " + msg.getValue("id")); - return; - } - } - - // TODO: we should wrap the socket in order to override the "write" method to write a text frame - // TODO: the current WriteStream assumes binary frames which are harder to handle on the browser - // TODO: maybe we could make this configurable (binary/text) - - // if we create a wraper, say an interface: - // interface SocketWriter { write(Buffer buff) } - // then we can create specific implementation wrappers for all kinds of sockets, netSocket, webSocket (binary or text) - - // given that the wraper is at the socket level (it's not that heavy in terms of garbage collection, 1 extra object per connection. - // And a connection is long lasting, not like HTTP - - dispatch( - socket, - method, - id, - msg, - registry, - replies); - }); - }, - // on failure - socket::close - ); - } - - private void checkCallHook(Supplier eventSupplier) { + protected void checkCallHook(Supplier> eventSupplier) { checkCallHook(eventSupplier, null, null); } - private void checkCallHook(Supplier eventSupplier, Runnable okAction) { + protected void checkCallHook(Supplier> eventSupplier, Runnable okAction) { checkCallHook(eventSupplier, okAction, null); } - private void checkCallHook(Supplier eventSupplier, Runnable okAction, Runnable rejectAction) { + protected void checkCallHook(Supplier> eventSupplier, Runnable okAction, Runnable rejectAction) { if (bridgeEventHandler == null) { if (okAction != null) { okAction.run(); } } else { - BridgeEvent event = eventSupplier.get(); + BridgeEvent event = eventSupplier.get(); bridgeEventHandler.handle(event); event.future().onComplete(res -> { if (res.succeeded()) { @@ -442,11 +294,11 @@ private void checkCallHook(Supplier eventSupplier, Runnable okActio } } - private boolean checkMatches(boolean inbound, String address) { + protected boolean checkMatches(boolean inbound, String address) { return checkMatches(inbound, address, null); } - private boolean checkMatches(boolean inbound, String address, Map> replies) { + protected boolean checkMatches(boolean inbound, String address, Map> replies) { // special case, when dealing with replies the addresses are not in the inbound/outbound list but on // the replies registry if (replies != null && inbound && replies.containsKey(address)) { @@ -479,7 +331,7 @@ private boolean checkMatches(boolean inbound, String address, Map { + + public TCPJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler) { + super(vertx, options, bridgeEventHandler); + } + + @Override + public void handle(NetSocket socket) { + checkCallHook( + // process the new socket according to the event handler + () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, socket), + // on success + () -> { + final Map> registry = new ConcurrentHashMap<>(); + final Map> replies = new ConcurrentHashMap<>(); + + socket + .exceptionHandler(t -> { + log.error(t.getMessage(), t); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }) + .endHandler(v -> { + registry.values().forEach(MessageConsumer::unregister); + // normal close, trigger the event + checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); + registry.clear(); + }) + .handler( + // create a protocol parser + new StreamParser() + .exceptionHandler(t -> { + log.error(t.getMessage(), t); + }) + .handler(buffer -> { + // TODO: handle content type + + // TODO: body may be an array (batching) + final JsonObject msg = new JsonObject(buffer); + + // validation + if (!"2.0".equals(msg.getString("jsonrpc"))) { + log.error("Invalid message: " + msg); + return; + } + + final String method = msg.getString("method"); + if (method == null) { + log.error("Invalid method: " + msg.getString("method")); + return; + } + + final Object id = msg.getValue("id"); + if (id != null) { + if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { + log.error("Invalid id: " + msg.getValue("id")); + return; + } + } + + dispatch( + socket, + method, + id, + msg, + registry, + replies); + })); + }, + // on failure + socket::close + ); + } +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TcpEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TcpEventBusBridgeImpl.java index ff2b111..7a8ab7e 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TcpEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TcpEventBusBridgeImpl.java @@ -59,10 +59,10 @@ public class TcpEventBusBridgeImpl implements TcpEventBusBridge { private final Map compiledREs = new HashMap<>(); private final BridgeOptions options; - private final Handler bridgeEventHandler; + private final Handler> bridgeEventHandler; - public TcpEventBusBridgeImpl(Vertx vertx, BridgeOptions options, NetServerOptions netServerOptions, Handler eventHandler) { + public TcpEventBusBridgeImpl(Vertx vertx, BridgeOptions options, NetServerOptions netServerOptions, Handler> eventHandler) { this.eb = vertx.eventBus(); this.options = options != null ? options : new BridgeOptions(); this.bridgeEventHandler = eventHandler; @@ -205,7 +205,7 @@ private void doSendOrPub(NetSocket socket, String address, JsonObject msg, Map new BridgeEventImpl(BridgeEventType.REGISTERED, msg, socket), null, null); + checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.REGISTERED, msg, socket), null, null); } else { sendErrFrame("access_denied", socket); } @@ -252,7 +252,7 @@ private void handler(NetSocket socket) { final String type = msg.getString("type", "message"); final String address = msg.getString("address"); BridgeEventType eventType = parseType(type); - checkCallHook(() -> new BridgeEventImpl(eventType, msg, socket), + checkCallHook(() -> new BridgeEventImpl<>(eventType, msg, socket), () -> { if (eventType != BridgeEventType.SOCKET_PING && address == null) { sendErrFrame("missing_address", socket); @@ -289,13 +289,13 @@ public Future close() { return server.close(); } - private void checkCallHook(Supplier eventSupplier, Runnable okAction, Runnable rejectAction) { + private void checkCallHook(Supplier> eventSupplier, Runnable okAction, Runnable rejectAction) { if (bridgeEventHandler == null) { if (okAction != null) { okAction.run(); } } else { - BridgeEventImpl event = eventSupplier.get(); + BridgeEventImpl event = eventSupplier.get(); bridgeEventHandler.handle(event); event.future().onComplete(res -> { if (res.succeeded()) { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java new file mode 100644 index 0000000..8a3ba6c --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -0,0 +1,101 @@ +package io.vertx.ext.eventbus.bridge.tcp.impl; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.http.WebSocketBase; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetSocket; +import io.vertx.ext.bridge.BridgeEventType; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class WebsocketJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { + + public WebsocketJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler) { + super(vertx, options, bridgeEventHandler); + } + + @Override + public void handle(WebSocketBase socket) { + checkCallHook( + // process the new socket according to the event handler + () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, null), + // on success + () -> { + final Map> registry = new ConcurrentHashMap<>(); + final Map> replies = new ConcurrentHashMap<>(); + + socket + .exceptionHandler(t -> { + log.error(t.getMessage(), t); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }) + .endHandler(v -> { + registry.values().forEach(MessageConsumer::unregister); + // normal close, trigger the event + checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, null)); + registry.clear(); + }) + .frameHandler(frame -> { + // TODO: this could be an [], in this case, after parsing, we should loop and call for each element the + // code bellow. + + // One idea from vs-jsonrpcstream was the use of content-types, so define how the message was formated + // by default json (like in the spec) but microsoft was suggesting messagepack as alternative. I'm not + // sure if we should implement this. The TCP parser was accounting for it, but is it a good idea? maybe not? + + // FIXME: seems to raise error upon tab close + final JsonObject msg = new JsonObject(frame.binaryData()); + + // validation + if (!"2.0".equals(msg.getString("jsonrpc"))) { + log.error("Invalid message: " + msg); + return; + } + + final String method = msg.getString("method"); + if (method == null) { + log.error("Invalid method: " + msg.getString("method")); + return; + } + + final Object id = msg.getValue("id"); + if (id != null) { + if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { + log.error("Invalid id: " + msg.getValue("id")); + return; + } + } + + // TODO: we should wrap the socket in order to override the "write" method to write a text frame + // TODO: the current WriteStream assumes binary frames which are harder to handle on the browser + // TODO: maybe we could make this configurable (binary/text) + + // if we create a wraper, say an interface: + // interface SocketWriter { write(Buffer buff) } + // then we can create specific implementation wrappers for all kinds of sockets, netSocket, webSocket (binary or text) + + // given that the wraper is at the socket level (it's not that heavy in terms of garbage collection, 1 extra object per connection. + // And a connection is long lasting, not like HTTP + + dispatch( + socket, + method, + id, + msg, + registry, + replies); + }); + }, + // on failure + socket::close + ); + } + +} diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index eec823d..10e3afd 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -1,14 +1,18 @@ package io.vertx.ext.eventbus.bridge.tcp; import io.vertx.core.AbstractVerticle; +import io.vertx.core.Handler; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.WebSocketBase; import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.JsonRPCStreamEventBusBridgeImpl; +import io.vertx.ext.eventbus.bridge.tcp.impl.TCPJsonRPCStreamEventBusBridgeImpl; public class InteropWebSocketServer extends AbstractVerticle { @@ -26,7 +30,7 @@ public void start(Promise start) { vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); // once we fix the interface we can avoid the casts - JsonRPCStreamEventBusBridgeImpl bridge = (JsonRPCStreamEventBusBridgeImpl) JsonRPCStreamEventBusBridge.create( + Handler bridge = JsonRPCStreamEventBusBridge.webSocketHandler( vertx, new BridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("hello")) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index 5a458ae..848dcbd 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -20,6 +20,7 @@ import io.vertx.core.eventbus.Message; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.StreamParser; @@ -43,7 +44,7 @@ public class JsonRPCStreamEventBusBridgeTest { @Rule public RunTestOnContext rule = new RunTestOnContext(); - private final Handler eventHandler = event -> event.complete(true); + private final Handler> eventHandler = event -> event.complete(true); @Before public void before(TestContext should) { @@ -57,7 +58,7 @@ public void before(TestContext should) { vertx.setPeriodic(1000, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); vertx.createNetServer() - .connectHandler(JsonRPCStreamEventBusBridge.create( + .connectHandler(JsonRPCStreamEventBusBridge.netSocketHandler( vertx, new BridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("hello")) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java index 0cc2cd5..7415c8e 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeEventTest.java @@ -67,9 +67,9 @@ public void before(TestContext context) { .setSsl(true) .setTrustStoreOptions(sslKeyPairCerts.getServerTrustStore()) .setKeyStoreOptions(sslKeyPairCerts.getServerKeyStore()), - be -> { + (BridgeEvent be) -> { logger.info("Handled a bridge event " + be.getRawMessage()); - if (be.socket().isSsl()) { + if (be.socket().isSsl()) { try { for (Certificate c : be.socket().peerCertificates()) { logger.info(((X509Certificate)c).getSubjectDN().toString()); From f6031f5cb2b559fedb3d1fcc7574496e1d8280c9 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 23 Jun 2022 09:45:31 +0530 Subject: [PATCH 13/34] Prevent crash when trying to parse CLOSE frames as JSON --- .../tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index 8a3ba6c..6d64bea 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -50,7 +50,11 @@ public void handle(WebSocketBase socket) { // by default json (like in the spec) but microsoft was suggesting messagepack as alternative. I'm not // sure if we should implement this. The TCP parser was accounting for it, but is it a good idea? maybe not? - // FIXME: seems to raise error upon tab close + // not handling CLOSE frames here, endHandler will be invoked on the socket later + if (frame.isClose()) { + return; + } + final JsonObject msg = new JsonObject(frame.binaryData()); // validation From fe5591c5742acf3f321744a694ce34b228bb8bd8 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 23 Jun 2022 17:11:28 +0530 Subject: [PATCH 14/34] Change WriteStream to Consumer in JSON helpers This way we can configure WS connections to send response in text or binary. --- .../tcp/impl/JsonRPCStreamEventBusBridgeImpl.java | 15 ++++++--------- .../impl/TCPJsonRPCStreamEventBusBridgeImpl.java | 2 +- .../WebsocketJsonRPCStreamEventBusBridgeImpl.java | 3 +-- .../bridge/tcp/impl/protocol/JsonRPCHelper.java | 15 +++++++-------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 0d4f938..78f16ad 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -22,8 +22,6 @@ import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonObject; -import io.vertx.core.net.NetSocket; -import io.vertx.core.streams.WriteStream; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; @@ -31,7 +29,7 @@ import io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,8 +56,7 @@ public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handl this.bridgeEventHandler = eventHandler; } - protected void dispatch(WriteStream socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { - + protected void dispatch(Consumer socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { switch (method) { case "send": checkCallHook( @@ -98,7 +95,7 @@ protected void dispatch(WriteStream socket, String method, Object id, Js } } - protected void unregister(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void unregister(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -123,7 +120,7 @@ protected void unregister(WriteStream socket, Object id, JsonObject msg, } } - protected void register(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void register(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -169,7 +166,7 @@ protected void register(WriteStream socket, Object id, JsonObject msg, M } } - protected void publish(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void publish(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -192,7 +189,7 @@ protected void publish(WriteStream socket, Object id, JsonObject msg, Ma } } - protected void send(WriteStream socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void send(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java index 1d79a98..d1dd645 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java @@ -74,7 +74,7 @@ public void handle(NetSocket socket) { } dispatch( - socket, + socket::write, method, id, msg, diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index 6d64bea..d840e5c 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -6,7 +6,6 @@ import io.vertx.core.eventbus.MessageConsumer; import io.vertx.core.http.WebSocketBase; import io.vertx.core.json.JsonObject; -import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; @@ -89,7 +88,7 @@ public void handle(WebSocketBase socket) { // And a connection is long lasting, not like HTTP dispatch( - socket, + socket::writeBinaryMessage, method, id, msg, diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java index 3991ab4..3606efb 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java @@ -21,8 +21,7 @@ import io.vertx.core.json.JsonObject; import io.vertx.core.streams.WriteStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; /** * Helper class to format and send frames over a socket @@ -85,16 +84,16 @@ public static void request(String method, JsonObject params, WriteStream request(method, null, params, null, handler); } - public static void response(Object id, Object result, WriteStream handler) { + public static void response(Object id, Object result, Consumer handler) { final JsonObject payload = new JsonObject() .put("jsonrpc", "2.0") .put("id", id) .put("result", result); - handler.write(payload.toBuffer().appendString("\r\n")); + handler.accept(payload.toBuffer().appendString("\r\n")); } - public static void error(Object id, Number code, String message, WriteStream handler) { + public static void error(Object id, Number code, String message, Consumer handler) { final JsonObject payload = new JsonObject() .put("jsonrpc", "2.0") .put("id", id); @@ -110,14 +109,14 @@ public static void error(Object id, Number code, String message, WriteStream handler) { + public static void error(Object id, ReplyException failure, Consumer handler) { error(id, failure.failureCode(), failure.getMessage(), handler); } - public static void error(Object id, String message, WriteStream handler) { + public static void error(Object id, String message, Consumer handler) { error(id, -32000, message, handler); } } From f9090fe07d5fe5d03fba2867a3aae16b72c3eeb3 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 23 Jun 2022 17:27:36 +0530 Subject: [PATCH 15/34] Add option to configure binary or text mode for Websockets bridge --- .../bridge/tcp/JsonRPCStreamEventBusBridge.java | 11 +++++++---- .../WebsocketJsonRPCStreamEventBusBridgeImpl.java | 15 +++++++++++++-- .../bridge/tcp/InteropWebSocketServer.java | 5 ++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index 2853279..4e73926 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -56,14 +56,17 @@ static Handler netSocketHandler(Vertx vertx, BridgeOptions options, H } static Handler webSocketHandler(Vertx vertx) { - return webSocketHandler(vertx, null, null); + return webSocketHandler(vertx, null, null, false); } static Handler webSocketHandler(Vertx vertx, BridgeOptions options) { - return webSocketHandler(vertx, options, null); + return webSocketHandler(vertx, options, null, false); } - static Handler webSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler) { - return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); + return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler, false); + } + + static Handler webSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler, boolean useText) { + return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler, useText); } } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index d840e5c..1a2373b 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -2,6 +2,7 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.Message; import io.vertx.core.eventbus.MessageConsumer; import io.vertx.core.http.WebSocketBase; @@ -12,11 +13,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; public class WebsocketJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { + boolean useText; - public WebsocketJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler) { + public WebsocketJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler, boolean useText) { super(vertx, options, bridgeEventHandler); + this.useText = useText; } @Override @@ -29,6 +33,13 @@ public void handle(WebSocketBase socket) { final Map> registry = new ConcurrentHashMap<>(); final Map> replies = new ConcurrentHashMap<>(); + Consumer consumer; + if (useText) { + consumer = buffer -> socket.writeTextMessage(buffer.toString()); + } else { + consumer = socket::writeBinaryMessage; + } + socket .exceptionHandler(t -> { log.error(t.getMessage(), t); @@ -88,7 +99,7 @@ public void handle(WebSocketBase socket) { // And a connection is long lasting, not like HTTP dispatch( - socket::writeBinaryMessage, + consumer, method, id, msg, diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index 10e3afd..5347d0c 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -38,7 +38,10 @@ public void start(Promise start) { .addInboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("echo")) .addOutboundPermitted(new PermittedOptions().setAddress("test")) - .addOutboundPermitted(new PermittedOptions().setAddress("ping"))); + .addOutboundPermitted(new PermittedOptions().setAddress("ping")), + null, + true + ); vertx .createHttpServer() From 1b9adb9c0b1928438f99b3685c3fe0f154f5707b Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 23 Jun 2022 23:54:57 +0530 Subject: [PATCH 16/34] Ignore ping frames --- .../tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index 1a2373b..60282a8 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -61,7 +61,8 @@ public void handle(WebSocketBase socket) { // sure if we should implement this. The TCP parser was accounting for it, but is it a good idea? maybe not? // not handling CLOSE frames here, endHandler will be invoked on the socket later - if (frame.isClose()) { + // ping frames are automatically handled by websockets so safe to ignore here + if (frame.isClose() || frame.isPing()) { return; } From 45e2a3d958e912378789881127fe1e017da039b9 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Mon, 27 Jun 2022 19:02:01 +0530 Subject: [PATCH 17/34] Add json schema to validate requests --- .../tcp/impl/protocol/jsonrpc.scehma.json | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/resources/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/jsonrpc.scehma.json diff --git a/src/main/resources/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/jsonrpc.scehma.json b/src/main/resources/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/jsonrpc.scehma.json new file mode 100644 index 0000000..c5ab7ee --- /dev/null +++ b/src/main/resources/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/jsonrpc.scehma.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://vertx.io/jsonrpc.schema.json", + "title": "Vert.x Event Bus Bridge JSON-RPC 2.0 Specification", + "description": "JSON-RPC schema to validate messages sent to a Vert.x Event Bus Bridge", + "anyOf": [ + { "$ref": "#/definitions/request" }, + { + "type": "array", + "items": { "$ref": "#/definitions/request" } + } + ], + "definitions": { + "request": { + "type": "object", + "properties": { + "jsonrpc": { + "description": "A String specifying the version of the JSON-RPC protocol. MUST be exactly \"2.0\".", + "const": "2.0" + }, + "method": { + "description": "A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else.", + "type": "string" + }, + "params": { + "description": "A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted.", + "type": ["object", "array"] + }, + "id": { + "description": "An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null and Numbers SHOULD NOT contain fractional parts.", + "type": ["string", "integer", "null"] + } + }, + "required": [ + "jsonrpc", + "method" + ], + "additionalProperties": false + } + } +} From d97df7e7c3f1c5687ded09b232b1fd910c51a420 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 30 Jun 2022 09:16:50 +0530 Subject: [PATCH 18/34] Validate incoming requests using vertx-json-schema --- pom.xml | 5 ++ .../impl/JsonRPCStreamEventBusBridgeImpl.java | 80 ++++++++++++++++++- .../TCPJsonRPCStreamEventBusBridgeImpl.java | 17 +--- ...socketJsonRPCStreamEventBusBridgeImpl.java | 16 +--- 4 files changed, 86 insertions(+), 32 deletions(-) diff --git a/pom.xml b/pom.xml index 6016780..a4c426e 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,11 @@ vertx-bridge-common + + io.vertx + vertx-json-schema + + io.vertx vertx-codegen diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 78f16ad..559764e 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -27,7 +27,23 @@ import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; import io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper; - +import io.vertx.json.schema.Draft; +import io.vertx.json.schema.JsonSchema; +import io.vertx.json.schema.JsonSchemaOptions; +import io.vertx.json.schema.OutputUnit; +import io.vertx.json.schema.Validator; + +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.function.Consumer; import java.util.function.Supplier; @@ -50,10 +66,72 @@ public abstract class JsonRPCStreamEventBusBridgeImpl implements Handler { protected final BridgeOptions options; protected final Handler> bridgeEventHandler; + private final Validator requestValidator; + public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> eventHandler) { this.eb = vertx.eventBus(); this.options = options != null ? options : new BridgeOptions(); this.bridgeEventHandler = eventHandler; + this.requestValidator = getRequestValidator(); + } + + private Validator getRequestValidator() { + String json = "{\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n" + + " \"$id\": \"https://vertx.io/jsonrpc.schema.json\",\n" + + " \"title\": \"Vert.x Event Bus Bridge JSON-RPC 2.0 Specification\",\n" + + " \"description\": \"JSON-RPC schema to validate messages sent to a Vert.x Event Bus Bridge\",\n" + + " \"anyOf\": [\n" + + " { \"$ref\": \"#/definitions/request\" },\n" + + " {\n" + + " \"type\": \"array\",\n" + + " \"items\": { \"$ref\": \"#/definitions/request\" }\n" + + " }\n" + + " ],\n" + + " \"definitions\": {\n" + + " \"request\": {\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"jsonrpc\": {\n" + + " \"description\": \"A String specifying the version of the JSON-RPC protocol. MUST be exactly \\\"2.0\\\".\",\n" + + " \"const\": \"2.0\"\n" + + " },\n" + + " \"method\": {\n" + + " \"description\": \"A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else.\",\n" + + " \"type\": \"string\"\n" + + " },\n" + + " \"params\": {\n" + + " \"description\": \"A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted.\",\n" + + " \"type\": [\"object\", \"array\"]\n" + + " },\n" + + " \"id\": {\n" + + " \"description\": \"An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null and Numbers SHOULD NOT contain fractional parts.\",\n" + + " \"type\": [\"string\", \"integer\", \"null\"]\n" + + " }\n" + + " },\n" + + " \"required\": [\n" + + " \"jsonrpc\",\n" + + " \"method\"\n" + + " ],\n" + + " \"additionalProperties\": false\n" + + " }\n" + + " }\n" + + "}\n"; + return Validator.create( + JsonSchema.of(new JsonObject(json)), + new JsonSchemaOptions() + .setDraft(Draft.DRAFT202012) + .setBaseUri("https://vertx.io") + ); + } + + protected boolean validate(JsonObject object) { + OutputUnit outputUnit = requestValidator.validate(object); + if (!outputUnit.getValid()) { + log.error("Invalid message. Error: " + outputUnit.getErrors() + " . Message: " + object); + return false; + } + return true; } protected void dispatch(Consumer socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java index d1dd645..0a0963b 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java @@ -52,26 +52,11 @@ public void handle(NetSocket socket) { // TODO: body may be an array (batching) final JsonObject msg = new JsonObject(buffer); - - // validation - if (!"2.0".equals(msg.getString("jsonrpc"))) { - log.error("Invalid message: " + msg); + if (!this.validate(msg)) { return; } - final String method = msg.getString("method"); - if (method == null) { - log.error("Invalid method: " + msg.getString("method")); - return; - } - final Object id = msg.getValue("id"); - if (id != null) { - if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { - log.error("Invalid id: " + msg.getValue("id")); - return; - } - } dispatch( socket::write, diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index 60282a8..2959a1c 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -67,26 +67,12 @@ public void handle(WebSocketBase socket) { } final JsonObject msg = new JsonObject(frame.binaryData()); - - // validation - if (!"2.0".equals(msg.getString("jsonrpc"))) { - log.error("Invalid message: " + msg); + if (!this.validate(msg)) { return; } final String method = msg.getString("method"); - if (method == null) { - log.error("Invalid method: " + msg.getString("method")); - return; - } - final Object id = msg.getValue("id"); - if (id != null) { - if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { - log.error("Invalid id: " + msg.getValue("id")); - return; - } - } // TODO: we should wrap the socket in order to override the "write" method to write a text frame // TODO: the current WriteStream assumes binary frames which are harder to handle on the browser From 62e9822076eaa4a77bb6fde2fab9176393c56ce3 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 30 Jun 2022 09:20:01 +0530 Subject: [PATCH 19/34] paritally working http transport --- .../tcp/JsonRPCStreamEventBusBridge.java | 14 ++++ .../HttpJsonRPCStreamEventBusBridgeImpl.java | 80 +++++++++++++++++++ .../bridge/tcp/InteropWebSocketServer.java | 1 + 3 files changed, 95 insertions(+) create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index 4e73926..4e49c3a 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -18,9 +18,11 @@ import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.WebSocketBase; import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.eventbus.bridge.tcp.impl.HttpJsonRPCStreamEventBusBridgeImpl; import io.vertx.ext.eventbus.bridge.tcp.impl.JsonRPCStreamEventBusBridgeImpl; import io.vertx.ext.eventbus.bridge.tcp.impl.TCPJsonRPCStreamEventBusBridgeImpl; import io.vertx.ext.eventbus.bridge.tcp.impl.WebsocketJsonRPCStreamEventBusBridgeImpl; @@ -69,4 +71,16 @@ static Handler webSocketHandler(Vertx vertx, BridgeOptions option static Handler webSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler, boolean useText) { return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler, useText); } + + static Handler httpSocketHandler(Vertx vertx) { + return httpSocketHandler(vertx, null, null); + } + + static Handler httpSocketHandler(Vertx vertx, BridgeOptions options) { + return httpSocketHandler(vertx, options, null); + } + + static Handler httpSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler) { + return new HttpJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); + } } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java new file mode 100644 index 0000000..bace686 --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -0,0 +1,80 @@ +package io.vertx.ext.eventbus.bridge.tcp.impl; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetSocket; +import io.vertx.ext.bridge.BridgeEventType; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; +import io.vertx.json.schema.JsonSchema; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class HttpJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { + + public HttpJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler) { + super(vertx, options, bridgeEventHandler); + } + + @Override + public void handle(HttpServerRequest socket) { + checkCallHook( + // process the new socket according to the event handler + () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, socket), + // on success + () -> { + final Map> registry = new ConcurrentHashMap<>(); + final Map> replies = new ConcurrentHashMap<>(); + + socket.exceptionHandler(t -> { + log.error(t.getMessage(), t); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }).endHandler(v -> { + registry.values().forEach(MessageConsumer::unregister); + // normal close, trigger the event + checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); + registry.clear(); + }).handler(buffer -> { + // TODO: handle content type + + // TODO: body may be an array (batching) + final JsonObject msg = new JsonObject(buffer); + System.out.println(msg); + + // validation + if (!"2.0".equals(msg.getString("jsonrpc"))) { + log.error("Invalid message: " + msg); + return; + } + + final String method = msg.getString("method"); + if (method == null) { + log.error("Invalid method: " + msg.getString("method")); + return; + } + + final Object id = msg.getValue("id"); + if (id != null) { + if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { + log.error("Invalid id: " + msg.getValue("id")); + return; + } + } + HttpServerResponse response = socket + .response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html"); + dispatch(response::end, method, id, msg, registry, replies); + }); + }, + // on failure + socket::end); + } +} diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index 5347d0c..37380ab 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -6,6 +6,7 @@ import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.WebSocketBase; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetSocket; From 73e6165cf126fcbb39caa0c3b577b9e0da62ad2a Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 7 Jul 2022 12:25:51 +0530 Subject: [PATCH 20/34] Read json schema from classpath --- .../impl/JsonRPCStreamEventBusBridgeImpl.java | 57 ++++++------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 559764e..c39859c 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -22,6 +22,7 @@ import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonObject; +import io.vertx.core.parsetools.JsonParser; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; @@ -33,6 +34,9 @@ import io.vertx.json.schema.OutputUnit; import io.vertx.json.schema.Validator; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; @@ -49,6 +53,7 @@ import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Abstract TCP EventBus bridge. Handles all common socket operations but has no knowledge on the payload. @@ -76,53 +81,23 @@ public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handl } private Validator getRequestValidator() { - String json = "{\n" - + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n" - + " \"$id\": \"https://vertx.io/jsonrpc.schema.json\",\n" - + " \"title\": \"Vert.x Event Bus Bridge JSON-RPC 2.0 Specification\",\n" - + " \"description\": \"JSON-RPC schema to validate messages sent to a Vert.x Event Bus Bridge\",\n" - + " \"anyOf\": [\n" - + " { \"$ref\": \"#/definitions/request\" },\n" - + " {\n" - + " \"type\": \"array\",\n" - + " \"items\": { \"$ref\": \"#/definitions/request\" }\n" - + " }\n" - + " ],\n" - + " \"definitions\": {\n" - + " \"request\": {\n" - + " \"type\": \"object\",\n" - + " \"properties\": {\n" - + " \"jsonrpc\": {\n" - + " \"description\": \"A String specifying the version of the JSON-RPC protocol. MUST be exactly \\\"2.0\\\".\",\n" - + " \"const\": \"2.0\"\n" - + " },\n" - + " \"method\": {\n" - + " \"description\": \"A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else.\",\n" - + " \"type\": \"string\"\n" - + " },\n" - + " \"params\": {\n" - + " \"description\": \"A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted.\",\n" - + " \"type\": [\"object\", \"array\"]\n" - + " },\n" - + " \"id\": {\n" - + " \"description\": \"An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null and Numbers SHOULD NOT contain fractional parts.\",\n" - + " \"type\": [\"string\", \"integer\", \"null\"]\n" - + " }\n" - + " },\n" - + " \"required\": [\n" - + " \"jsonrpc\",\n" - + " \"method\"\n" - + " ],\n" - + " \"additionalProperties\": false\n" - + " }\n" - + " }\n" - + "}\n"; + String path = "protocol/jsonrpc.scehma.json"; + try ( + InputStream stream = this.getClass().getResourceAsStream(path); + InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(reader) + ) { + String json = br.lines().collect(Collectors.joining()); return Validator.create( JsonSchema.of(new JsonObject(json)), new JsonSchemaOptions() .setDraft(Draft.DRAFT202012) .setBaseUri("https://vertx.io") ); + } + catch (IOException e) { + throw new RuntimeException(e); + } } protected boolean validate(JsonObject object) { From d85a7b30620d50004b5847aad7db5903cf0dc487 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Tue, 12 Jul 2022 23:07:26 +0530 Subject: [PATCH 21/34] Add changes to address feedback --- .../bridge/tcp/JsonRPCBridgeOptions.java | 64 +++++++++++++++++++ .../tcp/JsonRPCStreamEventBusBridge.java | 14 ++-- .../HttpJsonRPCStreamEventBusBridgeImpl.java | 7 +- .../impl/JsonRPCStreamEventBusBridgeImpl.java | 56 ++++++---------- .../TCPJsonRPCStreamEventBusBridgeImpl.java | 5 +- ...socketJsonRPCStreamEventBusBridgeImpl.java | 5 +- 6 files changed, 99 insertions(+), 52 deletions(-) create mode 100644 src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java new file mode 100644 index 0000000..e955c6d --- /dev/null +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java @@ -0,0 +1,64 @@ +package io.vertx.ext.eventbus.bridge.tcp; + +import io.vertx.core.json.JsonObject; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.bridge.BridgeOptionsConverter; + +public class JsonRPCBridgeOptions extends BridgeOptions { + private boolean websocketsTextAsFrame; + + public JsonRPCBridgeOptions() {} + + /** + * Creates a new instance of {@link JsonRPCBridgeOptions} by copying the content of another {@link JsonRPCBridgeOptions} + * + * @param that the {@link JsonRPCBridgeOptions} to copy. + */ + public JsonRPCBridgeOptions(JsonRPCBridgeOptions that) { + super(that); + this.websocketsTextAsFrame = that.websocketsTextAsFrame; + } + + /** + * Creates a new instance of {@link JsonRPCBridgeOptions} from its JSON representation. + * This method uses the generated converter. + * + * @param json the serialized {@link JsonRPCBridgeOptions} + * @see BridgeOptionsConverter + */ + public JsonRPCBridgeOptions(JsonObject json) { + BridgeOptionsConverter.fromJson(json, this); + this.websocketsTextAsFrame = json.getBoolean("websocketsTextAsFrame", false); + } + + /** + * Serializes the current {@link JsonRPCBridgeOptions} to JSON. This method uses the generated converter. + * + * @return the serialized object + */ + public JsonObject toJson() { + JsonObject json = new JsonObject(); + BridgeOptionsConverter.toJson(this, json); + json.put("websocketsTextAsFrame", websocketsTextAsFrame); + return json; + } + + /** + * Sets whether to use text format for websockets frames for the current {@link JsonRPCBridgeOptions}. + * + * @param websocketsTextAsFrame the choice whether to use text format + * @return the current {@link JsonRPCBridgeOptions}. + */ + public JsonRPCBridgeOptions setWebsocketsTextAsFrame(boolean websocketsTextAsFrame) { + this.websocketsTextAsFrame = websocketsTextAsFrame; + return this; + } + + /** + * @return whether to use text format for websockets frames. + */ + public boolean getWebsocketsTextAsFrame() { + return websocketsTextAsFrame; + } + +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index 4e49c3a..def2cad 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -49,11 +49,11 @@ static Handler netSocketHandler(Vertx vertx) { return netSocketHandler(vertx, null, null); } - static Handler netSocketHandler(Vertx vertx, BridgeOptions options) { + static Handler netSocketHandler(Vertx vertx, JsonRPCBridgeOptions options) { return netSocketHandler(vertx, options, null); } - static Handler netSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler) { + static Handler netSocketHandler(Vertx vertx, JsonRPCBridgeOptions options, Handler> eventHandler) { return new TCPJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); } @@ -61,14 +61,14 @@ static Handler webSocketHandler(Vertx vertx) { return webSocketHandler(vertx, null, null, false); } - static Handler webSocketHandler(Vertx vertx, BridgeOptions options) { + static Handler webSocketHandler(Vertx vertx, JsonRPCBridgeOptions options) { return webSocketHandler(vertx, options, null, false); } - static Handler webSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler) { + static Handler webSocketHandler(Vertx vertx, JsonRPCBridgeOptions options, Handler> eventHandler) { return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler, false); } - static Handler webSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler, boolean useText) { + static Handler webSocketHandler(Vertx vertx, JsonRPCBridgeOptions options, Handler> eventHandler, boolean useText) { return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler, useText); } @@ -76,11 +76,11 @@ static Handler httpSocketHandler(Vertx vertx) { return httpSocketHandler(vertx, null, null); } - static Handler httpSocketHandler(Vertx vertx, BridgeOptions options) { + static Handler httpSocketHandler(Vertx vertx, JsonRPCBridgeOptions options) { return httpSocketHandler(vertx, options, null); } - static Handler httpSocketHandler(Vertx vertx, BridgeOptions options, Handler> eventHandler) { + static Handler httpSocketHandler(Vertx vertx, JsonRPCBridgeOptions options, Handler> eventHandler) { return new HttpJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); } } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index bace686..ddc7547 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -12,6 +12,7 @@ import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; import io.vertx.json.schema.JsonSchema; import java.util.Map; @@ -19,7 +20,7 @@ public class HttpJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { - public HttpJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler) { + public HttpJsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options, Handler> bridgeEventHandler) { super(vertx, options, bridgeEventHandler); } @@ -70,11 +71,11 @@ public void handle(HttpServerRequest socket) { } HttpServerResponse response = socket .response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html"); + .putHeader(HttpHeaders.CONTENT_TYPE, "application/json"); dispatch(response::end, method, id, msg, registry, replies); }); }, // on failure - socket::end); + () -> socket.response().setStatusCode(500).setStatusMessage("Internal Server Error").end()); } } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index c39859c..4ad84da 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -22,11 +22,11 @@ import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonObject; -import io.vertx.core.parsetools.JsonParser; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper; import io.vertx.json.schema.Draft; import io.vertx.json.schema.JsonSchema; @@ -34,26 +34,11 @@ import io.vertx.json.schema.OutputUnit; import io.vertx.json.schema.Validator; -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UncheckedIOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * Abstract TCP EventBus bridge. Handles all common socket operations but has no knowledge on the payload. @@ -65,48 +50,43 @@ public abstract class JsonRPCStreamEventBusBridgeImpl implements Handler { protected static final Logger log = LoggerFactory.getLogger(JsonRPCStreamEventBusBridgeImpl.class); protected static final JsonObject EMPTY = new JsonObject(Collections.emptyMap()); + protected final Vertx vertx; + protected final EventBus eb; protected final Map compiledREs = new HashMap<>(); - protected final BridgeOptions options; + protected final JsonRPCBridgeOptions options; protected final Handler> bridgeEventHandler; private final Validator requestValidator; - public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> eventHandler) { + public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options, Handler> eventHandler) { + this.vertx = vertx; this.eb = vertx.eventBus(); - this.options = options != null ? options : new BridgeOptions(); + this.options = options != null ? options : new JsonRPCBridgeOptions(); this.bridgeEventHandler = eventHandler; this.requestValidator = getRequestValidator(); } private Validator getRequestValidator() { String path = "protocol/jsonrpc.scehma.json"; - try ( - InputStream stream = this.getClass().getResourceAsStream(path); - InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); - BufferedReader br = new BufferedReader(reader) - ) { - String json = br.lines().collect(Collectors.joining()); - return Validator.create( - JsonSchema.of(new JsonObject(json)), - new JsonSchemaOptions() - .setDraft(Draft.DRAFT202012) - .setBaseUri("https://vertx.io") - ); - } - catch (IOException e) { - throw new RuntimeException(e); - } + Buffer buffer = vertx.fileSystem().readFileBlocking(path); + JsonObject json = new JsonObject(buffer); + return Validator.create( + JsonSchema.of(json), + new JsonSchemaOptions() + .setDraft(Draft.DRAFT202012) + .setBaseUri("https://vertx.io") + ); } - protected boolean validate(JsonObject object) { + protected boolean isInvalid(JsonObject object) { OutputUnit outputUnit = requestValidator.validate(object); if (!outputUnit.getValid()) { log.error("Invalid message. Error: " + outputUnit.getErrors() + " . Message: " + object); - return false; + return true; } - return true; + return false; } protected void dispatch(Consumer socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java index 0a0963b..994cc4a 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java @@ -9,13 +9,14 @@ import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class TCPJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { - public TCPJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler) { + public TCPJsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options, Handler> bridgeEventHandler) { super(vertx, options, bridgeEventHandler); } @@ -52,7 +53,7 @@ public void handle(NetSocket socket) { // TODO: body may be an array (batching) final JsonObject msg = new JsonObject(buffer); - if (!this.validate(msg)) { + if (this.isInvalid(msg)) { return; } final String method = msg.getString("method"); diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index 2959a1c..570da70 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -10,6 +10,7 @@ import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -18,7 +19,7 @@ public class WebsocketJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { boolean useText; - public WebsocketJsonRPCStreamEventBusBridgeImpl(Vertx vertx, BridgeOptions options, Handler> bridgeEventHandler, boolean useText) { + public WebsocketJsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options, Handler> bridgeEventHandler, boolean useText) { super(vertx, options, bridgeEventHandler); this.useText = useText; } @@ -67,7 +68,7 @@ public void handle(WebSocketBase socket) { } final JsonObject msg = new JsonObject(frame.binaryData()); - if (!this.validate(msg)) { + if (this.isInvalid(msg)) { return; } From 7e54a8334d5f74b62978ed3fb03db6516a249069 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 13 Jul 2022 16:53:20 +0530 Subject: [PATCH 22/34] Override BridgeOptions methods in JsonRPCBridgeOptions to change return type to latter --- .../bridge/tcp/JsonRPCBridgeOptions.java | 64 +++++++++++++++++++ .../tcp/JsonRPCStreamEventBusBridgeTest.java | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java index e955c6d..9cea4d5 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java @@ -3,6 +3,9 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.BridgeOptionsConverter; +import io.vertx.ext.bridge.PermittedOptions; + +import java.util.List; public class JsonRPCBridgeOptions extends BridgeOptions { private boolean websocketsTextAsFrame; @@ -61,4 +64,65 @@ public boolean getWebsocketsTextAsFrame() { return websocketsTextAsFrame; } + + /** + * Adds an inbound permitted option to the current {@link JsonRPCBridgeOptions}. + * + * @param permitted the inbound permitted + * @return the current {@link JsonRPCBridgeOptions}. + */ + public JsonRPCBridgeOptions addInboundPermitted(PermittedOptions permitted) { + super.addInboundPermitted(permitted); + return this; + } + + /** + * @return the list of inbound permitted options. Empty if none. + */ + public List getInboundPermitteds() { + return super.getInboundPermitteds(); + } + + /** + * Sets the list of inbound permitted options. + * + * @param inboundPermitted the list to use, must not be {@link null}. This method use the direct list reference + * (and doesn't create a copy). + * @return the current {@link JsonRPCBridgeOptions}. + */ + public JsonRPCBridgeOptions setInboundPermitteds(List inboundPermitted) { + super.setInboundPermitteds(inboundPermitted); + return this; + } + + /** + * Adds an outbound permitted option to the current {@link JsonRPCBridgeOptions}. + * + * @param permitted the outbound permitted + * @return the current {@link JsonRPCBridgeOptions}. + */ + public JsonRPCBridgeOptions addOutboundPermitted(PermittedOptions permitted) { + super.addOutboundPermitted(permitted); + return this; + } + + /** + * @return the list of outbound permitted options. Empty if none. + */ + public List getOutboundPermitteds() { + return super.getOutboundPermitteds(); + } + + /** + * Sets the list of outbound permitted options. + * + * @param outboundPermitted the list to use, must not be {@link null}. This method use the direct list reference + * (and doesn't create a copy). + * @return the current {@link JsonRPCBridgeOptions}. + */ + public JsonRPCBridgeOptions setOutboundPermitteds(List outboundPermitted) { + super.setOutboundPermitteds(outboundPermitted); + return this; + } + } diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java index 848dcbd..637704c 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java @@ -60,7 +60,7 @@ public void before(TestContext should) { vertx.createNetServer() .connectHandler(JsonRPCStreamEventBusBridge.netSocketHandler( vertx, - new BridgeOptions() + new JsonRPCBridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("hello")) .addInboundPermitted(new PermittedOptions().setAddress("echo")) .addInboundPermitted(new PermittedOptions().setAddress("test")) From 09eafb138bb10055f9e79c72e903bb1791080410 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 13 Jul 2022 16:58:34 +0530 Subject: [PATCH 23/34] Handle register requests in http transport bridge --- .../HttpJsonRPCStreamEventBusBridgeImpl.java | 24 ++++++--------- .../impl/JsonRPCStreamEventBusBridgeImpl.java | 30 ++++++++++++++----- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index ddc7547..490b25c 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -50,29 +50,23 @@ public void handle(HttpServerRequest socket) { final JsonObject msg = new JsonObject(buffer); System.out.println(msg); - // validation - if (!"2.0".equals(msg.getString("jsonrpc"))) { - log.error("Invalid message: " + msg); + if (this.isInvalid(msg)) { return; } final String method = msg.getString("method"); - if (method == null) { - log.error("Invalid method: " + msg.getString("method")); - return; - } - final Object id = msg.getValue("id"); - if (id != null) { - if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) { - log.error("Invalid id: " + msg.getValue("id")); - return; - } - } HttpServerResponse response = socket .response() .putHeader(HttpHeaders.CONTENT_TYPE, "application/json"); - dispatch(response::end, method, id, msg, registry, replies); + Consumer writer; + if (method.equals("register")) { + response.setChunked(true); + writer = response::write; + } else { + writer = response::end; + } + dispatch(writer, method, id, msg, registry, replies); }); }, // on failure diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 4ad84da..74b8e4c 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -34,11 +34,17 @@ import io.vertx.json.schema.OutputUnit; import io.vertx.json.schema.Validator; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Abstract TCP EventBus bridge. Handles all common socket operations but has no knowledge on the payload. @@ -70,14 +76,22 @@ public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options private Validator getRequestValidator() { String path = "protocol/jsonrpc.scehma.json"; - Buffer buffer = vertx.fileSystem().readFileBlocking(path); - JsonObject json = new JsonObject(buffer); - return Validator.create( - JsonSchema.of(json), - new JsonSchemaOptions() - .setDraft(Draft.DRAFT202012) - .setBaseUri("https://vertx.io") - ); + try ( + InputStream stream = this.getClass().getResourceAsStream(path); + InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(reader) + ) { + String json = br.lines().collect(Collectors.joining()); + return Validator.create( + JsonSchema.of(new JsonObject(json)), + new JsonSchemaOptions() + .setDraft(Draft.DRAFT202012) + .setBaseUri("https://vertx.io") + ); + } + catch (IOException e) { + throw new RuntimeException(e); + } } protected boolean isInvalid(JsonObject object) { From 53b887f01e6a5defe110d505d6e666742a5675e2 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 13 Jul 2022 17:08:29 +0530 Subject: [PATCH 24/34] Remove endHandler from HttpRequest handler For http request, end handler will always be invoked after the request data has been read. While for websockets and tcp sockets, it is only invoked at end of connection. So, it makes sense to have it in the latter but not in http transport. Also, the actions we took in the end handler was remove existing subscriptions but that should actually be done when response has ended. Therefore, added it as bodyHandler to response instead. --- .../HttpJsonRPCStreamEventBusBridgeImpl.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index 490b25c..1c5d143 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -2,21 +2,20 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.Message; import io.vertx.core.eventbus.MessageConsumer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; -import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeEventType; -import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; -import io.vertx.json.schema.JsonSchema; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; public class HttpJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { @@ -31,6 +30,9 @@ public void handle(HttpServerRequest socket) { () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, socket), // on success () -> { + // TODO: make these maps persistent across requests otherwise replies won't work because + // http client cannot reply again in the same request after receiving a response and has + // to make a new request. final Map> registry = new ConcurrentHashMap<>(); final Map> replies = new ConcurrentHashMap<>(); @@ -38,11 +40,6 @@ public void handle(HttpServerRequest socket) { log.error(t.getMessage(), t); registry.values().forEach(MessageConsumer::unregister); registry.clear(); - }).endHandler(v -> { - registry.values().forEach(MessageConsumer::unregister); - // normal close, trigger the event - checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); - registry.clear(); }).handler(buffer -> { // TODO: handle content type @@ -58,7 +55,13 @@ public void handle(HttpServerRequest socket) { final Object id = msg.getValue("id"); HttpServerResponse response = socket .response() - .putHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + .putHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .bodyEndHandler(handler -> { + registry.values().forEach(MessageConsumer::unregister); + // normal close, trigger the event + checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); + registry.clear(); + }); Consumer writer; if (method.equals("register")) { response.setChunked(true); From 0f2d5f7f272db0adcc819148e2435e34ab292b81 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 13 Jul 2022 17:11:29 +0530 Subject: [PATCH 25/34] Make InteropWebSocketServer example use http transport --- .../bridge/tcp/InteropWebSocketServer.java | 42 +++++++++---------- src/test/resources/ws.html | 12 ++++-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index 37380ab..d9585d1 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -7,6 +7,7 @@ import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.WebSocketBase; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetSocket; @@ -31,42 +32,39 @@ public void start(Promise start) { vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); // once we fix the interface we can avoid the casts - Handler bridge = JsonRPCStreamEventBusBridge.webSocketHandler( + Handler bridge = JsonRPCStreamEventBusBridge.httpSocketHandler( vertx, - new BridgeOptions() + new JsonRPCBridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("hello")) .addInboundPermitted(new PermittedOptions().setAddress("echo")) .addInboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("echo")) .addOutboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("ping")), - null, - true + null ); vertx .createHttpServer() .requestHandler(req -> { // this is where any http request will land - - if ("/jsonrpc".equals(req.path())) { - // we switch from HTTP to WebSocket - req.toWebSocket() - .onFailure(err -> { - err.printStackTrace(); - req.response().setStatusCode(500).end(err.getMessage()); - }) - .onSuccess(bridge::handle); + // serve the base HTML application + if ("/".equals(req.path())) { + req.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") + .sendFile("ws.html"); + } else if ("/jsonrpc".equals(req.path())){ + bridge.handle(req); + } else if ("/test-chunked".equals(req.path())) { + HttpServerResponse resp = req.response().setChunked(true); + resp.write("Hello, World!\r\n"); + vertx.setTimer(5000, delay -> resp.write("Foo, Bar!\r\n")); + vertx.setTimer(15000, delay -> { + resp.write("Hello from India!\r\n"); + resp.end(); + }); } else { - // serve the base HTML application - if ("/".equals(req.path())) { - req.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .sendFile("ws.html"); - } else { - // 404 all the rest - req.response().setStatusCode(404).end("Not Found"); - } + req.response().setStatusCode(404).end("Not Found"); } }) .listen(8080) diff --git a/src/test/resources/ws.html b/src/test/resources/ws.html index 5207a4d..2d7a852 100644 --- a/src/test/resources/ws.html +++ b/src/test/resources/ws.html @@ -17,9 +17,15 @@ ws.onclose = console.log; ws.onerror = console.error; - function sendMsg() { - var message = document.getElementById("payload").value; - ws.send(message); + async function sendMsg() { + let message = document.getElementById("payload").value; + await fetch("http://localhost:8080/jsonrpc", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: message, + }); } From 2080059526a0008eaa3a707b903d6858327c887e Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 13 Jul 2022 17:08:29 +0530 Subject: [PATCH 26/34] Remove endHandler from HttpRequest handler For http request, end handler will always be invoked after the request data has been read. While for websockets and tcp sockets, it is only invoked at end of connection. So, it makes sense to have it in the latter but not in http transport. Also, the actions we took in the end handler was remove existing subscriptions but that should actually be done when response has ended. Therefore, added it as bodyHandler to response instead. --- .../HttpJsonRPCStreamEventBusBridgeImpl.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index 490b25c..801efb4 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -2,21 +2,20 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.Message; import io.vertx.core.eventbus.MessageConsumer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; -import io.vertx.core.net.NetSocket; import io.vertx.ext.bridge.BridgeEventType; -import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; -import io.vertx.json.schema.JsonSchema; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; public class HttpJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { @@ -31,6 +30,9 @@ public void handle(HttpServerRequest socket) { () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, socket), // on success () -> { + // TODO: make the reply map persistent across requests otherwise replies won't work because + // http client cannot reply again in the same request after receiving a response and has + // to make a new request. final Map> registry = new ConcurrentHashMap<>(); final Map> replies = new ConcurrentHashMap<>(); @@ -38,11 +40,6 @@ public void handle(HttpServerRequest socket) { log.error(t.getMessage(), t); registry.values().forEach(MessageConsumer::unregister); registry.clear(); - }).endHandler(v -> { - registry.values().forEach(MessageConsumer::unregister); - // normal close, trigger the event - checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); - registry.clear(); }).handler(buffer -> { // TODO: handle content type @@ -58,7 +55,13 @@ public void handle(HttpServerRequest socket) { final Object id = msg.getValue("id"); HttpServerResponse response = socket .response() - .putHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + .putHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .bodyEndHandler(handler -> { + registry.values().forEach(MessageConsumer::unregister); + // normal close, trigger the event + checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); + registry.clear(); + }); Consumer writer; if (method.equals("register")) { response.setChunked(true); From 7c0ec0e18e71e43190e53ef4f99e66ed49334f7a Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 13 Jul 2022 17:11:29 +0530 Subject: [PATCH 27/34] Make InteropWebSocketServer example use http transport --- .../bridge/tcp/InteropWebSocketServer.java | 42 +++++++++---------- src/test/resources/ws.html | 12 ++++-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index 37380ab..d9585d1 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -7,6 +7,7 @@ import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.WebSocketBase; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetSocket; @@ -31,42 +32,39 @@ public void start(Promise start) { vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); // once we fix the interface we can avoid the casts - Handler bridge = JsonRPCStreamEventBusBridge.webSocketHandler( + Handler bridge = JsonRPCStreamEventBusBridge.httpSocketHandler( vertx, - new BridgeOptions() + new JsonRPCBridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("hello")) .addInboundPermitted(new PermittedOptions().setAddress("echo")) .addInboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("echo")) .addOutboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("ping")), - null, - true + null ); vertx .createHttpServer() .requestHandler(req -> { // this is where any http request will land - - if ("/jsonrpc".equals(req.path())) { - // we switch from HTTP to WebSocket - req.toWebSocket() - .onFailure(err -> { - err.printStackTrace(); - req.response().setStatusCode(500).end(err.getMessage()); - }) - .onSuccess(bridge::handle); + // serve the base HTML application + if ("/".equals(req.path())) { + req.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") + .sendFile("ws.html"); + } else if ("/jsonrpc".equals(req.path())){ + bridge.handle(req); + } else if ("/test-chunked".equals(req.path())) { + HttpServerResponse resp = req.response().setChunked(true); + resp.write("Hello, World!\r\n"); + vertx.setTimer(5000, delay -> resp.write("Foo, Bar!\r\n")); + vertx.setTimer(15000, delay -> { + resp.write("Hello from India!\r\n"); + resp.end(); + }); } else { - // serve the base HTML application - if ("/".equals(req.path())) { - req.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .sendFile("ws.html"); - } else { - // 404 all the rest - req.response().setStatusCode(404).end("Not Found"); - } + req.response().setStatusCode(404).end("Not Found"); } }) .listen(8080) diff --git a/src/test/resources/ws.html b/src/test/resources/ws.html index 5207a4d..2d7a852 100644 --- a/src/test/resources/ws.html +++ b/src/test/resources/ws.html @@ -17,9 +17,15 @@ ws.onclose = console.log; ws.onerror = console.error; - function sendMsg() { - var message = document.getElementById("payload").value; - ws.send(message); + async function sendMsg() { + let message = document.getElementById("payload").value; + await fetch("http://localhost:8080/jsonrpc", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: message, + }); } From ec257d051caca0318d79b16537bfe1ed16eefc92 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 21 Jul 2022 01:09:45 +0530 Subject: [PATCH 28/34] Add tests for each transport --- pom.xml | 5 + .../bridge/tcp/JsonRPCBridgeOptions.java | 2 + .../HttpJsonRPCStreamEventBusBridgeImpl.java | 10 +- .../impl/JsonRPCStreamEventBusBridgeImpl.java | 28 +- .../tcp/impl/protocol/JsonRPCHelper.java | 17 +- ...tpJsonRPCStreamEventBusBridgeImplTest.java | 668 +++++++++++++++++ ...PJsonRPCStreamEventBusBridgeImplTest.java} | 64 +- ...etJsonRPCStreamEventBusBridgeImplTest.java | 682 ++++++++++++++++++ 8 files changed, 1416 insertions(+), 60 deletions(-) create mode 100644 src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java rename src/test/java/io/vertx/ext/eventbus/bridge/tcp/{JsonRPCStreamEventBusBridgeTest.java => TCPJsonRPCStreamEventBusBridgeImplTest.java} (96%) create mode 100644 src/test/java/io/vertx/ext/eventbus/bridge/tcp/WebsocketJsonRPCStreamEventBusBridgeImplTest.java diff --git a/pom.xml b/pom.xml index a4c426e..7b478ce 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,11 @@ test-jar test + + io.vertx + vertx-web-client + test + junit junit diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java index 9cea4d5..ec1f34d 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java @@ -1,5 +1,6 @@ package io.vertx.ext.eventbus.bridge.tcp; +import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.BridgeOptionsConverter; @@ -7,6 +8,7 @@ import java.util.List; +@DataObject(generateConverter = true) public class JsonRPCBridgeOptions extends BridgeOptions { private boolean websocketsTextAsFrame; diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index 801efb4..681906d 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -19,6 +19,10 @@ public class HttpJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { + // http client cannot reply in the same request in which it originally received + // a response so the replies map should be persistent across http request + final Map> replies = new ConcurrentHashMap<>(); + public HttpJsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options, Handler> bridgeEventHandler) { super(vertx, options, bridgeEventHandler); } @@ -30,11 +34,7 @@ public void handle(HttpServerRequest socket) { () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, socket), // on success () -> { - // TODO: make the reply map persistent across requests otherwise replies won't work because - // http client cannot reply again in the same request after receiving a response and has - // to make a new request. final Map> registry = new ConcurrentHashMap<>(); - final Map> replies = new ConcurrentHashMap<>(); socket.exceptionHandler(t -> { log.error(t.getMessage(), t); @@ -56,7 +56,7 @@ public void handle(HttpServerRequest socket) { HttpServerResponse response = socket .response() .putHeader(HttpHeaders.CONTENT_TYPE, "application/json") - .bodyEndHandler(handler -> { + .endHandler(handler -> { registry.values().forEach(MessageConsumer::unregister); // normal close, trigger the event checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 74b8e4c..3669e8c 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -75,23 +75,16 @@ public JsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options } private Validator getRequestValidator() { - String path = "protocol/jsonrpc.scehma.json"; - try ( - InputStream stream = this.getClass().getResourceAsStream(path); - InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); - BufferedReader br = new BufferedReader(reader) - ) { - String json = br.lines().collect(Collectors.joining()); - return Validator.create( - JsonSchema.of(new JsonObject(json)), - new JsonSchemaOptions() - .setDraft(Draft.DRAFT202012) - .setBaseUri("https://vertx.io") - ); - } - catch (IOException e) { - throw new RuntimeException(e); + String path = "io/vertx/ext/eventbus/bridge/tcp/impl/protocol/jsonrpc.scehma.json"; + try { + Buffer buffer = vertx.fileSystem().readFileBlocking(path); + JsonObject jsonObject = buffer.toJsonObject(); + return Validator.create(JsonSchema.of(jsonObject), + new JsonSchemaOptions().setDraft(Draft.DRAFT202012).setBaseUri("https://vertx.io")); + } catch (Exception e) { + e.printStackTrace(); } + return null; } protected boolean isInvalid(JsonObject object) { @@ -196,7 +189,6 @@ protected void register(Consumer socket, Object id, JsonObject msg, Map< .put("address", res1.address()) .put("replyAddress", res1.replyAddress()) .put("headers", responseHeaders) - .put("isSend", res1.isSend()) .put("body", res1.body()), socket); })); @@ -272,8 +264,6 @@ protected void send(Consumer socket, Object id, JsonObject msg, Map handler) { + public static void request(String method, Object id, JsonObject params, MultiMap headers, Consumer handler) { final JsonObject payload = new JsonObject().put("jsonrpc", "2.0"); @@ -57,30 +56,30 @@ public static void request(String method, Object id, JsonObject params, MultiMap // write if (headers != null) { headers.forEach(entry -> { - handler.write( + handler.accept( Buffer.buffer(entry.getKey()).appendString(": ").appendString(entry.getValue()).appendString("\r\n") ); }); // end of headers - handler.write(Buffer.buffer("\r\n")); + handler.accept(Buffer.buffer("\r\n")); } - handler.write(payload.toBuffer().appendString("\r\n")); + handler.accept(payload.toBuffer().appendString("\r\n")); } - public static void request(String method, Object id, JsonObject params, WriteStream handler) { + public static void request(String method, Object id, JsonObject params, Consumer handler) { request(method, id, params, null, handler); } - public static void request(String method, Object id, WriteStream handler) { + public static void request(String method, Object id, Consumer handler) { request(method, id, null, null, handler); } - public static void request(String method, WriteStream handler) { + public static void request(String method, Consumer handler) { request(method, null, null, null, handler); } - public static void request(String method, JsonObject params, WriteStream handler) { + public static void request(String method, JsonObject params, Consumer handler) { request(method, null, params, null, handler); } diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java new file mode 100644 index 0000000..6ad4025 --- /dev/null +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java @@ -0,0 +1,668 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetSocket; +import io.vertx.ext.bridge.BridgeOptions; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.impl.StreamParser; +import io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.RunTestOnContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.ext.web.client.WebClient; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.*; + +@RunWith(VertxUnitRunner.class) +public class HttpJsonRPCStreamEventBusBridgeImplTest { + + @Rule + public RunTestOnContext rule = new RunTestOnContext(); + + private final Handler> eventHandler = event -> event.complete(true); + + @Before + public void before(TestContext should) { + final Async test = should.async(); + final Vertx vertx = rule.vertx(); + + vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + + vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); + + vertx.setPeriodic(1000, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); + + vertx + .createHttpServer() + .requestHandler(JsonRPCStreamEventBusBridge.httpSocketHandler( + vertx, + new JsonRPCBridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("hello")) + .addInboundPermitted(new PermittedOptions().setAddress("echo")) + .addInboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("echo")) + .addOutboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("ping")), + eventHandler)) + .listen(7000, res -> { + should.assertTrue(res.succeeded()); + test.complete(); + }); + } + + @Test(timeout = 10_000L) + public void testSendVoidMessage(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + final WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + vertx.eventBus().consumer("test", (Message msg) -> { + client.close(); + test.complete(); + }); + + request( + "send", + new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer) + ); + + } + + @Test(timeout = 10_000L) + public void testNoHandlers(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + final WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("body", new JsonObject().put("value", "vert.x")), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + client.close(); + test.complete(); + }) + .onFailure(should::fail) + ); + } + + @Test(timeout = 10_000L) + public void testErrorReply(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + final WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + vertx.eventBus().consumer("test", (Message msg) -> { + msg.fail(0, "oops!"); + }); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("body", new JsonObject().put("value", "vert.x")), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + client.close(); + test.complete(); + }) + .onFailure(should::fail) + ); + + } + + @Test(timeout = 10_000L) + public void testSendsFromOtherSideOfBridge(TestContext should) { + final Vertx vertx = rule.vertx(); + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + final AtomicBoolean ack = new AtomicBoolean(false); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", "ping"), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + + if (!ack.getAndSet(true)) { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + } else { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals("hi", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + } + }) + .onFailure(should::fail) + ); + + } + + @Test(timeout = 10_000L) + public void testSendMessageWithReplyBacktrack(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "hello") + .put("body", new JsonObject().put("value", "vert.x")), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals("Hello vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + }) + .onFailure(should::fail) + ); + } + + @Test(timeout = 10_000L) + public void testSendMessageWithReplyBacktrackTimeout(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + // This does not reply and will provoke a timeout + vertx.eventBus().consumer("test", (Message msg) -> { /* Nothing! */ }); + + JsonObject headers = new JsonObject().put("timeout", 100L); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("headers", headers) + .put("body", new JsonObject().put("value", "vert.x")), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject error = frame.getJsonObject("error"); + + should.assertEquals("Timed out after waiting 100(ms) for a reply. address: __vertx.reply.1, repliedAddress: test", error.getString("message")); + should.assertEquals(-1, error.getInteger("code")); + + client.close(); + test.complete(); + }) + .onFailure(should::fail) + ); + + } + + @Test(timeout = 10_000L) + public void testSendMessageWithDuplicateReplyID(TestContext should) { + // replies must always return to the same origin + + final Vertx vertx = rule.vertx(); + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + vertx.eventBus().consumer("third-party-receiver", msg -> should.fail()); + + request( + "send", + "third-party-receiver", + new JsonObject() + .put("address", "hello") + .put("body", new JsonObject().put("value", "vert.x")), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + client.close(); + test.complete(); + }) + .onFailure(should::fail) + ); + } + + @Test(timeout = 10_000L) + public void testRegister(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + final AtomicInteger messageCount = new AtomicInteger(0); + + request( + "register", + "#backtrack", + new JsonObject().put("address", "echo"), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + // 2 messages will arrive + // 1) ACK for register message + // 2) MESSAGE for echo + if (messageCount.get() == 0) { + // ACK for register message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time ACK for publish is expected + should.assertTrue(messageCount.compareAndSet(0, 1)); + } else { + // reply for echo message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + } + }) + .onFailure(should::fail) + ); + + // now try to publish a message so it gets delivered both to the consumer registred on the startup and to this + // remote consumer + + request( + "publish", + "#backtrack", + new JsonObject() + .put("address", "echo") + .put("body", new JsonObject().put("value", "Vert.x")), + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + // ACK for publish message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + }) + .onFailure(should::fail) + ); + + } + +// @Test(timeout = 10_000L) +// public void testUnRegister(TestContext should) { +// // Send a request and get a response +// final Vertx vertx = rule.vertx(); +// WebClient client = WebClient.create(vertx); +// final Async test = should.async(); +// +// final String address = "test"; +// // 4 replies will arrive: +// // 1). ACK for register +// // 2). ACK for publish +// // 3). message published to test +// // 4). err of NO_HANDLERS because of consumer for 'test' is unregistered. +// final AtomicInteger messageCount = new AtomicInteger(0); +// final AtomicInteger messageCount2 = new AtomicInteger(0); +// final StreamParser parser = new StreamParser() +// .exceptionHandler(should::fail) +// .handler(body -> { +// JsonObject frame = new JsonObject(body); +// +// if (messageCount.get() == 0) { +// // ACK for register message +// should.assertFalse(frame.containsKey("error")); +// should.assertTrue(frame.containsKey("result")); +// should.assertEquals("#backtrack", frame.getValue("id")); +// // increment message count so that next time ACK for publish is expected +// should.assertTrue(messageCount.compareAndSet(0, 1)); +// } +// else if (messageCount.get() == 1) { +// // got message, then unregister the handler +// should.assertFalse(frame.containsKey("error")); +// JsonObject result = frame.getJsonObject("result"); +// should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); +// +// request( +// "unregister", +// "#backtrack", +// new JsonObject().put("address", address), +// buffer -> client +// .post(7000, "localhost", "/") +// .sendBuffer(buffer) +// .onSuccess(handler -> { +// JsonObject frame2 = handler.bodyAsJsonObject(); +// if (messageCount2.get() == 0) { +// // ACK for publish message +// should.assertFalse(frame2.containsKey("error")); +// should.assertTrue(frame2.containsKey("result")); +// should.assertEquals("#backtrack", frame2.getValue("id")); +// // increment message count so that next time reply for echo message is expected +// should.assertTrue(messageCount2.compareAndSet(0, 1)); +// } else { +// // ACK for unregister message +// should.assertFalse(frame.containsKey("error")); +// should.assertTrue(frame.containsKey("result")); +// should.assertEquals("#backtrack", frame.getValue("id")); +// // increment message count so that next time error reply for send message is expected +// should.assertTrue(messageCount.compareAndSet(3, 4)); +// +// request( +// "send", +// "#backtrack", +// new JsonObject() +// .put("address", address) +// .put("body", new JsonObject().put("value", "This will fail anyway!")), +// socket +// ); +// } +// }) +// .onFailure(should::fail) +// ); +// } else { +// // TODO: Check error handling of bridge for consistency +// // consumer on 'test' has been unregistered, send message will fail. +// should.assertTrue(frame.containsKey("error")); +// JsonObject error = frame.getJsonObject("error"); +// should.assertEquals(-1, error.getInteger("code")); +// should.assertEquals("No handlers for address test", error.getString("message")); +// +// client.close(); +// test.complete(); +// } +// }); +// +// request( +// "register", +// "#backtrack", +// new JsonObject() +// .put("address", address), +// buffer -> client +// .post(7000, "localhost", "/") +// .sendBuffer(buffer) +// .onSuccess(handler -> { +// JsonObject frame = handler.bodyAsJsonObject(); +// // ACK for publish message +// should.assertFalse(frame.containsKey("error")); +// should.assertTrue(frame.containsKey("result")); +// should.assertEquals("#backtrack", frame.getValue("id")); +// // increment message count so that next time reply for echo message is expected +// should.assertTrue(messageCount.compareAndSet(1, 2)); +// }) +// .onFailure(should::fail) +// ); +// +// request( +// "publish", +// "#backtrack", +// new JsonObject() +// .put("address", address) +// .put("body", new JsonObject().put("value", "Vert.x")), +// socket::end +// ); +// } +// +// @Test(timeout = 10_000L) +// public void testReplyFromClient(TestContext should) { +// // Send a request from java and get a response from the client +// final Vertx vertx = rule.vertx(); +// WebClient client = WebClient.create(vertx); +// final Async test = should.async(); +// final String address = "test"; +// +// final AtomicBoolean ack = new AtomicBoolean(false); +// final StreamParser parser = new StreamParser() +// .exceptionHandler(should::fail) +// .handler(body -> { +// JsonObject frame = new JsonObject(body); +// +// if (!ack.getAndSet(true)) { +// should.assertFalse(frame.containsKey("error")); +// should.assertTrue(frame.containsKey("result")); +// should.assertEquals("#backtrack", frame.getValue("id")); +// } else { +// JsonObject result = frame.getJsonObject("result"); +// should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); +// +// request( +// "send", +// "#backtrack", +// new JsonObject() +// .put("address", result.getString("replyAddress")) +// .put("body", new JsonObject().put("value", "You got it")), +// socket::end +// ); +// } +// }); +// +// request( +// "register", +// "#backtrack", +// new JsonObject() +// .put("address", address), +// buffer -> client +// .post(7000, "localhost", "/") +// .sendBuffer(buffer) +// .onSuccess(handler -> { +// JsonObject frame = handler.bodyAsJsonObject(); +// // ACK for publish message +// should.assertFalse(frame.containsKey("error")); +// should.assertTrue(frame.containsKey("result")); +// should.assertEquals("#backtrack", frame.getValue("id")); +// }) +// .onFailure(should::fail) +// ); +// +// // There is no way to know that the register actually happened, wait a bit before sending. +// vertx.setTimer(500L, timerId -> { +// vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { +// should.assertTrue(respMessage.succeeded()); +// should.assertEquals("You got it", respMessage.result().body().getString("value")); +// client.close(); +// test.complete(); +// }); +// }); +// +// } +// +// @Test(timeout = 10_000L) +// public void testFailFromClient(TestContext should) { +// // Send a request from java and get a response from the client +// final Vertx vertx = rule.vertx(); +// +// WebClient client = WebClient.create(vertx); +// final Async test = should.async(); +// final String address = "test"; +// client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { +// +// final AtomicBoolean ack = new AtomicBoolean(false); +// final StreamParser parser = new StreamParser() +// .exceptionHandler(should::fail) +// .handler(body -> { +// JsonObject frame = new JsonObject(body); +// if (!ack.getAndSet(true)) { +// should.assertFalse(frame.containsKey("error")); +// should.assertTrue(frame.containsKey("result")); +// should.assertEquals("#backtrack", frame.getValue("id")); +// } else { +// JsonObject result = frame.getJsonObject("result"); +// should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); +// +// request( +// "send", +// null, +// new JsonObject() +// .put("address", result.getString("replyAddress")) +// .put("error", new JsonObject().put("failureCode", 1234).put("message", "ooops!")), +// socket::end +// ); +// } +// }); +// +// socket.handler(parser); +// +// request( +// "register", +// "#backtrack", +// new JsonObject() +// .put("address", address), +// socket::end +// ); +// +// // There is now way to know that the register actually happened, wait a bit before sending. +// vertx.setTimer(500L, timerId -> { +// vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { +// should.assertTrue(respMessage.failed()); +// should.assertEquals("ooops!", respMessage.cause().getMessage()); +// client.close(); +// test.complete(); +// }); +// }); +// })); +// } + + @Test(timeout = 10_000L) + public void testSendPing(TestContext should) { + final Vertx vertx = rule.vertx(); + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + request( + "ping", + "#backtrack", + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + should.assertEquals("pong", frame.getString("result")); + client.close(); + test.complete(); + }) + .onFailure(should::fail) + ); + } + + @Test(timeout = 10_000L) + public void testNoAddress(TestContext should) { + final Vertx vertx = rule.vertx(); + + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + final AtomicBoolean errorOnce = new AtomicBoolean(false); + + request( + "send", + "#backtrack", + buffer -> client + .post(7000, "localhost", "/") + .sendBuffer(buffer) + .onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + if (!errorOnce.compareAndSet(false, true)) { + should.fail("Client gets error message twice!"); + } else { + should.assertTrue(frame.containsKey("error")); + should.assertEquals("invalid_parameters", frame.getJsonObject("error").getString("message")); + vertx.setTimer(200, l -> { + client.close(); + test.complete(); + }); + } + }) + .onFailure(should::fail) + ); + } + +} diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TCPJsonRPCStreamEventBusBridgeImplTest.java similarity index 96% rename from src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java rename to src/test/java/io/vertx/ext/eventbus/bridge/tcp/TCPJsonRPCStreamEventBusBridgeImplTest.java index 637704c..403dd9e 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridgeTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TCPJsonRPCStreamEventBusBridgeImplTest.java @@ -21,7 +21,6 @@ import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetClient; import io.vertx.core.net.NetSocket; -import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.StreamParser; import io.vertx.ext.unit.Async; @@ -39,7 +38,7 @@ import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.*; @RunWith(VertxUnitRunner.class) -public class JsonRPCStreamEventBusBridgeTest { +public class TCPJsonRPCStreamEventBusBridgeImplTest { @Rule public RunTestOnContext rule = new RunTestOnContext(); @@ -87,7 +86,7 @@ public void testSendVoidMessage(TestContext should) { }); client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { - request("send", new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), socket); + request("send", new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), socket::write); })); } @@ -120,7 +119,8 @@ public void testNoHandlers(TestContext should) { new JsonObject() .put("address", "test") .put("body", new JsonObject().put("value", "vert.x")), - socket); + socket::write + ); })); } @@ -158,7 +158,8 @@ public void testErrorReply(TestContext should) { new JsonObject() .put("address", "test") .put("body", new JsonObject().put("value", "vert.x")), - socket); + socket::write + ); })); } @@ -191,7 +192,6 @@ public void testSendsFromOtherSideOfBridge(TestContext should) { JsonObject result = frame.getJsonObject("result"); - should.assertEquals(true, result.getBoolean("isSend")); should.assertEquals("hi", result.getJsonObject("body").getString("value")); client.close(); test.complete(); @@ -205,7 +205,8 @@ public void testSendsFromOtherSideOfBridge(TestContext should) { "#backtrack", new JsonObject() .put("address", "ping"), - socket); + socket::write + ); })); } @@ -230,7 +231,6 @@ public void testSendMessageWithReplyBacktrack(TestContext should) { JsonObject result = frame.getJsonObject("result"); - should.assertEquals(true, result.getBoolean("send")); should.assertEquals("Hello vert.x", result.getJsonObject("body").getString("value")); client.close(); test.complete(); @@ -244,7 +244,8 @@ public void testSendMessageWithReplyBacktrack(TestContext should) { new JsonObject() .put("address", "hello") .put("body", new JsonObject().put("value", "vert.x")), - socket); + socket::write + ); })); } @@ -289,7 +290,8 @@ public void testSendMessageWithReplyBacktrackTimeout(TestContext should) { .put("address", "test") .put("headers", headers) .put("body", new JsonObject().put("value", "vert.x")), - socket); + socket::write + ); })); } @@ -322,7 +324,8 @@ public void testSendMessageWithDuplicateReplyID(TestContext should) { new JsonObject() .put("address", "hello") .put("body", new JsonObject().put("value", "vert.x")), - socket); + socket::write + ); })); } @@ -368,7 +371,6 @@ else if (messageCount.get() == 1) { JsonObject result = frame.getJsonObject("result"); - should.assertEquals(false, result.getBoolean("isSend")); should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); client.close(); test.complete(); @@ -382,7 +384,8 @@ else if (messageCount.get() == 1) { "#backtrack", new JsonObject() .put("address", "echo"), - socket); + socket::write + ); // now try to publish a message so it gets delivered both to the consumer registred on the startup and to this // remote consumer @@ -393,7 +396,8 @@ else if (messageCount.get() == 1) { new JsonObject() .put("address", "echo") .put("body", new JsonObject().put("value", "Vert.x")), - socket); + socket::write + ); })); } @@ -438,13 +442,12 @@ else if (messageCount.get() == 1) { // got message, then unregister the handler should.assertFalse(frame.containsKey("error")); JsonObject result = frame.getJsonObject("result"); - should.assertEquals(false, result.getBoolean("isSend")); should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); // increment message count so that next time ACK for unregister is expected should.assertTrue(messageCount.compareAndSet(2, 3)); - request("unregister", "#backtrack", new JsonObject().put("address", address), socket); + request("unregister", "#backtrack", new JsonObject().put("address", address), socket::write); } else if (messageCount.get() == 3) { // ACK for unregister message should.assertFalse(frame.containsKey("error")); @@ -459,7 +462,8 @@ else if (messageCount.get() == 1) { new JsonObject() .put("address", address) .put("body", new JsonObject().put("value", "This will fail anyway!")), - socket); + socket::write + ); } else { // TODO: Check error handling of bridge for consistency // consumer on 'test' has been unregistered, send message will fail. @@ -480,7 +484,8 @@ else if (messageCount.get() == 1) { "#backtrack", new JsonObject() .put("address", address), - socket); + socket::write + ); request( "publish", @@ -488,7 +493,8 @@ else if (messageCount.get() == 1) { new JsonObject() .put("address", address) .put("body", new JsonObject().put("value", "Vert.x")), - socket); + socket::write + ); })); } @@ -513,7 +519,6 @@ public void testReplyFromClient(TestContext should) { should.assertEquals("#backtrack", frame.getValue("id")); } else { JsonObject result = frame.getJsonObject("result"); - should.assertTrue(result.getBoolean("isSend")); should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); request( @@ -522,7 +527,8 @@ public void testReplyFromClient(TestContext should) { new JsonObject() .put("address", result.getString("replyAddress")) .put("body", new JsonObject().put("value", "You got it")), - socket); + socket::write + ); } }); @@ -533,7 +539,8 @@ public void testReplyFromClient(TestContext should) { "#backtrack", new JsonObject() .put("address", address), - socket); + socket::write + ); // There is now way to know that the register actually happened, wait a bit before sending. vertx.setTimer(500L, timerId -> { @@ -570,7 +577,6 @@ public void testFailFromClient(TestContext should) { should.assertEquals("#backtrack", frame.getValue("id")); } else { JsonObject result = frame.getJsonObject("result"); - should.assertTrue(result.getBoolean("isSend")); should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); request( @@ -579,7 +585,8 @@ public void testFailFromClient(TestContext should) { new JsonObject() .put("address", result.getString("replyAddress")) .put("error", new JsonObject().put("failureCode", 1234).put("message", "ooops!")), - socket); + socket::write + ); } }); @@ -590,7 +597,8 @@ public void testFailFromClient(TestContext should) { "#backtrack", new JsonObject() .put("address", address), - socket); + socket::write + ); // There is now way to know that the register actually happened, wait a bit before sending. vertx.setTimer(500L, timerId -> { @@ -629,7 +637,8 @@ public void testSendPing(TestContext should) { request( "ping", "#backtrack", - socket); + socket::write + ); })); } @@ -660,7 +669,8 @@ public void testNoAddress(TestContext should) { request( "send", "#backtrack", - socket); + socket::write + ); })); } diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/WebsocketJsonRPCStreamEventBusBridgeImplTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/WebsocketJsonRPCStreamEventBusBridgeImplTest.java new file mode 100644 index 0000000..abe0276 --- /dev/null +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/WebsocketJsonRPCStreamEventBusBridgeImplTest.java @@ -0,0 +1,682 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.eventbus.bridge.tcp; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.WebSocketBase; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.impl.StreamParser; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.RunTestOnContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.*; + +@RunWith(VertxUnitRunner.class) +public class WebsocketJsonRPCStreamEventBusBridgeImplTest { + + + @Rule + public RunTestOnContext rule = new RunTestOnContext(); + + private final Handler> eventHandler = event -> event.complete(true); + + @Before + public void before(TestContext should) { + final Async test = should.async(); + final Vertx vertx = rule.vertx(); + + vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + + vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); + + vertx.setPeriodic(1000, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); + + final Handler bridge = JsonRPCStreamEventBusBridge.webSocketHandler( + vertx, + new JsonRPCBridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("hello")) + .addInboundPermitted(new PermittedOptions().setAddress("echo")) + .addInboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("echo")) + .addOutboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("ping")), + eventHandler + ); + + vertx + .createHttpServer() + .webSocketHandler(bridge::handle) + .listen(7000, res -> { + should.assertTrue(res.succeeded()); + test.complete(); + }); + } + + @Test(timeout = 10_000L) + public void testSendVoidMessage(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + vertx.eventBus().consumer("test", (Message msg) -> { + client.close(); + test.complete(); + }); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> + request("send", new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), socket::write) + )); + } + + @Test(timeout = 10_000L) + public void testNoHandlers(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .handler(body -> { + JsonObject frame = new JsonObject(body); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + client.close(); + test.complete(); + }).exceptionHandler(should::fail); + + socket.handler(parser); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("body", new JsonObject().put("value", "vert.x")), + socket::write + ); + })); + } + + @Test(timeout = 10_000L) + public void testErrorReply(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + vertx.eventBus().consumer("test", (Message msg) -> { + msg.fail(0, "oops!"); + }); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + client.close(); + test.complete(); + }); + + socket.handler(parser); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("body", new JsonObject().put("value", "vert.x")), + socket::write + ); + })); + } + + @Test(timeout = 10_000L) + public void testSendsFromOtherSideOfBridge(TestContext should) { + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + final AtomicBoolean ack = new AtomicBoolean(false); + + // 2 replies will arrive: + // 1). acknowledge register + // 2). greeting + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + if (!ack.getAndSet(true)) { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + } else { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals("hi", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", "ping"), + socket::write + ); + })); + + } + + @Test(timeout = 10_000L) + public void testSendMessageWithReplyBacktrack(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals("Hello vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + }); + + socket.handler(parser); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "hello") + .put("body", new JsonObject().put("value", "vert.x")), + socket::write + ); + })); + } + + @Test(timeout = 10_000L) + public void testSendMessageWithReplyBacktrackTimeout(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + // This does not reply and will provoke a timeout + vertx.eventBus().consumer("test", (Message msg) -> { /* Nothing! */ }); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject error = frame.getJsonObject("error"); + + should.assertEquals("Timed out after waiting 100(ms) for a reply. address: __vertx.reply.1, repliedAddress: test", error.getString("message")); + should.assertEquals(-1, error.getInteger("code")); + + client.close(); + test.complete(); + }); + + socket.handler(parser); + + JsonObject headers = new JsonObject().put("timeout", 100L); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", "test") + .put("headers", headers) + .put("body", new JsonObject().put("value", "vert.x")), + socket::write + ); + })); + } + + @Test(timeout = 10_000L) + public void testSendMessageWithDuplicateReplyID(TestContext should) { + // replies must always return to the same origin + + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + vertx.eventBus().consumer("third-party-receiver", msg -> should.fail()); + + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + client.close(); + test.complete(); + }); + + socket.handler(parser); + + + request( + "send", + "third-party-receiver", + new JsonObject() + .put("address", "hello") + .put("body", new JsonObject().put("value", "vert.x")), + socket::write + ); + })); + } + + @Test(timeout = 10_000L) + public void testRegister(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + final AtomicInteger messageCount = new AtomicInteger(0); + + // 3 messages will arrive + // 1) ACK for register message + // 2) ACK for publish message + // 3) MESSAGE for echo + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + if (messageCount.get() == 0) { + // ACK for register message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time ACK for publish is expected + should.assertTrue(messageCount.compareAndSet(0, 1)); + } + else if (messageCount.get() == 1) { + // ACK for publish message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time reply for echo message is expected + should.assertTrue(messageCount.compareAndSet(1, 2)); + } else { + // reply for echo message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + JsonObject result = frame.getJsonObject("result"); + + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", "echo"), + socket::write + ); + + // now try to publish a message so it gets delivered both to the consumer registred on the startup and to this + // remote consumer + + request( + "publish", + "#backtrack", + new JsonObject() + .put("address", "echo") + .put("body", new JsonObject().put("value", "Vert.x")), + socket::write + ); + })); + + } + + @Test(timeout = 10_000L) + public void testUnRegister(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + + final String address = "test"; + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + // 4 replies will arrive: + // 1). ACK for register + // 2). ACK for publish + // 3). message published to test + // 4). err of NO_HANDLERS because of consumer for 'test' is unregistered. + final AtomicInteger messageCount = new AtomicInteger(0); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + if (messageCount.get() == 0) { + // ACK for register message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time ACK for publish is expected + should.assertTrue(messageCount.compareAndSet(0, 1)); + } + else if (messageCount.get() == 1) { + // ACK for publish message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time reply for echo message is expected + should.assertTrue(messageCount.compareAndSet(1, 2)); + } else if (messageCount.get() == 2) { + // got message, then unregister the handler + should.assertFalse(frame.containsKey("error")); + JsonObject result = frame.getJsonObject("result"); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + + // increment message count so that next time ACK for unregister is expected + should.assertTrue(messageCount.compareAndSet(2, 3)); + + request("unregister", "#backtrack", new JsonObject().put("address", address), socket::write); + } else if (messageCount.get() == 3) { + // ACK for unregister message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time error reply for send message is expected + should.assertTrue(messageCount.compareAndSet(3, 4)); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", address) + .put("body", new JsonObject().put("value", "This will fail anyway!")), + socket::write + ); + } else { + // TODO: Check error handling of bridge for consistency + // consumer on 'test' has been unregistered, send message will fail. + should.assertTrue(frame.containsKey("error")); + JsonObject error = frame.getJsonObject("error"); + should.assertEquals(-1, error.getInteger("code")); + should.assertEquals("No handlers for address test", error.getString("message")); + + client.close(); + test.complete(); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", address), + socket::write + ); + + request( + "publish", + "#backtrack", + new JsonObject() + .put("address", address) + .put("body", new JsonObject().put("value", "Vert.x")), + socket::write + ); + })); + } + + @Test(timeout = 10_000L) + public void testReplyFromClient(TestContext should) { + // Send a request from java and get a response from the client + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + final String address = "test"; + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + final AtomicBoolean ack = new AtomicBoolean(false); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + if (!ack.getAndSet(true)) { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + } else { + JsonObject result = frame.getJsonObject("result"); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + + request( + "send", + "#backtrack", + new JsonObject() + .put("address", result.getString("replyAddress")) + .put("body", new JsonObject().put("value", "You got it")), + socket::write + ); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", address), + socket::write + ); + + // There is now way to know that the register actually happened, wait a bit before sending. + vertx.setTimer(500L, timerId -> { + vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { + should.assertTrue(respMessage.succeeded()); + should.assertEquals("You got it", respMessage.result().body().getString("value")); + client.close(); + test.complete(); + }); + }); + + })); + + } + + @Test(timeout = 10_000L) + public void testFailFromClient(TestContext should) { + // Send a request from java and get a response from the client + final Vertx vertx = rule.vertx(); + + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + final String address = "test"; + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + + final AtomicBoolean ack = new AtomicBoolean(false); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + if (!ack.getAndSet(true)) { + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + } else { + JsonObject result = frame.getJsonObject("result"); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + + request( + "send", + null, + new JsonObject() + .put("address", result.getString("replyAddress")) + .put("error", new JsonObject().put("failureCode", 1234).put("message", "ooops!")), + socket::write + ); + } + }); + + socket.handler(parser); + + request( + "register", + "#backtrack", + new JsonObject() + .put("address", address), + socket::write + ); + + // There is now way to know that the register actually happened, wait a bit before sending. + vertx.setTimer(500L, timerId -> { + vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { + should.assertTrue(respMessage.failed()); + should.assertEquals("ooops!", respMessage.cause().getMessage()); + client.close(); + test.complete(); + }); + }); + })); + } + + @Test(timeout = 10_000L) + public void testSendPing(TestContext should) { + final Vertx vertx = rule.vertx(); + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + // MESSAGE for ping + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + should.assertEquals("pong", frame.getString("result")); + client.close(); + test.complete(); + }); + + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + socket.handler(parser); + request( + "ping", + "#backtrack", + socket::write + ); + })); + } + + @Test(timeout = 10_000L) + public void testNoAddress(TestContext should) { + final Vertx vertx = rule.vertx(); + + HttpClient client = vertx.createHttpClient(); + final Async test = should.async(); + final AtomicBoolean errorOnce = new AtomicBoolean(false); + final StreamParser parser = new StreamParser() + .exceptionHandler(should::fail) + .handler(body -> { + JsonObject frame = new JsonObject(body); + if (!errorOnce.compareAndSet(false, true)) { + should.fail("Client gets error message twice!"); + } else { + should.assertTrue(frame.containsKey("error")); + should.assertEquals("invalid_parameters", frame.getJsonObject("error").getString("message")); + vertx.setTimer(200, l -> { + client.close(); + test.complete(); + }); + } + }); + client.webSocket(7000, "localhost", "/", should.asyncAssertSuccess(socket -> { + socket.handler(parser); + request( + "send", + "#backtrack", + socket::write + ); + })); + } + +} From 21d44a76fc6cd824addf5916d18a0314df07741b Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 21 Jul 2022 01:21:10 +0530 Subject: [PATCH 29/34] Add converter for JsonRPCBridgeOptions --- .../ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java index ec1f34d..f4efa81 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCBridgeOptions.java @@ -32,8 +32,7 @@ public JsonRPCBridgeOptions(JsonRPCBridgeOptions that) { * @see BridgeOptionsConverter */ public JsonRPCBridgeOptions(JsonObject json) { - BridgeOptionsConverter.fromJson(json, this); - this.websocketsTextAsFrame = json.getBoolean("websocketsTextAsFrame", false); + JsonRPCBridgeOptionsConverter.fromJson(json, this); } /** @@ -43,8 +42,7 @@ public JsonRPCBridgeOptions(JsonObject json) { */ public JsonObject toJson() { JsonObject json = new JsonObject(); - BridgeOptionsConverter.toJson(this, json); - json.put("websocketsTextAsFrame", websocketsTextAsFrame); + JsonRPCBridgeOptionsConverter.toJson(this, json); return json; } @@ -126,5 +124,4 @@ public JsonRPCBridgeOptions setOutboundPermitteds(List outboun super.setOutboundPermitteds(outboundPermitted); return this; } - } From 82de124332dc4b48f3c059c19af3ede13b86f395 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 21 Jul 2022 01:21:22 +0530 Subject: [PATCH 30/34] Add test for json validator --- .../eventbus/bridge/tcp/ValidatorTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/test/java/io/vertx/ext/eventbus/bridge/tcp/ValidatorTest.java diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/ValidatorTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/ValidatorTest.java new file mode 100644 index 0000000..7e21306 --- /dev/null +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/ValidatorTest.java @@ -0,0 +1,60 @@ +package io.vertx.ext.eventbus.bridge.tcp; + + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.unit.junit.RunTestOnContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.json.schema.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertTrue; + +@RunWith(VertxUnitRunner.class) + public class ValidatorTest { + + @Rule + public RunTestOnContext rule = new RunTestOnContext(); + + @Test + public void testValidateSingle() { + String path = "io/vertx/ext/eventbus/bridge/tcp/impl/protocol/jsonrpc.scehma.json"; + + Validator validator = Validator.create( + JsonSchema.of(new JsonObject(rule.vertx().fileSystem().readFileBlocking(path))), + new JsonSchemaOptions() + .setDraft(Draft.DRAFT202012) + .setBaseUri("https://vertx.io") + ); + + JsonObject rpc = new JsonObject("{\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42, 23], \"id\": 1}"); + + assertTrue(validator.validate(rpc).getValid()); + } + + @Test + public void testValidateBatch() { + String path = "io/vertx/ext/eventbus/bridge/tcp/impl/protocol/jsonrpc.scehma.json"; + + Validator validator = Validator.create( + JsonSchema.of(new JsonObject(rule.vertx().fileSystem().readFileBlocking(path))), + new JsonSchemaOptions() + .setOutputFormat(OutputFormat.Basic) + .setDraft(Draft.DRAFT202012) + .setBaseUri("https://vertx.io") + ); + + JsonArray rpc = new JsonArray("[\n" + + " {\"jsonrpc\": \"2.0\", \"method\": \"sum\", \"params\": [1,2,4], \"id\": \"1\"},\n" + + " {\"jsonrpc\": \"2.0\", \"method\": \"notify_hello\", \"params\": [7]},\n" + + " {\"jsonrpc\": \"2.0\", \"method\": \"subtract\", \"params\": [42,23], \"id\": \"2\"},\n" + + " {\"jsonrpc\": \"2.0\", \"method\": \"foo.get\", \"params\": {\"name\": \"myself\"}, \"id\": \"5\"},\n" + + " {\"jsonrpc\": \"2.0\", \"method\": \"get_data\", \"id\": \"9\"} \n" + + " ]"); + + assertTrue(validator.validate(rpc).getValid()); + } + + } From c82d51307d7a7adae2f7fd41cebba8ea961eb6cd Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 3 Aug 2022 17:09:50 +0530 Subject: [PATCH 31/34] Add demos --- pom.xml | 3 +- src/main/java/examples/HttpBridgeExample.java | 60 ++ .../java/examples/HttpSSEBridgeExample.java | 81 +++ .../java/examples/WebsocketBridgeExample.java | 70 ++ .../tcp/JsonRPCStreamEventBusBridge.java | 10 +- .../HttpJsonRPCStreamEventBusBridgeImpl.java | 6 +- ...socketJsonRPCStreamEventBusBridgeImpl.java | 6 +- ...tpJsonRPCStreamEventBusBridgeImplTest.java | 680 ++++++------------ .../bridge/tcp/InteropWebSocketServer.java | 27 +- src/test/resources/http.html | 20 + src/test/resources/sse.html | 15 + src/test/resources/ws.html | 13 +- 12 files changed, 489 insertions(+), 502 deletions(-) create mode 100644 src/main/java/examples/HttpBridgeExample.java create mode 100644 src/main/java/examples/HttpSSEBridgeExample.java create mode 100644 src/main/java/examples/WebsocketBridgeExample.java create mode 100644 src/test/resources/http.html create mode 100644 src/test/resources/sse.html diff --git a/pom.xml b/pom.xml index 7b478ce..2d729b3 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,8 @@ io.vertx vertx-web-client - test + + junit diff --git a/src/main/java/examples/HttpBridgeExample.java b/src/main/java/examples/HttpBridgeExample.java new file mode 100644 index 0000000..6648791 --- /dev/null +++ b/src/main/java/examples/HttpBridgeExample.java @@ -0,0 +1,60 @@ +package examples; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Handler; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.WebSocketBase; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCStreamEventBusBridge; + +public class HttpBridgeExample extends AbstractVerticle { + + public static void main(String[] args) { + Vertx.vertx().deployVerticle(new HttpBridgeExample()); + } + + @Override + public void start(Promise start) { + vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); + vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); + + JsonRPCBridgeOptions options = new JsonRPCBridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("hello")) + .addInboundPermitted(new PermittedOptions().setAddress("echo")) + .addInboundPermitted(new PermittedOptions().setAddress("ping")) + .addOutboundPermitted(new PermittedOptions().setAddress("echo")) + .addOutboundPermitted(new PermittedOptions().setAddress("hello")) + .addOutboundPermitted(new PermittedOptions().setAddress("ping")); + + Handler bridge = JsonRPCStreamEventBusBridge.httpSocketHandler(vertx, options, null); + + vertx + .createHttpServer() + .requestHandler(req -> { + if ("/".equals(req.path())) { + req.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") + .sendFile("http.html"); + } else if ("/jsonrpc".equals(req.path())){ + bridge.handle(req); + } else { + req.response().setStatusCode(404).end("Not Found"); + } + }) + .listen(8080) + .onFailure(start::fail) + .onSuccess(server -> { + System.out.println("Server listening at http://localhost:8080"); + start.complete(); + }); + + } +} diff --git a/src/main/java/examples/HttpSSEBridgeExample.java b/src/main/java/examples/HttpSSEBridgeExample.java new file mode 100644 index 0000000..eb0c514 --- /dev/null +++ b/src/main/java/examples/HttpSSEBridgeExample.java @@ -0,0 +1,81 @@ +package examples; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Handler; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCStreamEventBusBridge; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.codec.BodyCodec; + +import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.request; + +public class HttpSSEBridgeExample extends AbstractVerticle { + + public static void main(String[] args) { + Vertx.vertx().deployVerticle(new HttpSSEBridgeExample()); + } + + @Override + public void start(Promise start) { + // just to have some messages flowing around + vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); + vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); + + // once we fix the interface we can avoid the casts + Handler bridge = JsonRPCStreamEventBusBridge.httpSocketHandler( + vertx, + new JsonRPCBridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("hello")) + .addInboundPermitted(new PermittedOptions().setAddress("echo")) + .addInboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("echo")) + .addOutboundPermitted(new PermittedOptions().setAddress("test")) + .addOutboundPermitted(new PermittedOptions().setAddress("ping")), + null + ); + + WebClient client = WebClient.create(vertx); + + vertx + .createHttpServer() + .requestHandler(req -> { + // this is where any http request will land + // serve the base HTML application + if ("/".equals(req.path())) { + req.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") + .sendFile("sse.html"); + } else if ("/jsonrpc".equals(req.path())){ + bridge.handle(req); + } else if ("/jsonrpc-sse".equals(req.path())) { + HttpServerResponse resp = req.response().setChunked(true).putHeader("Content-Type", "text/event-stream"); + request( + "register", + (int) (Math.random() * 100_000), + new JsonObject().put("address", "ping"), + buffer -> client + .post(8080, "localhost", "/jsonrpc") + .as(BodyCodec.pipe(resp)) + .sendBuffer(buffer) + ); + } else { + req.response().setStatusCode(404).end("Not Found"); + } + }) + .listen(8080) + .onFailure(start::fail) + .onSuccess(server -> { + System.out.println("Server listening at http://localhost:8080"); + start.complete(); + }); + } +} diff --git a/src/main/java/examples/WebsocketBridgeExample.java b/src/main/java/examples/WebsocketBridgeExample.java new file mode 100644 index 0000000..f5010c5 --- /dev/null +++ b/src/main/java/examples/WebsocketBridgeExample.java @@ -0,0 +1,70 @@ +package examples; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Handler; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.Message; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.WebSocketBase; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; +import io.vertx.ext.eventbus.bridge.tcp.JsonRPCStreamEventBusBridge; + +public class WebsocketBridgeExample extends AbstractVerticle { + + public static void main(String[] args) { + Vertx.vertx().deployVerticle(new WebsocketBridgeExample()); + } + + @Override + public void start(Promise start) { + vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); + vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); + + JsonRPCBridgeOptions options = new JsonRPCBridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("hello")) + .addInboundPermitted(new PermittedOptions().setAddress("echo")) + .addInboundPermitted(new PermittedOptions().setAddress("ping")) + .addOutboundPermitted(new PermittedOptions().setAddress("echo")) + .addOutboundPermitted(new PermittedOptions().setAddress("hello")) + .addOutboundPermitted(new PermittedOptions().setAddress("ping")) + // if set to false, then websockets messages are received on frontend as binary frames + .setWebsocketsTextAsFrame(true); + + Handler bridge = JsonRPCStreamEventBusBridge.webSocketHandler(vertx, options, null); + + vertx + .createHttpServer() + .requestHandler(req -> { + // this is where any http request will land + if ("/jsonrpc".equals(req.path())) { + // we switch from HTTP to WebSocket + req.toWebSocket() + .onFailure(err -> { + err.printStackTrace(); + req.response().setStatusCode(500).end(err.getMessage()); + }) + .onSuccess(bridge::handle); + } else { + // serve the base HTML application + if ("/".equals(req.path())) { + req.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") + .sendFile("ws.html"); + } else { + // 404 all the rest + req.response().setStatusCode(404).end("Not Found"); + } + } + }) + .listen(8080) + .onFailure(start::fail) + .onSuccess(server -> { + System.out.println("Server listening at http://localhost:8080"); + start.complete(); + }); + } +} diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index def2cad..2933559 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -58,18 +58,14 @@ static Handler netSocketHandler(Vertx vertx, JsonRPCBridgeOptions opt } static Handler webSocketHandler(Vertx vertx) { - return webSocketHandler(vertx, null, null, false); + return webSocketHandler(vertx, null, null); } static Handler webSocketHandler(Vertx vertx, JsonRPCBridgeOptions options) { - return webSocketHandler(vertx, options, null, false); + return webSocketHandler(vertx, options, null); } static Handler webSocketHandler(Vertx vertx, JsonRPCBridgeOptions options, Handler> eventHandler) { - return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler, false); - } - - static Handler webSocketHandler(Vertx vertx, JsonRPCBridgeOptions options, Handler> eventHandler, boolean useText) { - return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler, useText); + return new WebsocketJsonRPCStreamEventBusBridgeImpl(vertx, options, eventHandler); } static Handler httpSocketHandler(Vertx vertx) { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index 681906d..a41d16f 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -65,7 +65,11 @@ public void handle(HttpServerRequest socket) { Consumer writer; if (method.equals("register")) { response.setChunked(true); - writer = response::write; + writer = body -> { + JsonObject object = body.toJsonObject(); + response.write("event: " + object.getJsonObject("result").getString("address") + "\n"); + response.write("data: " + object.encode() + "\n\n"); + }; } else { writer = response::end; } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index 570da70..2706b90 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -17,11 +17,9 @@ import java.util.function.Consumer; public class WebsocketJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { - boolean useText; - public WebsocketJsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options, Handler> bridgeEventHandler, boolean useText) { + public WebsocketJsonRPCStreamEventBusBridgeImpl(Vertx vertx, JsonRPCBridgeOptions options, Handler> bridgeEventHandler) { super(vertx, options, bridgeEventHandler); - this.useText = useText; } @Override @@ -35,7 +33,7 @@ public void handle(WebSocketBase socket) { final Map> replies = new ConcurrentHashMap<>(); Consumer consumer; - if (useText) { + if (options.getWebsocketsTextAsFrame()) { consumer = buffer -> socket.writeTextMessage(buffer.toString()); } else { consumer = socket::writeBinaryMessage; diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java index 6ad4025..e952094 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/HttpJsonRPCStreamEventBusBridgeImplTest.java @@ -35,6 +35,7 @@ import io.vertx.ext.unit.junit.VertxUnitRunner; import io.vertx.ext.web.client.WebClient; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -57,28 +58,23 @@ public void before(TestContext should) { final Async test = should.async(); final Vertx vertx = rule.vertx(); - vertx.eventBus().consumer("hello", (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); + vertx.eventBus().consumer("hello", + (Message msg) -> msg.reply(new JsonObject().put("value", "Hello " + msg.body().getString("value")))); vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); vertx.setPeriodic(1000, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); - vertx - .createHttpServer() - .requestHandler(JsonRPCStreamEventBusBridge.httpSocketHandler( - vertx, - new JsonRPCBridgeOptions() - .addInboundPermitted(new PermittedOptions().setAddress("hello")) - .addInboundPermitted(new PermittedOptions().setAddress("echo")) - .addInboundPermitted(new PermittedOptions().setAddress("test")) - .addOutboundPermitted(new PermittedOptions().setAddress("echo")) - .addOutboundPermitted(new PermittedOptions().setAddress("test")) - .addOutboundPermitted(new PermittedOptions().setAddress("ping")), - eventHandler)) - .listen(7000, res -> { - should.assertTrue(res.succeeded()); - test.complete(); - }); + vertx.createHttpServer().requestHandler(JsonRPCStreamEventBusBridge.httpSocketHandler(vertx, + new JsonRPCBridgeOptions().addInboundPermitted(new PermittedOptions().setAddress("hello")).addInboundPermitted( + new PermittedOptions().setAddress("echo")).addInboundPermitted( + new PermittedOptions().setAddress("test")).addOutboundPermitted( + new PermittedOptions().setAddress("echo")).addOutboundPermitted( + new PermittedOptions().setAddress("test")).addOutboundPermitted(new PermittedOptions().setAddress("ping")), + eventHandler)).listen(7000, res -> { + should.assertTrue(res.succeeded()); + test.complete(); + }); } @Test(timeout = 10_000L) @@ -93,11 +89,8 @@ public void testSendVoidMessage(TestContext should) { test.complete(); }); - request( - "send", - new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), - buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer) - ); + request("send", new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer)); } @@ -108,27 +101,18 @@ public void testNoHandlers(TestContext should) { final WebClient client = WebClient.create(vertx); final Async test = should.async(); - request( - "send", - "#backtrack", - new JsonObject() - .put("address", "test") - .put("body", new JsonObject().put("value", "vert.x")), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); + request("send", "#backtrack", + new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); - should.assertTrue(frame.containsKey("error")); - should.assertFalse(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); - client.close(); - test.complete(); - }) - .onFailure(should::fail) - ); + client.close(); + test.complete(); + }).onFailure(should::fail)); } @Test(timeout = 10_000L) @@ -142,66 +126,17 @@ public void testErrorReply(TestContext should) { msg.fail(0, "oops!"); }); - request( - "send", - "#backtrack", - new JsonObject() - .put("address", "test") - .put("body", new JsonObject().put("value", "vert.x")), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); - should.assertTrue(frame.containsKey("error")); - should.assertFalse(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); - - client.close(); - test.complete(); - }) - .onFailure(should::fail) - ); - - } - - @Test(timeout = 10_000L) - public void testSendsFromOtherSideOfBridge(TestContext should) { - final Vertx vertx = rule.vertx(); - WebClient client = WebClient.create(vertx); - final Async test = should.async(); + request("send", "#backtrack", + new JsonObject().put("address", "test").put("body", new JsonObject().put("value", "vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + should.assertTrue(frame.containsKey("error")); + should.assertFalse(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); - final AtomicBoolean ack = new AtomicBoolean(false); - - request( - "register", - "#backtrack", - new JsonObject() - .put("address", "ping"), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); - - if (!ack.getAndSet(true)) { - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); - } else { - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); - - JsonObject result = frame.getJsonObject("result"); - - should.assertEquals("hi", result.getJsonObject("body").getString("value")); - client.close(); - test.complete(); - } - }) - .onFailure(should::fail) - ); + client.close(); + test.complete(); + }).onFailure(should::fail)); } @@ -212,30 +147,21 @@ public void testSendMessageWithReplyBacktrack(TestContext should) { WebClient client = WebClient.create(vertx); final Async test = should.async(); - request( - "send", - "#backtrack", - new JsonObject() - .put("address", "hello") - .put("body", new JsonObject().put("value", "vert.x")), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); + request("send", "#backtrack", + new JsonObject().put("address", "hello").put("body", new JsonObject().put("value", "vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); - JsonObject result = frame.getJsonObject("result"); + JsonObject result = frame.getJsonObject("result"); - should.assertEquals("Hello vert.x", result.getJsonObject("body").getString("value")); - client.close(); - test.complete(); - }) - .onFailure(should::fail) - ); + should.assertEquals("Hello vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + }).onFailure(should::fail)); } @Test(timeout = 10_000L) @@ -250,17 +176,9 @@ public void testSendMessageWithReplyBacktrackTimeout(TestContext should) { JsonObject headers = new JsonObject().put("timeout", 100L); - request( - "send", - "#backtrack", - new JsonObject() - .put("address", "test") - .put("headers", headers) - .put("body", new JsonObject().put("value", "vert.x")), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { + request("send", "#backtrack", new JsonObject().put("address", "test").put("headers", headers).put("body", + new JsonObject().put("value", "vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { JsonObject frame = handler.bodyAsJsonObject(); should.assertTrue(frame.containsKey("error")); @@ -269,14 +187,14 @@ public void testSendMessageWithReplyBacktrackTimeout(TestContext should) { JsonObject error = frame.getJsonObject("error"); - should.assertEquals("Timed out after waiting 100(ms) for a reply. address: __vertx.reply.1, repliedAddress: test", error.getString("message")); + should.assertEquals( + "Timed out after waiting 100(ms) for a reply. address: __vertx.reply.1, repliedAddress: test", + error.getString("message")); should.assertEquals(-1, error.getInteger("code")); client.close(); test.complete(); - }) - .onFailure(should::fail) - ); + }).onFailure(should::fail)); } @@ -290,25 +208,17 @@ public void testSendMessageWithDuplicateReplyID(TestContext should) { vertx.eventBus().consumer("third-party-receiver", msg -> should.fail()); - request( - "send", - "third-party-receiver", - new JsonObject() - .put("address", "hello") - .put("body", new JsonObject().put("value", "vert.x")), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); - client.close(); - test.complete(); - }) - .onFailure(should::fail) - ); + request("send", "third-party-receiver", + new JsonObject().put("address", "hello").put("body", new JsonObject().put("value", "vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + client.close(); + test.complete(); + }).onFailure(should::fail)); } @Test(timeout = 10_000L) + @Ignore public void testRegister(TestContext should) { // Send a request and get a response final Vertx vertx = rule.vertx(); @@ -317,296 +227,135 @@ public void testRegister(TestContext should) { final AtomicInteger messageCount = new AtomicInteger(0); - request( - "register", - "#backtrack", - new JsonObject().put("address", "echo"), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); - // 2 messages will arrive - // 1) ACK for register message - // 2) MESSAGE for echo - if (messageCount.get() == 0) { - // ACK for register message - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); - // increment message count so that next time ACK for publish is expected - should.assertTrue(messageCount.compareAndSet(0, 1)); - } else { - // reply for echo message - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); + request("register", "#backtrack", new JsonObject().put("address", "echo"), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + // 2 messages will arrive + // 1) ACK for register message + // 2) MESSAGE for echo + if (messageCount.get() == 0) { + // ACK for register message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time ACK for publish is expected + should.assertTrue(messageCount.compareAndSet(0, 1)); + } + else { + // reply for echo message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); - JsonObject result = frame.getJsonObject("result"); + JsonObject result = frame.getJsonObject("result"); - should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); - client.close(); - test.complete(); - } - }) - .onFailure(should::fail) - ); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + client.close(); + test.complete(); + } + }).onFailure(should::fail)); // now try to publish a message so it gets delivered both to the consumer registred on the startup and to this // remote consumer - request( - "publish", - "#backtrack", - new JsonObject() - .put("address", "echo") - .put("body", new JsonObject().put("value", "Vert.x")), - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); - // ACK for publish message - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); - }) - .onFailure(should::fail) - ); + request("publish", "#backtrack", + new JsonObject().put("address", "echo").put("body", new JsonObject().put("value", "Vert.x")), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + // ACK for publish message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + }).onFailure(should::fail)); } -// @Test(timeout = 10_000L) -// public void testUnRegister(TestContext should) { -// // Send a request and get a response -// final Vertx vertx = rule.vertx(); -// WebClient client = WebClient.create(vertx); -// final Async test = should.async(); -// -// final String address = "test"; -// // 4 replies will arrive: -// // 1). ACK for register -// // 2). ACK for publish -// // 3). message published to test -// // 4). err of NO_HANDLERS because of consumer for 'test' is unregistered. -// final AtomicInteger messageCount = new AtomicInteger(0); -// final AtomicInteger messageCount2 = new AtomicInteger(0); -// final StreamParser parser = new StreamParser() -// .exceptionHandler(should::fail) -// .handler(body -> { -// JsonObject frame = new JsonObject(body); -// -// if (messageCount.get() == 0) { -// // ACK for register message -// should.assertFalse(frame.containsKey("error")); -// should.assertTrue(frame.containsKey("result")); -// should.assertEquals("#backtrack", frame.getValue("id")); -// // increment message count so that next time ACK for publish is expected -// should.assertTrue(messageCount.compareAndSet(0, 1)); -// } -// else if (messageCount.get() == 1) { -// // got message, then unregister the handler -// should.assertFalse(frame.containsKey("error")); -// JsonObject result = frame.getJsonObject("result"); -// should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); -// -// request( -// "unregister", -// "#backtrack", -// new JsonObject().put("address", address), -// buffer -> client -// .post(7000, "localhost", "/") -// .sendBuffer(buffer) -// .onSuccess(handler -> { -// JsonObject frame2 = handler.bodyAsJsonObject(); -// if (messageCount2.get() == 0) { -// // ACK for publish message -// should.assertFalse(frame2.containsKey("error")); -// should.assertTrue(frame2.containsKey("result")); -// should.assertEquals("#backtrack", frame2.getValue("id")); -// // increment message count so that next time reply for echo message is expected -// should.assertTrue(messageCount2.compareAndSet(0, 1)); -// } else { -// // ACK for unregister message -// should.assertFalse(frame.containsKey("error")); -// should.assertTrue(frame.containsKey("result")); -// should.assertEquals("#backtrack", frame.getValue("id")); -// // increment message count so that next time error reply for send message is expected -// should.assertTrue(messageCount.compareAndSet(3, 4)); -// -// request( -// "send", -// "#backtrack", -// new JsonObject() -// .put("address", address) -// .put("body", new JsonObject().put("value", "This will fail anyway!")), -// socket -// ); -// } -// }) -// .onFailure(should::fail) -// ); -// } else { -// // TODO: Check error handling of bridge for consistency -// // consumer on 'test' has been unregistered, send message will fail. -// should.assertTrue(frame.containsKey("error")); -// JsonObject error = frame.getJsonObject("error"); -// should.assertEquals(-1, error.getInteger("code")); -// should.assertEquals("No handlers for address test", error.getString("message")); -// -// client.close(); -// test.complete(); -// } -// }); -// -// request( -// "register", -// "#backtrack", -// new JsonObject() -// .put("address", address), -// buffer -> client -// .post(7000, "localhost", "/") -// .sendBuffer(buffer) -// .onSuccess(handler -> { -// JsonObject frame = handler.bodyAsJsonObject(); -// // ACK for publish message -// should.assertFalse(frame.containsKey("error")); -// should.assertTrue(frame.containsKey("result")); -// should.assertEquals("#backtrack", frame.getValue("id")); -// // increment message count so that next time reply for echo message is expected -// should.assertTrue(messageCount.compareAndSet(1, 2)); -// }) -// .onFailure(should::fail) -// ); -// -// request( -// "publish", -// "#backtrack", -// new JsonObject() -// .put("address", address) -// .put("body", new JsonObject().put("value", "Vert.x")), -// socket::end -// ); -// } -// -// @Test(timeout = 10_000L) -// public void testReplyFromClient(TestContext should) { -// // Send a request from java and get a response from the client -// final Vertx vertx = rule.vertx(); -// WebClient client = WebClient.create(vertx); -// final Async test = should.async(); -// final String address = "test"; -// -// final AtomicBoolean ack = new AtomicBoolean(false); -// final StreamParser parser = new StreamParser() -// .exceptionHandler(should::fail) -// .handler(body -> { -// JsonObject frame = new JsonObject(body); -// -// if (!ack.getAndSet(true)) { -// should.assertFalse(frame.containsKey("error")); -// should.assertTrue(frame.containsKey("result")); -// should.assertEquals("#backtrack", frame.getValue("id")); -// } else { -// JsonObject result = frame.getJsonObject("result"); -// should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); -// -// request( -// "send", -// "#backtrack", -// new JsonObject() -// .put("address", result.getString("replyAddress")) -// .put("body", new JsonObject().put("value", "You got it")), -// socket::end -// ); -// } -// }); -// -// request( -// "register", -// "#backtrack", -// new JsonObject() -// .put("address", address), -// buffer -> client -// .post(7000, "localhost", "/") -// .sendBuffer(buffer) -// .onSuccess(handler -> { -// JsonObject frame = handler.bodyAsJsonObject(); -// // ACK for publish message -// should.assertFalse(frame.containsKey("error")); -// should.assertTrue(frame.containsKey("result")); -// should.assertEquals("#backtrack", frame.getValue("id")); -// }) -// .onFailure(should::fail) -// ); -// -// // There is no way to know that the register actually happened, wait a bit before sending. -// vertx.setTimer(500L, timerId -> { -// vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { -// should.assertTrue(respMessage.succeeded()); -// should.assertEquals("You got it", respMessage.result().body().getString("value")); -// client.close(); -// test.complete(); -// }); -// }); -// -// } -// -// @Test(timeout = 10_000L) -// public void testFailFromClient(TestContext should) { -// // Send a request from java and get a response from the client -// final Vertx vertx = rule.vertx(); -// -// WebClient client = WebClient.create(vertx); -// final Async test = should.async(); -// final String address = "test"; -// client.connect(7000, "localhost", should.asyncAssertSuccess(socket -> { -// -// final AtomicBoolean ack = new AtomicBoolean(false); -// final StreamParser parser = new StreamParser() -// .exceptionHandler(should::fail) -// .handler(body -> { -// JsonObject frame = new JsonObject(body); -// if (!ack.getAndSet(true)) { -// should.assertFalse(frame.containsKey("error")); -// should.assertTrue(frame.containsKey("result")); -// should.assertEquals("#backtrack", frame.getValue("id")); -// } else { -// JsonObject result = frame.getJsonObject("result"); -// should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); -// -// request( -// "send", -// null, -// new JsonObject() -// .put("address", result.getString("replyAddress")) -// .put("error", new JsonObject().put("failureCode", 1234).put("message", "ooops!")), -// socket::end -// ); -// } -// }); -// -// socket.handler(parser); -// -// request( -// "register", -// "#backtrack", -// new JsonObject() -// .put("address", address), -// socket::end -// ); -// -// // There is now way to know that the register actually happened, wait a bit before sending. -// vertx.setTimer(500L, timerId -> { -// vertx.eventBus().request(address, new JsonObject().put("value", "Vert.x"), respMessage -> { -// should.assertTrue(respMessage.failed()); -// should.assertEquals("ooops!", respMessage.cause().getMessage()); -// client.close(); -// test.complete(); -// }); -// }); -// })); -// } + @Test(timeout = 10_000L) + @Ignore + public void testUnRegister(TestContext should) { + // Send a request and get a response + final Vertx vertx = rule.vertx(); + WebClient client = WebClient.create(vertx); + final Async test = should.async(); + + final String address = "test"; + // 4 replies will arrive: + // 1). ACK for register + // 2). ACK for publish + // 3). message published to test + // 4). err of NO_HANDLERS because of consumer for 'test' is unregistered. + final AtomicInteger messageCount = new AtomicInteger(0); + final AtomicInteger messageCount2 = new AtomicInteger(0); + final StreamParser parser = new StreamParser().exceptionHandler(should::fail).handler(body -> { + JsonObject frame = new JsonObject(body); + + if (messageCount.get() == 0) { + // ACK for register message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time ACK for publish is expected + should.assertTrue(messageCount.compareAndSet(0, 1)); + } + else if (messageCount.get() == 1) { + // got message, then unregister the handler + should.assertFalse(frame.containsKey("error")); + JsonObject result = frame.getJsonObject("result"); + should.assertEquals("Vert.x", result.getJsonObject("body").getString("value")); + + request("unregister", "#backtrack", new JsonObject().put("address", address), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame2 = handler.bodyAsJsonObject(); + if (messageCount2.get() == 0) { + // ACK for publish message + should.assertFalse(frame2.containsKey("error")); + should.assertTrue(frame2.containsKey("result")); + should.assertEquals("#backtrack", frame2.getValue("id")); + // increment message count so that next time reply for echo message is expected + should.assertTrue(messageCount2.compareAndSet(0, 1)); + } + else { + // ACK for unregister message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time error reply for send message is expected + should.assertTrue(messageCount.compareAndSet(3, 4)); + + request("send", "#backtrack", new JsonObject().put("address", address).put("body", + new JsonObject().put("value", "This will fail anyway!")), buffer1 -> { + }); + } + }).onFailure(should::fail)); + } + else { + // TODO: Check error handling of bridge for consistency + // consumer on 'test' has been unregistered, send message will fail. + should.assertTrue(frame.containsKey("error")); + JsonObject error = frame.getJsonObject("error"); + should.assertEquals(-1, error.getInteger("code")); + should.assertEquals("No handlers for address test", error.getString("message")); + + client.close(); + test.complete(); + } + }); + + request("register", "#backtrack", new JsonObject().put("address", address), + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + // ACK for publish message + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + // increment message count so that next time reply for echo message is expected + should.assertTrue(messageCount.compareAndSet(1, 2)); + }).onFailure(should::fail)); + + request("publish", "#backtrack", + new JsonObject().put("address", address).put("body", new JsonObject().put("value", "Vert.x")), buffer -> { + }); + } @Test(timeout = 10_000L) public void testSendPing(TestContext should) { @@ -614,24 +363,17 @@ public void testSendPing(TestContext should) { WebClient client = WebClient.create(vertx); final Async test = should.async(); - request( - "ping", - "#backtrack", - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); - should.assertFalse(frame.containsKey("error")); - should.assertTrue(frame.containsKey("result")); - should.assertEquals("#backtrack", frame.getValue("id")); - - should.assertEquals("pong", frame.getString("result")); - client.close(); - test.complete(); - }) - .onFailure(should::fail) - ); + request("ping", "#backtrack", + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + should.assertFalse(frame.containsKey("error")); + should.assertTrue(frame.containsKey("result")); + should.assertEquals("#backtrack", frame.getValue("id")); + + should.assertEquals("pong", frame.getString("result")); + client.close(); + test.complete(); + }).onFailure(should::fail)); } @Test(timeout = 10_000L) @@ -642,27 +384,21 @@ public void testNoAddress(TestContext should) { final Async test = should.async(); final AtomicBoolean errorOnce = new AtomicBoolean(false); - request( - "send", - "#backtrack", - buffer -> client - .post(7000, "localhost", "/") - .sendBuffer(buffer) - .onSuccess(handler -> { - JsonObject frame = handler.bodyAsJsonObject(); - if (!errorOnce.compareAndSet(false, true)) { - should.fail("Client gets error message twice!"); - } else { - should.assertTrue(frame.containsKey("error")); - should.assertEquals("invalid_parameters", frame.getJsonObject("error").getString("message")); - vertx.setTimer(200, l -> { - client.close(); - test.complete(); - }); - } - }) - .onFailure(should::fail) - ); + request("send", "#backtrack", + buffer -> client.post(7000, "localhost", "/").sendBuffer(buffer).onSuccess(handler -> { + JsonObject frame = handler.bodyAsJsonObject(); + if (!errorOnce.compareAndSet(false, true)) { + should.fail("Client gets error message twice!"); + } + else { + should.assertTrue(frame.containsKey("error")); + should.assertEquals("invalid_parameters", frame.getJsonObject("error").getString("message")); + vertx.setTimer(200, l -> { + client.close(); + test.complete(); + }); + } + }).onFailure(should::fail)); } } diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index d9585d1..ea49f3e 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -15,6 +15,12 @@ import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.JsonRPCStreamEventBusBridgeImpl; import io.vertx.ext.eventbus.bridge.tcp.impl.TCPJsonRPCStreamEventBusBridgeImpl; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.codec.BodyCodec; + +import java.util.Random; + +import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.request; public class InteropWebSocketServer extends AbstractVerticle { @@ -44,6 +50,8 @@ public void start(Promise start) { null ); + WebClient client = WebClient.create(vertx); + vertx .createHttpServer() .requestHandler(req -> { @@ -55,14 +63,17 @@ public void start(Promise start) { .sendFile("ws.html"); } else if ("/jsonrpc".equals(req.path())){ bridge.handle(req); - } else if ("/test-chunked".equals(req.path())) { - HttpServerResponse resp = req.response().setChunked(true); - resp.write("Hello, World!\r\n"); - vertx.setTimer(5000, delay -> resp.write("Foo, Bar!\r\n")); - vertx.setTimer(15000, delay -> { - resp.write("Hello from India!\r\n"); - resp.end(); - }); + } else if ("/jsonrpc-sse".equals(req.path())) { + HttpServerResponse resp = req.response().setChunked(true).putHeader("Content-Type", "text/event-stream"); + request( + "register", + (int) (Math.random() * 100_000), + new JsonObject().put("address", "ping"), + buffer -> client + .post(8080, "localhost", "/jsonrpc") + .as(BodyCodec.pipe(resp)) + .sendBuffer(buffer) + ); } else { req.response().setStatusCode(404).end("Not Found"); } diff --git a/src/test/resources/http.html b/src/test/resources/http.html new file mode 100644 index 0000000..6c33126 --- /dev/null +++ b/src/test/resources/http.html @@ -0,0 +1,20 @@ + + + + Websockets Bridge Example + + + + + diff --git a/src/test/resources/sse.html b/src/test/resources/sse.html new file mode 100644 index 0000000..2806f0e --- /dev/null +++ b/src/test/resources/sse.html @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/test/resources/ws.html b/src/test/resources/ws.html index 2d7a852..268911e 100644 --- a/src/test/resources/ws.html +++ b/src/test/resources/ws.html @@ -17,17 +17,12 @@ ws.onclose = console.log; ws.onerror = console.error; - async function sendMsg() { - let message = document.getElementById("payload").value; - await fetch("http://localhost:8080/jsonrpc", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: message, - }); + function sendMsg() { + var message = document.getElementById("payload").value; + ws.send(message); } + Websockets Bridge Example From 1c75ffc34a702f705042789e68138be4d928bbcd Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 3 Aug 2022 20:01:02 +0530 Subject: [PATCH 32/34] Use Consumer instead of Consumer to write --- .../impl/HttpJsonRPCStreamEventBusBridgeImpl.java | 12 ++++++------ .../tcp/impl/JsonRPCStreamEventBusBridgeImpl.java | 10 +++++----- .../impl/TCPJsonRPCStreamEventBusBridgeImpl.java | 5 ++++- .../WebsocketJsonRPCStreamEventBusBridgeImpl.java | 6 +++--- .../bridge/tcp/impl/protocol/JsonRPCHelper.java | 13 +++++++------ 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index a41d16f..cf08a7f 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -8,6 +8,7 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; @@ -62,16 +63,15 @@ public void handle(HttpServerRequest socket) { checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); registry.clear(); }); - Consumer writer; + Consumer writer; if (method.equals("register")) { response.setChunked(true); - writer = body -> { - JsonObject object = body.toJsonObject(); - response.write("event: " + object.getJsonObject("result").getString("address") + "\n"); - response.write("data: " + object.encode() + "\n\n"); + writer = payload -> { + response.write("event: " + payload.getJsonObject("result").getString("address") + "\n"); + response.write("data: " + payload.encode() + "\n\n"); }; } else { - writer = response::end; + writer = payload -> response.end(payload.encode()); } dispatch(writer, method, id, msg, registry, replies); }); diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java index 3669e8c..29c827e 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/JsonRPCStreamEventBusBridgeImpl.java @@ -96,7 +96,7 @@ protected boolean isInvalid(JsonObject object) { return false; } - protected void dispatch(Consumer socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void dispatch(Consumer socket, String method, Object id, JsonObject msg, Map> registry, Map> replies) { switch (method) { case "send": checkCallHook( @@ -135,7 +135,7 @@ protected void dispatch(Consumer socket, String method, Object id, JsonO } } - protected void unregister(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void unregister(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -160,7 +160,7 @@ protected void unregister(Consumer socket, Object id, JsonObject msg, Ma } } - protected void register(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void register(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -205,7 +205,7 @@ protected void register(Consumer socket, Object id, JsonObject msg, Map< } } - protected void publish(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void publish(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); @@ -228,7 +228,7 @@ protected void publish(Consumer socket, Object id, JsonObject msg, Map socket, Object id, JsonObject msg, Map> registry, Map> replies) { + protected void send(Consumer socket, Object id, JsonObject msg, Map> registry, Map> replies) { final JsonObject params = msg.getJsonObject("params", EMPTY); final String address = params.getString("address"); diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java index 994cc4a..abffbd6 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/TCPJsonRPCStreamEventBusBridgeImpl.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; public class TCPJsonRPCStreamEventBusBridgeImpl extends JsonRPCStreamEventBusBridgeImpl { @@ -59,8 +60,10 @@ public void handle(NetSocket socket) { final String method = msg.getString("method"); final Object id = msg.getValue("id"); + Consumer writer = payload -> socket.write(payload.toBuffer().appendString("\r\n")); + dispatch( - socket::write, + writer, method, id, msg, diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java index 2706b90..c178db0 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/WebsocketJsonRPCStreamEventBusBridgeImpl.java @@ -32,11 +32,11 @@ public void handle(WebSocketBase socket) { final Map> registry = new ConcurrentHashMap<>(); final Map> replies = new ConcurrentHashMap<>(); - Consumer consumer; + Consumer consumer; if (options.getWebsocketsTextAsFrame()) { - consumer = buffer -> socket.writeTextMessage(buffer.toString()); + consumer = payload -> socket.writeTextMessage(payload.encode()); } else { - consumer = socket::writeBinaryMessage; + consumer = payload -> socket.writeBinaryMessage(payload.toBuffer()); } socket diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java index f3f245f..fd62041 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/protocol/JsonRPCHelper.java @@ -18,6 +18,7 @@ import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.ReplyException; +import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import java.util.function.Consumer; @@ -83,16 +84,16 @@ public static void request(String method, JsonObject params, Consumer ha request(method, null, params, null, handler); } - public static void response(Object id, Object result, Consumer handler) { + public static void response(Object id, Object result, Consumer handler) { final JsonObject payload = new JsonObject() .put("jsonrpc", "2.0") .put("id", id) .put("result", result); - handler.accept(payload.toBuffer().appendString("\r\n")); + handler.accept(payload); } - public static void error(Object id, Number code, String message, Consumer handler) { + public static void error(Object id, Number code, String message, Consumer handler) { final JsonObject payload = new JsonObject() .put("jsonrpc", "2.0") .put("id", id); @@ -108,14 +109,14 @@ public static void error(Object id, Number code, String message, Consumer handler) { + public static void error(Object id, ReplyException failure, Consumer handler) { error(id, failure.failureCode(), failure.getMessage(), handler); } - public static void error(Object id, String message, Consumer handler) { + public static void error(Object id, String message, Consumer handler) { error(id, -32000, message, handler); } } From af47fa79ec2bf7dbf71d75aada9dd318b278b549 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Thu, 4 Aug 2022 00:08:06 +0530 Subject: [PATCH 33/34] fix demos --- .../java/examples/HttpSSEBridgeExample.java | 35 +++++----------- .../tcp/JsonRPCStreamEventBusBridge.java | 10 ----- .../HttpJsonRPCStreamEventBusBridgeImpl.java | 41 ++++++++++++++++++- src/{test => main}/resources/http.html | 2 +- src/{test => main}/resources/sse.html | 0 src/{test => main}/resources/ws.html | 0 .../bridge/tcp/InteropWebSocketServer.java | 7 ---- 7 files changed, 52 insertions(+), 43 deletions(-) rename src/{test => main}/resources/http.html (93%) rename src/{test => main}/resources/sse.html (100%) rename src/{test => main}/resources/ws.html (100%) diff --git a/src/main/java/examples/HttpSSEBridgeExample.java b/src/main/java/examples/HttpSSEBridgeExample.java index eb0c514..996831b 100644 --- a/src/main/java/examples/HttpSSEBridgeExample.java +++ b/src/main/java/examples/HttpSSEBridgeExample.java @@ -1,21 +1,14 @@ package examples; import io.vertx.core.AbstractVerticle; -import io.vertx.core.Handler; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; -import io.vertx.ext.eventbus.bridge.tcp.JsonRPCStreamEventBusBridge; -import io.vertx.ext.web.client.WebClient; -import io.vertx.ext.web.codec.BodyCodec; - -import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.request; +import io.vertx.ext.eventbus.bridge.tcp.impl.HttpJsonRPCStreamEventBusBridgeImpl; public class HttpSSEBridgeExample extends AbstractVerticle { @@ -30,8 +23,8 @@ public void start(Promise start) { vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); - // once we fix the interface we can avoid the casts - Handler bridge = JsonRPCStreamEventBusBridge.httpSocketHandler( + // use explicit class because SSE method is not on the interface currently + HttpJsonRPCStreamEventBusBridgeImpl bridge = new HttpJsonRPCStreamEventBusBridgeImpl( vertx, new JsonRPCBridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("hello")) @@ -43,8 +36,6 @@ public void start(Promise start) { null ); - WebClient client = WebClient.create(vertx); - vertx .createHttpServer() .requestHandler(req -> { @@ -54,19 +45,15 @@ public void start(Promise start) { req.response() .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") .sendFile("sse.html"); - } else if ("/jsonrpc".equals(req.path())){ + } else if ("/jsonrpc".equals(req.path())) { bridge.handle(req); - } else if ("/jsonrpc-sse".equals(req.path())) { - HttpServerResponse resp = req.response().setChunked(true).putHeader("Content-Type", "text/event-stream"); - request( - "register", - (int) (Math.random() * 100_000), - new JsonObject().put("address", "ping"), - buffer -> client - .post(8080, "localhost", "/jsonrpc") - .as(BodyCodec.pipe(resp)) - .sendBuffer(buffer) - ); + } else if ("/jsonrpc-sse".equals(req.path())) { + JsonObject message = new JsonObject() + .put("jsonrpc", "2.0") + .put("method", "register") + .put("id", (int) (Math.random() * 100_000)) + .put("params", new JsonObject().put("address", "ping")); + bridge.handleSSE(req.response(), message); } else { req.response().setStatusCode(404).end("Not Found"); } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java index 2933559..ceb6487 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/JsonRPCStreamEventBusBridge.java @@ -32,16 +32,6 @@ * * @author Paulo Lopes */ - -// TODO: "extends Handler" was a bad idea because it locks the implementation to TCP sockets. Instead we -// should have explicit methods that either handle a NetSocket or a WebSocketBase: -// handle(NetSocket socket) handle(WebSocketBase socket) -// or: return a handler, e.g.: -// Handler webSocketHandler(); -// Handler netSocketHandler(); - -// How about generifying this interface as in JsonRPCStreamEventBusBridge extends Handler ? -// similarly create a base class for the impl and concrete impls for each protocol. @VertxGen public interface JsonRPCStreamEventBusBridge { diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index cf08a7f..861567e 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -46,7 +46,6 @@ public void handle(HttpServerRequest socket) { // TODO: body may be an array (batching) final JsonObject msg = new JsonObject(buffer); - System.out.println(msg); if (this.isInvalid(msg)) { return; @@ -79,4 +78,44 @@ public void handle(HttpServerRequest socket) { // on failure () -> socket.response().setStatusCode(500).setStatusMessage("Internal Server Error").end()); } + + // TODO: discuss implications of accepting response here. bridge events may not be emitted. + // but if accepting request cannot use handler as the request is usually empty and handler is + // not invoked until data has been read. also same thing for other cases + public void handleSSE(HttpServerResponse socket, JsonObject msg) { + final Map> registry = new ConcurrentHashMap<>(); + + socket.exceptionHandler(t -> { + log.error(t.getMessage(), t); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }); + if (this.isInvalid(msg)) { + return; + } + + HttpServerResponse response = socket + .setChunked(true) + .putHeader(HttpHeaders.CONTENT_TYPE, "text/event-stream") + .endHandler(handler -> { + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }); + + final String method = msg.getString("method"); + if (!method.equalsIgnoreCase("register")) { + log.error("Invalid method for SSE!"); + return; + } + + final Object id = msg.getValue("id"); + Consumer writer = payload -> { + // TODO: Should we use id or address for event name? + response.write("event: " + payload.getJsonObject("result").getString("address") + "\n"); + response.write("data: " + payload.encode() + "\n\n"); + }; + register(writer, id, msg, registry, replies); + } + + } diff --git a/src/test/resources/http.html b/src/main/resources/http.html similarity index 93% rename from src/test/resources/http.html rename to src/main/resources/http.html index 6c33126..aeb2be3 100644 --- a/src/test/resources/http.html +++ b/src/main/resources/http.html @@ -12,7 +12,7 @@ }); } - Websockets Bridge Example + HTTP Bridge Example diff --git a/src/test/resources/sse.html b/src/main/resources/sse.html similarity index 100% rename from src/test/resources/sse.html rename to src/main/resources/sse.html diff --git a/src/test/resources/ws.html b/src/main/resources/ws.html similarity index 100% rename from src/test/resources/ws.html rename to src/main/resources/ws.html diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index ea49f3e..d680cf3 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -8,18 +8,11 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.http.WebSocketBase; import io.vertx.core.json.JsonObject; -import io.vertx.core.net.NetSocket; -import io.vertx.ext.bridge.BridgeOptions; import io.vertx.ext.bridge.PermittedOptions; -import io.vertx.ext.eventbus.bridge.tcp.impl.JsonRPCStreamEventBusBridgeImpl; -import io.vertx.ext.eventbus.bridge.tcp.impl.TCPJsonRPCStreamEventBusBridgeImpl; import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.codec.BodyCodec; -import java.util.Random; - import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.request; public class InteropWebSocketServer extends AbstractVerticle { From 9d9697438d4003e27b57f75e0634c0cb7f3503f2 Mon Sep 17 00:00:00 2001 From: Lucifer Morningstar Date: Wed, 3 Aug 2022 17:09:50 +0530 Subject: [PATCH 34/34] Simplify handleSSE --- pom.xml | 3 +- .../java/examples/HttpSSEBridgeExample.java | 12 ++- .../HttpJsonRPCStreamEventBusBridgeImpl.java | 84 +++++++++---------- .../bridge/tcp/InteropWebSocketServer.java | 28 ++----- 4 files changed, 50 insertions(+), 77 deletions(-) diff --git a/pom.xml b/pom.xml index 2d729b3..7b478ce 100644 --- a/pom.xml +++ b/pom.xml @@ -80,8 +80,7 @@ io.vertx vertx-web-client - - + test junit diff --git a/src/main/java/examples/HttpSSEBridgeExample.java b/src/main/java/examples/HttpSSEBridgeExample.java index 996831b..42ee4ac 100644 --- a/src/main/java/examples/HttpSSEBridgeExample.java +++ b/src/main/java/examples/HttpSSEBridgeExample.java @@ -5,11 +5,14 @@ import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.PermittedOptions; import io.vertx.ext.eventbus.bridge.tcp.JsonRPCBridgeOptions; import io.vertx.ext.eventbus.bridge.tcp.impl.HttpJsonRPCStreamEventBusBridgeImpl; +import java.util.function.Consumer; + public class HttpSSEBridgeExample extends AbstractVerticle { public static void main(String[] args) { @@ -33,7 +36,7 @@ public void start(Promise start) { .addOutboundPermitted(new PermittedOptions().setAddress("echo")) .addOutboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("ping")), - null + event -> event.complete(true) ); vertx @@ -48,12 +51,7 @@ public void start(Promise start) { } else if ("/jsonrpc".equals(req.path())) { bridge.handle(req); } else if ("/jsonrpc-sse".equals(req.path())) { - JsonObject message = new JsonObject() - .put("jsonrpc", "2.0") - .put("method", "register") - .put("id", (int) (Math.random() * 100_000)) - .put("params", new JsonObject().put("address", "ping")); - bridge.handleSSE(req.response(), message); + bridge.handleSSE(req, (int) (Math.random() * 100_000), new JsonObject().put("address", "ping")); } else { req.response().setStatusCode(404).end("Not Found"); } diff --git a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java index e77f8bc..6baab4c 100644 --- a/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java +++ b/src/main/java/io/vertx/ext/eventbus/bridge/tcp/impl/HttpJsonRPCStreamEventBusBridgeImpl.java @@ -2,13 +2,11 @@ import io.vertx.core.Handler; import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.Message; import io.vertx.core.eventbus.MessageConsumer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.BridgeEventType; import io.vertx.ext.eventbus.bridge.tcp.BridgeEvent; @@ -35,9 +33,6 @@ public void handle(HttpServerRequest socket) { () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, socket), // on success () -> { - // TODO: make these maps persistent across requests otherwise replies won't work because - // http client cannot reply again in the same request after receiving a response and has - // to make a new request. final Map> registry = new ConcurrentHashMap<>(); socket.exceptionHandler(t -> { @@ -68,10 +63,7 @@ public void handle(HttpServerRequest socket) { Consumer writer; if (method.equals("register")) { response.setChunked(true); - writer = payload -> { - response.write("event: " + payload.getJsonObject("result").getString("address") + "\n"); - response.write("data: " + payload.encode() + "\n\n"); - }; + writer = payload -> response.write(payload.encode()); } else { writer = payload -> response.end(payload.encode()); } @@ -82,42 +74,44 @@ public void handle(HttpServerRequest socket) { () -> socket.response().setStatusCode(500).setStatusMessage("Internal Server Error").end()); } - // TODO: discuss implications of accepting response here. bridge events may not be emitted. - // but if accepting request cannot use handler as the request is usually empty and handler is - // not invoked until data has been read. also same thing for other cases - public void handleSSE(HttpServerResponse socket, JsonObject msg) { - final Map> registry = new ConcurrentHashMap<>(); - - socket.exceptionHandler(t -> { - log.error(t.getMessage(), t); - registry.values().forEach(MessageConsumer::unregister); - registry.clear(); - }); - if (this.isInvalid(msg)) { - return; - } - - HttpServerResponse response = socket - .setChunked(true) - .putHeader(HttpHeaders.CONTENT_TYPE, "text/event-stream") - .endHandler(handler -> { - registry.values().forEach(MessageConsumer::unregister); - registry.clear(); - }); - - final String method = msg.getString("method"); - if (!method.equalsIgnoreCase("register")) { - log.error("Invalid method for SSE!"); - return; - } - - final Object id = msg.getValue("id"); - Consumer writer = payload -> { - // TODO: Should we use id or address for event name? - response.write("event: " + payload.getJsonObject("result").getString("address") + "\n"); - response.write("data: " + payload.encode() + "\n\n"); - }; - register(writer, id, msg, registry, replies); + // TODO: Discuss. Currently we are only adding such methods because SSE doesn't have a body, maybe we could + // instead mandate some query params in the request to signal SSE. but bodyHandler is not invoked + // in that case so how to handle the request. endHandler or check query params first before applying + // bodyHandler ? + public void handleSSE(HttpServerRequest socket, Object id, JsonObject msg) { + checkCallHook( + // process the new socket according to the event handler + () -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CREATED, null, socket), + () -> { + final Map> registry = new ConcurrentHashMap<>(); + + socket.exceptionHandler(t -> { + log.error(t.getMessage(), t); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }); + + HttpServerResponse response = socket.response().setChunked(true).putHeader(HttpHeaders.CONTENT_TYPE, + "text/event-stream").endHandler(handler -> { + checkCallHook(() -> new BridgeEventImpl<>(BridgeEventType.SOCKET_CLOSED, null, socket)); + registry.values().forEach(MessageConsumer::unregister); + registry.clear(); + }); + + Consumer writer = payload -> { + JsonObject result = payload.getJsonObject("result"); + if (result != null) { + String address = result.getString("address"); + if (address != null) { + response.write("event: " + address + "\n"); + response.write("data: " + payload.encode() + "\n\n"); + } + } + }; + register(writer, id, msg, registry, replies); + }, + () -> socket.response().setStatusCode(500).setStatusMessage("Internal Server Error").end() + ); } diff --git a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java index a02eb7b..3b8b519 100644 --- a/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java +++ b/src/test/java/io/vertx/ext/eventbus/bridge/tcp/InteropWebSocketServer.java @@ -1,20 +1,13 @@ package io.vertx.ext.eventbus.bridge.tcp; import io.vertx.core.AbstractVerticle; -import io.vertx.core.Handler; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.PermittedOptions; -import io.vertx.ext.web.client.WebClient; -import io.vertx.ext.web.codec.BodyCodec; - -import static io.vertx.ext.eventbus.bridge.tcp.impl.protocol.JsonRPCHelper.request; +import io.vertx.ext.eventbus.bridge.tcp.impl.HttpJsonRPCStreamEventBusBridgeImpl; public class InteropWebSocketServer extends AbstractVerticle { @@ -31,8 +24,7 @@ public void start(Promise start) { vertx.eventBus().consumer("echo", (Message msg) -> msg.reply(msg.body())); vertx.setPeriodic(1000L, __ -> vertx.eventBus().send("ping", new JsonObject().put("value", "hi"))); - // once we fix the interface we can avoid the casts - Handler bridge = JsonRPCStreamEventBusBridge.httpSocketHandler( + HttpJsonRPCStreamEventBusBridgeImpl bridge = (HttpJsonRPCStreamEventBusBridgeImpl) JsonRPCStreamEventBusBridge.httpSocketHandler( vertx, new JsonRPCBridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("hello")) @@ -41,11 +33,9 @@ public void start(Promise start) { .addOutboundPermitted(new PermittedOptions().setAddress("echo")) .addOutboundPermitted(new PermittedOptions().setAddress("test")) .addOutboundPermitted(new PermittedOptions().setAddress("ping")), - null + null ); - WebClient client = WebClient.create(vertx); - vertx .createHttpServer() .requestHandler(req -> { @@ -58,16 +48,8 @@ public void start(Promise start) { } else if ("/jsonrpc".equals(req.path())){ bridge.handle(req); } else if ("/jsonrpc-sse".equals(req.path())) { - HttpServerResponse resp = req.response().setChunked(true).putHeader("Content-Type", "text/event-stream"); - request( - "register", - (int) (Math.random() * 100_000), - new JsonObject().put("address", "ping"), - buffer -> client - .post(8080, "localhost", "/jsonrpc") - .as(BodyCodec.pipe(resp)) - .sendBuffer(buffer) - ); + JsonObject params = new JsonObject().put("params", new JsonObject().put("address", "ping")); + bridge.handleSSE(req, (int) (Math.random() * 100_000), params); } else { req.response().setStatusCode(404).end("Not Found"); }