diff --git a/packages/rsocket-examples/package.json b/packages/rsocket-examples/package.json index 00f646e..b8202e2 100644 --- a/packages/rsocket-examples/package.json +++ b/packages/rsocket-examples/package.json @@ -23,7 +23,9 @@ "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-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-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/server.ts b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/server.ts index 4ee3cf3..994789d 100644 --- a/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/server.ts +++ b/packages/rsocket-examples/src/composite-metadata/bearer-token-auth/server.ts @@ -183,72 +183,76 @@ function makeServer() { }, }), acceptor: { - accept: async () => ({ - requestResponse: ( - payload: Payload, - responderStream: OnTerminalSubscriber & - OnNextSubscriber & - OnExtensionSubscriber - ) => { - const echoService = new EchoService(); - const authService = new AuthService(); - const mappedMetaData = mapMetaData(payload); + 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 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 - ); + const authError = authMiddleware(mappedMetaData); + if (authError) { + Logger.error(`Auth error: ${authError}`); + responderStream.onError(authError); + return defaultSubscriber; } - default: { + const route = mappedMetaData.get( + MESSAGE_RSOCKET_ROUTING.toString() + ); + if (!route) { responderStream.onError( new RSocketError( ErrorCodes.REJECTED, - "The encoded route was unknown by the server." + "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; + } + } + }, + }; + }, }, }); }