diff --git a/packages/rsocket-examples/package.json b/packages/rsocket-examples/package.json index 4bc6d40..b8202e2 100644 --- a/packages/rsocket-examples/package.json +++ b/packages/rsocket-examples/package.json @@ -21,7 +21,11 @@ "start-client-server-rxjs-messaging-composite-metadata-route": "ts-node -r tsconfig-paths/register src/rxjs/RxjsMessagingCompositeMetadataRouteExample.ts", "start-client-server-rxjs-requester-responder": "ts-node -r tsconfig-paths/register src/rxjs/RxjsRequesterResponderExample.ts", "start-client-apollo-graphql": "ts-node -r tsconfig-paths/register src/graphql/apollo/client/example.ts", - "start-client-server-apollo-graphql": "ts-node -r tsconfig-paths/register src/graphql/apollo/client-server/example.ts" + "start-client-server-apollo-graphql": "ts-node -r tsconfig-paths/register src/graphql/apollo/client-server/example.ts", + "start-client-server-composite-metadata-auth-example-client": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth/client.ts", + "start-client-server-composite-metadata-auth-example-server": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth/server.ts", + "start-client-server-composite-metadata-auth-setup-frame-example-client": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth-setup-frame/client.ts", + "start-client-server-composite-metadata-auth-setup-frame-example-server": "ts-node -r tsconfig-paths/register src/composite-metadata/bearer-token-auth-setup-frame/server.ts" }, "dependencies": { "@apollo/client": "^3.5.10", diff --git a/packages/rsocket-examples/src/composite-metadata/bearer-token-auth-setup-frame/client.ts b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth-setup-frame/client.ts new file mode 100644 index 0000000..16ac4de --- /dev/null +++ b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth-setup-frame/client.ts @@ -0,0 +1,158 @@ +/* + * Copyright 2021-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Payload, RSocket, RSocketConnector } from "rsocket-core"; +import { TcpClientTransport } from "rsocket-tcp-client"; +import { + encodeBearerAuthMetadata, + encodeCompositeMetadata, + encodeRoute, + WellKnownMimeType, +} from "rsocket-composite-metadata"; +import { exit } from "process"; +import Logger from "../../shared/logger"; +import MESSAGE_RSOCKET_ROUTING = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; +import MESSAGE_RSOCKET_AUTHENTICATION = WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION; + +function makeMetadata(bearerToken?: string, route?: string) { + const map = new Map(); + + if (bearerToken) { + map.set( + MESSAGE_RSOCKET_AUTHENTICATION, + encodeBearerAuthMetadata(Buffer.from(bearerToken)) + ); + } + + if (route) { + const encodedRoute = encodeRoute(route); + map.set(MESSAGE_RSOCKET_ROUTING, encodedRoute); + } + + return encodeCompositeMetadata(map); +} + +function makeConnector(token: string) { + // NOTE: THIS EXAMPLE DOES NOT COVER TLS. + // ALWAYS USE A SECURE CONNECTION SUCH AS TLS WHEN TRANSMITTING SENSITIVE INFORMATION SUCH AS AUTH TOKENS. + return new RSocketConnector({ + transport: new TcpClientTransport({ + connectionOptions: { + host: "127.0.0.1", + port: 9090, + }, + }), + setup: { + payload: { + data: Buffer.from([]), + metadata: makeMetadata(token), + }, + }, + }); +} + +async function requestResponse( + rsocket: RSocket, + compositeMetaData: Buffer, + message: string = "" +): Promise { + return new Promise((resolve, reject) => { + return rsocket.requestResponse( + { + data: Buffer.from(message), + metadata: compositeMetaData, + }, + { + onError: (e) => { + reject(e); + }, + onNext: (payload, isComplete) => { + Logger.info( + `onNext payload[data: ${payload.data}; metadata: ${payload.metadata}]|${isComplete}` + ); + resolve(payload); + }, + onComplete: () => {}, + onExtension: () => {}, + } + ); + }); +} + +async function main() { + try { + // we expect this connection to fail because we aren't passing a valid token + const connector = makeConnector(""); + const rsocket = await connector.connect(); + await new Promise(function (resolve, reject) { + Logger.info("Rejecting once socket closes..."); + rsocket.onClose((e) => { + reject(e); + }); + }); + } catch (e) { + Logger.error(`Expected error: ${e}`); + } + + // NOTE: YOU SHOULD NEVER HARD CODE AN AUTH TOKEN IN A FILE IN THIS WAY. THIS IS PURELY FOR EXAMPLE PURPOSES. + // The SHA1 HASH of rsocket-js-2024-10 + const exampleToken = "8a7d50f76ef86c75bd3563e55f8835515189dbff"; + + // we expect this connection to succeed because we pass a valid token + const connector = makeConnector(exampleToken); + const rsocket = await connector.connect(); + + // this request SHOULD pass + const echoResponse = await requestResponse( + rsocket, + makeMetadata(null, "EchoService.echo"), + "Hello World" + ); + Logger.info(`EchoService.echo response: ${echoResponse.data.toString()}`); + + // this request will reject (unknown route) + try { + await requestResponse( + rsocket, + makeMetadata(null, "UnknownService.unknown"), + "Hello World" + ); + } catch (e) { + Logger.error(`Expected error: ${e}`); + } + + // this request will reject (no routing data) + try { + await requestResponse(rsocket, makeMetadata(null), "Hello World"); + } catch (e) { + Logger.error(`Expected error: ${e}`); + } + + const whoAmiResponse = await requestResponse( + rsocket, + makeMetadata(exampleToken, "AuthService.whoAmI") + ); + Logger.info(`AuthService.whoAmI response: ${whoAmiResponse.data.toString()}`); +} + +main() + .then(() => exit()) + .catch((error: Error) => { + Logger.error(error); + setTimeout(() => { + exit(1); + }); + }); diff --git a/packages/rsocket-examples/src/composite-metadata/bearer-token-auth-setup-frame/server.ts b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth-setup-frame/server.ts new file mode 100644 index 0000000..edbfbe1 --- /dev/null +++ b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth-setup-frame/server.ts @@ -0,0 +1,294 @@ +/* + * Copyright 2021-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Closeable, + ErrorCodes, + OnExtensionSubscriber, + OnNextSubscriber, + OnTerminalSubscriber, + Payload, + RSocket, + RSocketError, + RSocketServer, + SetupPayload, +} from "rsocket-core"; +import { TcpServerTransport } from "rsocket-tcp-server"; +import { + decodeAuthMetadata, + decodeCompositeMetadata, + decodeRoutes, + WellKnownAuthType, + WellKnownMimeType, +} from "rsocket-composite-metadata"; +import { exit } from "process"; +import Logger from "../../shared/logger"; +import MESSAGE_RSOCKET_ROUTING = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; +import MESSAGE_RSOCKET_AUTHENTICATION = WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION; +import BEARER = WellKnownAuthType.BEARER; + +let serverCloseable: Closeable; + +// NOTE: YOU SHOULD NEVER HARD CODE AN AUTH TOKEN IN A FILE IN THIS WAY. THIS IS PURELY FOR EXAMPLE PURPOSES. +// The SHA1 HASH of rsocket-js-2024-10 +const expectedExampleToken = "8a7d50f76ef86c75bd3563e55f8835515189dbff"; + +const tokenToUserContext = { + [expectedExampleToken]: { + firstName: "bob", + lastName: "builder", + }, +}; + +function mapMetaData(payload: Payload) { + const mappedMetaData = new Map(); + if (payload.metadata) { + const decodedCompositeMetaData = decodeCompositeMetadata(payload.metadata); + + for (let metaData of decodedCompositeMetaData) { + switch (metaData.mimeType) { + case MESSAGE_RSOCKET_ROUTING.toString(): { + const tags = []; + for (let decodedRoute of decodeRoutes(metaData.content)) { + tags.push(decodedRoute); + } + const joinedRoute = tags.join("."); + mappedMetaData.set(MESSAGE_RSOCKET_ROUTING.toString(), joinedRoute); + break; + } + + case MESSAGE_RSOCKET_AUTHENTICATION.toString(): { + const auth = decodeAuthMetadata(metaData.content); + mappedMetaData.set(MESSAGE_RSOCKET_AUTHENTICATION.toString(), auth); + break; + } + } + } + } + return mappedMetaData; +} + +class EchoService { + handleEchoRequestResponse( + responderStream: OnTerminalSubscriber & + OnNextSubscriber & + OnExtensionSubscriber, + payload: Payload, + mappedMetaData: Map + ) { + const timeout = setTimeout(() => { + responderStream.onNext( + { + data: Buffer.concat([Buffer.from("Echo: "), payload.data]), + }, + true + ); + }, 1000); + return { + cancel: () => { + clearTimeout(timeout); + Logger.info("Request cancelled..."); + }, + onExtension: () => { + Logger.info("Received Extension request"); + }, + }; + } +} + +class AuthService { + getUserContextForToken(mappedMetaData: Map) { + const authContext = mappedMetaData.get( + MESSAGE_RSOCKET_AUTHENTICATION.toString() + ); + const authToken = authContext.payload.toString(); + return tokenToUserContext[authToken]; + } + + handleWhoAmIRequestResponse( + responderStream: OnTerminalSubscriber & + OnNextSubscriber & + OnExtensionSubscriber, + payload: Payload, + mappedMetaData: Map + ) { + const timeout = setTimeout(() => { + const userContext = this.getUserContextForToken(mappedMetaData); + if (!userContext) { + responderStream.onError( + new RSocketError( + ErrorCodes.REJECTED, + "No user found for given token." + ) + ); + return; + } + responderStream.onNext( + { + data: Buffer.from(JSON.stringify(userContext)), + }, + true + ); + }); + return { + cancel: () => { + clearTimeout(timeout); + Logger.info("Request cancelled..."); + }, + onExtension: () => { + Logger.info("Received Extension request"); + }, + }; + } +} + +function authMiddleware(mappedMetaData: Map) { + const auth = mappedMetaData.get(MESSAGE_RSOCKET_AUTHENTICATION.toString()); + if (!auth) { + return new RSocketError( + ErrorCodes.REJECTED, + "Missing authentication context." + ); + } + if (auth.type.identifier !== BEARER.identifier) { + return new RSocketError( + ErrorCodes.REJECTED, + `Unsupported authentication type provided. Identifier=${auth.type.identifier}` + ); + } + const token = auth.payload.toString(); + if (token !== expectedExampleToken) { + return new RSocketError( + ErrorCodes.REJECTED, + `Invalid Bearer token provided.` + ); + } +} + +function makeServer() { + // NOTE: THIS EXAMPLE DOES NOT COVER TLS. + // ALWAYS USE A SECURE CONNECTION SUCH AS TLS WHEN TRANSMITTING SENSITIVE INFORMATION SUCH AS AUTH TOKENS. + return new RSocketServer({ + transport: new TcpServerTransport({ + listenOptions: { + port: 9090, + host: "127.0.0.1", + }, + }), + acceptor: { + accept: async (payload: SetupPayload, remotePeer: RSocket) => { + const echoService = new EchoService(); + const authService = new AuthService(); + const setupMetaData = mapMetaData(payload); + const authError = authMiddleware(setupMetaData); + if (authError) { + Logger.error( + `Auth error during setup. Peer will be closed. Caused by: ${authError}` + ); + remotePeer.close(authError); + return {}; + } + const userContext = authService.getUserContextForToken(setupMetaData); + Logger.info(`User connected... ${JSON.stringify(userContext)}`); + remotePeer.onClose(() => { + Logger.info(`User disconnected... ${JSON.stringify(userContext)}`); + }); + return { + requestResponse: ( + payload: Payload, + responderStream: OnTerminalSubscriber & + OnNextSubscriber & + OnExtensionSubscriber + ) => { + const mappedMetaData = mapMetaData(payload); + + const defaultSubscriber = { + cancel() { + Logger.info("Request cancelled..."); + }, + onExtension() {}, + }; + + const route = mappedMetaData.get( + MESSAGE_RSOCKET_ROUTING.toString() + ); + if (!route) { + responderStream.onError( + new RSocketError( + ErrorCodes.REJECTED, + "Composite metadata did not include routing information." + ) + ); + return defaultSubscriber; + } + + Logger.info(`Handling ${route}`); + + switch (route) { + case "EchoService.echo": { + return echoService.handleEchoRequestResponse( + responderStream, + payload, + mappedMetaData + ); + } + case "AuthService.whoAmI": { + return authService.handleWhoAmIRequestResponse( + responderStream, + payload, + mappedMetaData + ); + } + + default: { + responderStream.onError( + new RSocketError( + ErrorCodes.REJECTED, + "The encoded route was unknown by the server." + ) + ); + return defaultSubscriber; + } + } + }, + }; + }, + }, + }); +} + +async function main() { + const server = makeServer(); + + serverCloseable = await server.bind(); + + Logger.info("Server bound..."); + + await new Promise((resolve, reject) => { + serverCloseable.onClose((e) => { + Logger.info("Server closed..."); + if (e) return reject(e); + resolve(null); + }); + }); +} + +main() + .then(() => exit()) + .catch((error: Error) => { + console.error(error); + exit(1); + }); diff --git a/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/client.ts b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/client.ts new file mode 100644 index 0000000..5894d0d --- /dev/null +++ b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/client.ts @@ -0,0 +1,160 @@ +/* + * Copyright 2021-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Payload, RSocket, RSocketConnector } from "rsocket-core"; +import { TcpClientTransport } from "rsocket-tcp-client"; +import { + encodeBearerAuthMetadata, + encodeCompositeMetadata, + encodeRoute, + WellKnownMimeType, +} from "rsocket-composite-metadata"; +import { exit } from "process"; +import Logger from "../../shared/logger"; +import MESSAGE_RSOCKET_ROUTING = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; +import MESSAGE_RSOCKET_AUTHENTICATION = WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION; + +function makeConnector() { + // NOTE: THIS EXAMPLE DOES NOT COVER TLS. + // ALWAYS USE A SECURE CONNECTION SUCH AS TLS WHEN TRANSMITTING SENSITIVE INFORMATION SUCH AS AUTH TOKENS. + return new RSocketConnector({ + transport: new TcpClientTransport({ + connectionOptions: { + host: "127.0.0.1", + port: 9090, + }, + }), + }); +} + +function makeMetadata(bearerToken?: string, route?: string) { + const map = new Map(); + + if (bearerToken) { + map.set( + MESSAGE_RSOCKET_AUTHENTICATION, + encodeBearerAuthMetadata(Buffer.from(bearerToken)) + ); + } + + if (route) { + const encodedRoute = encodeRoute(route); + map.set(MESSAGE_RSOCKET_ROUTING, encodedRoute); + } + + return encodeCompositeMetadata(map); +} + +async function requestResponse( + rsocket: RSocket, + compositeMetaData: Buffer, + message: string = "" +): Promise { + return new Promise((resolve, reject) => { + return rsocket.requestResponse( + { + data: Buffer.from(message), + metadata: compositeMetaData, + }, + { + onError: (e) => { + reject(e); + }, + onNext: (payload, isComplete) => { + Logger.info( + `payload[data: ${payload.data}; metadata: ${payload.metadata}]|${isComplete}` + ); + resolve(payload); + }, + onComplete: () => {}, + onExtension: () => {}, + } + ); + }); +} + +async function main() { + const connector = makeConnector(); + + const rsocket = await connector.connect(); + + // NOTE: YOU SHOULD NEVER HARD CODE AN AUTH TOKEN IN A FILE IN THIS WAY. THIS IS PURELY FOR EXAMPLE PURPOSES. + // The SHA1 HASH of rsocket-js-2024-10 + const exampleToken = "8a7d50f76ef86c75bd3563e55f8835515189dbff"; + + // this request SHOULD pass + const echoResponse = await requestResponse( + rsocket, + makeMetadata(exampleToken, "EchoService.echo"), + "Hello World" + ); + Logger.info(`EchoService.echo response: ${echoResponse.data.toString()}`); + + // this request will reject (unknown route) + try { + await requestResponse( + rsocket, + makeMetadata(exampleToken, "UnknownService.unknown"), + "Hello World" + ); + } catch (e) { + Logger.error(`Expected error: ${e}`); + } + + // this request will reject (no routing data) + try { + await requestResponse(rsocket, makeMetadata(exampleToken), "Hello World"); + } catch (e) { + Logger.error(`Expected error: ${e}`); + } + + // this request will reject (unknown auth token) + try { + await requestResponse( + rsocket, + makeMetadata("abc12345", "EchoService.echo"), + "Hello World" + ); + } catch (e) { + Logger.error(`Expected error: ${e}`); + } + + // this request will reject no auth token) + try { + await requestResponse( + rsocket, + makeMetadata(null, "EchoService.echo"), + "Hello World" + ); + } catch (e) { + Logger.error(`Expected error: ${e}`); + } + + const whoAmiResponse = await requestResponse( + rsocket, + makeMetadata(exampleToken, "AuthService.whoAmI") + ); + Logger.info(`AuthService.whoAmI response: ${whoAmiResponse.data.toString()}`); +} + +main() + .then(() => exit()) + .catch((error: Error) => { + Logger.error(error); + setTimeout(() => { + exit(1); + }); + }); diff --git a/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/server.ts b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/server.ts new file mode 100644 index 0000000..994789d --- /dev/null +++ b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/server.ts @@ -0,0 +1,281 @@ +/* + * Copyright 2021-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Closeable, + ErrorCodes, + OnExtensionSubscriber, + OnNextSubscriber, + OnTerminalSubscriber, + Payload, + RSocketError, + RSocketServer, +} from "rsocket-core"; +import { TcpServerTransport } from "rsocket-tcp-server"; +import { + decodeAuthMetadata, + decodeCompositeMetadata, + decodeRoutes, + WellKnownAuthType, + WellKnownMimeType, +} from "rsocket-composite-metadata"; +import { exit } from "process"; +import Logger from "../../shared/logger"; +import MESSAGE_RSOCKET_ROUTING = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; +import MESSAGE_RSOCKET_AUTHENTICATION = WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION; +import BEARER = WellKnownAuthType.BEARER; + +let serverCloseable: Closeable; + +// NOTE: YOU SHOULD NEVER HARD CODE AN AUTH TOKEN IN A FILE IN THIS WAY. THIS IS PURELY FOR EXAMPLE PURPOSES. +// The SHA1 HASH of rsocket-js-2024-10 +const expectedExampleToken = "8a7d50f76ef86c75bd3563e55f8835515189dbff"; + +const tokenToUserContext = { + [expectedExampleToken]: { + firstName: "bob", + lastName: "builder", + }, +}; + +function mapMetaData(payload: Payload) { + const mappedMetaData = new Map(); + if (payload.metadata) { + const decodedCompositeMetaData = decodeCompositeMetadata(payload.metadata); + + for (let metaData of decodedCompositeMetaData) { + switch (metaData.mimeType) { + case MESSAGE_RSOCKET_ROUTING.toString(): { + const tags = []; + for (let decodedRoute of decodeRoutes(metaData.content)) { + tags.push(decodedRoute); + } + const joinedRoute = tags.join("."); + mappedMetaData.set(MESSAGE_RSOCKET_ROUTING.toString(), joinedRoute); + break; + } + + case MESSAGE_RSOCKET_AUTHENTICATION.toString(): { + const auth = decodeAuthMetadata(metaData.content); + mappedMetaData.set(MESSAGE_RSOCKET_AUTHENTICATION.toString(), auth); + break; + } + } + } + } + return mappedMetaData; +} + +class EchoService { + handleEchoRequestResponse( + responderStream: OnTerminalSubscriber & + OnNextSubscriber & + OnExtensionSubscriber, + payload: Payload, + mappedMetaData: Map + ) { + const timeout = setTimeout(() => { + responderStream.onNext( + { + data: Buffer.concat([Buffer.from("Echo: "), payload.data]), + }, + true + ); + }, 1000); + return { + cancel: () => { + clearTimeout(timeout); + Logger.info("Request cancelled..."); + }, + onExtension: () => { + Logger.info("Received Extension request"); + }, + }; + } +} + +class AuthService { + handleWhoAmIRequestResponse( + responderStream: OnTerminalSubscriber & + OnNextSubscriber & + OnExtensionSubscriber, + payload: Payload, + mappedMetaData: Map + ) { + const timeout = setTimeout(() => { + const authContext = mappedMetaData.get( + MESSAGE_RSOCKET_AUTHENTICATION.toString() + ); + const authToken = authContext.payload.toString(); + const userContext = tokenToUserContext[authToken]; + if (!userContext) { + responderStream.onError( + new RSocketError( + ErrorCodes.REJECTED, + "No user found for given token." + ) + ); + return; + } + responderStream.onNext( + { + data: Buffer.from(JSON.stringify(userContext)), + }, + true + ); + }); + return { + cancel: () => { + clearTimeout(timeout); + Logger.info("Request cancelled..."); + }, + onExtension: () => { + Logger.info("Received Extension request"); + }, + }; + } +} + +function authMiddleware(mappedMetaData: Map) { + const auth = mappedMetaData.get(MESSAGE_RSOCKET_AUTHENTICATION.toString()); + if (!auth) { + return new RSocketError( + ErrorCodes.REJECTED, + "Missing authentication context." + ); + } + if (auth.type.identifier !== BEARER.identifier) { + return new RSocketError( + ErrorCodes.REJECTED, + `Unsupported authentication type provided. Identifier=${auth.type.identifier}` + ); + } + const token = auth.payload.toString(); + if (token !== expectedExampleToken) { + return new RSocketError( + ErrorCodes.REJECTED, + `Invalid Bearer token provided.` + ); + } +} + +function makeServer() { + // NOTE: THIS EXAMPLE DOES NOT COVER TLS. + // ALWAYS USE A SECURE CONNECTION SUCH AS TLS WHEN TRANSMITTING SENSITIVE INFORMATION SUCH AS AUTH TOKENS. + return new RSocketServer({ + transport: new TcpServerTransport({ + listenOptions: { + port: 9090, + host: "127.0.0.1", + }, + }), + acceptor: { + accept: async () => { + const echoService = new EchoService(); + const authService = new AuthService(); + return { + requestResponse: ( + payload: Payload, + responderStream: OnTerminalSubscriber & + OnNextSubscriber & + OnExtensionSubscriber + ) => { + const mappedMetaData = mapMetaData(payload); + + const defaultSubscriber = { + cancel() { + Logger.info("Request cancelled..."); + }, + onExtension() {}, + }; + + const authError = authMiddleware(mappedMetaData); + if (authError) { + Logger.error(`Auth error: ${authError}`); + responderStream.onError(authError); + return defaultSubscriber; + } + + const route = mappedMetaData.get( + MESSAGE_RSOCKET_ROUTING.toString() + ); + if (!route) { + responderStream.onError( + new RSocketError( + ErrorCodes.REJECTED, + "Composite metadata did not include routing information." + ) + ); + return defaultSubscriber; + } + + Logger.info(`Handling ${route}`); + + switch (route) { + case "EchoService.echo": { + return echoService.handleEchoRequestResponse( + responderStream, + payload, + mappedMetaData + ); + } + case "AuthService.whoAmI": { + return authService.handleWhoAmIRequestResponse( + responderStream, + payload, + mappedMetaData + ); + } + + default: { + responderStream.onError( + new RSocketError( + ErrorCodes.REJECTED, + "The encoded route was unknown by the server." + ) + ); + return defaultSubscriber; + } + } + }, + }; + }, + }, + }); +} + +async function main() { + const server = makeServer(); + + serverCloseable = await server.bind(); + + Logger.info("Server bound..."); + + await new Promise((resolve, reject) => { + serverCloseable.onClose((e) => { + Logger.info("Server closed..."); + if (e) return reject(e); + resolve(null); + }); + }); +} + +main() + .then(() => exit()) + .catch((error: Error) => { + console.error(error); + exit(1); + }); diff --git a/packages/rsocket-examples/src/webpack/browser-bundle/README.md b/packages/rsocket-examples/src/webpack/browser-bundle/README.md index a50f51a..62989e1 100644 --- a/packages/rsocket-examples/src/webpack/browser-bundle/README.md +++ b/packages/rsocket-examples/src/webpack/browser-bundle/README.md @@ -26,3 +26,41 @@ __index.html__ Note: `index.html` does not show how to load the built `rsocket.js` file as that will be up to you/your implementation to decide. Note: when running the `serve` npm script webpack will automatically host the `index.html` file and inject the `rsocket.js` script into the `` block. + +## Run the server + +**Open a terminal:** + +Open a terminal in the `simple/server` directory one level up from this README. + +**Install dependencies:** + +```bash +npm install +``` + +**Run the server:** + +```bash +npm run start +``` + +## Run the client + +**Open a terminal in this folder and install dependencies:** + +```bash +npm install +``` + +**Run the NPM server script:** + +``` +npm run serve +``` + +The above script will run the webpack dev server, which will first compile the "app" and then host the index.html. + +**Open in browser:** + +Visit [localhost:9000](http://localhost:9000). diff --git a/packages/rsocket-examples/src/webpack/browser-bundle/index.html b/packages/rsocket-examples/src/webpack/browser-bundle/index.html index b3a0480..50ffb12 100644 --- a/packages/rsocket-examples/src/webpack/browser-bundle/index.html +++ b/packages/rsocket-examples/src/webpack/browser-bundle/index.html @@ -15,9 +15,18 @@

RSocket Webpack Example

var state = 'CONNECTING'; var outputDiv = document.querySelector("#output"); var _rsocket = null; + var errorColor = '#eb4034'; + var infoColor = '#348CEBFF'; + var messageColor = '#2ccd20'; function sendMessage(message) { - if (state !== 'CONNECTED') return; + if (state !== 'CONNECTED') { + const div = document.createElement("div"); + div.textContent = `[${new Date().toISOString()}] not connected. cannot send messages!`; + div.style.color = errorColor; + outputDiv.appendChild(div); + return; + } const bufferData = rsocket.createBuffer(message || ""); _rsocket.requestResponse( { @@ -29,9 +38,10 @@

RSocket Webpack Example

}, onNext: function(payload, isComplete) { const div = document.createElement("div"); - div.textContent = `[${new Date().toISOString()}] payload[data: ${ + div.textContent = `[${new Date().toISOString()}] received: payload[data: ${ payload.data }; metadata: ${payload.metadata}]|${isComplete}`; + div.style.color = messageColor; outputDiv.appendChild(div); }, onComplete: function() { @@ -48,7 +58,17 @@

RSocket Webpack Example

sendButton.addEventListener("click", function() { var input = document.querySelector("#input-field"); var value = input.value; - if (!value.length) return; + if (!value.length) { + const div = document.createElement("div"); + div.textContent = `[${new Date().toISOString()}] please include a message!`; + div.style.color = errorColor; + outputDiv.appendChild(div); + return; + } + const div = document.createElement("div"); + div.textContent = `[${new Date().toISOString()}] sending: ${value}`; + div.style.color = infoColor; + outputDiv.appendChild(div); sendMessage(value); }); @@ -59,14 +79,23 @@

RSocket Webpack Example

.then(function (_r) { state = 'CONNECTED'; _rsocket = _r; + const div = document.createElement("div"); + div.textContent = `[${new Date().toISOString()}] connected!`; + div.style.color = infoColor; + outputDiv.appendChild(div); }) .catch(function (err) { + const errorMessage = err?.message || "failed to connect to rsocket! check the console for more details."; if (err) { console.error("failed to connect rsocket: " + err.message) } else { console.error("failed to connect rsocket!") } + const div = document.createElement("div"); + div.textContent = `[${new Date().toISOString()}] ${errorMessage}`; + div.style.color = errorColor; + outputDiv.appendChild(div); }); })(); diff --git a/packages/rsocket-examples/src/webpack/simple/server/server.js b/packages/rsocket-examples/src/webpack/simple/server/server.js index 24ce292..164585f 100644 --- a/packages/rsocket-examples/src/webpack/simple/server/server.js +++ b/packages/rsocket-examples/src/webpack/simple/server/server.js @@ -19,11 +19,11 @@ const server = new RSocketServer({ () => responderStream.onNext( { - data: Buffer.concat([Buffer.from("Echo: "), payload.data]), + data: Buffer.concat([Buffer.from("ECHO: "), payload.data]), }, true ), - 1000 + 100 ); return { cancel: () => {