diff --git a/README.md b/README.md index 2214ffd..7d4d0d3 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ public function main() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); } ``` @@ -172,7 +172,7 @@ public function main() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); } ``` @@ -213,7 +213,7 @@ public function main() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); } ``` diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 9008e6f..d82d21b 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "soap" -version = "0.8.1" +version = "0.9.0" authors = ["Ballerina"] export=["soap", "soap.soap11", "soap.soap12"] keywords = ["soap"] @@ -19,8 +19,8 @@ graalvmCompatible = true [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" artifactId = "soap-native" -version = "0.8.1" -path = "../native/build/libs/soap-native-0.8.1.jar" +version = "0.9.0" +path = "../native/build/libs/soap-native-0.9.0-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "org.apache.wss4j" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index f390a49..e906e2c 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -269,7 +269,7 @@ dependencies = [ [[package]] org = "ballerina" name = "soap" -version = "0.8.1" +version = "0.9.0" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "http"}, diff --git a/ballerina/Module.md b/ballerina/Module.md index 3da335a..8bd2916 100644 --- a/ballerina/Module.md +++ b/ballerina/Module.md @@ -47,7 +47,7 @@ public function main() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); } ``` @@ -162,7 +162,7 @@ public function main() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); } ``` @@ -202,7 +202,7 @@ public function main() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); } ``` diff --git a/ballerina/Package.md b/ballerina/Package.md index 685dae7..7eba60f 100644 --- a/ballerina/Package.md +++ b/ballerina/Package.md @@ -162,7 +162,7 @@ public function main() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(envelope, "http://tempuri.org/Add"); } ``` diff --git a/ballerina/modules/soap11/soap11.bal b/ballerina/modules/soap11/soap11.bal index 33ea2ee..a7eeb8e 100644 --- a/ballerina/modules/soap11/soap11.bal +++ b/ballerina/modules/soap11/soap11.bal @@ -19,8 +19,9 @@ import soap.wssec; import ballerina/http; import ballerina/mime; +import ballerina/jballerina.java; -# Object for the basic SOAP client endpoint. +# Object for the basic SOAP 1.1 client endpoint. public isolated client class Client { private final http:Client soapClient; private final readonly & wssec:InboundSecurityConfig|wssec:InboundSecurityConfig[] inboundSecurity; @@ -45,25 +46,35 @@ public isolated client class Client { # Sends SOAP request and expects a response. # ```ballerina - # xml|mime:Entity[] response = check soapClient->sendReceive(body, action); + # xml response = check soapClient->sendReceive(body, action); + # -- OR -- + # mime:Entity[] response = check soapClient->sendReceive(body, action); # ``` # # + body - SOAP request body as an `XML` or `mime:Entity[]` to work with SOAP attachments # + action - SOAP action as a `string` # + headers - SOAP headers as a `map` # + path - The resource path + # + T - Default parameter use to infer the user specified type (`xml` or `mime:Entity[]`) # + return - If successful, returns the response. Else, returns an error - remote isolated function sendReceive(xml|mime:Entity[] body, string action, - map headers = {}, string path = "") + remote isolated function sendReceive(xml|mime:Entity[] body, string action, map headers = {}, + string path = "", typedesc T = <>) + returns T|Error = @java:Method { + 'class: "io.ballerina.lib.soap.Soap", + name: "sendReceive11" + } external; + + isolated function generateResponse(xml|mime:Entity[] body, string action, + map headers = {}, string path = "") returns xml|mime:Entity[]|Error { do { xml securedBody; xml mimeEntity = body is xml ? body : check body[0].getXml(); lock { xml envelope = body is xml ? body.clone() : mimeEntity.clone(); - securedBody = check soap:applySecurityPolicies(self.inboundSecurity.clone(), envelope.clone()); + securedBody = check soap:applySecurityPolicies(self.inboundSecurity.clone(), envelope.clone(), false); } - xml response; + xml|mime:Entity[] response; if body is mime:Entity[] { body[0].setXml(securedBody); response = check soap:sendReceive(body, self.soapClient, action, headers, path, false); @@ -73,13 +84,18 @@ public isolated client class Client { lock { wssec:OutboundSecurityConfig? outboundSecurity = self.outboundSecurity.clone(); do { - if outboundSecurity is wssec:OutboundSecurityConfig { - return check soap:applyOutboundConfig(outboundSecurity.clone(), response.clone()); + if outboundSecurity is wssec:OutboundSecurityConfig && outboundSecurity != {} { + if response is xml { + return check soap:applyOutboundConfig(outboundSecurity.clone(), response.clone(), false); + } else { + return check soap:applyOutboundConfig(outboundSecurity.clone(), + check response[0].getXml().clone(), false); + } } } on fail var e { return error Error(INVALID_OUTBOUND_SECURITY_ERROR, e.cause()); } - return response.clone(); + return response; } } on fail var e { return error Error(SOAP_ERROR, e.cause()); @@ -104,7 +120,7 @@ public isolated client class Client { xml mimeEntity = body is xml ? body : check body[0].getXml(); lock { xml envelope = body is xml ? body.clone() : mimeEntity.clone(); - securedBody = check soap:applySecurityPolicies(self.inboundSecurity.clone(), envelope.clone()); + securedBody = check soap:applySecurityPolicies(self.inboundSecurity.clone(), envelope.clone(), false); } return check soap:sendOnly(securedBody, self.soapClient, action, headers, path, false); } on fail var e { diff --git a/ballerina/modules/soap11/tests/http_soap_service.bal b/ballerina/modules/soap11/tests/http_soap_service.bal index 75c69d5..98f561f 100644 --- a/ballerina/modules/soap11/tests/http_soap_service.bal +++ b/ballerina/modules/soap11/tests/http_soap_service.bal @@ -13,9 +13,9 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. - import ballerina/crypto; import ballerina/http; +import ballerina/mime; import ballerina/soap; const crypto:KeyStore serverKeyStore = { @@ -23,7 +23,7 @@ const crypto:KeyStore serverKeyStore = { password: KEY_PASSWORD }; crypto:PrivateKey serverPrivateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(serverKeyStore, KEY_ALIAS, - KEY_PASSWORD); + KEY_PASSWORD); crypto:PublicKey serverPublicKey = check crypto:decodeRsaPublicKeyFromTrustStore(serverKeyStore, KEY_ALIAS); service / on new http:Listener(9090) { @@ -34,6 +34,19 @@ service / on new http:Listener(9090) { return response; } + resource function post getMimePayload(http:Request request) returns http:Response|error { + http:Response response = new; + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(check (check request.getBodyParts())[0].getXml()); + mtomMessage.push(envelope); + response.setBodyParts(mtomMessage); + response.setPayload(mtomMessage); + return response; + } + resource function post getSamePayload(http:Request request) returns http:Response|error { xml payload = check request.getXmlPayload(); http:Response response = new; @@ -41,6 +54,40 @@ service / on new http:Listener(9090) { return response; } + resource function post getSecuredMimePayload(http:Request request) returns http:Response|error { + xml payload = check (check request.getBodyParts())[0].getXml(); + xml applyOutboundConfig = check soap:applyOutboundConfig( + { + verificationKey: clientPublicKey, + signatureAlgorithm: soap:RSA_SHA256, + decryptionAlgorithm: soap:RSA_ECB, + decryptionKey: serverPrivateKey + }, + payload, + false + ); + xml securedEnv = check soap:applySecurityPolicies( + { + signatureAlgorithm: soap:RSA_SHA256, + encryptionAlgorithm: soap:RSA_ECB, + signatureKey: serverPrivateKey, + encryptionKey: clientPublicKey + }, + applyOutboundConfig, + false + ); + http:Response response = new; + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(securedEnv); + mtomMessage.push(envelope); + response.setBodyParts(mtomMessage); + response.setPayload(mtomMessage); + return response; + } + resource function post getSecuredPayload(http:Request request) returns http:Response|error { xml payload = check request.getXmlPayload(); xml applyOutboundConfig = check soap:applyOutboundConfig( @@ -49,14 +96,20 @@ service / on new http:Listener(9090) { signatureAlgorithm: soap:RSA_SHA256, decryptionAlgorithm: soap:RSA_ECB, decryptionKey: serverPrivateKey - }, payload); + }, + payload, + false + ); xml securedEnv = check soap:applySecurityPolicies( { signatureAlgorithm: soap:RSA_SHA256, encryptionAlgorithm: soap:RSA_ECB, signatureKey: serverPrivateKey, encryptionKey: clientPublicKey - }, applyOutboundConfig); + }, + applyOutboundConfig, + false + ); http:Response response = new; response.setPayload(securedEnv); return response; diff --git a/ballerina/modules/soap11/tests/soap11_client_test.bal b/ballerina/modules/soap11/tests/soap11_client_test.bal index 37640a7..0f27b04 100644 --- a/ballerina/modules/soap11/tests/soap11_client_test.bal +++ b/ballerina/modules/soap11/tests/soap11_client_test.bal @@ -48,7 +48,7 @@ crypto:PrivateKey symmetricKey = check crypto:decodeRsaPrivateKeyFromKeyStore(ke crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keyStore, KEY_ALIAS); @test:Config { - groups: ["soap11", "send_receive", "mime", "aa"] + groups: ["soap11", "send_receive", "mime"] } function testSendReceiveWithMime() returns error? { Client soapClient = check new ("http://localhost:9090"); @@ -82,10 +82,88 @@ function testSendReceiveWithMime() returns error? { bytesPart.setContentId(""); mtomMessage.push(bytesPart); - xml|mime:Entity[] response = check soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getPayload"); + xml response = check soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getPayload"); test:assertEquals(response, body); } +@test:Config { + groups: ["soap11", "send_receive", "mime"] +} +function testSendReceiveWithMime2() returns error? { + Client soapClient = check new ("http://localhost:9090"); + xml body = xml ` + + + 2 + 3 + + + `; + + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(body); + mtomMessage.push(envelope); + + mime:Entity bytesPart = new; + string readContent = check io:fileReadString(FILE_PATH); + bytesPart.setFileAsEntityBody(FILE_PATH); + string|byte[]|io:ReadableByteChannel|mime:EncodeError bytes = mime:base64Encode(readContent.toBytes()); + if bytes !is byte[] { + return error("error"); + } + bytesPart.setBody(bytes); + check bytesPart.setContentType("image/jpeg"); + bytesPart.setContentId(""); + mtomMessage.push(bytesPart); + + xml response = check soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getPayload"); + test:assertEquals(response, body); +} + +@test:Config { + groups: ["soap11", "send_receive", "mime"] +} +function testSendReceiveWithMime3() returns error? { + Client soapClient = check new ("http://localhost:9090"); + xml body = xml ` + + + 2 + 3 + + + `; + + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(body); + mtomMessage.push(envelope); + + mime:Entity bytesPart = new; + string readContent = check io:fileReadString(FILE_PATH); + bytesPart.setFileAsEntityBody(FILE_PATH); + string|byte[]|io:ReadableByteChannel|mime:EncodeError bytes = mime:base64Encode(readContent.toBytes()); + if bytes !is byte[] { + return error("error"); + } + bytesPart.setBody(bytes); + check bytesPart.setContentType("image/jpeg"); + bytesPart.setContentId(""); + mtomMessage.push(bytesPart); + + mime:Entity[] response = check soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getMimePayload"); + test:assertEquals(response[0].getXml(), check mtomMessage[0].getXml()); +} + @test:Config { groups: ["soap11", "send_only"] } @@ -105,10 +183,30 @@ function testSendOnly() returns error? { check soapClient->sendOnly(body, "http://tempuri.org/Add"); } +@test:Config { + groups: ["soap11", "send_only"] +} +function testSendOnlyError() returns error? { + xml body = xml ` + + + 2 + 3 + + + `; + + Client soapClient = check new ("error-url"); + Error? response = soapClient->sendOnly(body, "http://tempuri.org/Add", path = "/error"); + test:assertTrue(response is Error); +} + @test:Config { groups: ["soap11", "send_receive"] } -function testSendReceive() returns error? { +function testsendReceive() returns error? { Client soapClient = check new ("http://www.dneonline.com/calculator.asmx?WSDL", { inboundSecurity: NO_POLICY, @@ -126,7 +224,7 @@ function testSendReceive() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `5`; test:assertEquals(response, expected); } @@ -148,7 +246,7 @@ function testSendReceiveWithHeaders() returns error? { Client soapClient = check new ("http://www.dneonline.com/calculator.asmx?WSDL"); - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add", {foo: ["bar1", "bar2"]}); xml expected = xml `5`; test:assertEquals(response, expected); @@ -193,13 +291,13 @@ function testSendReceiveError() returns error? { `; - xml|mime:Entity[]|Error response = soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml|Error response = soapClient->sendReceive(body, "http://tempuri.org/Add"); test:assertTrue(response is Error); test:assertEquals((response).message(), SOAP_ERROR); } @test:Config { - groups: ["soap11", "send_receive", "kl"] + groups: ["soap11", "send_receive"] } function testSendReceiveWithTimestampTokenSecurity() returns error? { Client soapClient = check new ("http://www.dneonline.com/calculator.asmx?WSDL", @@ -221,7 +319,7 @@ function testSendReceiveWithTimestampTokenSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -254,7 +352,7 @@ function testSendReceiveWithUsernameTokenSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -301,7 +399,7 @@ function testSendReceiveWithAsymmetricBindingSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -346,7 +444,7 @@ function testSendReceiveWithSymmetricBindingSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -371,7 +469,7 @@ function testSoapEndpoint() returns error? { } ); xml body = xml `23`; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSamePayload"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSamePayload"); return soap:assertUsernameToken(response.toString(), username, password, wssec:TEXT, string `23`); } @@ -396,12 +494,12 @@ function testSoapReceiveWithSymmetricBindingAndOutboundConfig() returns error? { } ); xml body = xml `23`; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSamePayload"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSamePayload"); return soap:assertSymmetricBinding(response.toString(), string `23`); } @test:Config { - groups: ["soap11", "send_receive", "j"] + groups: ["soap11", "send_receive"] } function testSendReceiveWithAsymmetricBindingAndOutboundConfig() returns error? { Client soapClient = check new ("http://localhost:9090", @@ -421,6 +519,114 @@ function testSendReceiveWithAsymmetricBindingAndOutboundConfig() returns error? } ); xml body = xml `23`; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSecuredPayload"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSecuredPayload"); return soap:assertSymmetricBinding(response.toString(), string `23`); } + +@test:Config { + groups: ["soap11", "send_receive", "mime"] +} +function testOutboundConfigWithMime2() returns error? { + Client soapClient = check new ("http://localhost:9090", + { + inboundSecurity: { + signatureAlgorithm: soap:RSA_SHA256, + encryptionAlgorithm: soap:RSA_ECB, + signatureKey: clientPrivateKey, + encryptionKey: serverPublicKey + }, + outboundSecurity: { + verificationKey: serverPublicKey, + signatureAlgorithm: soap:RSA_SHA256, + decryptionAlgorithm: soap:RSA_ECB, + decryptionKey: clientPrivateKey + } + } + ); + xml body = xml ` + + + 2 + 3 + + + `; + + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(body); + mtomMessage.push(envelope); + + mime:Entity bytesPart = new; + string readContent = check io:fileReadString(FILE_PATH); + bytesPart.setFileAsEntityBody(FILE_PATH); + string|byte[]|io:ReadableByteChannel|mime:EncodeError bytes = mime:base64Encode(readContent.toBytes()); + if bytes !is byte[] { + return error("error"); + } + bytesPart.setBody(bytes); + check bytesPart.setContentType("image/jpeg"); + bytesPart.setContentId(""); + mtomMessage.push(bytesPart); + xml response = check soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getSecuredMimePayload"); + xmlns "http://schemas.xmlsoap.org/soap/envelope/" as soap11; + test:assertEquals(response/, body/); +} + +@test:Config { + groups: ["soap11", "send_receive", "mime"] +} +function testInvalidOutboundConfigWithMime() returns error? { + Client soapClient = check new ("http://localhost:9090", + { + inboundSecurity: { + signatureAlgorithm: soap:RSA_SHA256, + encryptionAlgorithm: soap:RSA_ECB, + signatureKey: clientPrivateKey, + encryptionKey: serverPublicKey + }, + outboundSecurity: { + verificationKey: clientPublicKey, + signatureAlgorithm: soap:RSA_SHA256, + decryptionAlgorithm: soap:RSA_ECB, + decryptionKey: serverPrivateKey + } + } + ); + xml body = xml ` + + + 2 + 3 + + + `; + + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(body); + mtomMessage.push(envelope); + + mime:Entity bytesPart = new; + string readContent = check io:fileReadString(FILE_PATH); + bytesPart.setFileAsEntityBody(FILE_PATH); + string|byte[]|io:ReadableByteChannel|mime:EncodeError bytes = mime:base64Encode(readContent.toBytes()); + if bytes !is byte[] { + return error("error"); + } + bytesPart.setBody(bytes); + check bytesPart.setContentType("image/jpeg"); + bytesPart.setContentId(""); + mtomMessage.push(bytesPart); + xml|Error response = soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getSecuredMimePayload"); + test:assertTrue(response is Error); + test:assertEquals((response).message(), "Outbound security configurations do not match with the SOAP response."); +} diff --git a/ballerina/modules/soap12/soap12.bal b/ballerina/modules/soap12/soap12.bal index fe8706a..d69833a 100644 --- a/ballerina/modules/soap12/soap12.bal +++ b/ballerina/modules/soap12/soap12.bal @@ -19,8 +19,9 @@ import soap.wssec; import ballerina/http; import ballerina/mime; +import ballerina/jballerina.java; -# Object for the basic SOAP client endpoint. +# Object for the basic SOAP 1.2 client endpoint. public isolated client class Client { private final http:Client soapClient; private final readonly & wssec:InboundSecurityConfig|wssec:InboundSecurityConfig[] inboundSecurity; @@ -45,25 +46,35 @@ public isolated client class Client { # Sends SOAP request and expects a response. # ```ballerina - # xml|mime:Entity[] response = check soapClient->sendReceive(body); + # xml response = check soapClient->sendReceive(body); + # -- OR -- + # mime:Entity[] response = check soapClient->sendReceive(body); # ``` # # + body - SOAP request body as an `XML` or `mime:Entity[]` to work with SOAP attachments # + action - SOAP action as a `string` # + headers - SOAP headers as a `map` # + path - The resource path + # + T - Default parameter use to infer the user specified type (`xml` or `mime:Entity[]`) # + return - If successful, returns the response. Else, returns an error - remote isolated function sendReceive(xml|mime:Entity[] body, string? action = (), + remote isolated function sendReceive(xml|mime:Entity[] body, string? action = (), map headers = {}, + string path = "", typedesc T = <>) + returns T|Error = @java:Method { + 'class: "io.ballerina.lib.soap.Soap", + name: "sendReceive12" + } external; + + isolated function generateResponse(xml|mime:Entity[] body, string? action = (), map headers = {}, string path = "") returns xml|mime:Entity[]|Error { do { xml securedBody; xml mimeEntity = body is xml ? body : check body[0].getXml(); lock { - securedBody = body is xml ? check soap:applySecurityPolicies(self.inboundSecurity.clone(), body.clone()) - : check soap:applySecurityPolicies(self.inboundSecurity.clone(), mimeEntity.clone()); + xml envelope = body is xml ? body.clone() : mimeEntity.clone(); + securedBody = check soap:applySecurityPolicies(self.inboundSecurity.clone(), envelope.clone()); } - xml response; + xml|mime:Entity[] response; if body is mime:Entity[] { body[0].setXml(securedBody); response = check soap:sendReceive(body, self.soapClient, action, headers, path); @@ -73,13 +84,18 @@ public isolated client class Client { lock { wssec:OutboundSecurityConfig? outboundSecurity = self.outboundSecurity.clone(); do { - if outboundSecurity is wssec:OutboundSecurityConfig { - return check soap:applyOutboundConfig(outboundSecurity.clone(), response.clone()); + if outboundSecurity is wssec:OutboundSecurityConfig && outboundSecurity != {} { + if response is xml { + return check soap:applyOutboundConfig(outboundSecurity.clone(), response.clone()); + } else { + return check soap:applyOutboundConfig(outboundSecurity.clone(), + check response[0].getXml().clone()); + } } } on fail var e { return error Error(INVALID_OUTBOUND_SECURITY_ERROR, e.cause()); } - return response.clone(); + return response; } } on fail var e { return error Error(SOAP_ERROR, e.cause()); @@ -103,12 +119,8 @@ public isolated client class Client { xml securedBody; xml mimeEntity = body is xml ? body : check body[0].getXml(); lock { - securedBody = body is xml ? check soap:applySecurityPolicies(self.inboundSecurity.clone(), body.clone()) - : check soap:applySecurityPolicies(self.inboundSecurity.clone(), mimeEntity.clone()); - } - if body is mime:Entity[] { - body[0].setXml(securedBody); - return check soap:sendOnly(body, self.soapClient, action, headers, path); + xml envelope = body is xml ? body.clone() : mimeEntity.clone(); + securedBody = check soap:applySecurityPolicies(self.inboundSecurity.clone(), envelope.clone()); } return check soap:sendOnly(securedBody, self.soapClient, action, headers, path); } on fail var e { diff --git a/ballerina/modules/soap12/tests/http_soap_service.bal b/ballerina/modules/soap12/tests/http_soap_service.bal index 5acebab..f4cc2bd 100644 --- a/ballerina/modules/soap12/tests/http_soap_service.bal +++ b/ballerina/modules/soap12/tests/http_soap_service.bal @@ -16,6 +16,7 @@ import ballerina/crypto; import ballerina/http; +import ballerina/mime; import ballerina/soap; const crypto:KeyStore serverKeyStore = { @@ -34,6 +35,19 @@ service / on new http:Listener(9090) { return response; } + resource function post getMimePayload(http:Request request) returns http:Response|error { + http:Response response = new; + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(check (check request.getBodyParts())[0].getXml()); + mtomMessage.push(envelope); + response.setBodyParts(mtomMessage); + response.setPayload(mtomMessage); + return response; + } + resource function post getSamePayload(http:Request request) returns http:Response|error { xml payload = check request.getXmlPayload(); http:Response response = new; @@ -49,16 +63,52 @@ service / on new http:Listener(9090) { signatureAlgorithm: soap:RSA_SHA256, decryptionAlgorithm: soap:RSA_ECB, decryptionKey: serverPrivateKey - }, payload); + }, + payload + ); xml securedEnv = check soap:applySecurityPolicies( { signatureAlgorithm: soap:RSA_SHA256, encryptionAlgorithm: soap:RSA_ECB, signatureKey: serverPrivateKey, encryptionKey: clientPublicKey - }, applyOutboundConfig); + }, + applyOutboundConfig + ); http:Response response = new; response.setPayload(securedEnv); return response; } + + resource function post getSecuredMimePayload(http:Request request) returns http:Response|error { + xml payload = check (check request.getBodyParts())[0].getXml(); + xml applyOutboundConfig = check soap:applyOutboundConfig( + { + verificationKey: clientPublicKey, + signatureAlgorithm: soap:RSA_SHA256, + decryptionAlgorithm: soap:RSA_ECB, + decryptionKey: serverPrivateKey + }, + payload + ); + xml securedEnv = check soap:applySecurityPolicies( + { + signatureAlgorithm: soap:RSA_SHA256, + encryptionAlgorithm: soap:RSA_ECB, + signatureKey: serverPrivateKey, + encryptionKey: clientPublicKey + }, + applyOutboundConfig + ); + http:Response response = new; + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(securedEnv); + mtomMessage.push(envelope); + response.setBodyParts(mtomMessage); + response.setPayload(mtomMessage); + return response; + } } diff --git a/ballerina/modules/soap12/tests/soap12_client_test.bal b/ballerina/modules/soap12/tests/soap12_client_test.bal index 500e006..331848f 100644 --- a/ballerina/modules/soap12/tests/soap12_client_test.bal +++ b/ballerina/modules/soap12/tests/soap12_client_test.bal @@ -18,11 +18,14 @@ import soap; import soap.wssec; import ballerina/crypto; +import ballerina/io; import ballerina/mime; import ballerina/test; const KEY_ALIAS = "wss40"; const KEY_PASSWORD = "security"; +const IMAGE_PATH = "../ballerina/icon.png"; +const FILE_PATH = "../ballerina/Module.md"; const KEY_STORE_PATH = "modules/wssec/tests/resources/wss40.p12"; const X509_KEY_STORE_PATH = "modules/wssec/tests/resources/x509_certificate.p12"; const X509_KEY_STORE_PATH_2 = "modules/wssec/tests/resources/x509_certificate_2.p12"; @@ -64,6 +67,26 @@ function testSendOnly12() returns error? { check soapClient->sendOnly(body, "http://tempuri.org/Add"); } +@test:Config { + groups: ["soap12", "send_only"] +} +function testSendOnlyError12() returns error? { + xml body = xml ` + + + 2 + 3 + + + `; + + Client soapClient = check new ("error-url"); + Error? response = soapClient->sendOnly(body, "http://tempuri.org/Add", path = "/error"); + test:assertTrue(response is Error); +} + @test:Config { groups: ["soap12", "send_receive"] } @@ -79,12 +102,90 @@ function testSendReceive12() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `5`; test:assertEquals(response, expected); } +@test:Config { + groups: ["soap12", "send_receive"] +} +function testSendReceive12Mime() returns error? { + Client soapClient = check new ("http://localhost:9090"); + xml body = xml ` + + + 2 + 3 + + + `; + + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(body); + mtomMessage.push(envelope); + + mime:Entity bytesPart = new; + string readContent = check io:fileReadString(FILE_PATH); + bytesPart.setFileAsEntityBody(FILE_PATH); + string|byte[]|io:ReadableByteChannel|mime:EncodeError bytes = mime:base64Encode(readContent.toBytes()); + if bytes !is byte[] { + return error("error"); + } + bytesPart.setBody(bytes); + check bytesPart.setContentType("image/jpeg"); + bytesPart.setContentId(""); + mtomMessage.push(bytesPart); + + mime:Entity[] response = check soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getMimePayload"); + test:assertEquals(response[0].getXml(), check mtomMessage[0].getXml()); +} + +@test:Config { + groups: ["soap11", "send_receive", "mime"] +} +function testSendReceive12WithMime2() returns error? { + Client soapClient = check new ("http://localhost:9090"); + xml body = xml ` + + + 2 + 3 + + + `; + + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(body); + mtomMessage.push(envelope); + + mime:Entity bytesPart = new; + string readContent = check io:fileReadString(FILE_PATH); + bytesPart.setFileAsEntityBody(FILE_PATH); + string|byte[]|io:ReadableByteChannel|mime:EncodeError bytes = mime:base64Encode(readContent.toBytes()); + if bytes !is byte[] { + return error("error"); + } + bytesPart.setBody(bytes); + check bytesPart.setContentType("image/jpeg"); + bytesPart.setContentId(""); + mtomMessage.push(bytesPart); + + xml response = check soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getPayload"); + test:assertEquals(response, body); +} + @test:Config { groups: ["soap12", "send_receive"] } @@ -102,8 +203,8 @@ function testSendReceive12WithHeaders() returns error? { Client soapClient = check new ("http://www.dneonline.com/calculator.asmx?WSDL"); - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", - {foo: ["bar1", "bar2"]}); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add", + {foo: ["bar1", "bar2"]}); xml expected = xml `5`; test:assertEquals(response, expected); @@ -126,7 +227,7 @@ function testSendReceive12WithoutSoapAction() returns error? { Client soapClient = check new ("http://www.dneonline.com/calculator.asmx?WSDL"); - xml|mime:Entity[] response = check soapClient->sendReceive(body); + xml response = check soapClient->sendReceive(body); xml expected = xml `5`; test:assertEquals(response, expected); @@ -169,7 +270,7 @@ function testSendReceive12IncludingHeadersWithoutSoapAction() returns error? { Client soapClient = check new ("http://www.dneonline.com/calculator.asmx?WSDL"); - xml|mime:Entity[] response = check soapClient->sendReceive(body, (), {foo: ["bar1", "bar2"]}); + xml response = check soapClient->sendReceive(body, (), {foo: ["bar1", "bar2"]}); xml expected = xml `5`; test:assertEquals(response, expected); } @@ -211,7 +312,7 @@ function testSendReceiveError() returns error? { `; - xml|mime:Entity[]|Error response = soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml|Error response = soapClient->sendReceive(body, "http://tempuri.org/Add"); test:assertTrue(response is Error); test:assertEquals((response).message(), SOAP_ERROR); } @@ -239,7 +340,7 @@ function testSendReceiveWithTimestampTokenSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -273,7 +374,7 @@ function testSendReceiveWithUsernameTokenSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -320,7 +421,7 @@ function testSendReceiveWithAsymmetricBindingSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -366,7 +467,7 @@ function testSendReceiveWithSymmetricBindingSecurity() returns error? { `; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add"); xml expected = xml `soap:MustUnderstandSystem.Web.Services.Protocols.SoapHeaderException: SOAP header Security was not understood. at System.Web.Services.Protocols.SoapHeaderHandling.SetHeaderMembers(SoapHeaderCollection headers, Object target, SoapHeaderMapping[] mappings, SoapHeaderDirection direction, Boolean client) at System.Web.Services.Protocols.SoapServerProtocol.CreateServerInstance() @@ -375,6 +476,9 @@ function testSendReceiveWithSymmetricBindingSecurity() returns error? { test:assertEquals(response.toString(), expected.toString()); } +@test:Config { + groups: ["soap12", "send_receive"] +} function testSoapEndpoint() returns error? { string username = "user"; string password = "password"; @@ -387,13 +491,13 @@ function testSoapEndpoint() returns error? { } } ); - xml body = xml `23`; - xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSamePayload"); + xml body = xml `23`; + xml response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSamePayload"); return soap:assertUsernameToken(response.toString(), username, password, wssec:TEXT, string `23`); } @test:Config { - groups: ["soap11", "send_receive"] + groups: ["soap12", "send_receive"] } function testSoapReceiveWithSymmetricBindingAndOutboundConfig() returns error? { Client soapClient = check new ("http://localhost:9090", @@ -412,13 +516,13 @@ function testSoapReceiveWithSymmetricBindingAndOutboundConfig() returns error? { } } ); - xml body = xml `23`; + xml body = xml `23`; xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSamePayload"); return soap:assertSymmetricBinding(response.toString(), string `23`); } @test:Config { - groups: ["soap11", "send_receive", "j"] + groups: ["soap12", "send_receive"] } function testSendReceiveWithAsymmetricBindingAndOutboundConfig() returns error? { Client soapClient = check new ("http://localhost:9090", @@ -438,7 +542,62 @@ function testSendReceiveWithAsymmetricBindingAndOutboundConfig() returns error? } ); - xml body = xml `23`; + xml body = xml `23`; xml|mime:Entity[] response = check soapClient->sendReceive(body, "http://tempuri.org/Add", path = "/getSecuredPayload"); return soap:assertSymmetricBinding(response.toString(), string `23`); } + +@test:Config { + groups: ["soap12", "send_receive"] +} +function testInvalidOutboundConfigWithMime12() returns error? { + Client soapClient = check new ("http://localhost:9090", + { + inboundSecurity: { + signatureAlgorithm: soap:RSA_SHA256, + encryptionAlgorithm: soap:RSA_ECB, + signatureKey: clientPrivateKey, + encryptionKey: serverPublicKey + }, + outboundSecurity: { + verificationKey: clientPublicKey, + signatureAlgorithm: soap:RSA_SHA256, + decryptionAlgorithm: soap:RSA_ECB, + decryptionKey: serverPrivateKey + } + } + ); + xml body = xml ` + + + 2 + 3 + + + `; + + mime:Entity[] mtomMessage = []; + mime:Entity envelope = new; + check envelope.setContentType("application/xop+xml"); + envelope.setContentId(""); + envelope.setBody(body); + mtomMessage.push(envelope); + + mime:Entity bytesPart = new; + string readContent = check io:fileReadString(FILE_PATH); + bytesPart.setFileAsEntityBody(FILE_PATH); + string|byte[]|io:ReadableByteChannel|mime:EncodeError bytes = mime:base64Encode(readContent.toBytes()); + if bytes !is byte[] { + return error("error"); + } + bytesPart.setBody(bytes); + check bytesPart.setContentType("image/jpeg"); + bytesPart.setContentId(""); + mtomMessage.push(bytesPart); + + mime:Entity[]|Error response = soapClient->sendReceive(mtomMessage, "http://tempuri.org/Add", path = "/getSecuredMimePayload"); + test:assertTrue(response is Error); + test:assertEquals((response).message(), "Outbound security configurations do not match with the SOAP response."); +} diff --git a/ballerina/modules/wssec/tests/ws_security_tests.bal b/ballerina/modules/wssec/tests/ws_security_tests.bal index ffd3145..29addfa 100644 --- a/ballerina/modules/wssec/tests/ws_security_tests.bal +++ b/ballerina/modules/wssec/tests/ws_security_tests.bal @@ -211,7 +211,7 @@ function testSymmetricBindingPolicyWithSignatureOnly() returns error? { servicePublicKey: serverPublicKey }; - xml securedEnvelope = check applySymmetricBinding(envelope, symmetricBinding); + xml securedEnvelope = check applySymmetricBinding(envelope, false, symmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -239,7 +239,7 @@ function testSymmetricBindingPolicyEncryptionOnly() returns error? { servicePublicKey: serverPublicKey }; - xml securedEnvelope = check applySymmetricBinding(envelope, symmetricBinding); + xml securedEnvelope = check applySymmetricBinding(envelope, false, symmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] encData = check getEncryptedData(securedEnvelope); @@ -266,7 +266,7 @@ function testSymmetricBindingWithSignatureAndEncryption() returns error? { symmetricKey: symmetricKey, servicePublicKey: serverPublicKey }; - xml securedEnvelope = check applySymmetricBinding(envelope, symmetricBinding); + xml securedEnvelope = check applySymmetricBinding(envelope, false, symmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -302,7 +302,7 @@ function testSymmetricBindingPolicyWithX509SignatureAndEncryption() returns erro x509Token: X509_PUBLIC_CERT_PATH_2 }; - xml securedEnvelope = check applySymmetricBinding(envelope, symmetricBinding); + xml securedEnvelope = check applySymmetricBinding(envelope, false, symmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -344,7 +344,7 @@ function testUsernameTokenWithSymmetricBinding() returns error? { symmetricKey: symmetricKey, servicePublicKey: serverPublicKey }; - xml securedEnvelope = check applySymmetricBinding(envelope, symmetricBinding); + xml securedEnvelope = check applySymmetricBinding(envelope, false, symmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -403,7 +403,7 @@ function testUsernameTokenTimestampWithSymmetricBindingAndX509Token() returns er x509Token: X509_PUBLIC_CERT_PATH_2 }; - xml securedEnvelope = check applySymmetricBinding(envelope, symmetricBinding); + xml securedEnvelope = check applySymmetricBinding(envelope, false, symmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -452,7 +452,7 @@ function testSymmetricBindingWithOutboundConfig() returns error? { servicePublicKey: serverPublicKey }; - xml securedEnvelope = check applySymmetricBinding(envelope, symmetricBinding); + xml securedEnvelope = check applySymmetricBinding(envelope, false, symmetricBinding); string envelopeString = securedEnvelope.toString(); OutboundSecurityConfig outboundConfig = { @@ -492,7 +492,7 @@ function testAsymmetricBindingWithSignatureRsaSha256() returns error? { signatureKey: clientPrivateKey, encryptionKey: serverPublicKey }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); Error? validity = check verifyData((envelope//*).toString().toBytes(), signedData, @@ -518,7 +518,7 @@ function testAsymmetricBindingWithX509Signature() returns error? { encryptionKey: serverPublicKey, x509Token: X509_PUBLIC_CERT_PATH_2 }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -544,7 +544,7 @@ function testAsymmetricBindingWithEncryption() returns error? { signatureKey: clientPrivateKey, encryptionKey: serverPublicKey }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] encData = check getEncryptedData(securedEnvelope); @@ -571,7 +571,7 @@ function testAsymmetricBindingWithSignatureAndEncryption() returns error? { encryptionKey: serverPublicKey }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -604,7 +604,7 @@ function testAsymmetricBindingWithX509SignatureAndEncryption() returns error? { encryptionKey: serverPublicKey, x509Token: X509_PUBLIC_CERT_PATH_2 }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -644,7 +644,7 @@ function testUsernameTokenWithAsymmetricBindingAndX509() returns error? { encryptionKey: serverPublicKey, x509Token: X509_PUBLIC_CERT_PATH_2 }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -686,7 +686,7 @@ function testUsernameTokenTimestampWithAsymmetricBindingAndX509() returns error? encryptionKey: serverPublicKey, x509Token: X509_PUBLIC_CERT_PATH_2 }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); @@ -729,7 +729,7 @@ function testAsymmetricBindingWithOutboundConfig() returns error? { decryptionKey: serverPrivateKey }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope,false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); crypto:PrivateKey|crypto:PublicKey? privateKey = outboundConfig.decryptionKey; if privateKey is crypto:PrivateKey|crypto:PublicKey { @@ -760,7 +760,7 @@ function testAsymmetricBindingWithSignatureWithRsaSha1() returns error? { signatureKey: clientPrivateKey, encryptionKey: serverPublicKey }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); Error? validity = check verifyData((envelope//*).toString().toBytes(), signedData, @@ -785,7 +785,7 @@ function testAsymmetricBindingWithSignatureWithRsaSha384() returns error? { signatureKey: clientPrivateKey, encryptionKey: serverPublicKey }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); Error? validity = check verifyData((envelope//*).toString().toBytes(), signedData, @@ -810,7 +810,7 @@ function testAsymmetricBindingWithSignatureWithRsaSha512() returns error? { signatureKey: clientPrivateKey, encryptionKey: serverPublicKey }; - xml securedEnvelope = check applyAsymmetricBinding(envelope, asymmetricBinding); + xml securedEnvelope = check applyAsymmetricBinding(envelope, false, asymmetricBinding); string envelopeString = securedEnvelope.toString(); byte[] signedData = check getSignatureData(securedEnvelope); Error? validity = check verifyData((envelope//*).toString().toBytes(), signedData, diff --git a/ballerina/modules/wssec/ws_security_methods.bal b/ballerina/modules/wssec/ws_security_methods.bal index 9e328d0..9fb5ae7 100644 --- a/ballerina/modules/wssec/ws_security_methods.bal +++ b/ballerina/modules/wssec/ws_security_methods.bal @@ -17,7 +17,8 @@ import ballerina/crypto; import ballerina/lang.regexp; -xmlns "http://schemas.xmlsoap.org/soap/envelope/" as soap; +xmlns "http://schemas.xmlsoap.org/soap/envelope/" as soap11; +xmlns "http://www.w3.org/2003/05/soap-envelope" as soap12; xmlns "http://www.w3.org/2000/09/xmldsig#" as ds; isolated function addSecurityHeader(Document document) returns WSSecurityHeader|Error { @@ -129,9 +130,10 @@ public isolated function applyUsernameToken(xml envelope, *UsernameTokenConfig u # Apply symmetric binding security policy with username token to the SOAP envelope. # # + envelope - The SOAP envelope +# + soap12 - A boolean flag. Set to `true` for SOAP 1.2, or `false` for SOAP 1.1. # + symmetricBinding - The `SymmetricBindingConfig` record with the required parameters # + return - A `xml` type of SOAP envelope if the security binding is successfully added or else `wssec:Error` -public isolated function applySymmetricBinding(xml envelope, *SymmetricBindingConfig symmetricBinding) +public isolated function applySymmetricBinding(xml envelope, boolean soap12, *SymmetricBindingConfig symmetricBinding) returns xml|crypto:Error|Error { Document document = check new (envelope); WSSecurityHeader wsSecurityHeader = check addSecurityHeader(document); @@ -140,8 +142,15 @@ public isolated function applySymmetricBinding(xml envelope, *SymmetricBindingCo EncryptionAlgorithm? encryptionAlgorithm = symmetricBinding.encryptionAlgorithm; if signatureAlgorithm is SignatureAlgorithm { Signature signature = check new (); - byte[] signedData = check signature.signData((envelope//*).toString(), signatureAlgorithm - , symmetricBinding.symmetricKey); + byte[] signedData; + if soap12 { + signedData = check signature.signData((envelope//*).toString(), signatureAlgorithm, + symmetricBinding.symmetricKey); + } else { + signedData = check signature.signData((envelope//*).toString(), signatureAlgorithm, + symmetricBinding.symmetricKey); + } + Signature signatureResult = check addSignature(signature, signatureAlgorithm, signedData); WsSecurity wsSecurity = new; securedEnvelope = check wsSecurity.applySignatureOnlyPolicy(wsSecurityHeader, signatureResult, @@ -149,8 +158,14 @@ public isolated function applySymmetricBinding(xml envelope, *SymmetricBindingCo } if encryptionAlgorithm is EncryptionAlgorithm { Encryption encryption = check new (); - byte[] encryptData = check crypto:encryptRsaEcb((envelope//*).toString().toBytes(), - symmetricBinding.symmetricKey); + byte[] encryptData; + if soap12 { + encryptData = check crypto:encryptRsaEcb((envelope//*).toString().toBytes(), + symmetricBinding.symmetricKey); + } else { + encryptData = check crypto:encryptRsaEcb((envelope//*).toString().toBytes(), + symmetricBinding.symmetricKey); + } Encryption encryptionResult = check addEncryption(encryption, encryptionAlgorithm, encryptData); WsSecurity wsSecurity = new; securedEnvelope = check wsSecurity.applyEncryptionOnlyPolicy(wsSecurityHeader, encryptionResult); @@ -163,9 +178,12 @@ public isolated function applySymmetricBinding(xml envelope, *SymmetricBindingCo # Apply asymmetric binding security policy with X509 token to the SOAP envelope. # # + envelope - The SOAP envelope +# + soap12 - A boolean flag. Set to `true` for SOAP 1.2, or `false` for SOAP 1.1. # + asymmetricBinding - The `AsymmetricBindingConfig` record with the required parameters # + return - A `xml` type of SOAP envelope if the security binding is successfully added or else `wssec:Error` -public isolated function applyAsymmetricBinding(xml envelope, *AsymmetricBindingConfig asymmetricBinding) returns xml|crypto:Error|Error { +public isolated function applyAsymmetricBinding(xml envelope, boolean soap12, + *AsymmetricBindingConfig asymmetricBinding) + returns xml|crypto:Error|Error { Document document = check new (envelope); WSSecurityHeader wsSecurityHeader = check addSecurityHeader(document); string securedEnvelope = envelope.toBalString(); @@ -177,8 +195,14 @@ public isolated function applyAsymmetricBinding(xml envelope, *AsymmetricBinding if signatureKey !is crypto:PrivateKey { return error Error("Signature key cannot be nil"); } - byte[] signedData = check signature.signData((envelope//*).toString(), - signatureAlgorithm, signatureKey); + byte[] signedData; + if soap12 { + signedData = check signature.signData((envelope//*).toString(), + signatureAlgorithm, signatureKey); + } else { + signedData = check signature.signData((envelope//*).toString(), + signatureAlgorithm, signatureKey); + } Signature signatureResult = check addSignature(signature, signatureAlgorithm, signedData); WsSecurity wsSecurity = new; securedEnvelope = check wsSecurity.applySignatureOnlyPolicy(wsSecurityHeader, signatureResult, @@ -190,7 +214,12 @@ public isolated function applyAsymmetricBinding(xml envelope, *AsymmetricBinding if encryptionKey !is crypto:PublicKey { return error Error("Encryption key cannot be nil"); } - byte[] encryptData = check crypto:encryptRsaEcb((envelope//*).toString().toBytes(), encryptionKey); + byte[] encryptData; + if soap12 { + encryptData = check crypto:encryptRsaEcb((envelope//*).toString().toBytes(), encryptionKey); + } else { + encryptData = check crypto:encryptRsaEcb((envelope//*).toString().toBytes(), encryptionKey); + } Encryption encryptionResult = check addEncryption(encryption, encryptionAlgorithm, encryptData); WsSecurity wsSecurity = new; securedEnvelope = check wsSecurity.applyEncryptionOnlyPolicy(wsSecurityHeader, encryptionResult); diff --git a/ballerina/soap_utils.bal b/ballerina/soap_utils.bal index ae2bc99..0e36532 100644 --- a/ballerina/soap_utils.bal +++ b/ballerina/soap_utils.bal @@ -18,9 +18,9 @@ import soap.wssec; import ballerina/crypto; import ballerina/http; -import ballerina/mime; -import ballerina/lang.regexp; import ballerina/jballerina.java; +import ballerina/lang.regexp; +import ballerina/mime; import ballerina/test; public isolated function validateTransportBindingPolicy(ClientConfig config) returns Error? { @@ -39,19 +39,20 @@ public isolated function validateTransportBindingPolicy(ClientConfig config) ret } public isolated function getReadOnlyClientConfig(ClientConfig original) returns readonly & ClientConfig = @java:Method { - 'class: "org.wssec.WsSecurity" + 'class: "org.wssec.WsSecurity" } external; public isolated function applySecurityPolicies(wssec:InboundSecurityConfig|wssec:InboundSecurityConfig[] security, - xml envelope) returns xml|crypto:Error|wssec:Error { + xml envelope, boolean soap12 = true) + returns xml|crypto:Error|wssec:Error { if security is wssec:TimestampTokenConfig { return wssec:applyTimestampToken(envelope, security); } else if security is wssec:UsernameTokenConfig { return wssec:applyUsernameToken(envelope, security); } else if security is wssec:SymmetricBindingConfig { - return wssec:applySymmetricBinding(envelope, security); + return wssec:applySymmetricBinding(envelope, soap12, security); } else if security is wssec:AsymmetricBindingConfig { - return wssec:applyAsymmetricBinding(envelope, security); + return wssec:applyAsymmetricBinding(envelope, soap12, security); } else if security is wssec:InboundSecurityConfig { return envelope; } else { @@ -63,9 +64,10 @@ public isolated function applySecurityPolicies(wssec:InboundSecurityConfig|wssec } } -public isolated function applyOutboundConfig(wssec:OutboundSecurityConfig outboundSecurity, xml envelope) - returns xml|Error { - xmlns "http://schemas.xmlsoap.org/soap/envelope/" as soap; +public isolated function applyOutboundConfig(wssec:OutboundSecurityConfig outboundSecurity, xml envelope, + boolean soap12 = true) returns xml|Error { + xmlns "http://schemas.xmlsoap.org/soap/envelope/" as soap11; + xmlns "http://www.w3.org/2003/05/soap-envelope" as soap12; xml soapEnvelope = envelope; do { wssec:EncryptionAlgorithm? encryptionAlgorithm = outboundSecurity.decryptionAlgorithm; @@ -76,7 +78,7 @@ public isolated function applyOutboundConfig(wssec:OutboundSecurityConfig outbou byte[] decryptDataResult = check crypto:decryptRsaEcb(encData, clientPrivateKey); string decryptedBody = "" + check string:fromBytes(decryptDataResult) + ""; string decryptedEnv = regexp:replace(re `.*`, soapEnvelope.toString(), - decryptedBody); + decryptedBody); soapEnvelope = check xml:fromString(decryptedEnv); } } @@ -85,8 +87,14 @@ public isolated function applyOutboundConfig(wssec:OutboundSecurityConfig outbou crypto:PublicKey? serverPublicKey = outboundSecurity.verificationKey; if serverPublicKey is crypto:PublicKey { byte[] signatureData = check wssec:getSignatureData(soapEnvelope); - check wssec:verifyData((soapEnvelope//*).toString().toBytes(), - signatureData, serverPublicKey, signatureAlgorithm); + if soap12 { + check wssec:verifyData((soapEnvelope//*).toString().toBytes(), + signatureData, serverPublicKey, signatureAlgorithm); + } else { + check wssec:verifyData((soapEnvelope//*).toString().toBytes(), + signatureData, serverPublicKey, signatureAlgorithm); + } + } } return soapEnvelope; @@ -97,22 +105,23 @@ public isolated function applyOutboundConfig(wssec:OutboundSecurityConfig outbou public isolated function sendReceive(xml|mime:Entity[] body, http:Client httpClient, string? soapAction = (), map headers = {}, string path = "", boolean soap12 = true) - returns xml|Error { + returns xml|mime:Entity[]|Error { http:Request req = soap12 ? createSoap12HttpRequest(body, soapAction, headers) : createSoap11HttpRequest(body, soapAction, headers); do { http:Response response = check httpClient->post(path, req); if soap12 { - return check createSoap12Response(response); + return check createSoapResponse(response); } - return check createSoap11Response(response); + return check createSoapResponse(response); } on fail var err { return error Error(SOAP_RESPONSE_ERROR, err); } } public isolated function sendOnly(xml|mime:Entity[] body, http:Client httpClient, string? soapAction = (), - map headers = {}, string path = "", boolean soap12 = true) returns Error? { + map headers = {}, string path = "", boolean soap12 = true) + returns Error? { http:Request req = soap12 ? createSoap12HttpRequest(body, soapAction, headers) : createSoap11HttpRequest(body, soapAction, headers); http:Response|http:ClientError response = httpClient->post(path, req); @@ -139,21 +148,17 @@ isolated function createSoap11HttpRequest(xml|mime:Entity[] body, string soapAct } isolated function createSoap12HttpRequest(xml|mime:Entity[] body, string? soapAction, - map headers = {}) returns http:Request { + map headers = {}) returns http:Request { http:Request req = new; if body is xml { req.setXmlPayload(body); + req.setHeader(mime:CONTENT_TYPE, mime:TEXT_XML); } else { req.setBodyParts(body); + req.setHeader(mime:CONTENT_TYPE, mime:MULTIPART_MIXED); } if soapAction is string { - var mediaType = mime:getMediaType(mime:APPLICATION_SOAP_XML); - if mediaType is mime:MediaType { - mediaType.parameters = {[ACTION]: soapAction}; - req.setHeader(mime:CONTENT_TYPE, mediaType.toString()); - } - } else { - req.setHeader(mime:CONTENT_TYPE, mime:APPLICATION_SOAP_XML); + req.addHeader(SOAP_ACTION, soapAction); } foreach string key in headers.keys() { req.addHeader(key, headers[key].toBalString()); @@ -161,21 +166,17 @@ isolated function createSoap12HttpRequest(xml|mime:Entity[] body, string? soapAc return req; } -isolated function createSoap12Response(http:Response response) returns xml|error { - xml payload = check response.getXmlPayload(); - xmlns "http://www.w3.org/2003/05/soap-envelope" as soap12; - return payload; -} - -isolated function createSoap11Response(http:Response response) returns xml|error { - xml payload = check response.getXmlPayload(); - xmlns "http://schemas.xmlsoap.org/soap/envelope/" as soap11; +isolated function createSoapResponse(http:Response response) returns xml|mime:Entity[]|error { + mime:Entity[]|http:ClientError payload = response.getBodyParts(); + if payload !is mime:Entity[] { + return response.getXmlPayload(); + } return payload; } public function assertUsernameToken(string envelopeString, string username, string password, - wssec:PasswordType passwordType, string body) returns error? { - string:RegExp bodyData = check regexp:fromString(body); + wssec:PasswordType passwordType, string body) returns error? { + string:RegExp bodyData = check regexp:fromString(body); test:assertTrue(envelopeString.includesMatch(bodyData)); string:RegExp usernameTokenTag = re `.*`; string:RegExp usernameTag = re `${username}`; @@ -186,7 +187,7 @@ public function assertUsernameToken(string envelopeString, string username, stri } public function assertSymmetricBinding(string envelopeString, string body) returns error? { - string:RegExp bodyData = check regexp:fromString(body); + string:RegExp bodyData = check regexp:fromString(body); test:assertTrue(envelopeString.includesMatch(bodyData)); assertSignatureWithoutX509(envelopeString); } diff --git a/gradle.properties b/gradle.properties index 703bea6..1cb266f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=0.8.2-SNAPSHOT +version=0.9.0-SNAPSHOT checkstylePluginVersion=10.12.0 spotbugsPluginVersion=5.0.14 diff --git a/native/src/main/java/io/ballerina/lib/soap/ExecutionCallback.java b/native/src/main/java/io/ballerina/lib/soap/ExecutionCallback.java new file mode 100644 index 0000000..4f17f34 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/soap/ExecutionCallback.java @@ -0,0 +1,45 @@ +// Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. 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. + +package io.ballerina.lib.soap; + +import io.ballerina.runtime.api.Future; +import io.ballerina.runtime.api.async.Callback; +import io.ballerina.runtime.api.values.BError; + +/** + * Callback class for executing Ballerina dependently type methods. + */ +public class ExecutionCallback implements Callback { + private final Future future; + + ExecutionCallback(Future future) { + this.future = future; + } + @Override + public void notifySuccess(Object o) { + this.future.complete(o); + } + + @Override + public void notifyFailure(BError bError) { + bError.printStackTrace(); + // Service level `panic` is captured in this method. + // Since, `panic` is due to a critical application bug or resource exhaustion we need to exit the application. + // Please refer: https://github.com/ballerina-platform/ballerina-standard-library/issues/2714 + System.exit(1); + } +} diff --git a/native/src/main/java/io/ballerina/lib/soap/Soap.java b/native/src/main/java/io/ballerina/lib/soap/Soap.java new file mode 100644 index 0000000..eb76da0 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/soap/Soap.java @@ -0,0 +1,63 @@ +// Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. 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. + +package io.ballerina.lib.soap; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.Future; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.async.StrandMetadata; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.types.UnionType; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; + +import static org.wssec.ModuleUtils.getModule; + +public class Soap { + private static final String REMOTE_FUNCTION = "generateResponse"; + public static final StrandMetadata REMOTE_EXECUTION_STRAND = new StrandMetadata( + getModule().getOrg(), + getModule().getName(), + getModule().getMajorVersion(), + REMOTE_FUNCTION); + + public static Object sendReceive11(Environment env, BObject soap11, Object body, BString action, + BMap headers, BString path, BTypedesc typeDesc) { + Future future = env.markAsync(); + ExecutionCallback executionCallback = new ExecutionCallback(future); + UnionType typeUnion = TypeCreator.createUnionType(PredefinedTypes.TYPE_XML, PredefinedTypes.TYPE_ANYDATA_ARRAY, + PredefinedTypes.TYPE_ERROR); + Object[] arguments = new Object[]{body, true, action, true, headers, true, path, true}; + env.getRuntime().invokeMethodAsyncConcurrently(soap11, REMOTE_FUNCTION, null, REMOTE_EXECUTION_STRAND, + executionCallback, null, typeUnion, arguments); + return null; + } + + public static Object sendReceive12(Environment env, BObject soap12, Object body, Object action, + BMap headers, BString path, BTypedesc typeDesc) { + Future future = env.markAsync(); + ExecutionCallback executionCallback = new ExecutionCallback(future); + UnionType typeUnion = TypeCreator.createUnionType(PredefinedTypes.TYPE_XML, PredefinedTypes.TYPE_ANYDATA_ARRAY, + PredefinedTypes.TYPE_ERROR); + Object[] arguments = new Object[]{body, true, action, true, headers, true, path, true}; + env.getRuntime().invokeMethodAsyncConcurrently(soap12, REMOTE_FUNCTION, null, REMOTE_EXECUTION_STRAND, + executionCallback, null, typeUnion, arguments); + return null; + } +} diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 26bf2bc..e22fda1 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -1,7 +1,7 @@ - + + + +