├── .github ├── release-drafter.yml ├── FUNDING.yml └── workflows │ ├── branch-ci.yml │ ├── pre-release-ci.yml │ └── release-ci.yml ├── .editorconfig ├── .gitignore ├── .gitattributes ├── .yamllint.yml ├── services-gateway-netty ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── scalecube │ │ │ └── services │ │ │ └── gateway │ │ │ ├── GatewaySession.java │ │ │ ├── ws │ │ │ ├── Signal.java │ │ │ ├── WebsocketContextException.java │ │ │ ├── GatewayMessages.java │ │ │ └── WebsocketGateway.java │ │ │ ├── ReferenceCountUtil.java │ │ │ ├── GatewayTemplate.java │ │ │ ├── rsocket │ │ │ ├── RSocketGatewayAcceptor.java │ │ │ ├── RSocketGateway.java │ │ │ └── RSocketGatewaySession.java │ │ │ ├── GatewaySessionHandler.java │ │ │ ├── ServiceMessageCodec.java │ │ │ └── http │ │ │ ├── HttpGateway.java │ │ │ └── HttpGatewayAcceptor.java │ └── test │ │ └── java │ │ └── io │ │ └── scalecube │ │ └── services │ │ └── gateway │ │ └── ws │ │ └── TestInputs.java └── pom.xml ├── services-gateway-tests ├── src │ └── test │ │ ├── java │ │ └── io │ │ │ └── scalecube │ │ │ └── services │ │ │ └── gateway │ │ │ ├── exeptions │ │ │ ├── ErrorService.java │ │ │ ├── SomeException.java │ │ │ ├── ErrorServiceImpl.java │ │ │ └── GatewayErrorMapperImpl.java │ │ │ ├── websocket │ │ │ ├── ReactiveOperator.java │ │ │ ├── WebsocketGatewayExtension.java │ │ │ ├── CancelledSubscriber.java │ │ │ ├── WebsocketLocalWithAuthExtension.java │ │ │ ├── WebsocketLocalGatewayExtension.java │ │ │ ├── WebsocketClientErrorMapperTest.java │ │ │ ├── WebsocketLocalGatewayErrorMapperTest.java │ │ │ ├── ReactiveAdapter.java │ │ │ ├── WebsocketLocalGatewayAuthTest.java │ │ │ ├── WebsocketLocalGatewayTest.java │ │ │ ├── WebsocketGatewayTest.java │ │ │ └── WebsocketServerTest.java │ │ │ ├── TestService.java │ │ │ ├── TestUtils.java │ │ │ ├── TestServiceImpl.java │ │ │ ├── http │ │ │ ├── HttpGatewayExtension.java │ │ │ ├── HttpLocalGatewayExtension.java │ │ │ ├── HttpClientErrorMapperTest.java │ │ │ ├── HttpLocalGatewayErrorMapperTest.java │ │ │ ├── HttpLocalGatewayTest.java │ │ │ ├── HttpGatewayTest.java │ │ │ ├── HttpClientConnectionTest.java │ │ │ └── CorsTest.java │ │ │ ├── rsocket │ │ │ ├── RSocketGatewayExtension.java │ │ │ ├── RSocketLocalWithAuthExtension.java │ │ │ ├── RSocketLocalGatewayExtension.java │ │ │ ├── RSocketClientErrorMapperTest.java │ │ │ ├── RSocketLocalGatewayErrorMapperTest.java │ │ │ ├── RSocketLocalGatewayAuthTest.java │ │ │ ├── RSocketLocalGatewayTest.java │ │ │ └── RSocketGatewayTest.java │ │ │ ├── BaseTest.java │ │ │ ├── SecuredService.java │ │ │ ├── TestGatewaySessionHandler.java │ │ │ ├── GatewaySessionHandlerImpl.java │ │ │ ├── AuthRegistry.java │ │ │ ├── SecuredServiceImpl.java │ │ │ ├── AbstractLocalGatewayExtension.java │ │ │ └── AbstractGatewayExtension.java │ │ └── resources │ │ ├── log4j2-test.xml │ │ └── index.html └── pom.xml ├── services-gateway-client-transport ├── src │ └── main │ │ └── java │ │ └── io │ │ └── scalecube │ │ └── services │ │ └── gateway │ │ └── transport │ │ ├── GatewayClientTransport.java │ │ ├── websocket │ │ └── Signal.java │ │ ├── GatewayClientChannel.java │ │ ├── StaticAddressRouter.java │ │ ├── GatewayClientCodec.java │ │ ├── GatewayClient.java │ │ ├── http │ │ ├── HttpGatewayClientCodec.java │ │ └── HttpGatewayClient.java │ │ ├── GatewayClientTransports.java │ │ └── rsocket │ │ └── RSocketGatewayClientCodec.java └── pom.xml ├── services-gateway-examples ├── src │ └── main │ │ └── java │ │ └── io │ │ └── scalecube │ │ └── services │ │ └── examples │ │ └── gateway │ │ ├── HttpGatewayExample.java │ │ └── WebsocketGatewayExample.java └── pom.xml └── README.md /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | - https://www.om2.com/ 3 | - https://exberry.io/ 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.gitattributes 4 | !.github 5 | !.editorconfig 6 | !.*.yml 7 | !.env.example 8 | **/target/ 9 | *.iml 10 | **/logs/*.log 11 | **/logs/*.log.* 12 | *.db 13 | *.csv 14 | *.log 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.txt text 4 | *.sh text eol=lf 5 | *.html text eol=lf diff=html 6 | *.css text eol=lf 7 | *.js text eol=lf 8 | *.jpg -text 9 | *.pdf -text 10 | *.java text diff=java 11 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | rules: 3 | document-start: 4 | present: false 5 | truthy: disable 6 | comments: 7 | min-spaces-from-content: 1 8 | line-length: 9 | max: 150 10 | braces: 11 | min-spaces-inside: 0 12 | max-spaces-inside: 0 13 | brackets: 14 | min-spaces-inside: 0 15 | max-spaces-inside: 0 16 | indentation: 17 | indent-sequences: consistent 18 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/GatewaySession.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import java.util.Map; 4 | 5 | public interface GatewaySession { 6 | 7 | /** 8 | * Session id representation to be unique per client session. 9 | * 10 | * @return session id 11 | */ 12 | long sessionId(); 13 | 14 | /** 15 | * Returns headers associated with session. 16 | * 17 | * @return headers map 18 | */ 19 | Map headers(); 20 | } 21 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/exeptions/ErrorService.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.exeptions; 2 | 3 | import io.scalecube.services.annotations.Service; 4 | import io.scalecube.services.annotations.ServiceMethod; 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | @Service 9 | public interface ErrorService { 10 | 11 | @ServiceMethod 12 | Flux manyError(); 13 | 14 | @ServiceMethod 15 | Mono oneError(); 16 | } 17 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/exeptions/SomeException.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.exeptions; 2 | 3 | import io.scalecube.services.exceptions.ServiceException; 4 | 5 | public class SomeException extends ServiceException { 6 | 7 | public static final int ERROR_TYPE = 4020; 8 | public static final int ERROR_CODE = 42; 9 | public static final String ERROR_MESSAGE = "smth happened"; 10 | 11 | public SomeException() { 12 | super(ERROR_CODE, ERROR_MESSAGE); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/exeptions/ErrorServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.exeptions; 2 | 3 | import reactor.core.publisher.Flux; 4 | import reactor.core.publisher.Mono; 5 | 6 | public class ErrorServiceImpl implements ErrorService { 7 | 8 | @Override 9 | public Flux manyError() { 10 | return Flux.error(new SomeException()); 11 | } 12 | 13 | @Override 14 | public Mono oneError() { 15 | return Mono.error(new SomeException()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/ReactiveOperator.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import reactor.core.Disposable; 4 | 5 | public interface ReactiveOperator extends Disposable { 6 | 7 | void dispose(Throwable throwable); 8 | 9 | void lastError(Throwable throwable); 10 | 11 | Throwable lastError(); 12 | 13 | void tryNext(Object fragment); 14 | 15 | boolean isFastPath(); 16 | 17 | void commitProduced(); 18 | 19 | long incrementProduced(); 20 | 21 | long requested(long limit); 22 | } 23 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/TestService.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.scalecube.services.annotations.Service; 4 | import io.scalecube.services.annotations.ServiceMethod; 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | @Service 9 | public interface TestService { 10 | 11 | @ServiceMethod("manyNever") 12 | Flux manyNever(); 13 | 14 | @ServiceMethod 15 | Mono one(String one); 16 | 17 | @ServiceMethod 18 | Mono oneErr(String one); 19 | } 20 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/TestUtils.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import java.time.Duration; 4 | import java.util.function.BooleanSupplier; 5 | import reactor.core.publisher.Mono; 6 | 7 | public final class TestUtils { 8 | 9 | public static final Duration TIMEOUT = Duration.ofSeconds(10); 10 | 11 | private TestUtils() {} 12 | 13 | /** 14 | * Waits until the given condition is done 15 | * 16 | * @param condition condition 17 | * @return operation's result 18 | */ 19 | public static Mono await(BooleanSupplier condition) { 20 | return Mono.delay(Duration.ofMillis(100)).repeat(() -> !condition.getAsBoolean()).then(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransport.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport; 2 | 3 | import io.scalecube.services.ServiceReference; 4 | import io.scalecube.services.transport.api.ClientChannel; 5 | import io.scalecube.services.transport.api.ClientTransport; 6 | 7 | public class GatewayClientTransport implements ClientTransport { 8 | 9 | private final GatewayClient gatewayClient; 10 | 11 | public GatewayClientTransport(GatewayClient gatewayClient) { 12 | this.gatewayClient = gatewayClient; 13 | } 14 | 15 | @Override 16 | public ClientChannel create(ServiceReference serviceReference) { 17 | return new GatewayClientChannel(gatewayClient); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/TestServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.scalecube.services.exceptions.ForbiddenException; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | 7 | public class TestServiceImpl implements TestService { 8 | 9 | private final Runnable onClose; 10 | 11 | public TestServiceImpl(Runnable onClose) { 12 | this.onClose = onClose; 13 | } 14 | 15 | @Override 16 | public Flux manyNever() { 17 | return Flux.never().log(">>> ").doOnCancel(onClose); 18 | } 19 | 20 | @Override 21 | public Mono one(String one) { 22 | return Mono.just(one); 23 | } 24 | 25 | @Override 26 | public Mono oneErr(String one) { 27 | throw new ForbiddenException("forbidden"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/HttpGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractGatewayExtension; 5 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 6 | 7 | class HttpGatewayExtension extends AbstractGatewayExtension { 8 | 9 | private static final String GATEWAY_ALIAS_NAME = "http"; 10 | 11 | HttpGatewayExtension(Object serviceInstance) { 12 | this(ServiceInfo.fromServiceInstance(serviceInstance).build()); 13 | } 14 | 15 | HttpGatewayExtension(ServiceInfo serviceInfo) { 16 | super( 17 | serviceInfo, 18 | opts -> new HttpGateway(opts.id(GATEWAY_ALIAS_NAME)), 19 | GatewayClientTransports::httpGatewayClientTransport); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/ws/Signal.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.ws; 2 | 3 | public enum Signal { 4 | COMPLETE(1), 5 | ERROR(2), 6 | CANCEL(3); 7 | 8 | private final int code; 9 | 10 | Signal(int code) { 11 | this.code = code; 12 | } 13 | 14 | public int code() { 15 | return code; 16 | } 17 | 18 | /** 19 | * Return appropriate instance of {@link Signal} for given signal code. 20 | * 21 | * @param code signal code 22 | * @return signal instance 23 | */ 24 | public static Signal from(int code) { 25 | switch (code) { 26 | case 1: 27 | return COMPLETE; 28 | case 2: 29 | return ERROR; 30 | case 3: 31 | return CANCEL; 32 | default: 33 | throw new IllegalArgumentException("Unknown signal: " + code); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractGatewayExtension; 5 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 6 | 7 | class RSocketGatewayExtension extends AbstractGatewayExtension { 8 | 9 | private static final String GATEWAY_ALIAS_NAME = "rsws"; 10 | 11 | RSocketGatewayExtension(Object serviceInstance) { 12 | this(ServiceInfo.fromServiceInstance(serviceInstance).build()); 13 | } 14 | 15 | RSocketGatewayExtension(ServiceInfo serviceInfo) { 16 | super( 17 | serviceInfo, 18 | opts -> new RSocketGateway(opts.id(GATEWAY_ALIAS_NAME)), 19 | GatewayClientTransports::rsocketGatewayClientTransport); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractGatewayExtension; 5 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 6 | import io.scalecube.services.gateway.ws.WebsocketGateway; 7 | 8 | class WebsocketGatewayExtension extends AbstractGatewayExtension { 9 | 10 | private static final String GATEWAY_ALIAS_NAME = "ws"; 11 | 12 | WebsocketGatewayExtension(Object serviceInstance) { 13 | this(ServiceInfo.fromServiceInstance(serviceInstance).build()); 14 | } 15 | 16 | WebsocketGatewayExtension(ServiceInfo serviceInfo) { 17 | super( 18 | serviceInfo, 19 | opts -> new WebsocketGateway(opts.id(GATEWAY_ALIAS_NAME)), 20 | GatewayClientTransports::websocketGatewayClientTransport); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/BaseTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.TestInfo; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public abstract class BaseTest { 10 | 11 | protected static final Logger LOGGER = LoggerFactory.getLogger(BaseTest.class); 12 | 13 | @BeforeEach 14 | public final void baseSetUp(TestInfo testInfo) { 15 | LOGGER.info( 16 | "***** Test started : " 17 | + getClass().getSimpleName() 18 | + "." 19 | + testInfo.getDisplayName() 20 | + " *****"); 21 | } 22 | 23 | @AfterEach 24 | public final void baseTearDown(TestInfo testInfo) { 25 | System.gc(); 26 | LOGGER.info( 27 | "***** Test finished : " 28 | + getClass().getSimpleName() 29 | + "." 30 | + testInfo.getDisplayName() 31 | + " *****"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/CancelledSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import reactor.core.CoreSubscriber; 6 | 7 | public class CancelledSubscriber implements CoreSubscriber { 8 | 9 | private static final Logger LOGGER = LoggerFactory.getLogger(CancelledSubscriber.class); 10 | 11 | public static final CancelledSubscriber INSTANCE = new CancelledSubscriber(); 12 | 13 | private CancelledSubscriber() { 14 | // Do not instantiate 15 | } 16 | 17 | @Override 18 | public void onSubscribe(org.reactivestreams.Subscription s) { 19 | // no-op 20 | } 21 | 22 | @Override 23 | public void onNext(Object o) { 24 | LOGGER.warn("Received ({}) which will be dropped immediately due cancelled aeron inbound", o); 25 | } 26 | 27 | @Override 28 | public void onError(Throwable t) { 29 | // no-op 30 | } 31 | 32 | @Override 33 | public void onComplete() { 34 | // no-op 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/SecuredService.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import static io.scalecube.services.gateway.SecuredService.NS; 4 | 5 | import io.scalecube.services.annotations.RequestType; 6 | import io.scalecube.services.annotations.Service; 7 | import io.scalecube.services.annotations.ServiceMethod; 8 | import io.scalecube.services.api.ServiceMessage; 9 | import io.scalecube.services.auth.Secured; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | 13 | /** Authentication service and the service body itself in one class. */ 14 | @Service(NS) 15 | public interface SecuredService { 16 | String NS = "gw.auth"; 17 | 18 | @ServiceMethod 19 | @RequestType(String.class) 20 | Mono createSession(ServiceMessage request); 21 | 22 | @ServiceMethod 23 | @RequestType(String.class) 24 | @Secured 25 | Mono requestOne(String req); 26 | 27 | @ServiceMethod 28 | @RequestType(Integer.class) 29 | @Secured 30 | Flux requestN(Integer req); 31 | } 32 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/ReferenceCountUtil.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.netty.util.ReferenceCounted; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public final class ReferenceCountUtil { 8 | 9 | private static final Logger LOGGER = LoggerFactory.getLogger(ReferenceCountUtil.class); 10 | 11 | private ReferenceCountUtil() { 12 | // Do not instantiate 13 | } 14 | 15 | /** 16 | * Try to release input object iff it's instance is of {@link ReferenceCounted} type and its 17 | * refCount greater than zero. 18 | * 19 | * @return true if msg release taken place 20 | */ 21 | public static boolean safestRelease(Object msg) { 22 | try { 23 | return (msg instanceof ReferenceCounted) 24 | && ((ReferenceCounted) msg).refCnt() > 0 25 | && ((ReferenceCounted) msg).release(); 26 | } catch (Throwable t) { 27 | LOGGER.warn("Failed to release reference counted object: {}, cause: {}", msg, t.toString()); 28 | return false; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketLocalWithAuthExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractLocalGatewayExtension; 5 | import io.scalecube.services.gateway.AuthRegistry; 6 | import io.scalecube.services.gateway.GatewaySessionHandlerImpl; 7 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 8 | 9 | public class RSocketLocalWithAuthExtension extends AbstractLocalGatewayExtension { 10 | 11 | private static final String GATEWAY_ALIAS_NAME = "rsws"; 12 | 13 | RSocketLocalWithAuthExtension(Object serviceInstance, AuthRegistry authReg) { 14 | this(ServiceInfo.fromServiceInstance(serviceInstance).build(), authReg); 15 | } 16 | 17 | RSocketLocalWithAuthExtension(ServiceInfo serviceInfo, AuthRegistry authReg) { 18 | super( 19 | serviceInfo, 20 | opts -> 21 | new RSocketGateway(opts.id(GATEWAY_ALIAS_NAME), new GatewaySessionHandlerImpl(authReg)), 22 | GatewayClientTransports::rsocketGatewayClientTransport); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractLocalGatewayExtension; 5 | import io.scalecube.services.gateway.GatewayOptions; 6 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 7 | import java.util.function.Function; 8 | 9 | class HttpLocalGatewayExtension extends AbstractLocalGatewayExtension { 10 | 11 | private static final String GATEWAY_ALIAS_NAME = "http"; 12 | 13 | HttpLocalGatewayExtension(Object serviceInstance) { 14 | this(ServiceInfo.fromServiceInstance(serviceInstance).build()); 15 | } 16 | 17 | HttpLocalGatewayExtension(ServiceInfo serviceInfo) { 18 | this(serviceInfo, HttpGateway::new); 19 | } 20 | 21 | HttpLocalGatewayExtension( 22 | ServiceInfo serviceInfo, Function gatewaySupplier) { 23 | super( 24 | serviceInfo, 25 | opts -> gatewaySupplier.apply(opts.id(GATEWAY_ALIAS_NAME)), 26 | GatewayClientTransports::httpGatewayClientTransport); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketLocalGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractLocalGatewayExtension; 5 | import io.scalecube.services.gateway.GatewayOptions; 6 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 7 | import java.util.function.Function; 8 | 9 | class RSocketLocalGatewayExtension extends AbstractLocalGatewayExtension { 10 | 11 | private static final String GATEWAY_ALIAS_NAME = "rsws"; 12 | 13 | RSocketLocalGatewayExtension(Object serviceInstance) { 14 | this(ServiceInfo.fromServiceInstance(serviceInstance).build()); 15 | } 16 | 17 | RSocketLocalGatewayExtension(ServiceInfo serviceInfo) { 18 | this(serviceInfo, RSocketGateway::new); 19 | } 20 | 21 | RSocketLocalGatewayExtension( 22 | ServiceInfo serviceInfo, Function gatewaySupplier) { 23 | super( 24 | serviceInfo, 25 | opts -> gatewaySupplier.apply(opts.id(GATEWAY_ALIAS_NAME)), 26 | GatewayClientTransports::rsocketGatewayClientTransport); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/branch-ci.yml: -------------------------------------------------------------------------------- 1 | name: Branch CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '.github/workflows/**' 7 | - '*.md' 8 | - '*.txt' 9 | branches-ignore: 10 | - 'release*' 11 | 12 | jobs: 13 | build: 14 | name: Branch CI 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/cache@v1 19 | with: 20 | path: ~/.m2/repository 21 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 22 | restore-keys: | 23 | ${{ runner.os }}-maven- 24 | - name: Set up JDK 1.8 25 | uses: actions/setup-java@v1 26 | with: 27 | java-version: 1.8 28 | server-id: github 29 | server-username: GITHUB_ACTOR 30 | server-password: GITHUB_TOKEN 31 | - name: Maven Build 32 | run: mvn clean install -DskipTests=true -Dmaven.javadoc.skip=true -Ddockerfile.skip=true -B -V 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.ORGANIZATION_TOKEN }} 35 | - name: Maven Verify 36 | run: | 37 | sudo echo "127.0.0.1 $(eval hostname)" | sudo tee -a /etc/hosts 38 | mvn verify -B 39 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalWithAuthExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractLocalGatewayExtension; 5 | import io.scalecube.services.gateway.AuthRegistry; 6 | import io.scalecube.services.gateway.GatewaySessionHandlerImpl; 7 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 8 | import io.scalecube.services.gateway.ws.WebsocketGateway; 9 | 10 | public class WebsocketLocalWithAuthExtension extends AbstractLocalGatewayExtension { 11 | 12 | private static final String GATEWAY_ALIAS_NAME = "ws"; 13 | 14 | WebsocketLocalWithAuthExtension(Object serviceInstance, AuthRegistry authReg) { 15 | this(ServiceInfo.fromServiceInstance(serviceInstance).build(), authReg); 16 | } 17 | 18 | WebsocketLocalWithAuthExtension(ServiceInfo serviceInfo, AuthRegistry authReg) { 19 | super( 20 | serviceInfo, 21 | opts -> 22 | new WebsocketGateway( 23 | opts.id(GATEWAY_ALIAS_NAME), new GatewaySessionHandlerImpl(authReg)), 24 | GatewayClientTransports::websocketGatewayClientTransport); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/TestGatewaySessionHandler.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.scalecube.services.api.ServiceMessage; 4 | import java.util.concurrent.CountDownLatch; 5 | import java.util.concurrent.atomic.AtomicReference; 6 | import reactor.util.context.Context; 7 | 8 | public class TestGatewaySessionHandler implements GatewaySessionHandler { 9 | 10 | public final CountDownLatch msgLatch = new CountDownLatch(1); 11 | public final CountDownLatch connLatch = new CountDownLatch(1); 12 | public final CountDownLatch disconnLatch = new CountDownLatch(1); 13 | private final AtomicReference lastSession = new AtomicReference<>(); 14 | 15 | @Override 16 | public ServiceMessage mapMessage(GatewaySession s, ServiceMessage req, Context context) { 17 | msgLatch.countDown(); 18 | return req; 19 | } 20 | 21 | @Override 22 | public void onSessionOpen(GatewaySession s) { 23 | connLatch.countDown(); 24 | lastSession.set(s); 25 | } 26 | 27 | @Override 28 | public void onSessionClose(GatewaySession s) { 29 | disconnLatch.countDown(); 30 | } 31 | 32 | public GatewaySession lastSession() { 33 | return lastSession.get(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import io.scalecube.services.ServiceInfo; 4 | import io.scalecube.services.gateway.AbstractLocalGatewayExtension; 5 | import io.scalecube.services.gateway.GatewayOptions; 6 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 7 | import io.scalecube.services.gateway.ws.WebsocketGateway; 8 | import java.util.function.Function; 9 | 10 | class WebsocketLocalGatewayExtension extends AbstractLocalGatewayExtension { 11 | 12 | private static final String GATEWAY_ALIAS_NAME = "ws"; 13 | 14 | WebsocketLocalGatewayExtension(Object serviceInstance) { 15 | this(ServiceInfo.fromServiceInstance(serviceInstance).build()); 16 | } 17 | 18 | WebsocketLocalGatewayExtension(ServiceInfo serviceInfo) { 19 | this(serviceInfo, WebsocketGateway::new); 20 | } 21 | 22 | WebsocketLocalGatewayExtension( 23 | ServiceInfo serviceInfo, Function gatewaySupplier) { 24 | super( 25 | serviceInfo, 26 | opts -> gatewaySupplier.apply(opts.id(GATEWAY_ALIAS_NAME)), 27 | GatewayClientTransports::websocketGatewayClientTransport); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/websocket/Signal.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport.websocket; 2 | 3 | public enum Signal { 4 | COMPLETE(1), 5 | ERROR(2), 6 | CANCEL(3); 7 | 8 | private final int code; 9 | 10 | Signal(int code) { 11 | this.code = code; 12 | } 13 | 14 | public int code() { 15 | return code; 16 | } 17 | 18 | public String codeAsString() { 19 | return String.valueOf(code); 20 | } 21 | 22 | /** 23 | * Return appropriate instance of {@link Signal} for given signal code. 24 | * 25 | * @param code signal code 26 | * @return signal instance 27 | */ 28 | public static Signal from(String code) { 29 | return from(Integer.parseInt(code)); 30 | } 31 | 32 | /** 33 | * Return appropriate instance of {@link Signal} for given signal code. 34 | * 35 | * @param code signal code 36 | * @return signal instance 37 | */ 38 | public static Signal from(int code) { 39 | switch (code) { 40 | case 1: 41 | return COMPLETE; 42 | case 2: 43 | return ERROR; 44 | case 3: 45 | return CANCEL; 46 | default: 47 | throw new IllegalArgumentException("Unknown signal: " + code); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/ws/WebsocketContextException.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.ws; 2 | 3 | import io.scalecube.services.api.ServiceMessage; 4 | import io.scalecube.services.gateway.ReferenceCountUtil; 5 | 6 | public class WebsocketContextException extends RuntimeException { 7 | 8 | private final ServiceMessage request; 9 | private final ServiceMessage response; 10 | 11 | private WebsocketContextException( 12 | Throwable cause, ServiceMessage request, ServiceMessage response) { 13 | super(cause); 14 | this.request = request; 15 | this.response = response; 16 | } 17 | 18 | public static WebsocketContextException badRequest(String errorMessage, ServiceMessage request) { 19 | return new WebsocketContextException( 20 | new io.scalecube.services.exceptions.BadRequestException(errorMessage), request, null); 21 | } 22 | 23 | public ServiceMessage request() { 24 | return request; 25 | } 26 | 27 | public ServiceMessage response() { 28 | return response; 29 | } 30 | 31 | /** 32 | * Releases request data if any. 33 | * 34 | * @return self 35 | */ 36 | public WebsocketContextException releaseRequest() { 37 | if (request != null) { 38 | ReferenceCountUtil.safestRelease(request.data()); 39 | } 40 | return this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/GatewaySessionHandlerImpl.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.scalecube.services.api.ServiceMessage; 5 | import io.scalecube.services.auth.Authenticator; 6 | import java.util.Optional; 7 | import reactor.util.context.Context; 8 | 9 | public class GatewaySessionHandlerImpl implements GatewaySessionHandler { 10 | 11 | private final AuthRegistry authRegistry; 12 | 13 | public GatewaySessionHandlerImpl(AuthRegistry authRegistry) { 14 | this.authRegistry = authRegistry; 15 | } 16 | 17 | @Override 18 | public Context onRequest(GatewaySession session, ByteBuf byteBuf, Context context) { 19 | Optional authData = authRegistry.getAuth(session.sessionId()); 20 | return authData.map(s -> context.put(Authenticator.AUTH_CONTEXT_KEY, s)).orElse(context); 21 | } 22 | 23 | @Override 24 | public ServiceMessage mapMessage( 25 | GatewaySession session, ServiceMessage message, Context context) { 26 | return ServiceMessage.from(message) 27 | .header(AuthRegistry.SESSION_ID, session.sessionId()) 28 | .build(); 29 | } 30 | 31 | @Override 32 | public void onSessionOpen(GatewaySession s) { 33 | LOGGER.info("Session opened: {}", s); 34 | } 35 | 36 | @Override 37 | public void onSessionClose(GatewaySession session) { 38 | LOGGER.info("Session removed: {}", session); 39 | authRegistry.removeAuth(session.sessionId()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/exeptions/GatewayErrorMapperImpl.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.exeptions; 2 | 3 | import io.scalecube.services.api.ErrorData; 4 | import io.scalecube.services.api.ServiceMessage; 5 | import io.scalecube.services.exceptions.DefaultErrorMapper; 6 | import io.scalecube.services.exceptions.ServiceClientErrorMapper; 7 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 8 | 9 | public class GatewayErrorMapperImpl 10 | implements ServiceProviderErrorMapper, ServiceClientErrorMapper { 11 | 12 | public static final GatewayErrorMapperImpl ERROR_MAPPER = new GatewayErrorMapperImpl(); 13 | 14 | @Override 15 | public Throwable toError(ServiceMessage message) { 16 | if (SomeException.ERROR_TYPE == message.errorType()) { 17 | final ErrorData data = message.data(); 18 | if (SomeException.ERROR_CODE == data.getErrorCode()) { 19 | return new SomeException(); 20 | } 21 | } 22 | return DefaultErrorMapper.INSTANCE.toError(message); 23 | } 24 | 25 | @Override 26 | public ServiceMessage toMessage(String qualifier, Throwable throwable) { 27 | if (throwable instanceof SomeException) { 28 | final int errorCode = ((SomeException) throwable).errorCode(); 29 | final int errorType = SomeException.ERROR_TYPE; 30 | final String errorMessage = throwable.getMessage(); 31 | return ServiceMessage.error(qualifier, errorType, errorCode, errorMessage); 32 | } 33 | 34 | return DefaultErrorMapper.INSTANCE.toMessage(qualifier, throwable); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/HttpClientErrorMapperTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import static io.scalecube.services.gateway.TestUtils.TIMEOUT; 4 | import static io.scalecube.services.gateway.exeptions.GatewayErrorMapperImpl.ERROR_MAPPER; 5 | 6 | import io.scalecube.services.ServiceInfo; 7 | import io.scalecube.services.gateway.BaseTest; 8 | import io.scalecube.services.gateway.exeptions.ErrorService; 9 | import io.scalecube.services.gateway.exeptions.ErrorServiceImpl; 10 | import io.scalecube.services.gateway.exeptions.SomeException; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Disabled; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.RegisterExtension; 15 | import reactor.test.StepVerifier; 16 | 17 | @Disabled("Cannot deserialize instance of `java.lang.String` out of START_OBJECT token") 18 | class HttpClientErrorMapperTest extends BaseTest { 19 | 20 | @RegisterExtension 21 | static HttpGatewayExtension extension = 22 | new HttpGatewayExtension( 23 | ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) 24 | .errorMapper(ERROR_MAPPER) 25 | .build()); 26 | 27 | private ErrorService service; 28 | 29 | @BeforeEach 30 | void initService() { 31 | service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); 32 | } 33 | 34 | @Test 35 | void shouldReturnSomeExceptionOnMono() { 36 | StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/GatewayClientChannel.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport; 2 | 3 | import io.scalecube.services.api.ServiceMessage; 4 | import io.scalecube.services.transport.api.ClientChannel; 5 | import io.scalecube.services.transport.api.ServiceMessageCodec; 6 | import java.lang.reflect.Type; 7 | import org.reactivestreams.Publisher; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | 11 | public class GatewayClientChannel implements ClientChannel { 12 | 13 | private final GatewayClient gatewayClient; 14 | 15 | GatewayClientChannel(GatewayClient gatewayClient) { 16 | this.gatewayClient = gatewayClient; 17 | } 18 | 19 | @Override 20 | public Mono requestResponse(ServiceMessage clientMessage, Type responseType) { 21 | return gatewayClient 22 | .requestResponse(clientMessage) 23 | .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)); 24 | } 25 | 26 | @Override 27 | public Flux requestStream(ServiceMessage clientMessage, Type responseType) { 28 | return gatewayClient 29 | .requestStream(clientMessage) 30 | .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)); 31 | } 32 | 33 | @Override 34 | public Flux requestChannel( 35 | Publisher clientMessageStream, Type responseType) { 36 | return gatewayClient 37 | .requestChannel(Flux.from(clientMessageStream)) 38 | .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/StaticAddressRouter.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport; 2 | 3 | import io.scalecube.net.Address; 4 | import io.scalecube.services.ServiceEndpoint; 5 | import io.scalecube.services.ServiceMethodDefinition; 6 | import io.scalecube.services.ServiceReference; 7 | import io.scalecube.services.ServiceRegistration; 8 | import io.scalecube.services.api.ServiceMessage; 9 | import io.scalecube.services.registry.api.ServiceRegistry; 10 | import io.scalecube.services.routing.Router; 11 | import java.util.Collections; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | /** Syntethic router for returning preconstructed static service reference with given address. */ 16 | public class StaticAddressRouter implements Router { 17 | 18 | private final ServiceReference staticServiceReference; 19 | 20 | /** 21 | * Constructor. 22 | * 23 | * @param address address 24 | */ 25 | public StaticAddressRouter(Address address) { 26 | this.staticServiceReference = 27 | new ServiceReference( 28 | new ServiceMethodDefinition(UUID.randomUUID().toString()), 29 | new ServiceRegistration( 30 | UUID.randomUUID().toString(), Collections.emptyMap(), Collections.emptyList()), 31 | ServiceEndpoint.builder().id(UUID.randomUUID().toString()).address(address).build()); 32 | } 33 | 34 | @Override 35 | public Optional route(ServiceRegistry serviceRegistry, ServiceMessage request) { 36 | return Optional.of(staticServiceReference); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/GatewayClientCodec.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport; 2 | 3 | import io.scalecube.services.api.ServiceMessage; 4 | import io.scalecube.services.exceptions.MessageCodecException; 5 | import io.scalecube.services.transport.api.ServiceMessageCodec; 6 | import java.lang.reflect.Type; 7 | 8 | /** 9 | * Describes encoding/decoding operations for {@link ServiceMessage} to/from {@link T} type. 10 | * 11 | * @param represents source or result for decoding or encoding operations respectively 12 | */ 13 | public interface GatewayClientCodec { 14 | 15 | /** 16 | * Data decoder function. 17 | * 18 | * @param message client message. 19 | * @param dataType data type class. 20 | * @return client message object. 21 | * @throws MessageCodecException in case if data decoding fails. 22 | */ 23 | default ServiceMessage decodeData(ServiceMessage message, Type dataType) 24 | throws MessageCodecException { 25 | return ServiceMessageCodec.decodeData(message, dataType); 26 | } 27 | 28 | /** 29 | * Encodes {@link ServiceMessage} to {@link T} type. 30 | * 31 | * @param message client message to encode 32 | * @return encoded message represented by {@link T} type 33 | */ 34 | T encode(ServiceMessage message); 35 | 36 | /** 37 | * Decodes message represented by {@link T} type to {@link ServiceMessage} object. 38 | * 39 | * @param encodedMessage message to decode 40 | * @return decoded message represented by {@link ServiceMessage} type 41 | */ 42 | ServiceMessage decode(T encodedMessage); 43 | } 44 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketClientErrorMapperTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import static io.scalecube.services.gateway.TestUtils.TIMEOUT; 4 | import static io.scalecube.services.gateway.exeptions.GatewayErrorMapperImpl.ERROR_MAPPER; 5 | 6 | import io.scalecube.services.ServiceInfo; 7 | import io.scalecube.services.gateway.BaseTest; 8 | import io.scalecube.services.gateway.exeptions.ErrorService; 9 | import io.scalecube.services.gateway.exeptions.ErrorServiceImpl; 10 | import io.scalecube.services.gateway.exeptions.SomeException; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.RegisterExtension; 14 | import reactor.test.StepVerifier; 15 | 16 | class RSocketClientErrorMapperTest extends BaseTest { 17 | 18 | @RegisterExtension 19 | static RSocketGatewayExtension extension = 20 | new RSocketGatewayExtension( 21 | ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) 22 | .errorMapper(ERROR_MAPPER) 23 | .build()); 24 | 25 | private ErrorService service; 26 | 27 | @BeforeEach 28 | void initService() { 29 | service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); 30 | } 31 | 32 | @Test 33 | void shouldReturnSomeExceptionOnFlux() { 34 | StepVerifier.create(service.manyError()).expectError(SomeException.class).verify(TIMEOUT); 35 | } 36 | 37 | @Test 38 | void shouldReturnSomeExceptionOnMono() { 39 | StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services-gateway-examples/src/main/java/io/scalecube/services/examples/gateway/HttpGatewayExample.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.examples.gateway; 2 | 3 | import io.scalecube.net.Address; 4 | import io.scalecube.services.gateway.Gateway; 5 | import io.scalecube.services.gateway.GatewayOptions; 6 | import java.net.InetSocketAddress; 7 | import java.time.Duration; 8 | import java.util.concurrent.ThreadLocalRandom; 9 | import reactor.core.publisher.Mono; 10 | 11 | public class HttpGatewayExample implements Gateway { 12 | 13 | private final GatewayOptions options; 14 | private final InetSocketAddress address; 15 | 16 | public HttpGatewayExample(GatewayOptions options) { 17 | this.options = options; 18 | this.address = new InetSocketAddress(options.port()); 19 | } 20 | 21 | @Override 22 | public String id() { 23 | return options.id(); 24 | } 25 | 26 | @Override 27 | public Address address() { 28 | return Address.create(address.getHostString(), address.getPort()); 29 | } 30 | 31 | @Override 32 | public Mono start() { 33 | return Mono.defer( 34 | () -> { 35 | System.out.println("Starting HTTP gateway..."); 36 | 37 | return Mono.delay(Duration.ofMillis(ThreadLocalRandom.current().nextInt(100, 500))) 38 | .map(tick -> this) 39 | .doOnSuccess(gw -> System.out.println("HTTP gateway is started on " + gw.address)); 40 | }); 41 | } 42 | 43 | @Override 44 | public Mono stop() { 45 | return Mono.defer( 46 | () -> { 47 | System.out.println("Stopping HTTP gateway..."); 48 | return Mono.empty(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientErrorMapperTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import static io.scalecube.services.gateway.TestUtils.TIMEOUT; 4 | import static io.scalecube.services.gateway.exeptions.GatewayErrorMapperImpl.ERROR_MAPPER; 5 | 6 | import io.scalecube.services.ServiceInfo; 7 | import io.scalecube.services.gateway.BaseTest; 8 | import io.scalecube.services.gateway.exeptions.ErrorService; 9 | import io.scalecube.services.gateway.exeptions.ErrorServiceImpl; 10 | import io.scalecube.services.gateway.exeptions.SomeException; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.RegisterExtension; 14 | import reactor.test.StepVerifier; 15 | 16 | class WebsocketClientErrorMapperTest extends BaseTest { 17 | 18 | @RegisterExtension 19 | static WebsocketGatewayExtension extension = 20 | new WebsocketGatewayExtension( 21 | ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) 22 | .errorMapper(ERROR_MAPPER) 23 | .build()); 24 | 25 | private ErrorService service; 26 | 27 | @BeforeEach 28 | void initService() { 29 | service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); 30 | } 31 | 32 | @Test 33 | void shouldReturnSomeExceptionOnFlux() { 34 | StepVerifier.create(service.manyError()).expectError(SomeException.class).verify(TIMEOUT); 35 | } 36 | 37 | @Test 38 | void shouldReturnSomeExceptionOnMono() { 39 | StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services-gateway-client-transport/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scalecube-gateway-parent 5 | io.scalecube 6 | 2.10.18-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | 11 | scalecube-services-gateway-client-transport 12 | 13 | 14 | 15 | io.scalecube 16 | scalecube-services-transport-rsocket 17 | 18 | 19 | io.scalecube 20 | scalecube-services 21 | 22 | 23 | 24 | com.fasterxml.jackson.datatype 25 | jackson-datatype-jsr310 26 | 27 | 28 | com.fasterxml.jackson.core 29 | jackson-core 30 | 31 | 32 | com.fasterxml.jackson.core 33 | jackson-databind 34 | 35 | 36 | 37 | org.jctools 38 | jctools-core 39 | 40 | 41 | org.slf4j 42 | slf4j-api 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /services-gateway-examples/src/main/java/io/scalecube/services/examples/gateway/WebsocketGatewayExample.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.examples.gateway; 2 | 3 | import io.scalecube.net.Address; 4 | import io.scalecube.services.gateway.Gateway; 5 | import io.scalecube.services.gateway.GatewayOptions; 6 | import java.net.InetSocketAddress; 7 | import java.time.Duration; 8 | import java.util.concurrent.ThreadLocalRandom; 9 | import reactor.core.publisher.Mono; 10 | 11 | public class WebsocketGatewayExample implements Gateway { 12 | 13 | private final GatewayOptions options; 14 | private final InetSocketAddress address; 15 | 16 | public WebsocketGatewayExample(GatewayOptions options) { 17 | this.options = options; 18 | this.address = new InetSocketAddress(options.port()); 19 | } 20 | 21 | @Override 22 | public String id() { 23 | return options.id(); 24 | } 25 | 26 | @Override 27 | public Address address() { 28 | return Address.create(address.getHostString(), address.getPort()); 29 | } 30 | 31 | @Override 32 | public Mono start() { 33 | return Mono.defer( 34 | () -> { 35 | System.out.println("Starting WS gateway..."); 36 | 37 | return Mono.delay(Duration.ofMillis(ThreadLocalRandom.current().nextInt(100, 500))) 38 | .map(tick -> this) 39 | .doOnSuccess(gw -> System.out.println("WS gateway is started on " + gw.address)); 40 | }); 41 | } 42 | 43 | @Override 44 | public Mono stop() { 45 | return Mono.defer( 46 | () -> { 47 | System.out.println("Stopping WS gateway..."); 48 | return Mono.empty(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayErrorMapperTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import static io.scalecube.services.gateway.TestUtils.TIMEOUT; 4 | import static io.scalecube.services.gateway.exeptions.GatewayErrorMapperImpl.ERROR_MAPPER; 5 | 6 | import io.scalecube.services.ServiceInfo; 7 | import io.scalecube.services.gateway.BaseTest; 8 | import io.scalecube.services.gateway.exeptions.ErrorService; 9 | import io.scalecube.services.gateway.exeptions.ErrorServiceImpl; 10 | import io.scalecube.services.gateway.exeptions.SomeException; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Disabled; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.RegisterExtension; 15 | import reactor.test.StepVerifier; 16 | 17 | @Disabled("Cannot deserialize instance of `java.lang.String` out of START_OBJECT token") 18 | class HttpLocalGatewayErrorMapperTest extends BaseTest { 19 | 20 | @RegisterExtension 21 | static HttpLocalGatewayExtension extension = 22 | new HttpLocalGatewayExtension( 23 | ServiceInfo.fromServiceInstance(new ErrorServiceImpl()).errorMapper(ERROR_MAPPER).build(), 24 | opts -> new HttpGateway(opts.call(opts.call().errorMapper(ERROR_MAPPER)), ERROR_MAPPER)); 25 | 26 | private ErrorService service; 27 | 28 | @BeforeEach 29 | void initService() { 30 | service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); 31 | } 32 | 33 | @Test 34 | void shouldReturnSomeExceptionOnMono() { 35 | StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/GatewayClient.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport; 2 | 3 | import io.scalecube.services.api.ServiceMessage; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | 7 | public interface GatewayClient { 8 | 9 | /** 10 | * Communication mode that gives single response to single request. 11 | * 12 | * @param request request message. 13 | * @return Publisher that emits single response form remote server as it's ready. 14 | */ 15 | Mono requestResponse(ServiceMessage request); 16 | 17 | /** 18 | * Communication mode that gives stream of responses to single request. 19 | * 20 | * @param request request message. 21 | * @return Publisher that emits responses from remote server. 22 | */ 23 | Flux requestStream(ServiceMessage request); 24 | 25 | /** 26 | * Communication mode that gives stream of responses to stream of requests. 27 | * 28 | * @param requests request stream. 29 | * @return Publisher that emits responses from remote server. 30 | */ 31 | Flux requestChannel(Flux requests); 32 | 33 | /** 34 | * Initiate cleaning of underlying resources (if any) like closing websocket connection or rSocket 35 | * session. Subsequent calls of requestOne() or requestMany() must issue new connection creation. 36 | * Note that close is not the end of client lifecycle. 37 | */ 38 | void close(); 39 | 40 | /** 41 | * Return close completion signal of the gateway client. 42 | * 43 | * @return close completion signal 44 | */ 45 | Mono onClose(); 46 | } 47 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/resources/log4j2-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %level{length=1} %date{MMdd-HHmm:ss,SSS} %logger{1.} %message [%thread]%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketLocalGatewayErrorMapperTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import static io.scalecube.services.gateway.TestUtils.TIMEOUT; 4 | import static io.scalecube.services.gateway.exeptions.GatewayErrorMapperImpl.ERROR_MAPPER; 5 | 6 | import io.scalecube.services.ServiceInfo; 7 | import io.scalecube.services.gateway.BaseTest; 8 | import io.scalecube.services.gateway.exeptions.ErrorService; 9 | import io.scalecube.services.gateway.exeptions.ErrorServiceImpl; 10 | import io.scalecube.services.gateway.exeptions.SomeException; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.RegisterExtension; 14 | import reactor.test.StepVerifier; 15 | 16 | class RSocketLocalGatewayErrorMapperTest extends BaseTest { 17 | 18 | @RegisterExtension 19 | static RSocketLocalGatewayExtension extension = 20 | new RSocketLocalGatewayExtension( 21 | ServiceInfo.fromServiceInstance(new ErrorServiceImpl()).errorMapper(ERROR_MAPPER).build(), 22 | opts -> 23 | new RSocketGateway(opts.call(opts.call().errorMapper(ERROR_MAPPER)), ERROR_MAPPER)); 24 | 25 | private ErrorService service; 26 | 27 | @BeforeEach 28 | void initService() { 29 | service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); 30 | } 31 | 32 | @Test 33 | void shouldReturnSomeExceptionOnFlux() { 34 | StepVerifier.create(service.manyError()).expectError(SomeException.class).verify(TIMEOUT); 35 | } 36 | 37 | @Test 38 | void shouldReturnSomeExceptionOnMono() { 39 | StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayErrorMapperTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import static io.scalecube.services.gateway.TestUtils.TIMEOUT; 4 | import static io.scalecube.services.gateway.exeptions.GatewayErrorMapperImpl.ERROR_MAPPER; 5 | 6 | import io.scalecube.services.ServiceInfo; 7 | import io.scalecube.services.gateway.BaseTest; 8 | import io.scalecube.services.gateway.exeptions.ErrorService; 9 | import io.scalecube.services.gateway.exeptions.ErrorServiceImpl; 10 | import io.scalecube.services.gateway.exeptions.SomeException; 11 | import io.scalecube.services.gateway.ws.WebsocketGateway; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.RegisterExtension; 15 | import reactor.test.StepVerifier; 16 | 17 | class WebsocketLocalGatewayErrorMapperTest extends BaseTest { 18 | 19 | @RegisterExtension 20 | static WebsocketLocalGatewayExtension extension = 21 | new WebsocketLocalGatewayExtension( 22 | ServiceInfo.fromServiceInstance(new ErrorServiceImpl()).errorMapper(ERROR_MAPPER).build(), 23 | opts -> 24 | new WebsocketGateway(opts.call(opts.call().errorMapper(ERROR_MAPPER)), ERROR_MAPPER)); 25 | 26 | private ErrorService service; 27 | 28 | @BeforeEach 29 | void initService() { 30 | service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); 31 | } 32 | 33 | @Test 34 | void shouldReturnSomeExceptionOnFlux() { 35 | StepVerifier.create(service.manyError()).expectError(SomeException.class).verify(TIMEOUT); 36 | } 37 | 38 | @Test 39 | void shouldReturnSomeExceptionOnMono() { 40 | StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/AuthRegistry.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import java.util.Optional; 4 | import java.util.Set; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentMap; 7 | 8 | /** So called "guess username" authentication. All preconfigured users can be authenticated. */ 9 | public class AuthRegistry { 10 | 11 | static final String SESSION_ID = "SESSION_ID"; 12 | 13 | /** Preconfigured userName-s that are allowed to be authenticated. */ 14 | private final Set allowedUsers; 15 | 16 | private ConcurrentMap loggedInUsers = new ConcurrentHashMap<>(); 17 | 18 | public AuthRegistry(Set allowedUsers) { 19 | this.allowedUsers = allowedUsers; 20 | } 21 | 22 | /** 23 | * Get session's auth data if exists. 24 | * 25 | * @param sessionId session id to get auth info for 26 | * @return auth info for given session if exists 27 | */ 28 | public Optional getAuth(long sessionId) { 29 | return Optional.ofNullable(loggedInUsers.get(sessionId)); 30 | } 31 | 32 | /** 33 | * Add session with auth t registry. 34 | * 35 | * @param sessionId session id to add auth info for 36 | * @param auth auth info for given session id 37 | * @return auth info added for session id or empty if auth info is invalid 38 | */ 39 | public Optional addAuth(long sessionId, String auth) { 40 | if (allowedUsers.contains(auth)) { 41 | loggedInUsers.putIfAbsent(sessionId, auth); 42 | return Optional.of(auth); 43 | } else { 44 | System.err.println("User not in list of ALLOWED: " + auth); 45 | } 46 | return Optional.empty(); 47 | } 48 | 49 | /** 50 | * Remove session from registry. 51 | * 52 | * @param sessionId session id to be removed from registry 53 | * @return true if session had auth info, false - otherwise 54 | */ 55 | public String removeAuth(long sessionId) { 56 | return loggedInUsers.remove(sessionId); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClientCodec.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.ByteBufAllocator; 5 | import io.netty.buffer.ByteBufOutputStream; 6 | import io.scalecube.services.api.ServiceMessage; 7 | import io.scalecube.services.exceptions.MessageCodecException; 8 | import io.scalecube.services.gateway.transport.GatewayClientCodec; 9 | import io.scalecube.services.transport.api.DataCodec; 10 | import io.scalecube.services.transport.api.ReferenceCountUtil; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | public final class HttpGatewayClientCodec implements GatewayClientCodec { 15 | 16 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayClientCodec.class); 17 | 18 | private final DataCodec dataCodec; 19 | 20 | /** 21 | * Constructor for codec which encode/decode client message to/from {@link ByteBuf}. 22 | * 23 | * @param dataCodec data message codec. 24 | */ 25 | public HttpGatewayClientCodec(DataCodec dataCodec) { 26 | this.dataCodec = dataCodec; 27 | } 28 | 29 | @Override 30 | public ByteBuf encode(ServiceMessage message) { 31 | ByteBuf content; 32 | 33 | if (message.hasData(ByteBuf.class)) { 34 | content = message.data(); 35 | } else { 36 | content = ByteBufAllocator.DEFAULT.buffer(); 37 | try { 38 | dataCodec.encode(new ByteBufOutputStream(content), message.data()); 39 | } catch (Throwable t) { 40 | ReferenceCountUtil.safestRelease(content); 41 | LOGGER.error("Failed to encode data on: {}, cause: {}", message, t); 42 | throw new MessageCodecException( 43 | "Failed to encode data on message q=" + message.qualifier(), t); 44 | } 45 | } 46 | 47 | return content; 48 | } 49 | 50 | @Override 51 | public ServiceMessage decode(ByteBuf encodedMessage) { 52 | return ServiceMessage.builder().data(encodedMessage).build(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /services-gateway-netty/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scalecube-gateway-parent 5 | io.scalecube 6 | 2.10.18-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | scalecube-services-gateway-netty 11 | 12 | 13 | 14 | io.scalecube 15 | scalecube-services 16 | 17 | 18 | 19 | io.projectreactor.netty 20 | reactor-netty 21 | 22 | 23 | 24 | io.rsocket 25 | rsocket-core 26 | 27 | 28 | io.rsocket 29 | rsocket-transport-netty 30 | 31 | 32 | 33 | com.fasterxml.jackson.datatype 34 | jackson-datatype-jsr310 35 | 36 | 37 | com.fasterxml.jackson.core 38 | jackson-core 39 | 40 | 41 | com.fasterxml.jackson.core 42 | jackson-databind 43 | 44 | 45 | 46 | 47 | io.scalecube 48 | scalecube-services-transport-rsocket 49 | test 50 | 51 | 52 | io.scalecube 53 | scalecube-services-discovery 54 | test 55 | 56 | 57 | io.scalecube 58 | scalecube-services-transport-jackson 59 | test 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /services-gateway-examples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scalecube-gateway-parent 5 | io.scalecube 6 | 2.10.18-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | scalecube-services-gateway-examples 11 | 12 | 13 | 14 | io.scalecube 15 | scalecube-services-gateway-netty 16 | ${project.version} 17 | 18 | 19 | io.scalecube 20 | scalecube-services-gateway-client-transport 21 | ${project.version} 22 | 23 | 24 | 25 | io.scalecube 26 | scalecube-services 27 | 28 | 29 | io.scalecube 30 | scalecube-services-discovery 31 | 32 | 33 | io.scalecube 34 | scalecube-services-transport-rsocket 35 | 36 | 37 | io.scalecube 38 | scalecube-services-transport-jackson 39 | 40 | 41 | 42 | it.unimi.dsi 43 | fastutil 44 | 8.1.1 45 | 46 | 47 | 48 | org.slf4j 49 | slf4j-api 50 | 51 | 52 | org.apache.logging.log4j 53 | log4j-slf4j-impl 54 | 55 | 56 | org.apache.logging.log4j 57 | log4j-core 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /services-gateway-netty/src/test/java/io/scalecube/services/gateway/ws/TestInputs.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.ws; 2 | 3 | public interface TestInputs { 4 | 5 | long SID = 42L; 6 | int I = 423; 7 | int SIG = 422; 8 | String Q = "/test/test"; 9 | 10 | String NO_DATA = 11 | "{" 12 | + " \"q\":\"" 13 | + Q 14 | + "\"," 15 | + " \"sid\":" 16 | + SID 17 | + "," 18 | + " \"sig\":" 19 | + SIG 20 | + "," 21 | + " \"i\":" 22 | + I 23 | + "}"; 24 | 25 | String STRING_DATA_PATTERN_Q_SIG_SID_D = 26 | "{" + "\"q\":\"%s\"," + "\"sig\":%d," + "\"sid\":%d," + "\"d\":%s" + "}"; 27 | 28 | String STRING_DATA_PATTERN_D_SIG_SID_Q = 29 | "{" + "\"d\": %s," + "\"sig\":%d," + "\"sid\": %d," + "\"q\":\"%s\"" + "}"; 30 | 31 | class Entity { 32 | private String text; 33 | private Integer number; 34 | private Boolean check; 35 | 36 | Entity() {} 37 | 38 | public Entity(String text, Integer number, Boolean check) { 39 | this.text = text; 40 | this.number = number; 41 | this.check = check; 42 | } 43 | 44 | public String text() { 45 | return text; 46 | } 47 | 48 | public Integer number() { 49 | return number; 50 | } 51 | 52 | public Boolean check() { 53 | return check; 54 | } 55 | 56 | @Override 57 | public boolean equals(Object o) { 58 | if (this == o) { 59 | return true; 60 | } 61 | if (o == null || getClass() != o.getClass()) { 62 | return false; 63 | } 64 | 65 | Entity entity = (Entity) o; 66 | 67 | if (text != null ? !text.equals(entity.text) : entity.text != null) { 68 | return false; 69 | } 70 | if (number != null ? !number.equals(entity.number) : entity.number != null) { 71 | return false; 72 | } 73 | return check != null ? check.equals(entity.check) : entity.check == null; 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | int result = text != null ? text.hashCode() : 0; 79 | result = 31 * result + (number != null ? number.hashCode() : 0); 80 | result = 31 * result + (check != null ? check.hashCode() : 0); 81 | return result; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/pre-release-ci.yml: -------------------------------------------------------------------------------- 1 | name: Pre-release CI 2 | 3 | on: 4 | release: 5 | types: [prereleased] 6 | 7 | jobs: 8 | build: 9 | name: Pre-release CI 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/cache@v1 14 | with: 15 | path: ~/.m2/repository 16 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 17 | restore-keys: | 18 | ${{ runner.os }}-maven- 19 | - name: Set up Java for publishing to GitHub Packages 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | server-id: github 24 | server-username: GITHUB_ACTOR 25 | server-password: GITHUB_TOKEN 26 | - name: Deploy pre-release version to GitHub Packages 27 | run: | 28 | sudo echo "127.0.0.1 $(eval hostname)" | sudo tee -a /etc/hosts 29 | pre_release_version=${{ github.event.release.tag_name }} 30 | echo Pre-release version $pre_release_version 31 | mvn versions:set -DnewVersion=$pre_release_version -DgenerateBackupPoms=false 32 | mvn versions:commit 33 | mvn clean deploy -Pdeploy2Github -B -V 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.ORGANIZATION_TOKEN }} 36 | - name: Set up Java for publishing to Maven Central Repository 37 | uses: actions/setup-java@v1 38 | with: 39 | java-version: 1.8 40 | server-id: ossrh 41 | server-username: MAVEN_USERNAME 42 | server-password: MAVEN_PASSWORD 43 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 44 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 45 | - name: Deploy pre-release version to the Maven Central Repository 46 | run: | 47 | pre_release_version=${{ github.event.release.tag_name }} 48 | echo Pre-release version $pre_release_version 49 | mvn versions:set -DnewVersion=$pre_release_version -DgenerateBackupPoms=false 50 | mvn versions:commit 51 | mvn deploy -Pdeploy2Maven -DskipTests -B -V 52 | env: 53 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 54 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 55 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 56 | - name: Rollback pre-release (remove tag) 57 | if: failure() 58 | run: git push origin :refs/tags/${{ github.event.release.tag_name }} 59 | -------------------------------------------------------------------------------- /services-gateway-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scalecube-gateway-parent 5 | io.scalecube 6 | 2.10.18-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | scalecube-services-gateway-tests 11 | 12 | 13 | 14 | io.scalecube 15 | scalecube-services-examples 16 | ${scalecube-services.version} 17 | 18 | 19 | io.scalecube 20 | scalecube-services-gateway-netty 21 | ${project.version} 22 | 23 | 24 | io.scalecube 25 | scalecube-services-gateway-client-transport 26 | ${project.version} 27 | 28 | 29 | 30 | io.scalecube 31 | scalecube-services 32 | 33 | 34 | io.scalecube 35 | scalecube-services-discovery 36 | 37 | 38 | io.scalecube 39 | scalecube-services-transport-rsocket 40 | 41 | 42 | io.scalecube 43 | scalecube-services-transport-jackson 44 | 45 | 46 | io.scalecube 47 | scalecube-transport-netty 48 | 49 | 50 | 51 | org.slf4j 52 | slf4j-api 53 | 54 | 55 | org.apache.logging.log4j 56 | log4j-slf4j-impl 57 | 58 | 59 | org.apache.logging.log4j 60 | log4j-core 61 | 62 | 63 | com.lmax 64 | disruptor 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scalecube-gateway 2 | 3 | ScaLecube API Gateway allows service consumers interact with scalecube microservices cluster. 4 | 5 | ![image](https://user-images.githubusercontent.com/1706296/58700290-4c569d80-83a8-11e9-8322-7e9d757dfad2.png) 6 | 7 | Read about it here: 8 | - [Api Gateway Pattern by Chris Richardson](https://microservices.io/patterns/apigateway.html) 9 | - [Api Gateway Pattern by Ronen Nachmias](https://www.linkedin.com/pulse/api-gateway-pattern-ronen-hamias/) 10 | 11 | ## API-Gateway: 12 | 13 | Available api-gateways are [rsocket](/services-gateway-rsocket), [http](/services-gateway-http) and [websocket](/services-gateway-websocket) 14 | 15 | Basic API-Gateway example: 16 | 17 | ```java 18 | 19 | Microservices.builder() 20 | .discovery(options -> options.seeds(seed.discovery().address())) 21 | .services(...) // OPTIONAL: services (if any) as part of this node. 22 | 23 | // configure list of gateways plugins exposing the apis 24 | .gateway(options -> new WebsocketGateway(options.id("ws").port(8080))) 25 | .gateway(options -> new HttpGateway(options.id("http").port(7070))) 26 | .gateway(options -> new RSocketGateway(options.id("rsws").port(9090))) 27 | 28 | .startAwait(); 29 | 30 | // HINT: you can try connect using the api sandbox to these ports to try the api. 31 | // http://scalecube.io/api-sandbox/app/index.html 32 | ``` 33 | 34 | **Service API-Gateway providers:** 35 | 36 | releases: https://github.com/scalecube/scalecube-services/releases 37 | 38 | * HTTP-Gateway - [scalecube-services-gateway-http](/services-gateway-http) 39 | * RSocket-Gateway - [scalecube-services-gateway-rsocket](/services-gateway-rsocket) 40 | * WebSocket - [scalecube-services-gateway-websocket](services-gateway-websocket) 41 | 42 | 43 | 46 | 47 | 48 | 49 | io.scalecube 50 | scalecube-services-transport-jackson 51 | ${scalecube.version} 52 | 53 | 54 | 55 | 56 | io.scalecube 57 | scalecube-services-transport-protostuff 58 | ${scalecube.version} 59 | 60 | 61 | 64 | 65 | io.scalecube 66 | scalecube-services-discovery 67 | ${scalecube.version} 68 | 69 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/GatewayTemplate.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import java.net.InetSocketAddress; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import reactor.core.publisher.Mono; 7 | import reactor.netty.DisposableServer; 8 | import reactor.netty.http.server.HttpServer; 9 | import reactor.netty.resources.LoopResources; 10 | 11 | public abstract class GatewayTemplate implements Gateway { 12 | 13 | private static final Logger LOGGER = LoggerFactory.getLogger(GatewayTemplate.class); 14 | 15 | protected final GatewayOptions options; 16 | 17 | protected GatewayTemplate(GatewayOptions options) { 18 | this.options = 19 | new GatewayOptions() 20 | .id(options.id()) 21 | .port(options.port()) 22 | .workerPool(options.workerPool()) 23 | .call(options.call()); 24 | } 25 | 26 | @Override 27 | public final String id() { 28 | return options.id(); 29 | } 30 | 31 | /** 32 | * Builds generic http server with given parameters. 33 | * 34 | * @param loopResources loop resources 35 | * @param port listen port 36 | * @return http server 37 | */ 38 | protected HttpServer prepareHttpServer(LoopResources loopResources, int port) { 39 | return HttpServer.create() 40 | .tcpConfiguration( 41 | tcpServer -> { 42 | if (loopResources != null) { 43 | tcpServer = tcpServer.runOn(loopResources); 44 | } 45 | return tcpServer.bindAddress(() -> new InetSocketAddress(port)); 46 | }); 47 | } 48 | 49 | /** 50 | * Shutting down loopResources if it's not null. 51 | * 52 | * @return mono handle 53 | */ 54 | protected final Mono shutdownLoopResources(LoopResources loopResources) { 55 | return Mono.defer( 56 | () -> { 57 | if (loopResources == null) { 58 | return Mono.empty(); 59 | } 60 | return loopResources 61 | .disposeLater() 62 | .doOnError(e -> LOGGER.warn("Failed to close loopResources: " + e)); 63 | }); 64 | } 65 | 66 | /** 67 | * Shutting down server of type {@link DisposableServer} if it's not null. 68 | * 69 | * @param server server 70 | * @return mono hanle 71 | */ 72 | protected final Mono shutdownServer(DisposableServer server) { 73 | return Mono.defer( 74 | () -> { 75 | if (server == null) { 76 | return Mono.empty(); 77 | } 78 | server.dispose(); 79 | return server.onDispose().doOnError(e -> LOGGER.warn("Failed to close server: " + e)); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/SecuredServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.scalecube.services.api.ServiceMessage; 4 | import io.scalecube.services.auth.Authenticator; 5 | import io.scalecube.services.exceptions.BadRequestException; 6 | import io.scalecube.services.exceptions.ForbiddenException; 7 | import java.util.Optional; 8 | import java.util.stream.IntStream; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | public class SecuredServiceImpl implements SecuredService { 15 | private static final Logger LOGGER = LoggerFactory.getLogger(SecuredServiceImpl.class); 16 | 17 | private static final String ALLOWED_USER = "VASYA_PUPKIN"; 18 | 19 | private final AuthRegistry authRegistry; 20 | 21 | public SecuredServiceImpl(AuthRegistry authRegistry) { 22 | this.authRegistry = authRegistry; 23 | } 24 | 25 | @Override 26 | public Mono createSession(ServiceMessage request) { 27 | String sessionId = request.header(AuthRegistry.SESSION_ID); 28 | if (sessionId == null) { 29 | return Mono.error(new BadRequestException("session Id is not present in request") {}); 30 | } 31 | String req = request.data(); 32 | Optional authResult = authRegistry.addAuth(Long.parseLong(sessionId), req); 33 | if (authResult.isPresent()) { 34 | return Mono.just(req); 35 | } else { 36 | return Mono.error(new ForbiddenException("User not allowed to use this service: " + req)); 37 | } 38 | } 39 | 40 | @Override 41 | public Mono requestOne(String req) { 42 | return Mono.deferContextual(context -> Mono.just(context.get(Authenticator.AUTH_CONTEXT_KEY))) 43 | .doOnNext(this::checkPermissions) 44 | .cast(String.class) 45 | .flatMap( 46 | auth -> { 47 | LOGGER.info("User {} has accessed secured call", auth); 48 | return Mono.just(auth + "@" + req); 49 | }); 50 | } 51 | 52 | @Override 53 | public Flux requestN(Integer times) { 54 | return Mono.deferContextual(context -> Mono.just(context.get(Authenticator.AUTH_CONTEXT_KEY))) 55 | .doOnNext(this::checkPermissions) 56 | .cast(String.class) 57 | .flatMapMany( 58 | auth -> { 59 | if (times <= 0) { 60 | return Flux.empty(); 61 | } 62 | return Flux.fromStream(IntStream.range(0, times).mapToObj(String::valueOf)); 63 | }); 64 | } 65 | 66 | private void checkPermissions(Object authData) { 67 | if (authData == null) { 68 | throw new ForbiddenException("Not allowed (authData is null)"); 69 | } 70 | if (!authData.equals(ALLOWED_USER)) { 71 | throw new ForbiddenException("Not allowed (wrong user)"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/rsocket/RSocketGatewayAcceptor.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.rsocket.ConnectionSetupPayload; 4 | import io.rsocket.RSocket; 5 | import io.rsocket.SocketAcceptor; 6 | import io.scalecube.services.ServiceCall; 7 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 8 | import io.scalecube.services.gateway.GatewaySessionHandler; 9 | import io.scalecube.services.gateway.ServiceMessageCodec; 10 | import io.scalecube.services.transport.api.HeadersCodec; 11 | import java.util.Map; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import reactor.core.publisher.Mono; 15 | import reactor.util.context.Context; 16 | 17 | public class RSocketGatewayAcceptor implements SocketAcceptor { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(RSocketGatewayAcceptor.class); 20 | 21 | private final ServiceCall serviceCall; 22 | private final GatewaySessionHandler sessionHandler; 23 | private final ServiceProviderErrorMapper errorMapper; 24 | 25 | /** 26 | * Creates new acceptor for RS gateway. 27 | * 28 | * @param serviceCall to call remote service 29 | * @param sessionHandler handler for session events 30 | * @param errorMapper error mapper 31 | */ 32 | public RSocketGatewayAcceptor( 33 | ServiceCall serviceCall, 34 | GatewaySessionHandler sessionHandler, 35 | ServiceProviderErrorMapper errorMapper) { 36 | this.serviceCall = serviceCall; 37 | this.sessionHandler = sessionHandler; 38 | this.errorMapper = errorMapper; 39 | } 40 | 41 | @Override 42 | public Mono accept(ConnectionSetupPayload setup, RSocket rsocket) { 43 | LOGGER.info("Accepted rsocket websocket: {}, connectionSetup: {}", rsocket, setup); 44 | 45 | // Prepare message codec together with headers from metainfo 46 | HeadersCodec headersCodec = HeadersCodec.getInstance(setup.metadataMimeType()); 47 | ServiceMessageCodec messageCodec = new ServiceMessageCodec(headersCodec); 48 | final RSocketGatewaySession gatewaySession = 49 | new RSocketGatewaySession( 50 | serviceCall, 51 | messageCodec, 52 | headers(messageCodec, setup), 53 | (session, req) -> sessionHandler.mapMessage(session, req, Context.empty()), 54 | errorMapper); 55 | sessionHandler.onSessionOpen(gatewaySession); 56 | rsocket 57 | .onClose() 58 | .doOnTerminate( 59 | () -> { 60 | LOGGER.info("Client disconnected: {}", rsocket); 61 | sessionHandler.onSessionClose(gatewaySession); 62 | }) 63 | .subscribe(null, th -> LOGGER.error("Exception on closing rsocket: {}", th.toString())); 64 | 65 | return Mono.just(gatewaySession); 66 | } 67 | 68 | private Map headers( 69 | ServiceMessageCodec messageCodec, ConnectionSetupPayload setup) { 70 | return messageCodec 71 | .decode(setup.sliceData().retain(), setup.sliceMetadata().retain()) 72 | .headers(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransports.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport; 2 | 3 | import io.scalecube.services.gateway.transport.http.HttpGatewayClient; 4 | import io.scalecube.services.gateway.transport.http.HttpGatewayClientCodec; 5 | import io.scalecube.services.gateway.transport.rsocket.RSocketGatewayClient; 6 | import io.scalecube.services.gateway.transport.rsocket.RSocketGatewayClientCodec; 7 | import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClient; 8 | import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClientCodec; 9 | import io.scalecube.services.transport.api.ClientTransport; 10 | import io.scalecube.services.transport.api.DataCodec; 11 | import io.scalecube.services.transport.api.HeadersCodec; 12 | import java.util.function.Function; 13 | 14 | public class GatewayClientTransports { 15 | 16 | private static final String CONTENT_TYPE = "application/json"; 17 | private static final HeadersCodec HEADERS_CODEC = HeadersCodec.getInstance(CONTENT_TYPE); 18 | 19 | public static final WebsocketGatewayClientCodec WEBSOCKET_CLIENT_CODEC = 20 | new WebsocketGatewayClientCodec(); 21 | 22 | public static final RSocketGatewayClientCodec RSOCKET_CLIENT_CODEC = 23 | new RSocketGatewayClientCodec(HEADERS_CODEC, DataCodec.getInstance(CONTENT_TYPE)); 24 | 25 | public static final HttpGatewayClientCodec HTTP_CLIENT_CODEC = 26 | new HttpGatewayClientCodec(DataCodec.getInstance(CONTENT_TYPE)); 27 | 28 | private GatewayClientTransports() { 29 | // utils 30 | } 31 | 32 | /** 33 | * ClientTransport that is capable of communicating with Gateway over rSocket. 34 | * 35 | * @param cs client settings for gateway client transport 36 | * @return client transport 37 | */ 38 | public static ClientTransport rsocketGatewayClientTransport(GatewayClientSettings cs) { 39 | final Function function = 40 | settings -> new RSocketGatewayClient(settings, RSOCKET_CLIENT_CODEC); 41 | return new GatewayClientTransport(function.apply(cs)); 42 | } 43 | 44 | /** 45 | * ClientTransport that is capable of communicating with Gateway over websocket. 46 | * 47 | * @param cs client settings for gateway client transport 48 | * @return client transport 49 | */ 50 | public static ClientTransport websocketGatewayClientTransport(GatewayClientSettings cs) { 51 | final Function function = 52 | settings -> new WebsocketGatewayClient(settings, WEBSOCKET_CLIENT_CODEC); 53 | return new GatewayClientTransport(function.apply(cs)); 54 | } 55 | 56 | /** 57 | * ClientTransport that is capable of communicating with Gateway over http. 58 | * 59 | * @param cs client settings for gateway client transport 60 | * @return client transport 61 | */ 62 | public static ClientTransport httpGatewayClientTransport(GatewayClientSettings cs) { 63 | final Function function = 64 | settings -> new HttpGatewayClient(settings, HTTP_CLIENT_CODEC); 65 | return new GatewayClientTransport(function.apply(cs)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/AbstractLocalGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.scalecube.net.Address; 4 | import io.scalecube.services.Microservices; 5 | import io.scalecube.services.ServiceCall; 6 | import io.scalecube.services.ServiceInfo; 7 | import io.scalecube.services.gateway.transport.GatewayClientSettings; 8 | import io.scalecube.services.gateway.transport.StaticAddressRouter; 9 | import io.scalecube.services.transport.api.ClientTransport; 10 | import java.util.Optional; 11 | import java.util.function.Function; 12 | import org.junit.jupiter.api.extension.AfterAllCallback; 13 | import org.junit.jupiter.api.extension.BeforeAllCallback; 14 | import org.junit.jupiter.api.extension.BeforeEachCallback; 15 | import org.junit.jupiter.api.extension.ExtensionContext; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import reactor.netty.resources.LoopResources; 19 | 20 | public abstract class AbstractLocalGatewayExtension 21 | implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLocalGatewayExtension.class); 24 | 25 | private final ServiceInfo serviceInfo; 26 | private final Function gatewaySupplier; 27 | private final Function clientSupplier; 28 | 29 | private Microservices gateway; 30 | private LoopResources clientLoopResources; 31 | private ServiceCall clientServiceCall; 32 | private String gatewayId; 33 | 34 | protected AbstractLocalGatewayExtension( 35 | ServiceInfo serviceInfo, 36 | Function gatewaySupplier, 37 | Function clientSupplier) { 38 | this.serviceInfo = serviceInfo; 39 | this.gatewaySupplier = gatewaySupplier; 40 | this.clientSupplier = clientSupplier; 41 | } 42 | 43 | @Override 44 | public final void beforeAll(ExtensionContext context) { 45 | 46 | gateway = 47 | Microservices.builder() 48 | .services(serviceInfo) 49 | .gateway( 50 | options -> { 51 | Gateway gateway = gatewaySupplier.apply(options); 52 | gatewayId = gateway.id(); 53 | return gateway; 54 | }) 55 | .startAwait(); 56 | 57 | clientLoopResources = LoopResources.create("gateway-client-transport-worker"); 58 | } 59 | 60 | @Override 61 | public final void beforeEach(ExtensionContext context) { 62 | Address address = gateway.gateway(gatewayId).address(); 63 | 64 | GatewayClientSettings settings = GatewayClientSettings.builder().address(address).build(); 65 | 66 | clientServiceCall = 67 | new ServiceCall() 68 | .transport(clientSupplier.apply(settings)) 69 | .router(new StaticAddressRouter(address)); 70 | } 71 | 72 | @Override 73 | public final void afterAll(ExtensionContext context) { 74 | Optional.ofNullable(clientLoopResources).ifPresent(LoopResources::dispose); 75 | shutdownGateway(); 76 | } 77 | 78 | public ServiceCall client() { 79 | return clientServiceCall; 80 | } 81 | 82 | private void shutdownGateway() { 83 | if (gateway != null) { 84 | try { 85 | gateway.shutdown().block(); 86 | } catch (Throwable ignore) { 87 | // ignore 88 | } 89 | LOGGER.info("Shutdown gateway {}", gateway); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/GatewaySessionHandler.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.scalecube.services.api.ServiceMessage; 5 | import java.util.Map; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import reactor.core.publisher.Mono; 9 | import reactor.util.context.Context; 10 | 11 | public interface GatewaySessionHandler { 12 | 13 | Logger LOGGER = LoggerFactory.getLogger(GatewaySessionHandler.class); 14 | 15 | GatewaySessionHandler DEFAULT_INSTANCE = new GatewaySessionHandler() {}; 16 | 17 | /** 18 | * Message mapper function. 19 | * 20 | * @param session webscoket session (not null) 21 | * @param message request message (not null) 22 | * @return message 23 | */ 24 | default ServiceMessage mapMessage( 25 | GatewaySession session, ServiceMessage message, Context context) { 26 | return message; 27 | } 28 | 29 | /** 30 | * Request mapper function. 31 | * 32 | * @param session session 33 | * @param byteBuf request buffer 34 | * @param context subscriber context 35 | * @return context 36 | */ 37 | default Context onRequest(GatewaySession session, ByteBuf byteBuf, Context context) { 38 | return context; 39 | } 40 | 41 | /** 42 | * On response handler. 43 | * 44 | * @param session session 45 | * @param byteBuf response buffer 46 | * @param message response message 47 | * @param context subscriber context 48 | */ 49 | default void onResponse( 50 | GatewaySession session, ByteBuf byteBuf, ServiceMessage message, Context context) { 51 | // no-op 52 | } 53 | 54 | /** 55 | * Error handler function. 56 | * 57 | * @param session webscoket session (not null) 58 | * @param throwable an exception that occurred (not null) 59 | * @param context subscriber context 60 | */ 61 | default void onError(GatewaySession session, Throwable throwable, Context context) { 62 | LOGGER.error( 63 | "Exception occurred on session: {}, on context: {}, cause:", 64 | session.sessionId(), 65 | context, 66 | throwable); 67 | } 68 | 69 | /** 70 | * Error handler function. 71 | * 72 | * @param session webscoket session (not null) 73 | * @param throwable an exception that occurred (not null) 74 | */ 75 | default void onSessionError(GatewaySession session, Throwable throwable) { 76 | LOGGER.error("Exception occurred on session: {}, cause:", session.sessionId(), throwable); 77 | } 78 | 79 | /** 80 | * On connection open handler. 81 | * 82 | * @param sessionId session id 83 | * @param headers connection/session headers 84 | * @return mono result 85 | */ 86 | default Mono onConnectionOpen(long sessionId, Map headers) { 87 | return Mono.fromRunnable( 88 | () -> 89 | LOGGER.debug( 90 | "Connection opened, sessionId: {}, headers({})", sessionId, headers.size())); 91 | } 92 | 93 | /** 94 | * On session open handler. 95 | * 96 | * @param session websocket session (not null) 97 | */ 98 | default void onSessionOpen(GatewaySession session) { 99 | LOGGER.info("Session opened: {}", session); 100 | } 101 | 102 | /** 103 | * On session close handler. 104 | * 105 | * @param session websocket session (not null) 106 | */ 107 | default void onSessionClose(GatewaySession session) { 108 | LOGGER.info("Session closed: {}", session); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/release-ci.yml: -------------------------------------------------------------------------------- 1 | name: Release CI 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | build: 9 | name: Release CI 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - run: git checkout ${{ github.event.release.target_commitish }} 16 | - uses: actions/cache@v1 17 | with: 18 | path: ~/.m2/repository 19 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 20 | restore-keys: | 21 | ${{ runner.os }}-maven- 22 | - name: Set up Java for publishing to GitHub Packages 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 1.8 26 | server-id: github 27 | server-username: GITHUB_ACTOR 28 | server-password: GITHUB_TOKEN 29 | - name: Maven Build 30 | run: mvn clean install -DskipTests=true -Ddockerfile.skip=true -B -V 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.ORGANIZATION_TOKEN }} 33 | - name: Maven Verify 34 | run: | 35 | sudo echo "127.0.0.1 $(eval hostname)" | sudo tee -a /etc/hosts 36 | mvn verify -B 37 | - name: Configure git 38 | run: | 39 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 40 | git config --global user.name "${GITHUB_ACTOR}" 41 | - name: Prepare release 42 | id: prepare_release 43 | run: | 44 | mvn -B build-helper:parse-version release:prepare \ 45 | -DreleaseVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.incrementalVersion} \ 46 | -Darguments="-DskipTests=true -Ddockerfile.skip=true" 47 | echo ::set-output name=release_tag::$(git describe --tags --abbrev=0) 48 | - name: Perform release 49 | run: mvn -B release:perform -Pdeploy2Github -Darguments="-DskipTests=true -Ddockerfile.skip=true -Pdeploy2Github" 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} 53 | - name: Set up Java for publishing to Maven Central Repository 54 | uses: actions/setup-java@v1 55 | with: 56 | java-version: 1.8 57 | server-id: ossrh 58 | server-username: MAVEN_USERNAME 59 | server-password: MAVEN_PASSWORD 60 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 61 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 62 | - name: Deploy release version to the Maven Central Repository 63 | run: | 64 | release_version=$(echo ${{ steps.prepare_release.outputs.release_tag }} | sed "s/release-//") 65 | echo release version $release_version 66 | mvn versions:set -DnewVersion=$release_version -DgenerateBackupPoms=false 67 | mvn versions:commit 68 | mvn deploy -Pdeploy2Maven -DskipTests -B -V 69 | env: 70 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 71 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 72 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 73 | - name: Rollback release 74 | if: failure() 75 | run: | 76 | mvn release:rollback || echo "nothing to rollback" 77 | git push origin :refs/tags/${{ github.event.release.tag_name }} 78 | if [ ! -z "${{ steps.prepare_release.outputs.release_tag }}" ] 79 | then 80 | git tag -d ${{ steps.prepare_release.outputs.release_tag }} 81 | git push origin :refs/tags/${{ steps.prepare_release.outputs.release_tag }} 82 | fi 83 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/ServiceMessageCodec.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.ByteBufAllocator; 5 | import io.netty.buffer.ByteBufInputStream; 6 | import io.netty.buffer.ByteBufOutputStream; 7 | import io.netty.buffer.Unpooled; 8 | import io.scalecube.services.api.ServiceMessage; 9 | import io.scalecube.services.exceptions.MessageCodecException; 10 | import io.scalecube.services.transport.api.DataCodec; 11 | import io.scalecube.services.transport.api.HeadersCodec; 12 | import java.util.function.BiFunction; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | public final class ServiceMessageCodec { 17 | 18 | private static final Logger LOGGER = LoggerFactory.getLogger(ServiceMessageCodec.class); 19 | 20 | private final HeadersCodec headersCodec; 21 | 22 | public ServiceMessageCodec(HeadersCodec headersCodec) { 23 | this.headersCodec = headersCodec; 24 | } 25 | 26 | /** 27 | * Encode a message, transform it to T. 28 | * 29 | * @param message the message to transform 30 | * @param transformer a function that accepts data and header {@link ByteBuf} and return the 31 | * required T 32 | * @return the object (transformed message) 33 | * @throws MessageCodecException when encoding cannot be done. 34 | */ 35 | public T encodeAndTransform( 36 | ServiceMessage message, BiFunction transformer) 37 | throws MessageCodecException { 38 | ByteBuf dataBuffer = Unpooled.EMPTY_BUFFER; 39 | ByteBuf headersBuffer = Unpooled.EMPTY_BUFFER; 40 | 41 | if (message.hasData(ByteBuf.class)) { 42 | dataBuffer = message.data(); 43 | } else if (message.hasData()) { 44 | dataBuffer = ByteBufAllocator.DEFAULT.buffer(); 45 | try { 46 | DataCodec dataCodec = DataCodec.getInstance(message.dataFormatOrDefault()); 47 | dataCodec.encode(new ByteBufOutputStream(dataBuffer), message.data()); 48 | } catch (Throwable ex) { 49 | ReferenceCountUtil.safestRelease(dataBuffer); 50 | LOGGER.error("Failed to encode data on: {}, cause: {}", message, ex); 51 | throw new MessageCodecException( 52 | "Failed to encode data on message q=" + message.qualifier(), ex); 53 | } 54 | } 55 | 56 | if (!message.headers().isEmpty()) { 57 | headersBuffer = ByteBufAllocator.DEFAULT.buffer(); 58 | try { 59 | headersCodec.encode(new ByteBufOutputStream(headersBuffer), message.headers()); 60 | } catch (Throwable ex) { 61 | ReferenceCountUtil.safestRelease(headersBuffer); 62 | ReferenceCountUtil.safestRelease(dataBuffer); // release data buf as well 63 | LOGGER.error("Failed to encode headers on: {}, cause: {}", message, ex); 64 | throw new MessageCodecException( 65 | "Failed to encode headers on message q=" + message.qualifier(), ex); 66 | } 67 | } 68 | 69 | return transformer.apply(dataBuffer, headersBuffer); 70 | } 71 | 72 | /** 73 | * Decode buffers. 74 | * 75 | * @param dataBuffer the buffer of the data (payload) 76 | * @param headersBuffer the buffer of the headers 77 | * @return a new Service message with {@link ByteBuf} data and with parsed headers. 78 | * @throws MessageCodecException when decode fails 79 | */ 80 | public ServiceMessage decode(ByteBuf dataBuffer, ByteBuf headersBuffer) 81 | throws MessageCodecException { 82 | ServiceMessage.Builder builder = ServiceMessage.builder(); 83 | 84 | if (dataBuffer.isReadable()) { 85 | builder.data(dataBuffer); 86 | } 87 | if (headersBuffer.isReadable()) { 88 | try (ByteBufInputStream stream = new ByteBufInputStream(headersBuffer, true)) { 89 | builder.headers(headersCodec.decode(stream)); 90 | } catch (Throwable ex) { 91 | ReferenceCountUtil.safestRelease(dataBuffer); // release data buf as well 92 | throw new MessageCodecException("Failed to decode message headers", ex); 93 | } 94 | } 95 | 96 | return builder.build(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/rsocket/RSocketGateway.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.rsocket.core.RSocketServer; 4 | import io.rsocket.frame.decoder.PayloadDecoder; 5 | import io.rsocket.transport.netty.server.CloseableChannel; 6 | import io.rsocket.transport.netty.server.WebsocketServerTransport; 7 | import io.scalecube.net.Address; 8 | import io.scalecube.services.exceptions.DefaultErrorMapper; 9 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 10 | import io.scalecube.services.gateway.Gateway; 11 | import io.scalecube.services.gateway.GatewayOptions; 12 | import io.scalecube.services.gateway.GatewaySessionHandler; 13 | import io.scalecube.services.gateway.GatewayTemplate; 14 | import java.net.InetSocketAddress; 15 | import java.util.StringJoiner; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import reactor.core.publisher.Flux; 19 | import reactor.core.publisher.Mono; 20 | import reactor.netty.resources.LoopResources; 21 | 22 | public class RSocketGateway extends GatewayTemplate { 23 | 24 | private static final Logger LOGGER = LoggerFactory.getLogger(RSocketGateway.class); 25 | 26 | private final GatewaySessionHandler sessionHandler; 27 | private final ServiceProviderErrorMapper errorMapper; 28 | 29 | private CloseableChannel server; 30 | private LoopResources loopResources; 31 | 32 | public RSocketGateway(GatewayOptions options) { 33 | this(options, GatewaySessionHandler.DEFAULT_INSTANCE, DefaultErrorMapper.INSTANCE); 34 | } 35 | 36 | public RSocketGateway(GatewayOptions options, GatewaySessionHandler sessionHandler) { 37 | this(options, sessionHandler, DefaultErrorMapper.INSTANCE); 38 | } 39 | 40 | public RSocketGateway(GatewayOptions options, ServiceProviderErrorMapper errorMapper) { 41 | this(options, GatewaySessionHandler.DEFAULT_INSTANCE, errorMapper); 42 | } 43 | 44 | /** 45 | * Constructor. 46 | * 47 | * @param options gateway options 48 | * @param sessionHandler session handler 49 | * @param errorMapper error mapper 50 | */ 51 | public RSocketGateway( 52 | GatewayOptions options, 53 | GatewaySessionHandler sessionHandler, 54 | ServiceProviderErrorMapper errorMapper) { 55 | super(options); 56 | this.sessionHandler = sessionHandler; 57 | this.errorMapper = errorMapper; 58 | } 59 | 60 | @Override 61 | public Mono start() { 62 | return Mono.defer( 63 | () -> { 64 | RSocketGatewayAcceptor acceptor = 65 | new RSocketGatewayAcceptor(options.call(), sessionHandler, errorMapper); 66 | 67 | loopResources = LoopResources.create("rsocket-gateway"); 68 | 69 | WebsocketServerTransport rsocketTransport = 70 | WebsocketServerTransport.create(prepareHttpServer(loopResources, options.port())); 71 | 72 | return RSocketServer.create() 73 | .acceptor(acceptor) 74 | .payloadDecoder(PayloadDecoder.DEFAULT) 75 | .bind(rsocketTransport) 76 | .doOnSuccess(server -> this.server = server) 77 | .thenReturn(this); 78 | }); 79 | } 80 | 81 | @Override 82 | public Address address() { 83 | InetSocketAddress address = server.address(); 84 | return Address.create(address.getHostString(), address.getPort()); 85 | } 86 | 87 | @Override 88 | public Mono stop() { 89 | return Flux.concatDelayError(shutdownServer(), shutdownLoopResources(loopResources)).then(); 90 | } 91 | 92 | private Mono shutdownServer() { 93 | return Mono.defer( 94 | () -> { 95 | if (server == null) { 96 | return Mono.empty(); 97 | } 98 | server.dispose(); 99 | return server.onClose().doOnError(e -> LOGGER.warn("Failed to close server: " + e)); 100 | }); 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | return new StringJoiner(", ", RSocketGateway.class.getSimpleName() + "[", "]") 106 | .add("server=" + server) 107 | .add("loopResources=" + loopResources) 108 | .add("options=" + options) 109 | .toString(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketLocalGatewayAuthTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import io.scalecube.services.api.ServiceMessage; 7 | import io.scalecube.services.exceptions.ForbiddenException; 8 | import io.scalecube.services.exceptions.UnauthorizedException; 9 | import io.scalecube.services.gateway.AuthRegistry; 10 | import io.scalecube.services.gateway.SecuredService; 11 | import io.scalecube.services.gateway.SecuredServiceImpl; 12 | import java.time.Duration; 13 | import java.util.Collections; 14 | import java.util.HashSet; 15 | import java.util.Set; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Disabled; 18 | import org.junit.jupiter.api.Test; 19 | import org.junit.jupiter.api.extension.RegisterExtension; 20 | import reactor.test.StepVerifier; 21 | 22 | public class RSocketLocalGatewayAuthTest { 23 | 24 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 25 | 26 | private static final String ALLOWED_USER = "VASYA_PUPKIN"; 27 | private static final Set ALLOWED_USERS = 28 | new HashSet<>(Collections.singletonList(ALLOWED_USER)); 29 | 30 | private static final AuthRegistry AUTH_REG = new AuthRegistry(ALLOWED_USERS); 31 | 32 | @RegisterExtension 33 | static RSocketLocalWithAuthExtension extension = 34 | new RSocketLocalWithAuthExtension(new SecuredServiceImpl(AUTH_REG), AUTH_REG); 35 | 36 | private SecuredService clientService; 37 | 38 | private static ServiceMessage createSessionReq(String username) { 39 | return ServiceMessage.builder() 40 | .qualifier("/" + SecuredService.NS + "/createSession") 41 | .data(username) 42 | .build(); 43 | } 44 | 45 | @BeforeEach 46 | void initService() { 47 | clientService = extension.client().api(SecuredService.class); 48 | } 49 | 50 | @Test 51 | void testCreateSession_succ() { 52 | StepVerifier.create(extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class)) 53 | .expectNextCount(1) 54 | .expectComplete() 55 | .verify(); 56 | } 57 | 58 | @Test 59 | void testCreateSession_forbiddenUser() { 60 | StepVerifier.create( 61 | extension.client().requestOne(createSessionReq("fake" + ALLOWED_USER), String.class)) 62 | .expectErrorSatisfies( 63 | th -> { 64 | ForbiddenException e = (ForbiddenException) th; 65 | assertEquals(403, e.errorCode(), "error code"); 66 | assertTrue(e.getMessage().contains("User not allowed to use this service")); 67 | }) 68 | .verify(); 69 | } 70 | 71 | @Test 72 | void testCallSecuredMethod_notAuthenticated() { 73 | StepVerifier.create(clientService.requestOne("echo")) 74 | .expectErrorSatisfies( 75 | th -> { 76 | UnauthorizedException e = (UnauthorizedException) th; 77 | assertEquals(401, e.errorCode(), "Authentication failed"); 78 | assertTrue(e.getMessage().contains("Authentication failed")); 79 | }) 80 | .verify(); 81 | } 82 | 83 | @Disabled("https://github.com/scalecube/scalecube-gateway/issues/121") 84 | void testCallSecuredMethod_authenticated() { 85 | // authenticate session 86 | extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class).block(TIMEOUT); 87 | // call secured service 88 | final String req = "echo"; 89 | StepVerifier.create(clientService.requestOne(req)) 90 | .expectNextMatches(resp -> resp.equals(ALLOWED_USER + "@" + req)) 91 | .expectComplete() 92 | .verify(); 93 | } 94 | 95 | @Test 96 | void testCallSecuredMethod_authenticatedInvalidUser() { 97 | // authenticate session 98 | StepVerifier.create( 99 | extension.client().requestOne(createSessionReq("fake" + ALLOWED_USER), String.class)) 100 | .expectErrorSatisfies(th -> assertTrue(th instanceof ForbiddenException)) 101 | .verify(); 102 | // call secured service 103 | final String req = "echo"; 104 | StepVerifier.create(clientService.requestOne(req)) 105 | .expectErrorSatisfies( 106 | th -> { 107 | UnauthorizedException e = (UnauthorizedException) th; 108 | assertEquals(401, e.errorCode()); 109 | assertTrue(e.getMessage().contains("Authentication failed")); 110 | }) 111 | .verify(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/rsocket/RSocketGatewaySession.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.rsocket.Payload; 4 | import io.rsocket.RSocket; 5 | import io.rsocket.util.ByteBufPayload; 6 | import io.scalecube.services.ServiceCall; 7 | import io.scalecube.services.api.ServiceMessage; 8 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 9 | import io.scalecube.services.gateway.GatewaySession; 10 | import io.scalecube.services.gateway.ReferenceCountUtil; 11 | import io.scalecube.services.gateway.ServiceMessageCodec; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.concurrent.atomic.AtomicLong; 16 | import java.util.function.BiFunction; 17 | import reactor.core.publisher.Flux; 18 | import reactor.core.publisher.Mono; 19 | 20 | /** 21 | * Extension class for rsocket. Holds gateway business logic in following methods: {@link 22 | * #fireAndForget(Payload)}, {@link #requestResponse(Payload)}, {@link #requestStream(Payload)} and 23 | * {@link #requestChannel(org.reactivestreams.Publisher)}. 24 | */ 25 | public final class RSocketGatewaySession implements RSocket, GatewaySession { 26 | 27 | private static final AtomicLong SESSION_ID_GENERATOR = new AtomicLong(System.currentTimeMillis()); 28 | 29 | private final ServiceCall serviceCall; 30 | private final ServiceMessageCodec messageCodec; 31 | private final long sessionId; 32 | private final BiFunction messageMapper; 33 | private final Map headers; 34 | private final ServiceProviderErrorMapper errorMapper; 35 | 36 | /** 37 | * Constructor for gateway rsocket. 38 | * 39 | * @param serviceCall service call coming from microservices. 40 | * @param messageCodec message messageCodec. 41 | * @param errorMapper error mapper 42 | */ 43 | public RSocketGatewaySession( 44 | ServiceCall serviceCall, 45 | ServiceMessageCodec messageCodec, 46 | Map headers, 47 | BiFunction messageMapper, 48 | ServiceProviderErrorMapper errorMapper) { 49 | this.serviceCall = serviceCall; 50 | this.messageCodec = messageCodec; 51 | this.messageMapper = messageMapper; 52 | this.sessionId = SESSION_ID_GENERATOR.incrementAndGet(); 53 | this.headers = Collections.unmodifiableMap(new HashMap<>(headers)); 54 | this.errorMapper = errorMapper; 55 | } 56 | 57 | @Override 58 | public long sessionId() { 59 | return this.sessionId; 60 | } 61 | 62 | @Override 63 | public Map headers() { 64 | return headers; 65 | } 66 | 67 | @Override 68 | public Mono fireAndForget(Payload payload) { 69 | return Mono.defer(() -> serviceCall.oneWay(toMessage(payload))); 70 | } 71 | 72 | @Override 73 | public Mono requestResponse(Payload payload) { 74 | return Mono.defer( 75 | () -> { 76 | ServiceMessage request = toMessage(payload); 77 | return serviceCall 78 | .requestOne(request) 79 | .doOnError(th -> releaseRequestOnError(request)) 80 | .onErrorResume(th -> Mono.just(errorMapper.toMessage(request.qualifier(), th))) 81 | .map(this::toPayload); 82 | }); 83 | } 84 | 85 | @Override 86 | public Flux requestStream(Payload payload) { 87 | return Flux.defer( 88 | () -> { 89 | ServiceMessage request = toMessage(payload); 90 | return serviceCall 91 | .requestMany(request) 92 | .doOnError(th -> releaseRequestOnError(request)) 93 | .onErrorResume(th -> Mono.just(errorMapper.toMessage(request.qualifier(), th))) 94 | .map(this::toPayload); 95 | }); 96 | } 97 | 98 | private ServiceMessage toMessage(Payload payload) { 99 | try { 100 | final ServiceMessage serviceMessage = 101 | messageCodec.decode(payload.sliceData().retain(), payload.sliceMetadata().retain()); 102 | return messageMapper.apply(this, serviceMessage); 103 | } finally { 104 | payload.release(); 105 | } 106 | } 107 | 108 | private Payload toPayload(ServiceMessage message) { 109 | return messageCodec.encodeAndTransform(message, ByteBufPayload::create); 110 | } 111 | 112 | private void releaseRequestOnError(ServiceMessage request) { 113 | ReferenceCountUtil.safestRelease(request.data()); 114 | } 115 | 116 | @Override 117 | public String toString() { 118 | return "RSocketGatewaySession[" + sessionId + ']'; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import io.scalecube.services.api.Qualifier; 6 | import io.scalecube.services.api.ServiceMessage; 7 | import io.scalecube.services.examples.EmptyGreetingRequest; 8 | import io.scalecube.services.examples.EmptyGreetingResponse; 9 | import io.scalecube.services.examples.GreetingRequest; 10 | import io.scalecube.services.examples.GreetingService; 11 | import io.scalecube.services.examples.GreetingServiceImpl; 12 | import io.scalecube.services.exceptions.InternalServiceException; 13 | import io.scalecube.services.gateway.BaseTest; 14 | import java.time.Duration; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.extension.RegisterExtension; 18 | import reactor.test.StepVerifier; 19 | 20 | class HttpLocalGatewayTest extends BaseTest { 21 | 22 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 23 | 24 | @RegisterExtension 25 | static HttpLocalGatewayExtension extension = 26 | new HttpLocalGatewayExtension(new GreetingServiceImpl()); 27 | 28 | private GreetingService service; 29 | 30 | @BeforeEach 31 | void initService() { 32 | service = extension.client().api(GreetingService.class); 33 | } 34 | 35 | @Test 36 | void shouldReturnSingleResponseWithSimpleRequest() { 37 | StepVerifier.create(service.one("hello")) 38 | .expectNext("Echo:hello") 39 | .expectComplete() 40 | .verify(TIMEOUT); 41 | } 42 | 43 | @Test 44 | void shouldReturnSingleResponseWithSimpleLongDataRequest() { 45 | String data = new String(new char[500]); 46 | StepVerifier.create(service.one(data)) 47 | .expectNext("Echo:" + data) 48 | .expectComplete() 49 | .verify(TIMEOUT); 50 | } 51 | 52 | @Test 53 | void shouldReturnSingleResponseWithPojoRequest() { 54 | StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) 55 | .expectNextMatches(response -> "Echo:hello".equals(response.getText())) 56 | .expectComplete() 57 | .verify(TIMEOUT); 58 | } 59 | 60 | @Test 61 | void shouldReturnListResponseWithPojoRequest() { 62 | StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) 63 | .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) 64 | .expectComplete() 65 | .verify(TIMEOUT); 66 | } 67 | 68 | @Test 69 | void shouldReturnNoContentWhenResponseIsEmpty() { 70 | StepVerifier.create(service.emptyOne("hello")).expectComplete().verify(TIMEOUT); 71 | } 72 | 73 | @Test 74 | void shouldReturnInternalServerErrorWhenServiceFails() { 75 | StepVerifier.create(service.failingOne("hello")) 76 | .expectErrorSatisfies( 77 | throwable -> { 78 | assertEquals(InternalServiceException.class, throwable.getClass()); 79 | assertEquals("hello", throwable.getMessage()); 80 | }) 81 | .verify(TIMEOUT); 82 | } 83 | 84 | @Test 85 | void shouldSuccessfullyReuseServiceProxy() { 86 | StepVerifier.create(service.one("hello")) 87 | .expectNext("Echo:hello") 88 | .expectComplete() 89 | .verify(TIMEOUT); 90 | 91 | StepVerifier.create(service.one("hello")) 92 | .expectNext("Echo:hello") 93 | .expectComplete() 94 | .verify(TIMEOUT); 95 | } 96 | 97 | @Test 98 | void shouldReturnNoEventOnNeverService() { 99 | StepVerifier.create(service.neverOne("hi")) 100 | .expectSubscription() 101 | .expectNoEvent(Duration.ofSeconds(1)) 102 | .thenCancel() 103 | .verify(); 104 | } 105 | 106 | @Test 107 | void shouldReturnOnEmptyGreeting() { 108 | StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) 109 | .expectSubscription() 110 | .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) 111 | .thenCancel() 112 | .verify(); 113 | } 114 | 115 | @Test 116 | void shouldReturnOnEmptyMessageGreeting() { 117 | String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); 118 | ServiceMessage request = 119 | ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); 120 | StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) 121 | .expectSubscription() 122 | .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) 123 | .thenCancel() 124 | .verify(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/rsocket/RSocketGatewayClientCodec.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport.rsocket; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.ByteBufAllocator; 5 | import io.netty.buffer.ByteBufInputStream; 6 | import io.netty.buffer.ByteBufOutputStream; 7 | import io.netty.buffer.Unpooled; 8 | import io.rsocket.Payload; 9 | import io.rsocket.util.ByteBufPayload; 10 | import io.scalecube.services.api.ServiceMessage; 11 | import io.scalecube.services.exceptions.MessageCodecException; 12 | import io.scalecube.services.gateway.transport.GatewayClientCodec; 13 | import io.scalecube.services.transport.api.DataCodec; 14 | import io.scalecube.services.transport.api.HeadersCodec; 15 | import io.scalecube.services.transport.api.ReferenceCountUtil; 16 | import java.util.function.BiFunction; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | public final class RSocketGatewayClientCodec implements GatewayClientCodec { 21 | 22 | private static final Logger LOGGER = LoggerFactory.getLogger(RSocketGatewayClientCodec.class); 23 | 24 | private final HeadersCodec headersCodec; 25 | private final DataCodec dataCodec; 26 | 27 | /** 28 | * Constructor for codec which encode/decode client message to/from rsocket payload. 29 | * 30 | * @param headersCodec headers message codec. 31 | * @param dataCodec data message codec. 32 | */ 33 | public RSocketGatewayClientCodec(HeadersCodec headersCodec, DataCodec dataCodec) { 34 | this.headersCodec = headersCodec; 35 | this.dataCodec = dataCodec; 36 | } 37 | 38 | @Override 39 | public Payload encode(ServiceMessage message) { 40 | return encodeAndTransform(message, ByteBufPayload::create); 41 | } 42 | 43 | @Override 44 | public ServiceMessage decode(Payload encodedMessage) { 45 | return decode(encodedMessage.sliceData(), encodedMessage.sliceMetadata()); 46 | } 47 | 48 | /** 49 | * Decoder function. Keeps data buffer untouched. 50 | * 51 | * @param dataBuffer data buffer. 52 | * @param headersBuffer headers buffer. 53 | * @return client message object. 54 | * @throws MessageCodecException in case if decode fails. 55 | */ 56 | private ServiceMessage decode(ByteBuf dataBuffer, ByteBuf headersBuffer) 57 | throws MessageCodecException { 58 | ServiceMessage.Builder builder = ServiceMessage.builder(); 59 | 60 | if (dataBuffer.isReadable()) { 61 | builder.data(dataBuffer); 62 | } 63 | 64 | if (headersBuffer.isReadable()) { 65 | try (ByteBufInputStream stream = new ByteBufInputStream(headersBuffer, true)) { 66 | builder.headers(headersCodec.decode(stream)); 67 | } catch (Throwable ex) { 68 | ReferenceCountUtil.safestRelease(dataBuffer); // release data as well 69 | throw new MessageCodecException("Failed to decode message headers", ex); 70 | } 71 | } 72 | 73 | return builder.build(); 74 | } 75 | 76 | /** 77 | * Encoder function. 78 | * 79 | * @param message client message. 80 | * @param transformer bi function transformer from two headers and data buffers to client 81 | * specified object of type T 82 | * @param client specified type which could be constructed out of headers and data bufs. 83 | * @return T object 84 | * @throws MessageCodecException in case if encoding fails 85 | */ 86 | private T encodeAndTransform( 87 | ServiceMessage message, BiFunction transformer) 88 | throws MessageCodecException { 89 | ByteBuf dataBuffer = Unpooled.EMPTY_BUFFER; 90 | ByteBuf headersBuffer = Unpooled.EMPTY_BUFFER; 91 | 92 | if (message.hasData(ByteBuf.class)) { 93 | dataBuffer = message.data(); 94 | } else if (message.hasData()) { 95 | dataBuffer = ByteBufAllocator.DEFAULT.buffer(); 96 | try { 97 | dataCodec.encode(new ByteBufOutputStream(dataBuffer), message.data()); 98 | } catch (Throwable ex) { 99 | ReferenceCountUtil.safestRelease(dataBuffer); 100 | LOGGER.error("Failed to encode data on: {}, cause: {}", message, ex); 101 | throw new MessageCodecException( 102 | "Failed to encode data on message q=" + message.qualifier(), ex); 103 | } 104 | } 105 | 106 | if (!message.headers().isEmpty()) { 107 | headersBuffer = ByteBufAllocator.DEFAULT.buffer(); 108 | try { 109 | headersCodec.encode(new ByteBufOutputStream(headersBuffer), message.headers()); 110 | } catch (Throwable ex) { 111 | ReferenceCountUtil.safestRelease(headersBuffer); 112 | ReferenceCountUtil.safestRelease(dataBuffer); // release data as well 113 | LOGGER.error("Failed to encode headers on: {}, cause: {}", message, ex); 114 | throw new MessageCodecException( 115 | "Failed to encode headers on message q=" + message.qualifier(), ex); 116 | } 117 | } 118 | 119 | return transformer.apply(dataBuffer, headersBuffer); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/HttpGatewayTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import static org.hamcrest.CoreMatchers.startsWith; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | import io.scalecube.services.api.Qualifier; 8 | import io.scalecube.services.api.ServiceMessage; 9 | import io.scalecube.services.examples.EmptyGreetingRequest; 10 | import io.scalecube.services.examples.EmptyGreetingResponse; 11 | import io.scalecube.services.examples.GreetingRequest; 12 | import io.scalecube.services.examples.GreetingService; 13 | import io.scalecube.services.examples.GreetingServiceImpl; 14 | import io.scalecube.services.exceptions.InternalServiceException; 15 | import io.scalecube.services.exceptions.ServiceUnavailableException; 16 | import io.scalecube.services.gateway.BaseTest; 17 | import java.time.Duration; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.RegisterExtension; 21 | import reactor.test.StepVerifier; 22 | 23 | class HttpGatewayTest extends BaseTest { 24 | 25 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 26 | 27 | @RegisterExtension 28 | static HttpGatewayExtension extension = new HttpGatewayExtension(new GreetingServiceImpl()); 29 | 30 | private GreetingService service; 31 | 32 | @BeforeEach 33 | void initService() { 34 | service = extension.client().api(GreetingService.class); 35 | } 36 | 37 | @Test 38 | void shouldReturnSingleResponseWithSimpleRequest() { 39 | StepVerifier.create(service.one("hello")) 40 | .expectNext("Echo:hello") 41 | .expectComplete() 42 | .verify(TIMEOUT); 43 | } 44 | 45 | @Test 46 | void shouldReturnSingleResponseWithSimpleLongDataRequest() { 47 | String data = new String(new char[500]); 48 | StepVerifier.create(service.one(data)) 49 | .expectNext("Echo:" + data) 50 | .expectComplete() 51 | .verify(TIMEOUT); 52 | } 53 | 54 | @Test 55 | void shouldReturnSingleResponseWithPojoRequest() { 56 | StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) 57 | .expectNextMatches(response -> "Echo:hello".equals(response.getText())) 58 | .expectComplete() 59 | .verify(TIMEOUT); 60 | } 61 | 62 | @Test 63 | void shouldReturnListResponseWithPojoRequest() { 64 | StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) 65 | .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) 66 | .expectComplete() 67 | .verify(TIMEOUT); 68 | } 69 | 70 | @Test 71 | void shouldReturnNoContentWhenResponseIsEmpty() { 72 | StepVerifier.create(service.emptyOne("hello")).expectComplete().verify(TIMEOUT); 73 | } 74 | 75 | @Test 76 | void shouldReturnServiceUnavailableWhenServiceIsDown() { 77 | extension.shutdownServices(); 78 | 79 | StepVerifier.create(service.one("hello")) 80 | .expectErrorSatisfies( 81 | throwable -> { 82 | assertEquals(ServiceUnavailableException.class, throwable.getClass()); 83 | assertThat( 84 | throwable.getMessage(), startsWith("No reachable member with such service:")); 85 | }) 86 | .verify(TIMEOUT); 87 | } 88 | 89 | @Test 90 | void shouldReturnInternalServerErrorWhenServiceFails() { 91 | StepVerifier.create(service.failingOne("hello")) 92 | .expectErrorSatisfies( 93 | throwable -> { 94 | assertEquals(InternalServiceException.class, throwable.getClass()); 95 | assertEquals("hello", throwable.getMessage()); 96 | }) 97 | .verify(TIMEOUT); 98 | } 99 | 100 | @Test 101 | void shouldSuccessfullyReuseServiceProxy() { 102 | StepVerifier.create(service.one("hello")) 103 | .expectNext("Echo:hello") 104 | .expectComplete() 105 | .verify(TIMEOUT); 106 | 107 | StepVerifier.create(service.one("hello")) 108 | .expectNext("Echo:hello") 109 | .expectComplete() 110 | .verify(TIMEOUT); 111 | } 112 | 113 | @Test 114 | void shouldReturnNoEventOnNeverService() { 115 | StepVerifier.create(service.neverOne("hi")) 116 | .expectSubscription() 117 | .expectNoEvent(Duration.ofSeconds(1)) 118 | .thenCancel() 119 | .verify(); 120 | } 121 | 122 | @Test 123 | void shouldReturnOnEmptyGreeting() { 124 | StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) 125 | .expectSubscription() 126 | .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) 127 | .thenCancel() 128 | .verify(); 129 | } 130 | 131 | @Test 132 | void shouldReturnOnEmptyMessageGreeting() { 133 | String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); 134 | ServiceMessage request = 135 | ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); 136 | StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) 137 | .expectSubscription() 138 | .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) 139 | .thenCancel() 140 | .verify(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/ReactiveAdapter.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import java.util.concurrent.atomic.AtomicLongFieldUpdater; 4 | import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; 5 | import org.reactivestreams.Subscription; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import reactor.core.CoreSubscriber; 9 | import reactor.core.Exceptions; 10 | import reactor.core.publisher.BaseSubscriber; 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Operators; 13 | 14 | public final class ReactiveAdapter extends BaseSubscriber implements ReactiveOperator { 15 | 16 | private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveAdapter.class); 17 | 18 | private static final AtomicLongFieldUpdater REQUESTED = 19 | AtomicLongFieldUpdater.newUpdater(ReactiveAdapter.class, "requested"); 20 | 21 | @SuppressWarnings("rawtypes") 22 | private static final AtomicReferenceFieldUpdater 23 | DESTINATION_SUBSCRIBER = 24 | AtomicReferenceFieldUpdater.newUpdater( 25 | ReactiveAdapter.class, CoreSubscriber.class, "destinationSubscriber"); 26 | 27 | private final FluxReceive inbound = new FluxReceive(); 28 | 29 | private volatile long requested; 30 | private volatile boolean fastPath; 31 | private long produced; 32 | private volatile CoreSubscriber destinationSubscriber; 33 | private Throwable lastError; 34 | 35 | @Override 36 | public boolean isDisposed() { 37 | return destinationSubscriber == CancelledSubscriber.INSTANCE; 38 | } 39 | 40 | @Override 41 | public void dispose(Throwable throwable) { 42 | Subscription upstream = upstream(); 43 | if (upstream != null) { 44 | upstream.cancel(); 45 | } 46 | CoreSubscriber destination = 47 | DESTINATION_SUBSCRIBER.getAndSet(this, CancelledSubscriber.INSTANCE); 48 | if (destination != null) { 49 | destination.onError(throwable); 50 | } 51 | } 52 | 53 | @Override 54 | public void dispose() { 55 | inbound.cancel(); 56 | } 57 | 58 | public Flux receive() { 59 | return inbound; 60 | } 61 | 62 | @Override 63 | public void lastError(Throwable throwable) { 64 | lastError = throwable; 65 | } 66 | 67 | @Override 68 | public Throwable lastError() { 69 | return lastError; 70 | } 71 | 72 | @Override 73 | public void tryNext(Object Object) { 74 | if (!isDisposed()) { 75 | destinationSubscriber.onNext(Object); 76 | } else { 77 | LOGGER.warn("[tryNext] reactiveAdapter is disposed, dropping : " + Object); 78 | } 79 | } 80 | 81 | @Override 82 | public boolean isFastPath() { 83 | return fastPath; 84 | } 85 | 86 | @Override 87 | public void commitProduced() { 88 | if (produced > 0) { 89 | Operators.produced(REQUESTED, this, produced); 90 | produced = 0; 91 | } 92 | } 93 | 94 | @Override 95 | public long incrementProduced() { 96 | return ++produced; 97 | } 98 | 99 | @Override 100 | public long requested(long limit) { 101 | return Math.min(requested, limit); 102 | } 103 | 104 | @Override 105 | protected void hookOnSubscribe(Subscription subscription) { 106 | subscription.request(requested); 107 | } 108 | 109 | @Override 110 | protected void hookOnNext(Object Object) { 111 | tryNext(Object); 112 | } 113 | 114 | @Override 115 | protected void hookOnComplete() { 116 | dispose(); 117 | } 118 | 119 | @Override 120 | protected void hookOnError(Throwable throwable) { 121 | dispose(throwable); 122 | } 123 | 124 | @Override 125 | protected void hookOnCancel() { 126 | dispose(); 127 | } 128 | 129 | class FluxReceive extends Flux implements Subscription { 130 | 131 | @Override 132 | public void request(long n) { 133 | Subscription upstream = upstream(); 134 | if (upstream != null) { 135 | upstream.request(n); 136 | } 137 | if (fastPath) { 138 | return; 139 | } 140 | if (n == Long.MAX_VALUE) { 141 | fastPath = true; 142 | requested = Long.MAX_VALUE; 143 | return; 144 | } 145 | Operators.addCap(REQUESTED, ReactiveAdapter.this, n); 146 | } 147 | 148 | @Override 149 | public void cancel() { 150 | Subscription upstream = upstream(); 151 | if (upstream != null) { 152 | upstream.cancel(); 153 | } 154 | CoreSubscriber destination = 155 | DESTINATION_SUBSCRIBER.getAndSet(ReactiveAdapter.this, CancelledSubscriber.INSTANCE); 156 | if (destination != null) { 157 | destination.onComplete(); 158 | } 159 | } 160 | 161 | @Override 162 | public void subscribe(CoreSubscriber destinationSubscriber) { 163 | boolean result = 164 | DESTINATION_SUBSCRIBER.compareAndSet(ReactiveAdapter.this, null, destinationSubscriber); 165 | if (result) { 166 | destinationSubscriber.onSubscribe(this); 167 | } else { 168 | Operators.error( 169 | destinationSubscriber, 170 | isDisposed() 171 | ? Exceptions.failWithCancel() 172 | : Exceptions.duplicateOnSubscribeException()); 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayAuthTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import io.scalecube.services.api.ServiceMessage; 7 | import io.scalecube.services.exceptions.ForbiddenException; 8 | import io.scalecube.services.exceptions.UnauthorizedException; 9 | import io.scalecube.services.gateway.AuthRegistry; 10 | import io.scalecube.services.gateway.SecuredService; 11 | import io.scalecube.services.gateway.SecuredServiceImpl; 12 | import java.time.Duration; 13 | import java.util.Collections; 14 | import java.util.HashSet; 15 | import java.util.Set; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.junit.jupiter.api.extension.RegisterExtension; 19 | import reactor.test.StepVerifier; 20 | 21 | public class WebsocketLocalGatewayAuthTest { 22 | 23 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 24 | 25 | private static final String ALLOWED_USER = "VASYA_PUPKIN"; 26 | private static final Set ALLOWED_USERS = 27 | new HashSet<>(Collections.singletonList(ALLOWED_USER)); 28 | 29 | private static final AuthRegistry AUTH_REG = new AuthRegistry(ALLOWED_USERS); 30 | 31 | @RegisterExtension 32 | static WebsocketLocalWithAuthExtension extension = 33 | new WebsocketLocalWithAuthExtension(new SecuredServiceImpl(AUTH_REG), AUTH_REG); 34 | 35 | private SecuredService clientService; 36 | 37 | private static ServiceMessage createSessionReq(String username) { 38 | return ServiceMessage.builder() 39 | .qualifier("/" + SecuredService.NS + "/createSession") 40 | .data(username) 41 | .build(); 42 | } 43 | 44 | @BeforeEach 45 | void initService() { 46 | clientService = extension.client().api(SecuredService.class); 47 | } 48 | 49 | @Test 50 | void testCreateSession_succ() { 51 | StepVerifier.create(extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class)) 52 | .expectNextCount(1) 53 | .expectComplete() 54 | .verify(); 55 | } 56 | 57 | @Test 58 | void testCreateSession_forbiddenUser() { 59 | StepVerifier.create( 60 | extension.client().requestOne(createSessionReq("fake" + ALLOWED_USER), String.class)) 61 | .expectErrorSatisfies( 62 | th -> { 63 | ForbiddenException e = (ForbiddenException) th; 64 | assertEquals(403, e.errorCode(), "error code"); 65 | assertTrue(e.getMessage().contains("User not allowed to use this service")); 66 | }) 67 | .verify(); 68 | } 69 | 70 | @Test 71 | void testCallSecuredMethod_notAuthenticated() { 72 | StepVerifier.create(clientService.requestOne("echo")) 73 | .expectErrorSatisfies( 74 | th -> { 75 | UnauthorizedException e = (UnauthorizedException) th; 76 | assertEquals(401, e.errorCode(), "Authentication failed"); 77 | assertTrue(e.getMessage().contains("Authentication failed")); 78 | }) 79 | .verify(); 80 | } 81 | 82 | @Test 83 | void testCallSecuredMethod_authenticated() { 84 | // authenticate session 85 | extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class).block(TIMEOUT); 86 | // call secured service 87 | final String req = "echo"; 88 | StepVerifier.create(clientService.requestOne(req)) 89 | .expectNextMatches(resp -> resp.equals(ALLOWED_USER + "@" + req)) 90 | .expectComplete() 91 | .verify(); 92 | } 93 | 94 | @Test 95 | void testCallSecuredMethod_authenticatedInvalidUser() { 96 | // authenticate session 97 | StepVerifier.create( 98 | extension.client().requestOne(createSessionReq("fake" + ALLOWED_USER), String.class)) 99 | .expectErrorSatisfies(th -> assertTrue(th instanceof ForbiddenException)) 100 | .verify(); 101 | // call secured service 102 | final String req = "echo"; 103 | StepVerifier.create(clientService.requestOne(req)) 104 | .expectErrorSatisfies( 105 | th -> { 106 | UnauthorizedException e = (UnauthorizedException) th; 107 | assertEquals(401, e.errorCode(), "Authentication failed"); 108 | assertTrue(e.getMessage().contains("Authentication failed")); 109 | }) 110 | .verify(); 111 | } 112 | 113 | @Test 114 | void testCallSecuredMethod_notAuthenticatedRequestStream() { 115 | StepVerifier.create(clientService.requestN(10)) 116 | .expectErrorSatisfies( 117 | th -> { 118 | UnauthorizedException e = (UnauthorizedException) th; 119 | assertEquals(401, e.errorCode(), "Authentication failed"); 120 | assertTrue(e.getMessage().contains("Authentication failed")); 121 | }) 122 | .verify(); 123 | } 124 | 125 | @Test 126 | void testCallSecuredMethod_authenticatedReqStream() { 127 | // authenticate session 128 | extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class).block(TIMEOUT); 129 | // call secured service 130 | Integer times = 10; 131 | StepVerifier.create(clientService.requestN(times)) 132 | .expectNextCount(10) 133 | .expectComplete() 134 | .verify(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/ws/GatewayMessages.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.ws; 2 | 3 | import io.scalecube.services.api.ServiceMessage; 4 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 5 | 6 | public final class GatewayMessages { 7 | 8 | static final String QUALIFIER_FIELD = "q"; 9 | static final String STREAM_ID_FIELD = "sid"; 10 | static final String DATA_FIELD = "d"; 11 | static final String SIGNAL_FIELD = "sig"; 12 | static final String INACTIVITY_FIELD = "i"; 13 | static final String RATE_LIMIT_FIELD = "rlimit"; 14 | 15 | private GatewayMessages() { 16 | // Do not instantiate 17 | } 18 | 19 | /** 20 | * Returns cancel message by given arguments. 21 | * 22 | * @param sid sid 23 | * @param qualifier qualifier 24 | * @return {@link ServiceMessage} instance as the cancel signal 25 | */ 26 | public static ServiceMessage newCancelMessage(long sid, String qualifier) { 27 | return ServiceMessage.builder() 28 | .qualifier(qualifier) 29 | .header(STREAM_ID_FIELD, sid) 30 | .header(SIGNAL_FIELD, Signal.CANCEL.code()) 31 | .build(); 32 | } 33 | 34 | /** 35 | * Returns error message by given arguments. 36 | * 37 | * @param errorMapper error mapper 38 | * @param request request 39 | * @param th cause 40 | * @return {@link ServiceMessage} instance as the error signal 41 | */ 42 | public static ServiceMessage toErrorResponse( 43 | ServiceProviderErrorMapper errorMapper, ServiceMessage request, Throwable th) { 44 | 45 | final String qualifier = request.qualifier() != null ? request.qualifier() : "scalecube/error"; 46 | final String sid = request.header(STREAM_ID_FIELD); 47 | final ServiceMessage errorMessage = errorMapper.toMessage(qualifier, th); 48 | 49 | if (sid == null) { 50 | return ServiceMessage.from(errorMessage).header(SIGNAL_FIELD, Signal.ERROR.code()).build(); 51 | } 52 | 53 | return ServiceMessage.from(errorMessage) 54 | .header(SIGNAL_FIELD, Signal.ERROR.code()) 55 | .header(STREAM_ID_FIELD, sid) 56 | .build(); 57 | } 58 | 59 | /** 60 | * Returns complete message by given arguments. 61 | * 62 | * @param sid sid 63 | * @param qualifier qualifier 64 | * @return {@link ServiceMessage} instance as the complete signal 65 | */ 66 | public static ServiceMessage newCompleteMessage(long sid, String qualifier) { 67 | return ServiceMessage.builder() 68 | .qualifier(qualifier) 69 | .header(STREAM_ID_FIELD, sid) 70 | .header(SIGNAL_FIELD, Signal.COMPLETE.code()) 71 | .build(); 72 | } 73 | 74 | /** 75 | * Returns response message by given arguments. 76 | * 77 | * @param sid sid 78 | * @param message request 79 | * @param isErrorResponse should the message be marked as an error? 80 | * @return {@link ServiceMessage} instance as the response 81 | */ 82 | public static ServiceMessage newResponseMessage( 83 | long sid, ServiceMessage message, boolean isErrorResponse) { 84 | if (isErrorResponse) { 85 | return ServiceMessage.from(message) 86 | .header(STREAM_ID_FIELD, sid) 87 | .header(SIGNAL_FIELD, Signal.ERROR.code()) 88 | .build(); 89 | } 90 | return ServiceMessage.from(message).header(STREAM_ID_FIELD, sid).build(); 91 | } 92 | 93 | /** 94 | * Verifies the sid existence in a given message. 95 | * 96 | * @param message message 97 | * @return incoming message 98 | */ 99 | public static ServiceMessage validateSid(ServiceMessage message) { 100 | if (message.header(STREAM_ID_FIELD) == null) { 101 | throw WebsocketContextException.badRequest("sid is missing", message); 102 | } else { 103 | return message; 104 | } 105 | } 106 | 107 | /** 108 | * Verifies the sid is not used in a given session. 109 | * 110 | * @param session session 111 | * @param message message 112 | * @return incoming message 113 | */ 114 | public static ServiceMessage validateSidOnSession( 115 | WebsocketGatewaySession session, ServiceMessage message) { 116 | long sid = getSid(message); 117 | if (session.containsSid(sid)) { 118 | throw WebsocketContextException.badRequest("sid=" + sid + " is already registered", message); 119 | } else { 120 | return message; 121 | } 122 | } 123 | 124 | /** 125 | * Verifies the qualifier existence in a given message. 126 | * 127 | * @param message message 128 | * @return incoming message 129 | */ 130 | public static ServiceMessage validateQualifier(ServiceMessage message) { 131 | if (message.qualifier() == null) { 132 | throw WebsocketContextException.badRequest("qualifier is missing", message); 133 | } 134 | return message; 135 | } 136 | 137 | /** 138 | * Returns sid from a given message. 139 | * 140 | * @param message message 141 | * @return sid 142 | */ 143 | public static long getSid(ServiceMessage message) { 144 | return Long.parseLong(message.header(STREAM_ID_FIELD)); 145 | } 146 | 147 | /** 148 | * Returns signal from a given message. 149 | * 150 | * @param message message 151 | * @return signal 152 | */ 153 | public static Signal getSignal(ServiceMessage message) { 154 | String header = message.header(SIGNAL_FIELD); 155 | return header != null ? Signal.from(Integer.parseInt(header)) : null; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/AbstractGatewayExtension.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway; 2 | 3 | import io.scalecube.net.Address; 4 | import io.scalecube.services.Microservices; 5 | import io.scalecube.services.ServiceCall; 6 | import io.scalecube.services.ServiceEndpoint; 7 | import io.scalecube.services.ServiceInfo; 8 | import io.scalecube.services.discovery.ScalecubeServiceDiscovery; 9 | import io.scalecube.services.discovery.api.ServiceDiscovery; 10 | import io.scalecube.services.gateway.transport.GatewayClientSettings; 11 | import io.scalecube.services.gateway.transport.StaticAddressRouter; 12 | import io.scalecube.services.transport.api.ClientTransport; 13 | import io.scalecube.services.transport.rsocket.RSocketServiceTransport; 14 | import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; 15 | import java.util.function.Function; 16 | import org.junit.jupiter.api.extension.AfterAllCallback; 17 | import org.junit.jupiter.api.extension.AfterEachCallback; 18 | import org.junit.jupiter.api.extension.BeforeAllCallback; 19 | import org.junit.jupiter.api.extension.BeforeEachCallback; 20 | import org.junit.jupiter.api.extension.ExtensionContext; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | public abstract class AbstractGatewayExtension 25 | implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGatewayExtension.class); 28 | 29 | private final ServiceInfo serviceInfo; 30 | private final Function gatewaySupplier; 31 | private final Function clientSupplier; 32 | 33 | private String gatewayId; 34 | private Microservices gateway; 35 | private Microservices services; 36 | private ServiceCall clientServiceCall; 37 | 38 | protected AbstractGatewayExtension( 39 | ServiceInfo serviceInfo, 40 | Function gatewaySupplier, 41 | Function clientSupplier) { 42 | this.serviceInfo = serviceInfo; 43 | this.gatewaySupplier = gatewaySupplier; 44 | this.clientSupplier = clientSupplier; 45 | } 46 | 47 | @Override 48 | public final void beforeAll(ExtensionContext context) { 49 | gateway = 50 | Microservices.builder() 51 | .discovery( 52 | serviceEndpoint -> 53 | new ScalecubeServiceDiscovery() 54 | .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) 55 | .options(opts -> opts.metadata(serviceEndpoint))) 56 | .transport(RSocketServiceTransport::new) 57 | .gateway( 58 | options -> { 59 | Gateway gateway = gatewaySupplier.apply(options); 60 | gatewayId = gateway.id(); 61 | return gateway; 62 | }) 63 | .startAwait(); 64 | startServices(); 65 | } 66 | 67 | @Override 68 | public final void beforeEach(ExtensionContext context) { 69 | // if services was shutdown in test need to start them again 70 | if (services == null) { 71 | startServices(); 72 | } 73 | Address gatewayAddress = gateway.gateway(gatewayId).address(); 74 | GatewayClientSettings clintSettings = 75 | GatewayClientSettings.builder().address(gatewayAddress).build(); 76 | clientServiceCall = 77 | new ServiceCall() 78 | .transport(clientSupplier.apply(clintSettings)) 79 | .router(new StaticAddressRouter(gatewayAddress)); 80 | } 81 | 82 | @Override 83 | public final void afterEach(ExtensionContext context) { 84 | // no-op 85 | } 86 | 87 | @Override 88 | public final void afterAll(ExtensionContext context) { 89 | shutdownServices(); 90 | shutdownGateway(); 91 | } 92 | 93 | public ServiceCall client() { 94 | return clientServiceCall; 95 | } 96 | 97 | public void startServices() { 98 | services = 99 | Microservices.builder() 100 | .discovery(this::serviceDiscovery) 101 | .transport(RSocketServiceTransport::new) 102 | .services(serviceInfo) 103 | .startAwait(); 104 | LOGGER.info("Started services {} on {}", services, services.serviceAddress()); 105 | } 106 | 107 | private ServiceDiscovery serviceDiscovery(ServiceEndpoint serviceEndpoint) { 108 | return new ScalecubeServiceDiscovery() 109 | .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) 110 | .options(opts -> opts.metadata(serviceEndpoint)) 111 | .membership(opts -> opts.seedMembers(gateway.discoveryAddress())); 112 | } 113 | 114 | public void shutdownServices() { 115 | if (services != null) { 116 | try { 117 | services.shutdown().block(); 118 | } catch (Throwable ignore) { 119 | // ignore 120 | } 121 | LOGGER.info("Shutdown services {}", services); 122 | 123 | // if this method is called in particular test need to indicate that services are stopped to 124 | // start them again before another test 125 | services = null; 126 | } 127 | } 128 | 129 | private void shutdownGateway() { 130 | if (gateway != null) { 131 | try { 132 | gateway.shutdown().block(); 133 | } catch (Throwable ignore) { 134 | // ignore 135 | } 136 | LOGGER.info("Shutdown gateway {}", gateway); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/HttpClientConnectionTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.scalecube.net.Address; 5 | import io.scalecube.services.Microservices; 6 | import io.scalecube.services.ServiceCall; 7 | import io.scalecube.services.annotations.Service; 8 | import io.scalecube.services.annotations.ServiceMethod; 9 | import io.scalecube.services.discovery.ScalecubeServiceDiscovery; 10 | import io.scalecube.services.gateway.BaseTest; 11 | import io.scalecube.services.gateway.transport.GatewayClient; 12 | import io.scalecube.services.gateway.transport.GatewayClientCodec; 13 | import io.scalecube.services.gateway.transport.GatewayClientSettings; 14 | import io.scalecube.services.gateway.transport.GatewayClientTransport; 15 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 16 | import io.scalecube.services.gateway.transport.StaticAddressRouter; 17 | import io.scalecube.services.gateway.transport.http.HttpGatewayClient; 18 | import io.scalecube.services.transport.rsocket.RSocketServiceTransport; 19 | import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; 20 | import java.io.IOException; 21 | import java.time.Duration; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import org.junit.jupiter.api.AfterEach; 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | import reactor.core.publisher.Flux; 27 | import reactor.core.publisher.Mono; 28 | import reactor.test.StepVerifier; 29 | 30 | class HttpClientConnectionTest extends BaseTest { 31 | 32 | public static final GatewayClientCodec CLIENT_CODEC = 33 | GatewayClientTransports.HTTP_CLIENT_CODEC; 34 | 35 | private Microservices gateway; 36 | private Address gatewayAddress; 37 | private Microservices service; 38 | 39 | private static final AtomicInteger onCloseCounter = new AtomicInteger(); 40 | private GatewayClient client; 41 | 42 | @BeforeEach 43 | void beforEach() { 44 | gateway = 45 | Microservices.builder() 46 | .discovery( 47 | serviceEndpoint -> 48 | new ScalecubeServiceDiscovery() 49 | .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) 50 | .options(opts -> opts.metadata(serviceEndpoint))) 51 | .transport(RSocketServiceTransport::new) 52 | .gateway(options -> new HttpGateway(options.id("HTTP"))) 53 | .startAwait(); 54 | 55 | gatewayAddress = gateway.gateway("HTTP").address(); 56 | 57 | service = 58 | Microservices.builder() 59 | .discovery( 60 | serviceEndpoint -> 61 | new ScalecubeServiceDiscovery() 62 | .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) 63 | .options(opts -> opts.metadata(serviceEndpoint)) 64 | .membership(opts -> opts.seedMembers(gateway.discoveryAddress()))) 65 | .transport(RSocketServiceTransport::new) 66 | .services(new TestServiceImpl()) 67 | .startAwait(); 68 | 69 | onCloseCounter.set(0); 70 | } 71 | 72 | @AfterEach 73 | void afterEach() { 74 | Flux.concat( 75 | Mono.justOrEmpty(client).doOnNext(GatewayClient::close).flatMap(GatewayClient::onClose), 76 | Mono.justOrEmpty(gateway).map(Microservices::shutdown), 77 | Mono.justOrEmpty(service).map(Microservices::shutdown)) 78 | .then() 79 | .block(); 80 | } 81 | 82 | @Test 83 | void testCloseServiceStreamAfterLostConnection() { 84 | client = 85 | new HttpGatewayClient( 86 | GatewayClientSettings.builder().address(gatewayAddress).build(), CLIENT_CODEC); 87 | 88 | ServiceCall serviceCall = 89 | new ServiceCall() 90 | .transport(new GatewayClientTransport(client)) 91 | .router(new StaticAddressRouter(gatewayAddress)); 92 | 93 | StepVerifier.create(serviceCall.api(TestService.class).oneNever("body").log("<<< ")) 94 | .thenAwait(Duration.ofSeconds(5)) 95 | .then(() -> client.close()) 96 | .then(() -> client.onClose().block()) 97 | .expectError(IOException.class) 98 | .verify(Duration.ofSeconds(1)); 99 | } 100 | 101 | @Test 102 | public void testCallRepeatedlyByInvalidAddress() { 103 | Address invalidAddress = Address.create("localhost", 5050); 104 | 105 | client = 106 | new HttpGatewayClient( 107 | GatewayClientSettings.builder().address(invalidAddress).build(), CLIENT_CODEC); 108 | 109 | ServiceCall serviceCall = 110 | new ServiceCall() 111 | .transport(new GatewayClientTransport(client)) 112 | .router(new StaticAddressRouter(invalidAddress)); 113 | 114 | for (int i = 0; i < 100; i++) { 115 | StepVerifier.create(serviceCall.api(TestService.class).oneNever("body").log("<<< ")) 116 | .thenAwait(Duration.ofSeconds(1)) 117 | .expectError(IOException.class) 118 | .verify(Duration.ofSeconds(10)); 119 | } 120 | } 121 | 122 | @Service 123 | public interface TestService { 124 | 125 | @ServiceMethod("oneNever") 126 | Mono oneNever(String name); 127 | } 128 | 129 | private static class TestServiceImpl implements TestService { 130 | 131 | @Override 132 | public Mono oneNever(String name) { 133 | return Mono.never().log(">>> ").doOnCancel(onCloseCounter::incrementAndGet); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/http/CorsTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.containsString; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertNull; 7 | 8 | import io.netty.handler.codec.http.HttpHeaders; 9 | import io.netty.handler.codec.http.HttpResponseStatus; 10 | import io.scalecube.services.Microservices; 11 | import io.scalecube.services.discovery.ScalecubeServiceDiscovery; 12 | import io.scalecube.services.examples.GreetingService; 13 | import io.scalecube.services.examples.GreetingServiceImpl; 14 | import io.scalecube.services.gateway.BaseTest; 15 | import io.scalecube.services.transport.rsocket.RSocketServiceTransport; 16 | import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; 17 | import java.time.Duration; 18 | import org.junit.jupiter.api.AfterEach; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import reactor.core.publisher.Mono; 22 | import reactor.netty.ByteBufFlux; 23 | import reactor.netty.http.client.HttpClient; 24 | import reactor.netty.http.client.HttpClientResponse; 25 | import reactor.netty.resources.ConnectionProvider; 26 | 27 | public class CorsTest extends BaseTest { 28 | 29 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 30 | public static final int HTTP_PORT = 8999; 31 | 32 | private Microservices gateway; 33 | 34 | private final Microservices.Builder gatewayBuilder = 35 | Microservices.builder() 36 | .discovery( 37 | serviceEndpoint -> 38 | new ScalecubeServiceDiscovery() 39 | .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) 40 | .options(opts -> opts.metadata(serviceEndpoint))) 41 | .transport(RSocketServiceTransport::new) 42 | .services(new GreetingServiceImpl()); 43 | private HttpClient client; 44 | 45 | @BeforeEach 46 | void beforeEach() { 47 | client = HttpClient.create(ConnectionProvider.newConnection()).port(HTTP_PORT).wiretap(true); 48 | } 49 | 50 | @AfterEach 51 | void afterEach() { 52 | if (gateway != null) { 53 | gateway.shutdown().block(); 54 | } 55 | } 56 | 57 | @Test 58 | void testCrossOriginRequest() { 59 | gateway = 60 | gatewayBuilder 61 | .gateway( 62 | opts -> 63 | new HttpGateway(opts.id("http").port(HTTP_PORT)) 64 | .corsEnabled(true) 65 | .corsConfig( 66 | config -> 67 | config.allowedRequestHeaders("Content-Type", "X-Correlation-ID"))) 68 | .start() 69 | .block(TIMEOUT); 70 | 71 | HttpClientResponse response = 72 | client 73 | .headers( 74 | headers -> 75 | headers 76 | .add("Origin", "test.com") 77 | .add("Access-Control-Request-Method", "POST") 78 | .add("Access-Control-Request-Headers", "Content-Type,X-Correlation-ID")) 79 | .options() 80 | .response() 81 | .block(TIMEOUT); 82 | 83 | HttpHeaders responseHeaders = response.responseHeaders(); 84 | 85 | assertEquals(HttpResponseStatus.OK, response.status()); 86 | assertEquals("*", responseHeaders.get("Access-Control-Allow-Origin")); 87 | assertEquals("POST", responseHeaders.get("Access-Control-Allow-Methods")); 88 | assertThat(responseHeaders.get("Access-Control-Allow-Headers"), containsString("Content-Type")); 89 | assertThat( 90 | responseHeaders.get("Access-Control-Allow-Headers"), containsString("X-Correlation-ID")); 91 | 92 | response = 93 | client 94 | .headers( 95 | headers -> 96 | headers 97 | .add("Origin", "test.com") 98 | .add("X-Correlation-ID", "xxxxxx") 99 | .add("Content-Type", "application/json")) 100 | .post() 101 | .uri("/" + GreetingService.NAMESPACE + "/one") 102 | .send(ByteBufFlux.fromString(Mono.just("\"Hello\""))) 103 | .response() 104 | .block(TIMEOUT); 105 | 106 | responseHeaders = response.responseHeaders(); 107 | 108 | assertEquals(HttpResponseStatus.OK, response.status()); 109 | assertEquals("*", responseHeaders.get("Access-Control-Allow-Origin")); 110 | } 111 | 112 | @Test 113 | void testOptionRequestCorsDisabled() { 114 | gateway = 115 | gatewayBuilder 116 | .gateway(opts -> new HttpGateway(opts.id("http").port(HTTP_PORT)).corsEnabled(false)) 117 | .start() 118 | .block(TIMEOUT); 119 | 120 | HttpClientResponse response = 121 | client 122 | .headers( 123 | headers -> 124 | headers 125 | .add("Origin", "test.com") 126 | .add("Access-Control-Request-Method", "POST") 127 | .add("Access-Control-Request-Headers", "Content-Type,X-Correlation-ID")) 128 | .options() 129 | .response() 130 | .block(TIMEOUT); 131 | 132 | HttpHeaders responseHeaders = response.responseHeaders(); 133 | 134 | assertEquals(HttpResponseStatus.METHOD_NOT_ALLOWED, response.status()); 135 | assertNull(responseHeaders.get("Access-Control-Allow-Origin")); 136 | assertNull(responseHeaders.get("Access-Control-Allow-Methods")); 137 | assertNull(responseHeaders.get("Access-Control-Allow-Headers")); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/ws/WebsocketGateway.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.ws; 2 | 3 | import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; 4 | import io.scalecube.net.Address; 5 | import io.scalecube.services.exceptions.DefaultErrorMapper; 6 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 7 | import io.scalecube.services.gateway.Gateway; 8 | import io.scalecube.services.gateway.GatewayOptions; 9 | import io.scalecube.services.gateway.GatewaySessionHandler; 10 | import io.scalecube.services.gateway.GatewayTemplate; 11 | import java.net.InetSocketAddress; 12 | import java.time.Duration; 13 | import java.util.StringJoiner; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import reactor.core.publisher.Flux; 17 | import reactor.core.publisher.Mono; 18 | import reactor.netty.Connection; 19 | import reactor.netty.DisposableServer; 20 | import reactor.netty.resources.LoopResources; 21 | 22 | public class WebsocketGateway extends GatewayTemplate { 23 | 24 | private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketGateway.class); 25 | 26 | private final GatewaySessionHandler gatewayHandler; 27 | private final Duration keepAliveInterval; 28 | private final ServiceProviderErrorMapper errorMapper; 29 | 30 | private DisposableServer server; 31 | private LoopResources loopResources; 32 | 33 | /** 34 | * Constructor. 35 | * 36 | * @param options gateway options 37 | */ 38 | public WebsocketGateway(GatewayOptions options) { 39 | this( 40 | options, 41 | Duration.ZERO, 42 | GatewaySessionHandler.DEFAULT_INSTANCE, 43 | DefaultErrorMapper.INSTANCE); 44 | } 45 | 46 | /** 47 | * Constructor. 48 | * 49 | * @param options gateway options 50 | * @param keepAliveInterval keep alive interval 51 | */ 52 | public WebsocketGateway(GatewayOptions options, Duration keepAliveInterval) { 53 | this( 54 | options, 55 | keepAliveInterval, 56 | GatewaySessionHandler.DEFAULT_INSTANCE, 57 | DefaultErrorMapper.INSTANCE); 58 | } 59 | 60 | /** 61 | * Constructor. 62 | * 63 | * @param options gateway options 64 | * @param gatewayHandler gateway handler 65 | */ 66 | public WebsocketGateway(GatewayOptions options, GatewaySessionHandler gatewayHandler) { 67 | this(options, Duration.ZERO, gatewayHandler, DefaultErrorMapper.INSTANCE); 68 | } 69 | 70 | /** 71 | * Constructor. 72 | * 73 | * @param options gateway options 74 | * @param errorMapper error mapper 75 | */ 76 | public WebsocketGateway(GatewayOptions options, ServiceProviderErrorMapper errorMapper) { 77 | this(options, Duration.ZERO, GatewaySessionHandler.DEFAULT_INSTANCE, errorMapper); 78 | } 79 | 80 | /** 81 | * Constructor. 82 | * 83 | * @param options gateway options 84 | * @param keepAliveInterval keep alive interval 85 | * @param gatewayHandler gateway handler 86 | * @param errorMapper error mapper 87 | */ 88 | public WebsocketGateway( 89 | GatewayOptions options, 90 | Duration keepAliveInterval, 91 | GatewaySessionHandler gatewayHandler, 92 | ServiceProviderErrorMapper errorMapper) { 93 | super(options); 94 | this.keepAliveInterval = keepAliveInterval; 95 | this.gatewayHandler = gatewayHandler; 96 | this.errorMapper = errorMapper; 97 | } 98 | 99 | @Override 100 | public Mono start() { 101 | return Mono.defer( 102 | () -> { 103 | WebsocketGatewayAcceptor acceptor = 104 | new WebsocketGatewayAcceptor(options.call(), gatewayHandler, errorMapper); 105 | 106 | loopResources = LoopResources.create("websocket-gateway"); 107 | 108 | return prepareHttpServer(loopResources, options.port()) 109 | .doOnConnection(this::setupKeepAlive) 110 | .handle(acceptor) 111 | .bind() 112 | .doOnSuccess(server -> this.server = server) 113 | .thenReturn(this); 114 | }); 115 | } 116 | 117 | @Override 118 | public Address address() { 119 | InetSocketAddress address = (InetSocketAddress) server.address(); 120 | return Address.create(address.getHostString(), address.getPort()); 121 | } 122 | 123 | @Override 124 | public Mono stop() { 125 | return Flux.concatDelayError(shutdownServer(server), shutdownLoopResources(loopResources)) 126 | .then(); 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | return new StringJoiner(", ", WebsocketGateway.class.getSimpleName() + "[", "]") 132 | .add("server=" + server) 133 | .add("loopResources=" + loopResources) 134 | .add("options=" + options) 135 | .toString(); 136 | } 137 | 138 | private void setupKeepAlive(Connection connection) { 139 | if (keepAliveInterval != Duration.ZERO) { 140 | connection 141 | .onReadIdle(keepAliveInterval.toMillis(), () -> onReadIdle(connection)) 142 | .onWriteIdle(keepAliveInterval.toMillis(), () -> onWriteIdle(connection)); 143 | } 144 | } 145 | 146 | private void onWriteIdle(Connection connection) { 147 | LOGGER.debug("Sending keepalive on writeIdle"); 148 | connection 149 | .outbound() 150 | .sendObject(new PingWebSocketFrame()) 151 | .then() 152 | .subscribe(null, ex -> LOGGER.warn("Can't send keepalive on writeIdle: " + ex)); 153 | } 154 | 155 | private void onReadIdle(Connection connection) { 156 | LOGGER.debug("Sending keepalive on readIdle"); 157 | connection 158 | .outbound() 159 | .sendObject(new PingWebSocketFrame()) 160 | .then() 161 | .subscribe(null, ex -> LOGGER.warn("Can't send keepalive on readIdle: " + ex)); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import io.scalecube.services.api.Qualifier; 6 | import io.scalecube.services.api.ServiceMessage; 7 | import io.scalecube.services.examples.EmptyGreetingRequest; 8 | import io.scalecube.services.examples.EmptyGreetingResponse; 9 | import io.scalecube.services.examples.GreetingRequest; 10 | import io.scalecube.services.examples.GreetingResponse; 11 | import io.scalecube.services.examples.GreetingService; 12 | import io.scalecube.services.examples.GreetingServiceImpl; 13 | import io.scalecube.services.exceptions.InternalServiceException; 14 | import io.scalecube.services.gateway.BaseTest; 15 | import java.time.Duration; 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | import java.util.stream.IntStream; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.extension.RegisterExtension; 22 | import reactor.test.StepVerifier; 23 | 24 | class WebsocketLocalGatewayTest extends BaseTest { 25 | 26 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 27 | 28 | @RegisterExtension 29 | static WebsocketLocalGatewayExtension extension = 30 | new WebsocketLocalGatewayExtension(new GreetingServiceImpl()); 31 | 32 | private GreetingService service; 33 | 34 | @BeforeEach 35 | void initService() { 36 | service = extension.client().api(GreetingService.class); 37 | } 38 | 39 | @Test 40 | void shouldReturnSingleResponseWithSimpleRequest() { 41 | StepVerifier.create(service.one("hello")) 42 | .expectNext("Echo:hello") 43 | .expectComplete() 44 | .verify(TIMEOUT); 45 | } 46 | 47 | @Test 48 | void shouldReturnSingleResponseWithSimpleLongDataRequest() { 49 | String data = new String(new char[500]); 50 | StepVerifier.create(service.one(data)) 51 | .expectNext("Echo:" + data) 52 | .expectComplete() 53 | .verify(TIMEOUT); 54 | } 55 | 56 | @Test 57 | void shouldReturnSingleResponseWithPojoRequest() { 58 | StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) 59 | .expectNextMatches(response -> "Echo:hello".equals(response.getText())) 60 | .expectComplete() 61 | .verify(TIMEOUT); 62 | } 63 | 64 | @Test 65 | void shouldReturnListResponseWithPojoRequest() { 66 | StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) 67 | .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) 68 | .expectComplete() 69 | .verify(TIMEOUT); 70 | } 71 | 72 | @Test 73 | void shouldReturnManyResponsesWithSimpleRequest() { 74 | int expectedResponseNum = 3; 75 | List expected = 76 | IntStream.range(0, expectedResponseNum) 77 | .mapToObj(i -> "Greeting (" + i + ") to: hello") 78 | .collect(Collectors.toList()); 79 | 80 | StepVerifier.create(service.many("hello").take(expectedResponseNum)) 81 | .expectNextSequence(expected) 82 | .expectComplete() 83 | .verify(TIMEOUT); 84 | } 85 | 86 | @Test 87 | void shouldReturnManyResponsesWithPojoRequest() { 88 | int expectedResponseNum = 3; 89 | List expected = 90 | IntStream.range(0, expectedResponseNum) 91 | .mapToObj(i -> new GreetingResponse("Greeting (" + i + ") to: hello")) 92 | .collect(Collectors.toList()); 93 | 94 | StepVerifier.create(service.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) 95 | .expectNextSequence(expected) 96 | .expectComplete() 97 | .verify(TIMEOUT); 98 | } 99 | 100 | @Test 101 | void shouldReturnErrorDataWhenServiceFails() { 102 | StepVerifier.create(service.failingOne("hello")) 103 | .expectErrorMatches(throwable -> throwable instanceof InternalServiceException) 104 | .verify(TIMEOUT); 105 | } 106 | 107 | @Test 108 | void shouldReturnErrorDataWhenRequestDataIsEmpty() { 109 | StepVerifier.create(service.one(null)) 110 | .expectErrorMatches( 111 | throwable -> 112 | "Expected service request data of type: class java.lang.String, but received: null" 113 | .equals(throwable.getMessage())) 114 | .verify(TIMEOUT); 115 | } 116 | 117 | @Test 118 | void shouldReturnNoEventOnNeverService() { 119 | StepVerifier.create(service.neverOne("hi")) 120 | .expectSubscription() 121 | .expectNoEvent(Duration.ofSeconds(1)) 122 | .thenCancel() 123 | .verify(); 124 | } 125 | 126 | @Test 127 | void shouldReturnOnEmptyGreeting() { 128 | StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) 129 | .expectSubscription() 130 | .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) 131 | .thenCancel() 132 | .verify(); 133 | } 134 | 135 | @Test 136 | void shouldReturnOnEmptyMessageGreeting() { 137 | String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); 138 | ServiceMessage request = 139 | ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); 140 | StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) 141 | .expectSubscription() 142 | .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) 143 | .thenCancel() 144 | .verify(); 145 | } 146 | 147 | @Test 148 | public void testManyStreamBlockFirst() { 149 | for (int i = 0; i < 100; i++) { 150 | //noinspection ConstantConditions 151 | long first = service.manyStream(30L).filter(k -> k != 0).take(1).blockFirst(); 152 | assertEquals(1, first); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketLocalGatewayTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.scalecube.services.api.Qualifier; 4 | import io.scalecube.services.api.ServiceMessage; 5 | import io.scalecube.services.examples.EmptyGreetingRequest; 6 | import io.scalecube.services.examples.EmptyGreetingResponse; 7 | import io.scalecube.services.examples.GreetingRequest; 8 | import io.scalecube.services.examples.GreetingResponse; 9 | import io.scalecube.services.examples.GreetingService; 10 | import io.scalecube.services.examples.GreetingServiceImpl; 11 | import io.scalecube.services.exceptions.InternalServiceException; 12 | import io.scalecube.services.gateway.BaseTest; 13 | import java.time.Duration; 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.IntStream; 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | import org.junit.jupiter.api.extension.RegisterExtension; 20 | import reactor.core.publisher.Mono; 21 | import reactor.test.StepVerifier; 22 | 23 | class RSocketLocalGatewayTest extends BaseTest { 24 | 25 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 26 | 27 | @RegisterExtension 28 | static RSocketLocalGatewayExtension extension = 29 | new RSocketLocalGatewayExtension(new GreetingServiceImpl()); 30 | 31 | private GreetingService service; 32 | 33 | @BeforeEach 34 | void initService() { 35 | service = extension.client().api(GreetingService.class); 36 | } 37 | 38 | @Test 39 | void shouldReturnSingleResponse() { 40 | StepVerifier.create(service.one("hello")) 41 | .expectNext("Echo:hello") 42 | .expectComplete() 43 | .verify(TIMEOUT); 44 | } 45 | 46 | @Test 47 | void shouldReturnSingleResponseWithSimpleLongDataRequest() { 48 | String data = new String(new char[500]); 49 | StepVerifier.create(service.one(data)) 50 | .expectNext("Echo:" + data) 51 | .expectComplete() 52 | .verify(TIMEOUT); 53 | } 54 | 55 | @Test 56 | void shouldReturnSingleResponseWithPojoRequest() { 57 | StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) 58 | .expectNextMatches(response -> "Echo:hello".equals(response.getText())) 59 | .expectComplete() 60 | .verify(TIMEOUT); 61 | } 62 | 63 | @Test 64 | void shouldReturnListResponseWithPojoRequest() { 65 | StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) 66 | .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) 67 | .expectComplete() 68 | .verify(TIMEOUT); 69 | } 70 | 71 | @Test 72 | void shouldReturnManyResponses() { 73 | int expectedResponseNum = 3; 74 | List expected = 75 | IntStream.range(0, expectedResponseNum) 76 | .mapToObj(i -> "Greeting (" + i + ") to: hello") 77 | .collect(Collectors.toList()); 78 | 79 | StepVerifier.create(service.many("hello").take(expectedResponseNum)) 80 | .expectNextSequence(expected) 81 | .expectComplete() 82 | .verify(TIMEOUT); 83 | } 84 | 85 | @Test 86 | void shouldReturnManyResponsesWithPojoRequest() { 87 | int expectedResponseNum = 3; 88 | List expected = 89 | IntStream.range(0, expectedResponseNum) 90 | .mapToObj(i -> new GreetingResponse("Greeting (" + i + ") to: hello")) 91 | .collect(Collectors.toList()); 92 | 93 | StepVerifier.create(service.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) 94 | .expectNextSequence(expected) 95 | .expectComplete() 96 | .verify(TIMEOUT); 97 | } 98 | 99 | @Test 100 | void shouldReturnErrorDataWhenServiceFails() { 101 | String req = "hello"; 102 | Mono result = service.failingOne(req); 103 | 104 | StepVerifier.create(result) 105 | .expectErrorMatches(throwable -> throwable instanceof InternalServiceException) 106 | .verify(TIMEOUT); 107 | } 108 | 109 | @Test 110 | void shouldReturnErrorDataWhenRequestDataIsEmpty() { 111 | Mono result = service.one(null); 112 | StepVerifier.create(result) 113 | .expectErrorMatches( 114 | throwable -> 115 | "Expected service request data of type: class java.lang.String, but received: null" 116 | .equals(throwable.getMessage())) 117 | .verify(TIMEOUT); 118 | } 119 | 120 | @Test 121 | void shouldSuccessfullyReuseServiceProxy() { 122 | StepVerifier.create(service.one("hello")) 123 | .expectNext("Echo:hello") 124 | .expectComplete() 125 | .verify(TIMEOUT); 126 | 127 | StepVerifier.create(service.one("hello")) 128 | .expectNext("Echo:hello") 129 | .expectComplete() 130 | .verify(TIMEOUT); 131 | } 132 | 133 | @Test 134 | void shouldReturnNoEventOnNeverService() { 135 | StepVerifier.create(service.neverOne("hi")) 136 | .expectSubscription() 137 | .expectNoEvent(Duration.ofSeconds(1)) 138 | .thenCancel() 139 | .verify(); 140 | } 141 | 142 | @Test 143 | void shouldReturnOnEmptyGreeting() { 144 | StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) 145 | .expectSubscription() 146 | .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) 147 | .thenCancel() 148 | .verify(); 149 | } 150 | 151 | @Test 152 | void shouldReturnOnEmptyMessageGreeting() { 153 | String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); 154 | ServiceMessage request = 155 | ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); 156 | StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) 157 | .expectSubscription() 158 | .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) 159 | .thenCancel() 160 | .verify(); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/http/HttpGateway.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import io.netty.handler.codec.http.HttpMethod; 4 | import io.netty.handler.codec.http.cors.CorsConfig; 5 | import io.netty.handler.codec.http.cors.CorsConfigBuilder; 6 | import io.netty.handler.codec.http.cors.CorsHandler; 7 | import io.scalecube.net.Address; 8 | import io.scalecube.services.exceptions.DefaultErrorMapper; 9 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 10 | import io.scalecube.services.gateway.Gateway; 11 | import io.scalecube.services.gateway.GatewayOptions; 12 | import io.scalecube.services.gateway.GatewayTemplate; 13 | import java.net.InetSocketAddress; 14 | import java.util.Map.Entry; 15 | import java.util.StringJoiner; 16 | import java.util.function.UnaryOperator; 17 | import reactor.core.publisher.Flux; 18 | import reactor.core.publisher.Mono; 19 | import reactor.netty.DisposableServer; 20 | import reactor.netty.http.server.HttpServer; 21 | import reactor.netty.resources.LoopResources; 22 | 23 | public class HttpGateway extends GatewayTemplate { 24 | 25 | private final ServiceProviderErrorMapper errorMapper; 26 | 27 | private DisposableServer server; 28 | private LoopResources loopResources; 29 | 30 | private boolean corsEnabled = false; 31 | private CorsConfigBuilder corsConfigBuilder = 32 | CorsConfigBuilder.forAnyOrigin() 33 | .allowNullOrigin() 34 | .maxAge(3600) 35 | .allowedRequestMethods(HttpMethod.POST); 36 | 37 | public HttpGateway(GatewayOptions options) { 38 | this(options, DefaultErrorMapper.INSTANCE); 39 | } 40 | 41 | public HttpGateway(GatewayOptions options, ServiceProviderErrorMapper errorMapper) { 42 | super(options); 43 | this.errorMapper = errorMapper; 44 | } 45 | 46 | private HttpGateway(HttpGateway other) { 47 | super(other.options); 48 | this.server = other.server; 49 | this.loopResources = other.loopResources; 50 | this.corsEnabled = other.corsEnabled; 51 | this.corsConfigBuilder = copy(other.corsConfigBuilder); 52 | this.errorMapper = other.errorMapper; 53 | } 54 | 55 | /** 56 | * CORS enable. 57 | * 58 | * @param corsEnabled if set to true. 59 | * @return HttpGateway with CORS settings. 60 | */ 61 | public HttpGateway corsEnabled(boolean corsEnabled) { 62 | HttpGateway g = new HttpGateway(this); 63 | g.corsEnabled = corsEnabled; 64 | return g; 65 | } 66 | 67 | /** 68 | * Configure CORS with options. 69 | * 70 | * @param op for CORS. 71 | * @return HttpGateway with CORS settings. 72 | */ 73 | public HttpGateway corsConfig(UnaryOperator op) { 74 | HttpGateway g = new HttpGateway(this); 75 | g.corsConfigBuilder = copy(op.apply(g.corsConfigBuilder)); 76 | return g; 77 | } 78 | 79 | private CorsConfigBuilder copy(CorsConfigBuilder other) { 80 | CorsConfig config = other.build(); 81 | CorsConfigBuilder corsConfigBuilder; 82 | if (config.isAnyOriginSupported()) { 83 | corsConfigBuilder = CorsConfigBuilder.forAnyOrigin(); 84 | } else { 85 | corsConfigBuilder = CorsConfigBuilder.forOrigins(config.origins().toArray(new String[0])); 86 | } 87 | 88 | if (!config.isCorsSupportEnabled()) { 89 | corsConfigBuilder.disable(); 90 | } 91 | 92 | corsConfigBuilder 93 | .exposeHeaders(config.exposedHeaders().toArray(new String[0])) 94 | .allowedRequestHeaders(config.allowedRequestHeaders().toArray(new String[0])) 95 | .allowedRequestMethods(config.allowedRequestMethods().toArray(new HttpMethod[0])) 96 | .maxAge(config.maxAge()); 97 | 98 | for (Entry header : config.preflightResponseHeaders()) { 99 | corsConfigBuilder.preflightResponseHeader(header.getKey(), header.getValue()); 100 | } 101 | 102 | if (config.isShortCircuit()) { 103 | corsConfigBuilder.shortCircuit(); 104 | } 105 | 106 | if (config.isNullOriginAllowed()) { 107 | corsConfigBuilder.allowNullOrigin(); 108 | } 109 | 110 | if (config.isCredentialsAllowed()) { 111 | corsConfigBuilder.allowCredentials(); 112 | } 113 | 114 | return corsConfigBuilder; 115 | } 116 | 117 | @Override 118 | public Mono start() { 119 | return Mono.defer( 120 | () -> { 121 | HttpGatewayAcceptor acceptor = new HttpGatewayAcceptor(options.call(), errorMapper); 122 | 123 | loopResources = LoopResources.create("http-gateway"); 124 | 125 | return prepareHttpServer(loopResources, options.port()) 126 | .handle(acceptor) 127 | .bind() 128 | .doOnSuccess(server -> this.server = server) 129 | .thenReturn(this); 130 | }); 131 | } 132 | 133 | @Override 134 | public Address address() { 135 | InetSocketAddress address = (InetSocketAddress) server.address(); 136 | return Address.create(address.getHostString(), address.getPort()); 137 | } 138 | 139 | @Override 140 | public Mono stop() { 141 | return Flux.concatDelayError(shutdownServer(server), shutdownLoopResources(loopResources)) 142 | .then(); 143 | } 144 | 145 | protected HttpServer prepareHttpServer(LoopResources loopResources, int port) { 146 | HttpServer httpServer = HttpServer.create(); 147 | 148 | if (loopResources != null) { 149 | httpServer = httpServer.runOn(loopResources); 150 | } 151 | 152 | return httpServer 153 | .bindAddress(() -> new InetSocketAddress(port)) 154 | .doOnConnection( 155 | connection -> { 156 | if (corsEnabled) { 157 | connection.addHandlerLast(new CorsHandler(corsConfigBuilder.build())); 158 | } 159 | }); 160 | } 161 | 162 | @Override 163 | public String toString() { 164 | return new StringJoiner(", ", HttpGateway.class.getSimpleName() + "[", "]") 165 | .add("server=" + server) 166 | .add("loopResources=" + loopResources) 167 | .add("corsEnabled=" + corsEnabled) 168 | .add("corsConfigBuilder=" + corsConfigBuilder) 169 | .add("options=" + options) 170 | .toString(); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /services-gateway-netty/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.http; 2 | 3 | import static io.netty.handler.codec.http.HttpHeaderNames.ALLOW; 4 | import static io.netty.handler.codec.http.HttpMethod.POST; 5 | import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED; 6 | import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT; 7 | import static io.netty.handler.codec.http.HttpResponseStatus.OK; 8 | 9 | import io.netty.buffer.ByteBuf; 10 | import io.netty.buffer.ByteBufAllocator; 11 | import io.netty.buffer.ByteBufOutputStream; 12 | import io.netty.buffer.Unpooled; 13 | import io.netty.handler.codec.http.HttpResponseStatus; 14 | import io.scalecube.services.ServiceCall; 15 | import io.scalecube.services.api.ErrorData; 16 | import io.scalecube.services.api.ServiceMessage; 17 | import io.scalecube.services.exceptions.DefaultErrorMapper; 18 | import io.scalecube.services.exceptions.ServiceProviderErrorMapper; 19 | import io.scalecube.services.gateway.ReferenceCountUtil; 20 | import io.scalecube.services.transport.api.DataCodec; 21 | import java.util.function.BiFunction; 22 | import org.reactivestreams.Publisher; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import reactor.core.publisher.Mono; 26 | import reactor.netty.ByteBufMono; 27 | import reactor.netty.http.server.HttpServerRequest; 28 | import reactor.netty.http.server.HttpServerResponse; 29 | 30 | public class HttpGatewayAcceptor 31 | implements BiFunction> { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayAcceptor.class); 34 | 35 | private static final String ERROR_NAMESPACE = "io.scalecube.services.error"; 36 | 37 | private final ServiceCall serviceCall; 38 | private final ServiceProviderErrorMapper errorMapper; 39 | 40 | HttpGatewayAcceptor(ServiceCall serviceCall) { 41 | this(serviceCall, DefaultErrorMapper.INSTANCE); 42 | } 43 | 44 | HttpGatewayAcceptor(ServiceCall serviceCall, ServiceProviderErrorMapper errorMapper) { 45 | this.serviceCall = serviceCall; 46 | this.errorMapper = errorMapper; 47 | } 48 | 49 | @Override 50 | public Publisher apply(HttpServerRequest httpRequest, HttpServerResponse httpResponse) { 51 | LOGGER.debug( 52 | "Accepted request: {}, headers: {}, params: {}", 53 | httpRequest, 54 | httpRequest.requestHeaders(), 55 | httpRequest.params()); 56 | 57 | if (httpRequest.method() != POST) { 58 | LOGGER.error("Unsupported HTTP method. Expected POST, actual {}", httpRequest.method()); 59 | return methodNotAllowed(httpResponse); 60 | } 61 | 62 | return httpRequest 63 | .receive() 64 | .aggregate() 65 | .switchIfEmpty(Mono.defer(() -> ByteBufMono.just(Unpooled.EMPTY_BUFFER))) 66 | .map(ByteBuf::retain) 67 | .flatMap(content -> handleRequest(content, httpRequest, httpResponse)) 68 | .onErrorResume(t -> error(httpResponse, errorMapper.toMessage(ERROR_NAMESPACE, t))); 69 | } 70 | 71 | private Mono handleRequest( 72 | ByteBuf content, HttpServerRequest httpRequest, HttpServerResponse httpResponse) { 73 | 74 | ServiceMessage request = 75 | ServiceMessage.builder().qualifier(getQualifier(httpRequest)).data(content).build(); 76 | 77 | return serviceCall 78 | .requestOne(request) 79 | .switchIfEmpty(Mono.defer(() -> emptyMessage(httpRequest))) 80 | .doOnError(th -> releaseRequestOnError(request)) 81 | .flatMap( 82 | response -> 83 | response.isError() // check error 84 | ? error(httpResponse, response) 85 | : response.hasData() // check data 86 | ? ok(httpResponse, response) 87 | : noContent(httpResponse)); 88 | } 89 | 90 | private Mono emptyMessage(HttpServerRequest httpRequest) { 91 | return Mono.just(ServiceMessage.builder().qualifier(getQualifier(httpRequest)).build()); 92 | } 93 | 94 | private static String getQualifier(HttpServerRequest httpRequest) { 95 | return httpRequest.uri().substring(1); 96 | } 97 | 98 | private Publisher methodNotAllowed(HttpServerResponse httpResponse) { 99 | return httpResponse.addHeader(ALLOW, POST.name()).status(METHOD_NOT_ALLOWED).send(); 100 | } 101 | 102 | private Mono error(HttpServerResponse httpResponse, ServiceMessage response) { 103 | int code = response.errorType(); 104 | HttpResponseStatus status = HttpResponseStatus.valueOf(code); 105 | 106 | ByteBuf content = 107 | response.hasData(ErrorData.class) 108 | ? encodeData(response.data(), response.dataFormatOrDefault()) 109 | : ((ByteBuf) response.data()); 110 | 111 | // send with publisher (defer buffer cleanup to netty) 112 | return httpResponse.status(status).send(Mono.just(content)).then(); 113 | } 114 | 115 | private Mono noContent(HttpServerResponse httpResponse) { 116 | return httpResponse.status(NO_CONTENT).send(); 117 | } 118 | 119 | private Mono ok(HttpServerResponse httpResponse, ServiceMessage response) { 120 | ByteBuf content = 121 | response.hasData(ByteBuf.class) 122 | ? ((ByteBuf) response.data()) 123 | : encodeData(response.data(), response.dataFormatOrDefault()); 124 | 125 | // send with publisher (defer buffer cleanup to netty) 126 | return httpResponse.status(OK).send(Mono.just(content)).then(); 127 | } 128 | 129 | private ByteBuf encodeData(Object data, String dataFormat) { 130 | ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(); 131 | 132 | try { 133 | DataCodec.getInstance(dataFormat).encode(new ByteBufOutputStream(byteBuf), data); 134 | } catch (Throwable t) { 135 | ReferenceCountUtil.safestRelease(byteBuf); 136 | LOGGER.error("Failed to encode data: {}", data, t); 137 | return Unpooled.EMPTY_BUFFER; 138 | } 139 | 140 | return byteBuf; 141 | } 142 | 143 | private void releaseRequestOnError(ServiceMessage request) { 144 | ReferenceCountUtil.safestRelease(request.data()); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /services-gateway-client-transport/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClient.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.transport.http; 2 | 3 | import static io.scalecube.reactor.RetryNonSerializedEmitFailureHandler.RETRY_NON_SERIALIZED; 4 | 5 | import io.netty.buffer.ByteBuf; 6 | import io.scalecube.services.api.ServiceMessage; 7 | import io.scalecube.services.api.ServiceMessage.Builder; 8 | import io.scalecube.services.gateway.transport.GatewayClient; 9 | import io.scalecube.services.gateway.transport.GatewayClientCodec; 10 | import io.scalecube.services.gateway.transport.GatewayClientSettings; 11 | import java.util.function.BiFunction; 12 | import org.reactivestreams.Publisher; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | import reactor.core.publisher.Sinks; 18 | import reactor.netty.NettyOutbound; 19 | import reactor.netty.http.client.HttpClient; 20 | import reactor.netty.http.client.HttpClientRequest; 21 | import reactor.netty.http.client.HttpClientResponse; 22 | import reactor.netty.resources.ConnectionProvider; 23 | import reactor.netty.resources.LoopResources; 24 | 25 | public final class HttpGatewayClient implements GatewayClient { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayClient.class); 28 | 29 | private final GatewayClientCodec codec; 30 | private final HttpClient httpClient; 31 | private final LoopResources loopResources; 32 | private final boolean ownsLoopResources; 33 | 34 | private final Sinks.One close = Sinks.one(); 35 | private final Sinks.One onClose = Sinks.one(); 36 | 37 | /** 38 | * Constructor. 39 | * 40 | * @param settings settings 41 | * @param codec codec 42 | */ 43 | public HttpGatewayClient(GatewayClientSettings settings, GatewayClientCodec codec) { 44 | this(settings, codec, LoopResources.create("http-gateway-client"), true); 45 | } 46 | 47 | /** 48 | * Constructor. 49 | * 50 | * @param settings settings 51 | * @param codec codec 52 | * @param loopResources loopResources 53 | */ 54 | public HttpGatewayClient( 55 | GatewayClientSettings settings, 56 | GatewayClientCodec codec, 57 | LoopResources loopResources) { 58 | this(settings, codec, loopResources, false); 59 | } 60 | 61 | private HttpGatewayClient( 62 | GatewayClientSettings settings, 63 | GatewayClientCodec codec, 64 | LoopResources loopResources, 65 | boolean ownsLoopResources) { 66 | 67 | this.codec = codec; 68 | this.loopResources = loopResources; 69 | this.ownsLoopResources = ownsLoopResources; 70 | 71 | HttpClient httpClient = 72 | HttpClient.create(ConnectionProvider.create("http-gateway-client")) 73 | .headers(headers -> settings.headers().forEach(headers::add)) 74 | .followRedirect(settings.followRedirect()) 75 | .wiretap(settings.wiretap()) 76 | .runOn(loopResources) 77 | .host(settings.host()) 78 | .port(settings.port()); 79 | 80 | if (settings.sslProvider() != null) { 81 | httpClient = httpClient.secure(settings.sslProvider()); 82 | } 83 | 84 | this.httpClient = httpClient; 85 | 86 | // Setup cleanup 87 | close 88 | .asMono() 89 | .then(doClose()) 90 | .doFinally(s -> onClose.emitEmpty(RETRY_NON_SERIALIZED)) 91 | .doOnTerminate(() -> LOGGER.info("Closed HttpGatewayClient resources")) 92 | .subscribe(null, ex -> LOGGER.warn("Exception occurred on HttpGatewayClient close: " + ex)); 93 | } 94 | 95 | @Override 96 | public Mono requestResponse(ServiceMessage request) { 97 | return Mono.defer( 98 | () -> { 99 | BiFunction> sender = 100 | (httpRequest, out) -> { 101 | LOGGER.debug("Sending request {}", request); 102 | // prepare request headers 103 | request.headers().forEach(httpRequest::header); 104 | // send with publisher (defer buffer cleanup to netty) 105 | return out.sendObject(Mono.just(codec.encode(request))).then(); 106 | }; 107 | return httpClient 108 | .post() 109 | .uri("/" + request.qualifier()) 110 | .send(sender) 111 | .responseSingle( 112 | (httpResponse, bbMono) -> 113 | bbMono.map(ByteBuf::retain).map(content -> toMessage(httpResponse, content))); 114 | }); 115 | } 116 | 117 | @Override 118 | public Flux requestStream(ServiceMessage request) { 119 | return Flux.error( 120 | new UnsupportedOperationException("requestStream is not supported by HTTP/1.x")); 121 | } 122 | 123 | @Override 124 | public Flux requestChannel(Flux requests) { 125 | return Flux.error( 126 | new UnsupportedOperationException("requestChannel is not supported by HTTP/1.x")); 127 | } 128 | 129 | @Override 130 | public void close() { 131 | close.emitEmpty(RETRY_NON_SERIALIZED); 132 | } 133 | 134 | @Override 135 | public Mono onClose() { 136 | return onClose.asMono(); 137 | } 138 | 139 | private Mono doClose() { 140 | return ownsLoopResources ? Mono.defer(loopResources::disposeLater) : Mono.empty(); 141 | } 142 | 143 | private ServiceMessage toMessage(HttpClientResponse httpResponse, ByteBuf content) { 144 | Builder builder = ServiceMessage.builder().qualifier(httpResponse.uri()).data(content); 145 | 146 | int httpCode = httpResponse.status().code(); 147 | if (isError(httpCode)) { 148 | builder.header(ServiceMessage.HEADER_ERROR_TYPE, String.valueOf(httpCode)); 149 | } 150 | 151 | // prepare response headers 152 | httpResponse 153 | .responseHeaders() 154 | .entries() 155 | .forEach(entry -> builder.header(entry.getKey(), entry.getValue())); 156 | ServiceMessage message = builder.build(); 157 | 158 | LOGGER.debug("Received response {}", message); 159 | return message; 160 | } 161 | 162 | private boolean isError(int httpCode) { 163 | return httpCode >= 400 && httpCode <= 599; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import io.scalecube.services.api.Qualifier; 6 | import io.scalecube.services.api.ServiceMessage; 7 | import io.scalecube.services.examples.EmptyGreetingRequest; 8 | import io.scalecube.services.examples.EmptyGreetingResponse; 9 | import io.scalecube.services.examples.GreetingRequest; 10 | import io.scalecube.services.examples.GreetingResponse; 11 | import io.scalecube.services.examples.GreetingService; 12 | import io.scalecube.services.examples.GreetingServiceImpl; 13 | import io.scalecube.services.exceptions.InternalServiceException; 14 | import io.scalecube.services.exceptions.ServiceUnavailableException; 15 | import io.scalecube.services.gateway.BaseTest; 16 | import java.time.Duration; 17 | import java.util.List; 18 | import java.util.stream.Collectors; 19 | import java.util.stream.IntStream; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Disabled; 22 | import org.junit.jupiter.api.Test; 23 | import org.junit.jupiter.api.extension.RegisterExtension; 24 | import reactor.test.StepVerifier; 25 | 26 | class WebsocketGatewayTest extends BaseTest { 27 | 28 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 29 | 30 | @RegisterExtension 31 | static WebsocketGatewayExtension extension = 32 | new WebsocketGatewayExtension(new GreetingServiceImpl()); 33 | 34 | private GreetingService service; 35 | 36 | @BeforeEach 37 | void initService() { 38 | service = extension.client().api(GreetingService.class); 39 | } 40 | 41 | @Test 42 | void shouldReturnSingleResponseWithSimpleRequest() { 43 | StepVerifier.create(service.one("hello")) 44 | .expectNext("Echo:hello") 45 | .expectComplete() 46 | .verify(TIMEOUT); 47 | } 48 | 49 | @Test 50 | void shouldReturnSingleResponseWithSimpleLongDataRequest() { 51 | String data = new String(new char[500]); 52 | StepVerifier.create(service.one(data)) 53 | .expectNext("Echo:" + data) 54 | .expectComplete() 55 | .verify(TIMEOUT); 56 | } 57 | 58 | @Test 59 | void shouldReturnSingleResponseWithPojoRequest() { 60 | StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) 61 | .expectNextMatches(response -> "Echo:hello".equals(response.getText())) 62 | .expectComplete() 63 | .verify(TIMEOUT); 64 | } 65 | 66 | @Test 67 | void shouldReturnListResponseWithPojoRequest() { 68 | StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) 69 | .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) 70 | .expectComplete() 71 | .verify(TIMEOUT); 72 | } 73 | 74 | @Test 75 | void shouldReturnManyResponsesWithSimpleRequest() { 76 | int expectedResponseNum = 3; 77 | List expected = 78 | IntStream.range(0, expectedResponseNum) 79 | .mapToObj(i -> "Greeting (" + i + ") to: hello") 80 | .collect(Collectors.toList()); 81 | 82 | StepVerifier.create(service.many("hello").take(expectedResponseNum)) 83 | .expectNextSequence(expected) 84 | .expectComplete() 85 | .verify(TIMEOUT); 86 | } 87 | 88 | @Test 89 | void shouldReturnManyResponsesWithPojoRequest() { 90 | int expectedResponseNum = 3; 91 | List expected = 92 | IntStream.range(0, expectedResponseNum) 93 | .mapToObj(i -> new GreetingResponse("Greeting (" + i + ") to: hello")) 94 | .collect(Collectors.toList()); 95 | 96 | StepVerifier.create(service.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) 97 | .expectNextSequence(expected) 98 | .expectComplete() 99 | .verify(TIMEOUT); 100 | } 101 | 102 | @Test 103 | void shouldReturnExceptionWhenServiceIsDown() { 104 | extension.shutdownServices(); 105 | 106 | StepVerifier.create(service.one("hello")) 107 | .expectErrorMatches( 108 | throwable -> 109 | throwable instanceof ServiceUnavailableException 110 | && throwable.getMessage().startsWith("No reachable member with such service")) 111 | .verify(TIMEOUT); 112 | } 113 | 114 | @Test 115 | void shouldReturnErrorDataWhenServiceFails() { 116 | StepVerifier.create(service.failingOne("hello")) 117 | .expectErrorMatches(throwable -> throwable instanceof InternalServiceException) 118 | .verify(TIMEOUT); 119 | } 120 | 121 | @Test 122 | void shouldReturnErrorDataWhenRequestDataIsEmpty() { 123 | StepVerifier.create(service.one(null)) 124 | .expectErrorMatches( 125 | throwable -> 126 | "Expected service request data of type: class java.lang.String, but received: null" 127 | .equals(throwable.getMessage())) 128 | .verify(TIMEOUT); 129 | } 130 | 131 | @Test 132 | void shouldReturnNoEventOnNeverService() { 133 | StepVerifier.create(service.neverOne("hi")) 134 | .expectSubscription() 135 | .expectNoEvent(Duration.ofSeconds(1)) 136 | .thenCancel() 137 | .verify(); 138 | } 139 | 140 | @Test 141 | void shouldReturnOnEmptyGreeting() { 142 | StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) 143 | .expectSubscription() 144 | .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) 145 | .thenCancel() 146 | .verify(); 147 | } 148 | 149 | @Test 150 | void shouldReturnOnEmptyMessageGreeting() { 151 | String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); 152 | ServiceMessage request = 153 | ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); 154 | StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) 155 | .expectSubscription() 156 | .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) 157 | .thenCancel() 158 | .verify(); 159 | } 160 | 161 | @Disabled("https://github.com/scalecube/scalecube-services/issues/742") 162 | public void testManyStreamBlockFirst() { 163 | for (int i = 0; i < 100; i++) { 164 | //noinspection ConstantConditions 165 | long first = service.manyStream(30L).filter(k -> k != 0).take(1).blockFirst(); 166 | assertEquals(1, first); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/rsocket/RSocketGatewayTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.rsocket; 2 | 3 | import io.scalecube.services.api.Qualifier; 4 | import io.scalecube.services.api.ServiceMessage; 5 | import io.scalecube.services.examples.EmptyGreetingRequest; 6 | import io.scalecube.services.examples.EmptyGreetingResponse; 7 | import io.scalecube.services.examples.GreetingRequest; 8 | import io.scalecube.services.examples.GreetingResponse; 9 | import io.scalecube.services.examples.GreetingService; 10 | import io.scalecube.services.examples.GreetingServiceImpl; 11 | import io.scalecube.services.exceptions.InternalServiceException; 12 | import io.scalecube.services.exceptions.ServiceUnavailableException; 13 | import io.scalecube.services.gateway.BaseTest; 14 | import java.time.Duration; 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.IntStream; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.RegisterExtension; 21 | import reactor.core.publisher.Mono; 22 | import reactor.test.StepVerifier; 23 | 24 | class RSocketGatewayTest extends BaseTest { 25 | 26 | private static final Duration TIMEOUT = Duration.ofSeconds(3); 27 | 28 | @RegisterExtension 29 | static RSocketGatewayExtension extension = new RSocketGatewayExtension(new GreetingServiceImpl()); 30 | 31 | private GreetingService service; 32 | 33 | @BeforeEach 34 | void initService() { 35 | service = extension.client().api(GreetingService.class); 36 | } 37 | 38 | @Test 39 | void shouldReturnSingleResponse() { 40 | StepVerifier.create(service.one("hello")) 41 | .expectNext("Echo:hello") 42 | .expectComplete() 43 | .verify(TIMEOUT); 44 | } 45 | 46 | @Test 47 | void shouldReturnSingleResponseWithSimpleLongDataRequest() { 48 | String data = new String(new char[500]); 49 | StepVerifier.create(service.one(data)) 50 | .expectNext("Echo:" + data) 51 | .expectComplete() 52 | .verify(TIMEOUT); 53 | } 54 | 55 | @Test 56 | void shouldReturnSingleResponseWithPojoRequest() { 57 | StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) 58 | .expectNextMatches(response -> "Echo:hello".equals(response.getText())) 59 | .expectComplete() 60 | .verify(TIMEOUT); 61 | } 62 | 63 | @Test 64 | void shouldReturnListResponseWithPojoRequest() { 65 | StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) 66 | .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) 67 | .expectComplete() 68 | .verify(TIMEOUT); 69 | } 70 | 71 | @Test 72 | void shouldReturnManyResponses() { 73 | int expectedResponseNum = 3; 74 | List expected = 75 | IntStream.range(0, expectedResponseNum) 76 | .mapToObj(i -> "Greeting (" + i + ") to: hello") 77 | .collect(Collectors.toList()); 78 | 79 | StepVerifier.create(service.many("hello").take(expectedResponseNum)) 80 | .expectNextSequence(expected) 81 | .expectComplete() 82 | .verify(TIMEOUT); 83 | } 84 | 85 | @Test 86 | void shouldReturnManyResponsesWithPojoRequest() { 87 | int expectedResponseNum = 3; 88 | List expected = 89 | IntStream.range(0, expectedResponseNum) 90 | .mapToObj(i -> new GreetingResponse("Greeting (" + i + ") to: hello")) 91 | .collect(Collectors.toList()); 92 | 93 | StepVerifier.create(service.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) 94 | .expectNextSequence(expected) 95 | .expectComplete() 96 | .verify(TIMEOUT); 97 | } 98 | 99 | @Test 100 | void shouldReturnExceptionWhenServiceIsDown() { 101 | extension.shutdownServices(); 102 | 103 | StepVerifier.create(service.one("hello")) 104 | .expectErrorMatches( 105 | throwable -> 106 | throwable instanceof ServiceUnavailableException 107 | && throwable.getMessage().startsWith("No reachable member with such service")) 108 | .verify(TIMEOUT); 109 | } 110 | 111 | @Test 112 | void shouldReturnErrorDataWhenServiceFails() { 113 | String req = "hello"; 114 | Mono result = service.failingOne(req); 115 | 116 | StepVerifier.create(result) 117 | .expectErrorMatches(throwable -> throwable instanceof InternalServiceException) 118 | .verify(TIMEOUT); 119 | } 120 | 121 | @Test 122 | void shouldReturnErrorDataWhenRequestDataIsEmpty() { 123 | Mono result = service.one(null); 124 | StepVerifier.create(result) 125 | .expectErrorMatches( 126 | throwable -> 127 | "Expected service request data of type: class java.lang.String, but received: null" 128 | .equals(throwable.getMessage())) 129 | .verify(TIMEOUT); 130 | } 131 | 132 | @Test 133 | void shouldSuccessfullyReuseServiceProxy() { 134 | StepVerifier.create(service.one("hello")) 135 | .expectNext("Echo:hello") 136 | .expectComplete() 137 | .verify(TIMEOUT); 138 | 139 | StepVerifier.create(service.one("hello")) 140 | .expectNext("Echo:hello") 141 | .expectComplete() 142 | .verify(TIMEOUT); 143 | } 144 | 145 | @Test 146 | void shouldReturnNoEventOnNeverService() { 147 | StepVerifier.create(service.neverOne("hi")) 148 | .expectSubscription() 149 | .expectNoEvent(Duration.ofSeconds(1)) 150 | .thenCancel() 151 | .verify(); 152 | } 153 | 154 | @Test 155 | void shouldReturnOnEmptyGreeting() { 156 | StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) 157 | .expectSubscription() 158 | .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) 159 | .thenCancel() 160 | .verify(); 161 | } 162 | 163 | @Test 164 | void shouldReturnOnEmptyMessageGreeting() { 165 | String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); 166 | ServiceMessage request = 167 | ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); 168 | StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) 169 | .expectSubscription() 170 | .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) 171 | .thenCancel() 172 | .verify(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServerTest.java: -------------------------------------------------------------------------------- 1 | package io.scalecube.services.gateway.websocket; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.scalecube.net.Address; 5 | import io.scalecube.services.Microservices; 6 | import io.scalecube.services.ServiceCall; 7 | import io.scalecube.services.annotations.Service; 8 | import io.scalecube.services.annotations.ServiceMethod; 9 | import io.scalecube.services.discovery.ScalecubeServiceDiscovery; 10 | import io.scalecube.services.gateway.BaseTest; 11 | import io.scalecube.services.gateway.TestGatewaySessionHandler; 12 | import io.scalecube.services.gateway.transport.GatewayClient; 13 | import io.scalecube.services.gateway.transport.GatewayClientCodec; 14 | import io.scalecube.services.gateway.transport.GatewayClientSettings; 15 | import io.scalecube.services.gateway.transport.GatewayClientTransport; 16 | import io.scalecube.services.gateway.transport.GatewayClientTransports; 17 | import io.scalecube.services.gateway.transport.StaticAddressRouter; 18 | import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClient; 19 | import io.scalecube.services.gateway.ws.WebsocketGateway; 20 | import io.scalecube.services.transport.rsocket.RSocketServiceTransport; 21 | import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; 22 | import java.time.Duration; 23 | import java.util.stream.Collectors; 24 | import java.util.stream.IntStream; 25 | import org.junit.jupiter.api.AfterAll; 26 | import org.junit.jupiter.api.AfterEach; 27 | import org.junit.jupiter.api.BeforeAll; 28 | import org.junit.jupiter.api.RepeatedTest; 29 | import reactor.core.publisher.Flux; 30 | import reactor.core.publisher.Mono; 31 | import reactor.netty.resources.LoopResources; 32 | import reactor.test.StepVerifier; 33 | 34 | class WebsocketServerTest extends BaseTest { 35 | 36 | public static final GatewayClientCodec CLIENT_CODEC = 37 | GatewayClientTransports.WEBSOCKET_CLIENT_CODEC; 38 | 39 | private static Microservices gateway; 40 | private static Address gatewayAddress; 41 | private static GatewayClient client; 42 | private static LoopResources loopResources; 43 | 44 | @BeforeAll 45 | static void beforeAll() { 46 | loopResources = LoopResources.create("websocket-gateway-client"); 47 | 48 | gateway = 49 | Microservices.builder() 50 | .discovery( 51 | serviceEndpoint -> 52 | new ScalecubeServiceDiscovery() 53 | .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) 54 | .options(opts -> opts.metadata(serviceEndpoint))) 55 | .transport(RSocketServiceTransport::new) 56 | .gateway( 57 | options -> new WebsocketGateway(options.id("WS"), new TestGatewaySessionHandler())) 58 | .transport(RSocketServiceTransport::new) 59 | .services(new TestServiceImpl()) 60 | .startAwait(); 61 | gatewayAddress = gateway.gateway("WS").address(); 62 | } 63 | 64 | @AfterEach 65 | void afterEach() { 66 | final GatewayClient client = WebsocketServerTest.client; 67 | if (client != null) { 68 | client.close(); 69 | } 70 | } 71 | 72 | @AfterAll 73 | static void afterAll() { 74 | final GatewayClient client = WebsocketServerTest.client; 75 | if (client != null) { 76 | client.close(); 77 | } 78 | 79 | Mono.justOrEmpty(gateway).map(Microservices::shutdown).then().block(); 80 | 81 | if (loopResources != null) { 82 | loopResources.disposeLater().block(); 83 | } 84 | } 85 | 86 | @RepeatedTest(100) 87 | void testMessageSequence() { 88 | 89 | client = 90 | new WebsocketGatewayClient( 91 | GatewayClientSettings.builder().address(gatewayAddress).build(), 92 | CLIENT_CODEC, 93 | loopResources); 94 | 95 | ServiceCall serviceCall = 96 | new ServiceCall() 97 | .transport(new GatewayClientTransport(client)) 98 | .router(new StaticAddressRouter(gatewayAddress)); 99 | 100 | int count = 100; 101 | 102 | StepVerifier.create(serviceCall.api(TestService.class).many(count) /*.log("<<< ")*/) 103 | .expectNextSequence(IntStream.range(0, count).boxed().collect(Collectors.toList())) 104 | .expectComplete() 105 | .verify(Duration.ofSeconds(10)); 106 | } 107 | 108 | @Service 109 | public interface TestService { 110 | 111 | @ServiceMethod("many") 112 | Flux many(int count); 113 | } 114 | 115 | private static class TestServiceImpl implements TestService { 116 | 117 | @Override 118 | public Flux many(int count) { 119 | return Flux.using( 120 | ReactiveAdapter::new, 121 | reactiveAdapter -> 122 | reactiveAdapter 123 | .receive() 124 | .take(count) 125 | .cast(Integer.class) 126 | .doOnSubscribe( 127 | s -> 128 | new Thread( 129 | () -> { 130 | for (int i = 0; ; ) { 131 | int r = (int) reactiveAdapter.requested(100); 132 | 133 | if (reactiveAdapter.isFastPath()) { 134 | try { 135 | if (reactiveAdapter.isDisposed()) { 136 | return; 137 | } 138 | reactiveAdapter.tryNext(i++); 139 | reactiveAdapter.incrementProduced(); 140 | } catch (Throwable e) { 141 | reactiveAdapter.lastError(e); 142 | return; 143 | } 144 | } else if (r > 0) { 145 | try { 146 | if (reactiveAdapter.isDisposed()) { 147 | return; 148 | } 149 | reactiveAdapter.tryNext(i++); 150 | reactiveAdapter.incrementProduced(); 151 | } catch (Throwable e) { 152 | reactiveAdapter.lastError(e); 153 | return; 154 | } 155 | 156 | reactiveAdapter.commitProduced(); 157 | } 158 | } 159 | }) 160 | .start()), 161 | ReactiveAdapter::dispose); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /services-gateway-tests/src/test/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 39 | 40 | 41 | 200 | 201 | 202 |
203 |
204 |

205 | 206 | 207 |

208 | 209 |
210 |

211 |

212 | 213 |

214 |

215 |

216 |

217 | 218 | 224 |

225 |
226 |

227 | 228 |

229 |
230 |
231 |

Message Log

232 |

233 |
234 |
235 | 236 | 237 | --------------------------------------------------------------------------------