diff --git a/changelog.md b/changelog.md index 76fa67c8a..8f5a7b096 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,11 @@ This file contains all the notable changes done to the Ballerina WebSocket packa The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0-beta.2] - 2021-06-24 + +### Fixed +- [Implement declarative auth design for upgrade service](https://github.com/ballerina-platform/ballerina-standard-library/issues/1405) + ## [1.2.0-beta.1] - 2021-05-06 ### Fixed diff --git a/websocket-ballerina/annotation.bal b/websocket-ballerina/annotation.bal index 9a2630fc9..21b77b44b 100644 --- a/websocket-ballerina/annotation.bal +++ b/websocket-ballerina/annotation.bal @@ -26,10 +26,12 @@ # in the `websocket:Listener` which is applicable only for the initial HTTP upgrade request. # + maxFrameSize - The maximum payload size of a WebSocket frame in bytes. # If this is not set or is negative or zero, the default frame size which is 65536 will be used. +# + auth - Listener authenticaton configurations public type WSServiceConfig record {| string[] subProtocols = []; decimal idleTimeout = 0; int maxFrameSize = 65536; + ListenerAuthConfig[] auth?; |}; # The annotation which is used to configure a WebSocket service. diff --git a/websocket-ballerina/auth_desugar.bal b/websocket-ballerina/auth_desugar.bal new file mode 100644 index 000000000..bdf56d7a2 --- /dev/null +++ b/websocket-ballerina/auth_desugar.bal @@ -0,0 +1,119 @@ +// Copyright (c) 2021 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you 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 ballerina/auth; +import ballerina/http; +import ballerina/jballerina.java; +import ballerina/jwt; +import ballerina/oauth2; + +// This function is used for declarative auth design, where the authentication/authorization decision is taken by +// reading the auth annotations provided in service and the `Authorization` header taken with an interop call. +// This function is injected to the first lines of an websocket resource function. Then the logic will be executed +// during the runtime. +// If this function returns `()`, it will be moved to the execution of business logic, else there will be a 401/403 +// response sent. The execution flow will be broken by panic with a distinct error. +# Uses for declarative auth design, where the authentication/authorization decision is taken +# by reading the auth annotations provided in service/resource and the `Authorization` header of request. +# +# + serviceRef - The service reference where the resource locates +public isolated function authenticateResource(Service serviceRef) { + ListenerAuthConfig[]? authConfig = getServiceAuthConfig(serviceRef); + if (authConfig is ()) { + return; + } + string|http:HeaderNotFoundError header = getAuthorizationHeader(); + if (header is string) { + http:Unauthorized|http:Forbidden? result = tryAuthenticate(authConfig, header); + if (result is http:Unauthorized) { + notifyFailure(result.status.code); + } else if (result is http:Forbidden) { + notifyFailure(result.status.code); + } + } else { + notifyFailure(401); + } +} + +isolated function tryAuthenticate(ListenerAuthConfig[] authConfig, string header) returns http:Unauthorized|http:Forbidden? { + foreach ListenerAuthConfig config in authConfig { + if (config is FileUserStoreConfigWithScopes) { + http:ListenerFileUserStoreBasicAuthHandler handler = new(config.fileUserStoreConfig); + auth:UserDetails|http:Unauthorized authn = handler.authenticate(header); + string|string[]? scopes = config?.scopes; + if (authn is auth:UserDetails) { + if (scopes is string|string[]) { + http:Forbidden? authz = handler.authorize(authn, scopes); + return authz; + } + return; + } + } else if (config is LdapUserStoreConfigWithScopes) { + http:ListenerLdapUserStoreBasicAuthProvider handler = new(config.ldapUserStoreConfig); + auth:UserDetails|http:Unauthorized authn = handler->authenticate(header); + string|string[]? scopes = config?.scopes; + if (authn is auth:UserDetails) { + if (scopes is string|string[]) { + http:Forbidden? authz = handler->authorize(authn, scopes); + return authz; + } + return; + } + } else if (config is JwtValidatorConfigWithScopes) { + http:ListenerJwtAuthHandler handler = new(config.jwtValidatorConfig); + jwt:Payload|http:Unauthorized authn = handler.authenticate(header); + string|string[]? scopes = config?.scopes; + if (authn is jwt:Payload) { + if (scopes is string|string[]) { + http:Forbidden? authz = handler.authorize(authn, scopes); + return authz; + } + return; + } + } else { + // Here, config is OAuth2IntrospectionConfigWithScopes + http:ListenerOAuth2Handler handler = new(config.oauth2IntrospectionConfig); + oauth2:IntrospectionResponse|http:Unauthorized|http:Forbidden auth = handler->authorize(header, config?.scopes); + if (auth is oauth2:IntrospectionResponse) { + return; + } else if (auth is http:Forbidden) { + return auth; + } + } + } + http:Unauthorized unauthorized = {}; + return unauthorized; +} + +isolated function getServiceAuthConfig(Service serviceRef) returns ListenerAuthConfig[]? { + typedesc serviceTypeDesc = typeof serviceRef; + var serviceAnnotation = serviceTypeDesc.@ServiceConfig; + if (serviceAnnotation is ()) { + return; + } + WSServiceConfig serviceConfig = serviceAnnotation; + return serviceConfig?.auth; +} + +isolated function notifyFailure(int responseCode) { + // This panic is added to break the execution of the implementation inside the resource function after there is + // an authn/authz failure and responded with 401/403 internally. + panic error(responseCode.toString() + " received by auth desugar."); +} + +isolated function getAuthorizationHeader() returns string|http:HeaderNotFoundError = @java:Method { + 'class: "org.ballerinalang.net.websocket.WebSocketUtil" +} external; diff --git a/websocket-ballerina/auth_types.bal b/websocket-ballerina/auth_types.bal index cea851797..96c44995c 100644 --- a/websocket-ballerina/auth_types.bal +++ b/websocket-ballerina/auth_types.bal @@ -66,3 +66,71 @@ public type OAuth2RefreshTokenGrantConfig record {| # Represents OAuth2 grant configurations for OAuth2 authentication. public type OAuth2GrantConfig OAuth2ClientCredentialsGrantConfig|OAuth2PasswordGrantConfig|OAuth2RefreshTokenGrantConfig; + +# Represents file user store configurations for Basic Auth authentication. +public type FileUserStoreConfig record {| + *auth:FileUserStoreConfig; +|}; + +# Represents LDAP user store configurations for Basic Auth authentication. +public type LdapUserStoreConfig record {| + *auth:LdapUserStoreConfig; +|}; + +# Represents JWT validator configurations for JWT authentication. +# +# + scopeKey - The key used to fetch the scopes +public type JwtValidatorConfig record {| + *jwt:ValidatorConfig; + string scopeKey = "scope"; +|}; + +# Represents OAuth2 introspection server configurations for OAuth2 authentication. +# +# + scopeKey - The key used to fetch the scopes +public type OAuth2IntrospectionConfig record {| + *oauth2:IntrospectionConfig; + string scopeKey = "scope"; +|}; + +# Represents the auth annotation for file user store configurations with scopes. +# +# + fileUserStoreConfig - File user store configurations for Basic Auth authentication +# + scopes - Scopes allowed for authorization +public type FileUserStoreConfigWithScopes record {| + FileUserStoreConfig fileUserStoreConfig; + string|string[] scopes?; +|}; + +# Represents the auth annotation for LDAP user store configurations with scopes. +# +# + ldapUserStoreConfig - LDAP user store configurations for Basic Auth authentication +# + scopes - Scopes allowed for authorization +public type LdapUserStoreConfigWithScopes record {| + LdapUserStoreConfig ldapUserStoreConfig; + string|string[] scopes?; +|}; + +# Represents the auth annotation for JWT validator configurations with scopes. +# +# + jwtValidatorConfig - JWT validator configurations for JWT authentication +# + scopes - Scopes allowed for authorization +public type JwtValidatorConfigWithScopes record {| + JwtValidatorConfig jwtValidatorConfig; + string|string[] scopes?; +|}; + +# Represents the auth annotation for OAuth2 introspection server configurations with scopes. +# +# + oauth2IntrospectionConfig - OAuth2 introspection server configurations for OAuth2 authentication +# + scopes - Scopes allowed for authorization +public type OAuth2IntrospectionConfigWithScopes record {| + OAuth2IntrospectionConfig oauth2IntrospectionConfig; + string|string[] scopes?; +|}; + +# Defines the authentication configurations for the WebSocket listener. +public type ListenerAuthConfig FileUserStoreConfigWithScopes| + LdapUserStoreConfigWithScopes| + JwtValidatorConfigWithScopes| + OAuth2IntrospectionConfigWithScopes; diff --git a/websocket-ballerina/tests/Config.toml b/websocket-ballerina/tests/Config.toml new file mode 100644 index 000000000..d8275b017 --- /dev/null +++ b/websocket-ballerina/tests/Config.toml @@ -0,0 +1,13 @@ +[[ballerina.auth.users]] +username="alice" +password="xxx" +scopes=["write", "update"] + +[[ballerina.auth.users]] +username="bob" +password="yyy" +scopes=["read"] + +[[ballerina.auth.users]] +username="eve" +password="123" diff --git a/websocket-ballerina/tests/auth_declarative_basic_auth_test.bal b/websocket-ballerina/tests/auth_declarative_basic_auth_test.bal new file mode 100644 index 000000000..3f2abe495 --- /dev/null +++ b/websocket-ballerina/tests/auth_declarative_basic_auth_test.bal @@ -0,0 +1,88 @@ +// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you 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 +// +// //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 ballerina/lang.runtime as runtime; +import ballerina/test; + +listener Listener l49 = new(21318); +string wsService49Data = ""; + +@ServiceConfig { + auth: [ + { + fileUserStoreConfig: {}, + scopes: ["write", "update"] + } + ] +} +service /basicAuth on l49 { + resource function get .() returns Service { + return new WsService49(); + } +} + +service class WsService49 { + *Service; + remote function onTextMessage(string data) { + wsService49Data = data; + } +} + +@test:Config {} +public function testBasicAuthServiceAuthSuccess() returns Error? { + Client wsClient = check new("ws://localhost:21318/basicAuth/", { + auth: { + username: "alice", + password: "xxx" + } + }); + check wsClient->writeTextMessage("Hello, World!"); + runtime:sleep(0.5); + test:assertEquals(wsService49Data, "Hello, World!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); +} + +@test:Config {} +public function testBasicAuthServiceAuthzFailure() { + Client|Error wsClient = new("ws://localhost:21318/basicAuth/", { + auth: { + username: "bob", + password: "yyy" + } + }); + if (wsClient is Error) { + test:assertEquals(wsClient.message(), "InvalidHandshakeError: Invalid handshake response getStatus: 403 Forbidden"); + } else { + test:assertFail(msg = "Test Failed!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); + } +} + +@test:Config {} +public function testBasicAuthServiceAuthnFailure() { + Client|Error wsClient = new("ws://localhost:21318/basicAuth/", { + auth: { + username: "peter", + password: "123" + } + }); + if (wsClient is Error) { + test:assertEquals(wsClient.message(), "InvalidHandshakeError: Invalid handshake response getStatus: 401 Unauthorized"); + } else { + test:assertFail(msg = "Test Failed!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); + } +} diff --git a/websocket-ballerina/tests/auth_declarative_bearer_token_auth_test.bal b/websocket-ballerina/tests/auth_declarative_bearer_token_auth_test.bal new file mode 100644 index 000000000..89ce1f2a6 --- /dev/null +++ b/websocket-ballerina/tests/auth_declarative_bearer_token_auth_test.bal @@ -0,0 +1,97 @@ +// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you 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 +// +// //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 ballerina/lang.runtime as runtime; +import ballerina/test; + +listener Listener l54 = new(21324); +string wsService54Data = ""; + +@ServiceConfig { + auth: [ + { + oauth2IntrospectionConfig: { + url: "https://localhost:9445/oauth2/introspect", + tokenTypeHint: "access_token", + scopeKey: "scp", + clientConfig: { + secureSocket: { + cert: { + path: TRUSTSTORE_PATH, + password: "ballerina" + } + } + } + }, + scopes: ["write", "update"] + } + ] +} +service /bearerTokenAuth on l54 { + resource function get .() returns Service { + return new WsService54(); + } +} + +service class WsService54 { + *Service; + remote function onTextMessage(string data) { + wsService54Data = data; + } +} + +@test:Config {} +public function testBearerTokenAuthServiceAuthSuccess() returns Error? { + Client wsClient = check new("ws://localhost:21324/bearerTokenAuth/", { + auth: { + token: ACCESS_TOKEN_1 + } + }); + check wsClient->writeTextMessage("Hello, World!"); + runtime:sleep(0.5); + test:assertEquals(wsService54Data, "Hello, World!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); +} + +@test:Config {} +public function testBearerTokenAuthServiceAuthzFailure() { + Client|Error wsClient = new("ws://localhost:21324/bearerTokenAuth/", { + auth: { + token: ACCESS_TOKEN_2 + } + }); + if (wsClient is Error) { + test:assertEquals(wsClient.message(), "InvalidHandshakeError: Invalid handshake response getStatus: 403 Forbidden"); + } else { + test:assertFail(msg = "Test Failed!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); + } +} + +@test:Config {} +public function testBearerTokenAuthServiceAuthnFailure() { + Client|Error wsClient = new("ws://localhost:21324/bearerTokenAuth/", { + auth: { + token: ACCESS_TOKEN_3 + } + }); + if (wsClient is Error) { + test:assertEquals(wsClient.message(), "InvalidHandshakeError: Invalid handshake response getStatus: 401 Unauthorized"); + } else { + test:assertFail(msg = "Test Failed!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); + } +} diff --git a/websocket-ballerina/tests/auth_declarative_jwt_auth_test.bal b/websocket-ballerina/tests/auth_declarative_jwt_auth_test.bal new file mode 100644 index 000000000..f2c37b6db --- /dev/null +++ b/websocket-ballerina/tests/auth_declarative_jwt_auth_test.bal @@ -0,0 +1,143 @@ +// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you 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 +// +// //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 ballerina/lang.runtime as runtime; +import ballerina/test; + +listener Listener l51 = new(21320); +string wsService51Data = ""; + +@ServiceConfig { + auth: [ + { + jwtValidatorConfig: { + issuer: "ballerina", + audience: ["ballerina", "ballerina.org", "ballerina.io"], + signatureConfig: { + trustStoreConfig: { + trustStore: { + path: KEYSTORE_PATH, + password: "ballerina" + }, + certAlias: "ballerina" + } + }, + scopeKey: "scp" + }, + scopes: ["write", "update"] + } + ] +} +service /jwtAuth on l51 { + resource function get .() returns Service { + return new WsService51(); + } +} + +service class WsService51 { + *Service; + remote function onTextMessage(string data) { + wsService51Data = data; + } +} + +@test:Config {} +public function testJwtAuthServiceAuthSuccess() returns Error? { + Client wsClient = check new("ws://localhost:21320/jwtAuth/", { + auth: { + username: "wso2", + issuer: "ballerina", + audience: ["ballerina", "ballerina.org", "ballerina.io"], + keyId: "5a0b754-895f-4279-8843-b745e11a57e9", + customClaims: { "scp": "write" }, + expTime: 3600, + signatureConfig: { + config: { + keyAlias: "ballerina", + keyPassword: "ballerina", + keyStore: { + path: KEYSTORE_PATH, + password: "ballerina" + } + } + } + } + }); + check wsClient->writeTextMessage("Hello, World!"); + runtime:sleep(0.5); + test:assertEquals(wsService51Data, "Hello, World!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); +} + +@test:Config {} +public function testJwtAuthServiceAuthzFailure() { + Client|Error wsClient = new("ws://localhost:21320/jwtAuth/", { + auth: { + username: "wso2", + issuer: "ballerina", + audience: ["ballerina", "ballerina.org", "ballerina.io"], + keyId: "5a0b754-895f-4279-8843-b745e11a57e9", + customClaims: { "scp": "read" }, + expTime: 3600, + signatureConfig: { + config: { + keyAlias: "ballerina", + keyPassword: "ballerina", + keyStore: { + path: KEYSTORE_PATH, + password: "ballerina" + } + } + } + } + }); + if (wsClient is Error) { + test:assertEquals(wsClient.message(), "InvalidHandshakeError: Invalid handshake response getStatus: 403 Forbidden"); + } else { + test:assertFail(msg = "Test Failed!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); + } +} + +@test:Config {} +public function testJwtAuthServiceAuthnFailure() { + Client|Error wsClient = new("ws://localhost:21320/jwtAuth/", { + auth: { + username: "wso2", + issuer: "wso2", + audience: ["ballerina", "ballerina.org", "ballerina.io"], + keyId: "5a0b754-895f-4279-8843-b745e11a57e9", + customClaims: { "scp": "write" }, + expTime: 3600, + signatureConfig: { + config: { + keyAlias: "ballerina", + keyPassword: "ballerina", + keyStore: { + path: KEYSTORE_PATH, + password: "ballerina" + } + } + } + } + }); + if (wsClient is Error) { + test:assertEquals(wsClient.message(), "InvalidHandshakeError: Invalid handshake response getStatus: 401 Unauthorized"); + } else { + test:assertFail(msg = "Test Failed!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); + } +} diff --git a/websocket-ballerina/tests/auth_declarative_oauth2_test.bal b/websocket-ballerina/tests/auth_declarative_oauth2_test.bal new file mode 100644 index 000000000..fb97e9d0a --- /dev/null +++ b/websocket-ballerina/tests/auth_declarative_oauth2_test.bal @@ -0,0 +1,139 @@ +// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you 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 +// +// //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 ballerina/lang.runtime as runtime; +import ballerina/test; + +listener Listener l55 = new(21325); +string wsService55Data = ""; + +@ServiceConfig { + auth: [ + { + oauth2IntrospectionConfig: { + url: "https://localhost:9445/oauth2/introspect", + tokenTypeHint: "access_token", + scopeKey: "scp", + clientConfig: { + secureSocket: { + cert: { + path: TRUSTSTORE_PATH, + password: "ballerina" + } + } + } + }, + scopes: ["write", "update"] + } + ] +} +service /oauthService on l55 { + resource function get .() returns Service { + return new WsService55(); + } +} + +service class WsService55 { + *Service; + remote function onTextMessage(string data) { + wsService55Data = data; + } +} + +@test:Config { + before: clear +} +public function testOAuth2ClientCredentialsGrantAuthSuccess() returns Error? { + Client wsClient = check new("ws://localhost:21325/oauthService/", { + auth: { + tokenUrl: "https://localhost:9445/oauth2/token", + clientId: "3MVG9YDQS5WtC11paU2WcQjBB3L5w4gz52uriT8ksZ3nUVjKvrfQMrU4uvZohTftxStwNEW4cfStBEGRxRL68", + clientSecret: "9205371918321623741", + scopes: ["write", "update"], + clientConfig: { + secureSocket: { + cert: { + path: TRUSTSTORE_PATH, + password: "ballerina" + } + } + } + } + }); + check wsClient->writeTextMessage("Hello, World!"); + runtime:sleep(0.5); + test:assertEquals(wsService55Data, "Hello, World!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); +} + +@test:Config { + before: clear +} +public function testOAuth2PasswordGrantAuthSuccess() returns Error? { + Client wsClient = check new("ws://localhost:21325/oauthService/", { + auth: { + tokenUrl: "https://localhost:9445/oauth2/token", + username: "johndoe", + password: "A3ddj3w", + clientId: "3MVG9YDQS5WtC11paU2WcQjBB3L5w4gz52uriT8ksZ3nUVjKvrfQMrU4uvZohTftxStwNEW4cfStBEGRxRL68", + clientSecret: "9205371918321623741", + scopes: ["write", "update"], + clientConfig: { + secureSocket: { + cert: { + path: TRUSTSTORE_PATH, + password: "ballerina" + } + } + } + } + }); + check wsClient->writeTextMessage("Hello, World!"); + runtime:sleep(0.5); + test:assertEquals(wsService55Data, "Hello, World!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); +} + +@test:Config { + before: clear +} +public function testOAuth2RefreshTokenGrantAuthSuccess() returns Error? { + Client wsClient = check new("ws://localhost:21325/oauthService/", { + auth: { + refreshUrl: "https://localhost:9445/oauth2/token", + refreshToken: "XlfBs91yquexJqDaKEMzVg==", + clientId: "3MVG9YDQS5WtC11paU2WcQjBB3L5w4gz52uriT8ksZ3nUVjKvrfQMrU4uvZohTftxStwNEW4cfStBEGRxRL68", + clientSecret: "9205371918321623741", + scopes: ["write", "update"], + clientConfig: { + secureSocket: { + cert: { + path: TRUSTSTORE_PATH, + password: "ballerina" + } + } + } + } + }); + check wsClient->writeTextMessage("Hello, World!"); + runtime:sleep(0.5); + test:assertEquals(wsService55Data, "Hello, World!"); + error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); +} + +function clear() { + wsService55Data = ""; +} diff --git a/websocket-ballerina/tests/auth_test_commons.bal b/websocket-ballerina/tests/auth_test_commons.bal new file mode 100644 index 000000000..41c52e2bb --- /dev/null +++ b/websocket-ballerina/tests/auth_test_commons.bal @@ -0,0 +1,71 @@ +// Copyright (c) 2020 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you 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. + +// NOTE: All the tokens/credentials used in this test are dummy tokens/credentials and used only for testing purposes. + +import ballerina/http; +import ballerina/regex; + +const string KEYSTORE_PATH = "tests/certsAndKeys/ballerinaKeystore.p12"; +const string TRUSTSTORE_PATH = "tests/certsAndKeys/ballerinaTruststore.p12"; + +const string ACCESS_TOKEN_1 = "2YotnFZFEjr1zCsicMWpAA"; +const string ACCESS_TOKEN_2 = "1zCsicMWpAA2YotnFZFEjr"; +const string ACCESS_TOKEN_3 = "invalid-token"; + +// The mock authorization server, based with https://hub.docker.com/repository/docker/ldclakmal/ballerina-sts +listener http:Listener sts = new(9445, { + secureSocket: { + key: { + path: KEYSTORE_PATH, + password: "ballerina" + } + } +}); + +service /oauth2 on sts { + resource function post token() returns json { + json response = { + "access_token": ACCESS_TOKEN_1, + "token_type": "example", + "expires_in": 3600, + "example_parameter": "example_value" + }; + return response; + } + + resource function post introspect(http:Request request) returns json { + string|http:ClientError payload = request.getTextPayload(); + if (payload is string) { + string[] parts = regex:split(payload, "&"); + foreach string part in parts { + if (part.indexOf("token=") is int) { + string token = regex:split(part, "=")[1]; + if (token == ACCESS_TOKEN_1) { + json response = { "active": true, "exp": 3600, "scp": "write update" }; + return response; + } else if (token == ACCESS_TOKEN_2) { + json response = { "active": true, "exp": 3600, "scp": "read" }; + return response; + } else { + json response = { "active": false }; + return response; + } + } + } + } + } +} diff --git a/websocket-ballerina/tests/basic_auth_sync_client.bal b/websocket-ballerina/tests/basic_auth_sync_client.bal deleted file mode 100644 index ef1db6c46..000000000 --- a/websocket-ballerina/tests/basic_auth_sync_client.bal +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. -// -// WSO2 Inc. licenses this file to you 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 -// -// //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 ballerina/http; -import ballerina/lang.runtime as runtime; -import ballerina/test; - -string authHeader = ""; -listener Listener l49 = new(21318); - -service /basicAuthSyncService on l49 { - resource function get .(http:Request req) returns Service|UpgradeError { - string|error header = req.getHeader("Authorization"); - if (header is string) { - authHeader = header; - return new WsService49(); - } else { - authHeader = "Header not found"; - return error UpgradeError("Authentication failed"); - } - } -} - -service class WsService49 { - *Service; - remote function onTextMessage(Caller caller, string data) returns Error? { - } -} - -@test:Config {} -public function testSyncBasicAuth() returns Error? { - Client wsClient = check new("ws://localhost:21318/basicAuthSyncService/", config = { - auth: { - username: "alice2", - password: "1234" - } - }); - runtime:sleep(0.5); - test:assertEquals(authHeader, "Basic YWxpY2UyOjEyMzQ="); - error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); -} diff --git a/websocket-ballerina/tests/bearer_token_sync_client.bal b/websocket-ballerina/tests/bearer_token_sync_client.bal deleted file mode 100644 index d64a67f4b..000000000 --- a/websocket-ballerina/tests/bearer_token_sync_client.bal +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. -// -// WSO2 Inc. licenses this file to you 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 -// -// //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 ballerina/http; -import ballerina/lang.runtime as runtime; -import ballerina/test; - -listener Listener l54 = new(21324); - -service /bearerTokenSyncService on l54 { - resource function get .(http:Request req) returns Service|UpgradeError { - string|error header = req.getHeader("Authorization"); - if (header is string) { - authHeader = header; - return new WsService54(); - } else { - authHeader = "Header not found"; - return error UpgradeError("Authentication failed"); - } - } -} - -service class WsService54 { - *Service; - remote function onTextMessage(Caller caller, string data) returns Error? { - } -} - -@test:Config {} -public function testSyncBearerToken() returns Error? { - Client wsClient = check new("ws://localhost:21324/bearerTokenSyncService/", config = { - auth: { - token: "JlbmMiOiJBMTI4Q0JDLUhTMjU2Inikn" - } - }); - runtime:sleep(0.5); - test:assertEquals(authHeader, "Bearer JlbmMiOiJBMTI4Q0JDLUhTMjU2Inikn"); - error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); -} diff --git a/websocket-ballerina/tests/jwt_auth_sync_client.bal b/websocket-ballerina/tests/jwt_auth_sync_client.bal deleted file mode 100644 index e90b77812..000000000 --- a/websocket-ballerina/tests/jwt_auth_sync_client.bal +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. -// -// WSO2 Inc. licenses this file to you 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 -// -// //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 ballerina/http; -import ballerina/jwt; -import ballerina/lang.runtime as runtime; -import ballerina/test; - -listener Listener l51 = new(21320); -string strSyncData = ""; - -http:JwtValidatorConfig jwtConfig = { - issuer: "ballerina", - audience: ["ballerina", "ballerina.org", "ballerina.io"], - signatureConfig: { - trustStoreConfig: { - trustStore: { - path: "tests/certsAndKeys/ballerinaKeystore.p12", - password: "ballerina" - }, - certAlias: "ballerina" - } - }, - scopeKey: "scp" - }; - -http:ListenerJwtAuthHandler handler = new(jwtConfig); - -service /jwtSyncAuthService on l51 { - resource function get .(http:Request req) returns Service|UpgradeError { - jwt:Payload|http:Unauthorized authn1 = handler.authenticate(req); - if (authn1 is jwt:Payload) { - return new WsService51(); - } else { - return error UpgradeError("Authentication failed"); - } - } -} - -service class WsService51 { - *Service; - remote function onTextMessage(Caller caller, string data) returns Error? { - strSyncData = data; - } -} - -@test:Config {} -public function testSyncJwtAuth() returns Error? { - Client wsClient = check new("ws://localhost:21320/jwtSyncAuthService/", config = { - auth: { - username: "wso2", - issuer: "ballerina", - audience: ["ballerina", "ballerina.org", "ballerina.io"], - keyId: "5a0b754-895f-4279-8843-b745e11a57e9", - customClaims: { "scp": "hello" }, - expTime: 3600, - signatureConfig: { - config: { - keyAlias: "ballerina", - keyPassword: "ballerina", - keyStore: { - path: "tests/certsAndKeys/ballerinaKeystore.p12", - password: "ballerina" - } - } - } - } - }); - check wsClient->writeTextMessage("Authentication successful"); - runtime:sleep(0.5); - test:assertEquals(strSyncData, "Authentication successful"); - error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); -} diff --git a/websocket-ballerina/tests/mock_http_oauth2_service.bal b/websocket-ballerina/tests/mock_http_oauth2_service.bal deleted file mode 100644 index 6d69b4305..000000000 --- a/websocket-ballerina/tests/mock_http_oauth2_service.bal +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2021 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. -// -// WSO2 Inc. licenses this file to you 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 ballerina/http; - -const string ACCESS_TOKEN = "2YotnFZFEjr1zCsicMWpAA"; - -// Mock OAuth2 authorization server implementation, which treats the APIs with successful responses. -listener http:Listener oauth2Listener = new(9401, { - secureSocket: { - key: { - path: "tests/certsAndKeys/ballerinaKeystore.p12", - password: "ballerina" - } - } -}); - -service /oauth2 on oauth2Listener { - resource function post token() returns json { - json response = { - "access_token": ACCESS_TOKEN, - "token_type": "example", - "expires_in": 3600, - "example_parameter": "example_value" - }; - return response; - } - - resource function post token/refresh() returns json { - json response = { - "access_token": ACCESS_TOKEN, - "token_type": "example", - "expires_in": 3600, - "example_parameter": "example_value" - }; - return response; - } -} diff --git a/websocket-ballerina/tests/sync_client_oauth2_test.bal b/websocket-ballerina/tests/sync_client_oauth2_test.bal deleted file mode 100644 index 682801c35..000000000 --- a/websocket-ballerina/tests/sync_client_oauth2_test.bal +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2021 WSO2 Inc. (//www.wso2.org) All Rights Reserved. -// -// WSO2 Inc. licenses this file to you 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 -// -// //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 ballerina/http; -import ballerina/lang.runtime as runtime; -import ballerina/test; - -listener Listener l55 = new(21325); -string oauthHeader = ""; - -service /oauthService on l55 { - resource function get .(http:Request req) returns Service|UpgradeError { - string|error header = req.getHeader("Authorization"); - if (header is string) { - oauthHeader = header; - return new WsService55(); - } else { - oauthHeader = "Header not found"; - return error UpgradeError("Authentication failed"); - } - } -} - -service class WsService55 { - *Service; - remote function onTextMessage(Caller caller, string data) returns Error? { - } -} - -OAuth2ClientCredentialsGrantConfig config1 = { - tokenUrl: "https://localhost:9401/oauth2/token", - clientId: "3MVG9YDQS5WtC11paU2WcQjBB3L5w4gz52uriT8ksZ3nUVjKvrfQMrU4uvZohTftxStwNEW4cfStBEGRxRL68", - clientSecret: "9205371918321623741", - scopes: ["token-scope1", "token-scope2"], - clientConfig: { - secureSocket: { - cert: { - path: "tests/certsAndKeys/ballerinaTruststore.p12", - password: "ballerina" - } - } - } -}; - -OAuth2PasswordGrantConfig config2 = { - tokenUrl: "https://localhost:9401/oauth2/token", - username: "johndoe", - password: "A3ddj3w", - clientId: "3MVG9YDQS5WtC11paU2WcQjBB3L5w4gz52uriT8ksZ3nUVjKvrfQMrU4uvZohTftxStwNEW4cfStBEGRxRL68", - clientSecret: "9205371918321623741", - scopes: ["token-scope1", "token-scope2"], - clientConfig: { - secureSocket: { - cert: { - path: "tests/certsAndKeys/ballerinaTruststore.p12", - password: "ballerina" - } - } - } -}; - -OAuth2RefreshTokenGrantConfig config3 = { - refreshUrl: "https://localhost:9401/oauth2/token/refresh", - refreshToken: "XlfBs91yquexJqDaKEMzVg==", - clientId: "3MVG9YDQS5WtC11paU2WcQjBB3L5w4gz52uriT8ksZ3nUVjKvrfQMrU4uvZohTftxStwNEW4cfStBEGRxRL68", - clientSecret: "9205371918321623741", - scopes: ["token-scope1", "token-scope2"], - clientConfig: { - secureSocket: { - cert: { - path: "tests/certsAndKeys/ballerinaTruststore.p12", - password: "ballerina" - } - } - } -}; - -@test:Config {} -public function testOAuth2ClientCredentialsGrant() returns Error? { - Client wsClient = check new("ws://localhost:21325/oauthService/", config = {auth: config1}); - runtime:sleep(0.5); - test:assertEquals(oauthHeader, "Bearer 2YotnFZFEjr1zCsicMWpAA"); - error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); -} - -@test:Config {} -public function testOAuth2PasswordGrant() returns Error? { - Client wsClient = check new("ws://localhost:21325/oauthService/", config = {auth: config2}); - runtime:sleep(0.5); - test:assertEquals(oauthHeader, "Bearer 2YotnFZFEjr1zCsicMWpAA"); - error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); -} - -@test:Config {} -public function testOAuth2RefreshTokenGrant() returns Error? { - Client wsClient = check new("ws://localhost:21325/oauthService/", config = {auth: config3}); - runtime:sleep(0.5); - test:assertEquals(oauthHeader, "Bearer 2YotnFZFEjr1zCsicMWpAA"); - error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0); -} diff --git a/websocket-ballerina/tests/websocket_ssl_proxy.bal b/websocket-ballerina/tests/websocket_ssl_proxy.bal index 871bca161..27cd26038 100644 --- a/websocket-ballerina/tests/websocket_ssl_proxy.bal +++ b/websocket-ballerina/tests/websocket_ssl_proxy.bal @@ -18,9 +18,6 @@ import ballerina/io; import ballerina/lang.runtime as runtime; import ballerina/test; -final string TRUSTSTORE_PATH = "tests/certsAndKeys/ballerinaTruststore.p12"; -final string KEYSTORE_PATH = "tests/certsAndKeys/ballerinaKeystore.p12"; - listener Listener l24 = new(21027, { secureSocket: { key: { diff --git a/websocket-ballerina/websocket_errors.bal b/websocket-ballerina/websocket_errors.bal index 2eeadee8b..cb8bdb221 100644 --- a/websocket-ballerina/websocket_errors.bal +++ b/websocket-ballerina/websocket_errors.bal @@ -47,5 +47,11 @@ public type ReadTimedOutError distinct Error; # Defines the Auth error types that returned from the client public type AuthError distinct Error; +# Defines the authentication error type that returned from the listener +public type AuthnError distinct Error; + +# Defines the authorization error type that returned from the listener +public type AuthzError distinct Error; + # Raised when the SSL handshake fails public type SslError distinct Error; diff --git a/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketConstants.java b/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketConstants.java index a1dbd1d3e..3788c8e53 100644 --- a/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketConstants.java +++ b/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketConstants.java @@ -147,6 +147,8 @@ public enum ErrorCode { HandshakeTimedOut("HandshakeTimedOut"), ReadTimedOutError("ReadTimedOutError"), SslError("SslError"), + AuthzError("AuthzError"), + AuthnError("AuthnError"), Error("Error"); private String errorCode; diff --git a/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketResourceDispatcher.java b/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketResourceDispatcher.java index 45a81b948..ef34d0e47 100644 --- a/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketResourceDispatcher.java +++ b/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketResourceDispatcher.java @@ -150,9 +150,13 @@ public static void dispatchUpgrade(WebSocketHandshaker webSocketHandshaker, WebS break; } } + Map properties = new HashMap<>(); + properties.put(HttpConstants.INBOUND_MESSAGE, httpCarbonMessage); wsService.getRuntime().invokeMethodAsync(wsService.getBalService(), resourceFunction.getName(), null, - ModuleUtils.getOnUpgradeMetaData(), - new OnUpgradeResourceCallback(webSocketHandshaker, wsService, connectionManager), bValues); + ModuleUtils.getOnUpgradeMetaData(), + new OnUpgradeResourceCallback(webSocketHandshaker, wsService, + connectionManager), + properties, PredefinedTypes.TYPE_ANY, bValues); } private static String sanitizeSubPath(String subPath) { diff --git a/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketUtil.java b/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketUtil.java index 5154dd8d3..81d9f2db0 100644 --- a/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketUtil.java +++ b/websocket-native/src/main/java/org/ballerinalang/net/websocket/WebSocketUtil.java @@ -18,6 +18,7 @@ package org.ballerinalang.net.websocket; +import io.ballerina.runtime.api.Environment; import io.ballerina.runtime.api.Future; import io.ballerina.runtime.api.Module; import io.ballerina.runtime.api.Runtime; @@ -33,12 +34,16 @@ import io.netty.channel.ChannelFuture; import io.netty.handler.codec.CodecException; import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.websocketx.CorruptedWebSocketFrameException; import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import org.ballerinalang.net.http.HttpConstants; +import org.ballerinalang.net.http.HttpUtil; import org.ballerinalang.net.transport.contract.websocket.ClientHandshakeFuture; import org.ballerinalang.net.transport.contract.websocket.WebSocketClientConnector; import org.ballerinalang.net.transport.contract.websocket.WebSocketConnection; +import org.ballerinalang.net.transport.message.HttpCarbonMessage; import org.ballerinalang.net.websocket.client.listener.ClientHandshakeListener; import org.ballerinalang.net.websocket.client.listener.ExtendedConnectorListener; import org.ballerinalang.net.websocket.client.listener.ExtendedHandshakeListener; @@ -59,6 +64,7 @@ import javax.net.ssl.SSLException; +import static org.ballerinalang.net.http.HttpErrorType.HEADER_NOT_FOUND_ERROR; import static org.ballerinalang.net.websocket.WebSocketConstants.INITIALIZED_BY_SERVICE; import static org.ballerinalang.net.websocket.WebSocketConstants.NATIVE_DATA_MAX_FRAME_SIZE; import static org.ballerinalang.net.websocket.WebSocketConstants.SYNC_CLIENT; @@ -394,6 +400,15 @@ public static BError createWebsocketError(String message, WebSocketConstants.Err null, null); } + public static Object getAuthorizationHeader(Environment env) { + HttpCarbonMessage inboundMessage = (HttpCarbonMessage) env.getStrandLocal(HttpConstants.INBOUND_MESSAGE); + String authorizationHeader = inboundMessage.getHeader(HttpHeaderNames.AUTHORIZATION.toString()); + if (authorizationHeader == null) { + return HttpUtil.createHttpError("HTTP header does not exist", HEADER_NOT_FOUND_ERROR); + } + return StringUtils.fromString(authorizationHeader); + } + private WebSocketUtil() { } } diff --git a/websocket-native/src/main/java/org/ballerinalang/net/websocket/server/OnUpgradeResourceCallback.java b/websocket-native/src/main/java/org/ballerinalang/net/websocket/server/OnUpgradeResourceCallback.java index 4b6d38cfd..840ae280d 100644 --- a/websocket-native/src/main/java/org/ballerinalang/net/websocket/server/OnUpgradeResourceCallback.java +++ b/websocket-native/src/main/java/org/ballerinalang/net/websocket/server/OnUpgradeResourceCallback.java @@ -27,6 +27,7 @@ import io.netty.handler.codec.http.HttpHeaders; import org.ballerinalang.net.transport.contract.websocket.ServerHandshakeFuture; import org.ballerinalang.net.transport.contract.websocket.WebSocketHandshaker; +import org.ballerinalang.net.websocket.WebSocketConstants; import org.ballerinalang.net.websocket.WebSocketUtil; import static org.ballerinalang.net.websocket.WebSocketConstants.CUSTOM_HEADERS; @@ -49,6 +50,14 @@ public OnUpgradeResourceCallback(WebSocketHandshaker webSocketHandshaker, WebSoc @Override public void notifySuccess(Object result) { if (result instanceof BError) { + if (((BError) result).getType().getName().equals(WebSocketConstants.ErrorCode.AuthnError.errorCode())) { + webSocketHandshaker.cancelHandshake(401, ((BError) result).getErrorMessage().toString()); + return; + } + if (((BError) result).getType().getName().equals(WebSocketConstants.ErrorCode.AuthzError.errorCode())) { + webSocketHandshaker.cancelHandshake(403, ((BError) result).getErrorMessage().toString()); + return; + } webSocketHandshaker.cancelHandshake(400, ((BError) result).getErrorMessage().toString()); return; } @@ -67,6 +76,16 @@ public void notifySuccess(Object result) { @Override public void notifyFailure(BError error) { + // These checks are added to release the failure path since there is an authn/authz failure and responded + // with 401/403 internally. + if (error.getMessage().equals("401 received by auth desugar.")) { + webSocketHandshaker.cancelHandshake(401, error.getMessage()); + return; + } + if (error.getMessage().equals("403 received by auth desugar.")) { + webSocketHandshaker.cancelHandshake(403, error.getMessage()); + return; + } error.printStackTrace(); WebSocketConnectionInfo connectionInfo = connectionManager.getConnectionInfo(webSocketHandshaker.getChannelId());