/** * Licensed to the Austrian Association for Software Tool Integration (AASTI) * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. The AASTI 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 org.openengsb.ports.jms; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.StringWriter; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Arrays; import java.util.Dictionary; import java.util.Hashtable; import java.util.Map; import java.util.UUID; import javax.crypto.SecretKey; import javax.jms.ConnectionFactory; import javax.jms.JMSException; import javax.jms.MessageConsumer; import javax.jms.MessageProducer; import javax.jms.Queue; import javax.jms.Session; import javax.jms.TemporaryQueue; import javax.jms.TextMessage; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.commons.codec.binary.Base64; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.Authenticator; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.mgt.DefaultSecurityManager; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.openengsb.connector.usernamepassword.Password; import org.openengsb.core.api.remote.MethodCall; import org.openengsb.core.api.remote.MethodCallMessage; import org.openengsb.core.api.remote.MethodResult; import org.openengsb.core.api.remote.MethodResultMessage; import org.openengsb.core.api.remote.RequestHandler; import org.openengsb.core.api.security.Credentials; import org.openengsb.core.api.security.PrivateKeySource; import org.openengsb.core.api.security.model.Authentication; import org.openengsb.core.api.security.model.EncryptedMessage; import org.openengsb.core.common.remote.FilterChain; import org.openengsb.core.common.remote.FilterChainFactory; import org.openengsb.core.common.remote.JsonMethodCallMarshalFilter; import org.openengsb.core.common.remote.RequestMapperFilter; import org.openengsb.core.common.remote.XmlDecoderFilter; import org.openengsb.core.common.remote.XmlMethodCallMarshalFilter; import org.openengsb.core.services.filter.EncryptedJsonMessageMarshaller; import org.openengsb.core.services.filter.JsonSecureRequestMarshallerFilter; import org.openengsb.core.services.filter.MessageAuthenticatorFilterFactory; import org.openengsb.core.services.filter.MessageCryptoFilterFactory; import org.openengsb.core.services.filter.MessageVerifierFilter; import org.openengsb.core.services.internal.RequestHandlerImpl; import org.openengsb.core.services.internal.security.model.ShiroContext; import org.openengsb.core.test.AbstractOsgiMockServiceTest; import org.openengsb.core.util.CipherUtils; import org.openengsb.core.util.DefaultOsgiUtilsService; import org.openengsb.domain.authentication.AuthenticationDomain; import org.openengsb.domain.authentication.AuthenticationException; import org.openengsb.labs.delegation.service.ClassProvider; import org.openengsb.labs.delegation.service.Constants; import org.openengsb.labs.delegation.service.internal.ClassProviderImpl; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.core.SessionCallback; import org.springframework.jms.listener.SimpleMessageListenerContainer; import org.springframework.jms.support.JmsUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.Sets; public class JMSPortTest extends AbstractOsgiMockServiceTest { private static final String PUBLIC_KEY_64 = "" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEwQedUFElYBNOW71NYLgKEGSqKEbGQ9xhlCjS" + "9qd8A7MdaVub61Npc6wSuLJNK1qnrSufWkiZxuo7IsyFnZl9bqkr1D/x4UqKEBmGZIh4s4WIMymw" + "TGu2HmAKuKO7JypfQpHemZpLmXTsNse1xFhTfshxWJq4+WqBdeoYZ8p1iwIDAQAB"; private static final String PRIVATE_KEY_64 = "" + "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMTBB51QUSVgE05bvU1guAoQZKoo" + "RsZD3GGUKNL2p3wDsx1pW5vrU2lzrBK4sk0rWqetK59aSJnG6jsizIWdmX1uqSvUP/HhSooQGYZk" + "iHizhYgzKbBMa7YeYAq4o7snKl9Ckd6ZmkuZdOw2x7XEWFN+yHFYmrj5aoF16hhnynWLAgMBAAEC" + "gYEAmyZX+c4e3uke8DhZU04EcjMxHhi6jpdujifF9W147ssAEB5HlfEAinQjaXPpbf7U8soUTwlj" + "nJeFlvI+8tIu+J7wuP9m9R/EC02kbYjQUOdmrIXr11GmDNSeKCuklLaQTCKl+eRmVCKk373tmtHE" + "/HLAkWsTvdufrkFQi9iaTlECQQDpnHnha5DrcQuUarhwWta+ZDLL56XawfcJZpPfKK2Jgxoqbvg9" + "k3i6IRS/kh0g0K98CRK5UvxAiQtDKkDy5z3ZAkEA15xIN5OgfMbE12p83cD4fAU2SpvyzsPk9tTf" + "Zb6jnKDAm+hxq1arRyaxL04ppTM/xRRS8DKJLrsAi0HhFzkcAwJAbiuQQyHSX2aZmm3V+46rdXCV" + "kBn32rncwf8xP23UoWRFo7tfsNJqfgT53vqOaBpil/FDdkjPk7PNrugvZx5syQJBAJjAEbG+Fu8P" + "axkqSjhYpDJJBwOopEa0JhxxB6vveb5XbN2HujAnAMUxtknLWFm/iyg2k+O0Cdhfh60hCTUIsr0C" + "QFT8w7k8/FfcAFl+ysJ2lSGpeKkt213QkHpAn2HvHRviVErKSHgEKh10Nf7pU3cgPwHDXNEuQ6Bb" + "Ky/vHQD1rMM="; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final String METHOD_CALL = "" + "{" + " \"classes\":[\"java.lang.String\",\"java.lang.Integer\",\"org.openengsb.ports.jms.TestClass\"]," + " \"methodName\":\"method\"," + " \"args\":[\"123\",5,{\"test\":\"test\"}]," + " \"metaData\":{\"serviceId\":\"test\"}" + "}"; private static final String AUTH_DATA = "" + "{" + " \"className\":\"org.openengsb.connector.usernamepassword.Password\"," + " \"data\":" + " {" + " \"value\":\"password\"" + " }" + "}"; private static final String METHOD_CALL_REQUEST = "" + "{" + " \"callId\":\"12345\"," + " \"answer\":true," + " \"timestamp\":" + System.currentTimeMillis() + "," + " \"principal\": \"user\"," + " \"credentials\":" + AUTH_DATA + "," + " \"methodCall\":" + METHOD_CALL + "}"; private static final String XML_METHOD_CALL_REQUEST = "" + "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + "<MethodCallRequest>" + " <callId>123</callId>" + " <answer>true</answer>" + " <methodCall>" + " <args xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">123</args>" + " <args xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:int\">5</args>" + " <args xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"testClass\">" + " <test>test</test>" + " </args>" + " <classes>java.lang.String</classes>" + " <classes>java.lang.Integer</classes>" + " <classes>org.openengsb.ports.jms.TestClass</classes>" + " <metaData>" + " <entry>" + " <key>serviceId</key>" + " <value>test</value>" + " </entry>" + " </metaData>" + " <methodName>method</methodName>" + " </methodCall>" + "</MethodCallRequest>"; private MethodCallMessage call; private MethodResultMessage methodReturn; private JMSTemplateFactory jmsTemplateFactory; private JMSIncomingPort incomingPort; private RequestHandler handler; private SimpleMessageListenerContainer simpleMessageListenerConainer; private JmsTemplate jmsTemplate; private PrivateKey privateKey; private PublicKey publicKey; @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); @Before public void setup() { System.setProperty("org.apache.activemq.default.directory.prefix", tempFolder.getRoot().getAbsolutePath() + "/"); setupKeys(); String num = UUID.randomUUID().toString(); ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("vm://localhost" + num); jmsTemplate = new JmsTemplate(connectionFactory); jmsTemplateFactory = new JMSTemplateFactory() { @Override public SimpleMessageListenerContainer createMessageListenerContainer() { return simpleMessageListenerConainer; } @Override public JmsTemplate createJMSTemplate(DestinationUrl destinationUrl) { return jmsTemplate; } }; simpleMessageListenerConainer = new SimpleMessageListenerContainer(); incomingPort = new JMSIncomingPort(); incomingPort.setFactory(jmsTemplateFactory); incomingPort.setConnectionFactory(connectionFactory); RequestHandlerImpl requestHandlerImpl = new RequestHandlerImpl(); requestHandlerImpl.setUtilsService(new DefaultOsgiUtilsService(bundleContext)); handler = requestHandlerImpl; TestInterface mock2 = mock(TestInterface.class); registerServiceViaId(mock2, "test", TestInterface.class); when(mock2.method(anyString(), anyInt(), any(TestClass.class))).thenReturn(new TestClass("test")); Map<String, String> metaData = Maps.newHashMap(ImmutableMap.of("serviceId", "test")); MethodCall methodCall = new MethodCall("method", new Object[]{ "123", 5, new TestClass("test"), }, metaData); call = new MethodCallMessage(methodCall, "123"); call.setDestination("host?receive"); MethodResult result = new MethodResult(new TestClass("test")); result.setMetaData(metaData); methodReturn = new MethodResultMessage(result, "123"); Dictionary<String, Object> props = new Hashtable<String, Object>(); props.put(Constants.PROVIDED_CLASSES_KEY, Password.class.getName()); props.put(Constants.DELEGATION_CONTEXT_KEY, org.openengsb.core.api.Constants.DELEGATION_CONTEXT_CREDENTIALS); registerService(new ClassProviderImpl(bundle, Sets.newHashSet(Password.class.getName())), props, ClassProvider.class); DefaultSecurityManager securityManager = new DefaultSecurityManager(); securityManager.setAuthenticator(new Authenticator() { @Override public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws org.apache.shiro.authc.AuthenticationException { return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), authenticationToken .getCredentials(), "openengsb"); } }); SecurityUtils.setSecurityManager(securityManager); } private void setupKeys() { privateKey = CipherUtils.deserializePrivateKey(Base64.decodeBase64(PRIVATE_KEY_64), "RSA"); publicKey = CipherUtils.deserializePublicKey(Base64.decodeBase64(PUBLIC_KEY_64), "RSA"); } @Test(timeout = 100000) public void testStart_ShouldListenToIncomingCallsAndCallSetRequestHandler() throws Exception { FilterChainFactory<String, String> factory = new FilterChainFactory<String, String>(String.class, String.class); factory.setFilters(Arrays.asList(JsonMethodCallMarshalFilter.class, new RequestMapperFilter(handler))); incomingPort.setFilterChain(factory.create()); incomingPort.start(); String resultString = sendWithTempQueue(METHOD_CALL_REQUEST); JsonNode resultMessage = OBJECT_MAPPER.readTree(resultString); JsonNode readTree = resultMessage.get("result"); assertThat(readTree.get("className").toString(), equalTo("\"org.openengsb.ports.jms.TestClass\"")); assertThat(readTree.get("metaData").toString(), equalTo("{\"serviceId\":\"test\"}")); assertThat(readTree.get("type").toString(), equalTo("\"Object\"")); assertThat(readTree.get("arg").toString(), equalTo("{\"test\":\"test\"}")); } private String sendWithTempQueue(final String msg) { String resultString = jmsTemplate.execute(new SessionCallback<String>() { @Override public String doInJms(Session session) throws JMSException { Queue queue = session.createQueue("receive"); MessageProducer producer = session.createProducer(queue); TemporaryQueue tempQueue = session.createTemporaryQueue(); MessageConsumer consumer = session.createConsumer(tempQueue); TextMessage message = session.createTextMessage(msg); message.setJMSReplyTo(tempQueue); producer.send(message); TextMessage response = (TextMessage) consumer.receive(10000); assertThat("server should set the value of the correltion ID to the value of the received message id", response.getJMSCorrelationID(), is(message.getJMSMessageID())); JmsUtils.closeMessageProducer(producer); JmsUtils.closeMessageConsumer(consumer); return response != null ? response.getText() : null; } }, true); return resultString; } @Test(timeout = 60000) public void testSendEncryptedMethodCall_shouldSendEncryptedResult() throws Exception { FilterChain secureChain = createSecureFilterChain(); incomingPort.setFilterChain(secureChain); incomingPort.start(); SecretKey sessionKey = CipherUtils.generateKey(CipherUtils.DEFAULT_SYMMETRIC_ALGORITHM, CipherUtils.DEFAULT_SYMMETRIC_KEYSIZE); byte[] encryptedKey = CipherUtils.encrypt(sessionKey.getEncoded(), publicKey); byte[] encryptedContent = CipherUtils.encrypt(METHOD_CALL_REQUEST.getBytes(), sessionKey); EncryptedMessage encryptedMessage = new EncryptedMessage(encryptedContent, encryptedKey); final String encryptedString = new ObjectMapper().writeValueAsString(encryptedMessage); String resultString = sendWithTempQueue(encryptedString); byte[] result = CipherUtils.decrypt(Base64.decodeBase64(resultString), sessionKey); MethodResultMessage result2 = OBJECT_MAPPER.readValue(result, MethodResultMessage.class); MethodResult methodResult = result2.getResult(); Object realResultArg = OBJECT_MAPPER.convertValue(methodResult.getArg(), Class.forName(methodResult.getClassName())); assertThat(realResultArg, equalTo((Object) new TestClass("test"))); } private FilterChain createSecureFilterChain() throws Exception { AuthenticationDomain authenticationManager = mock(AuthenticationDomain.class); when(authenticationManager.authenticate(anyString(), any(Credentials.class))).thenAnswer( new Answer<Authentication>() { @Override public Authentication answer(InvocationOnMock invocation) throws Throwable { String user = (String) invocation.getArguments()[0]; Password credentials = (Password) invocation.getArguments()[1]; if ("user".equals(user) && credentials.getValue().equals("password")) { return new Authentication(user, credentials.toString()); } throw new AuthenticationException("username and password did not match"); } }); PrivateKeySource keySource = mock(PrivateKeySource.class); when(keySource.getPrivateKey()).thenReturn(privateKey); MessageCryptoFilterFactory cipherFactory = new MessageCryptoFilterFactory(keySource, "AES"); FilterChainFactory<String, String> factory = new FilterChainFactory<String, String>(String.class, String.class); factory.setFilters(Arrays.asList( EncryptedJsonMessageMarshaller.class, cipherFactory, JsonSecureRequestMarshallerFilter.class, MessageVerifierFilter.class, new MessageAuthenticatorFilterFactory(new DefaultOsgiUtilsService(bundleContext), new ShiroContext()), new RequestMapperFilter(handler))); FilterChain secureChain = factory.create(); return secureChain; } @Test(timeout = 5000) public void testPortWithXmlFormat_shouldWorkWithXmlFilterChain() throws Exception { FilterChainFactory<String, String> factory = new FilterChainFactory<String, String>(String.class, String.class); factory.setFilters(Arrays.asList( XmlDecoderFilter.class, XmlMethodCallMarshalFilter.class, new RequestMapperFilter(handler))); incomingPort.setFilterChain(factory.create()); incomingPort.start(); String resultString = sendWithTempQueue(XML_METHOD_CALL_REQUEST); assertThat(resultString, containsString("<callId>123</callId>")); assertThat(resultString, containsString("<type>Object</type>")); assertThat(resultString, containsString("<test>test</test>")); } @Test public void testStop_shouldNotReactToIncomingCalls() throws Exception { SimpleMessageListenerContainer orig = simpleMessageListenerConainer; SimpleMessageListenerContainer containerSpy = spy(orig); simpleMessageListenerConainer = containerSpy; ConnectionFactory cf = new ActiveMQConnectionFactory("vm://localhost2"); incomingPort.setConnectionFactory(cf); incomingPort.start(); incomingPort.stop(); verify(containerSpy).stop(); } @Test public void testRequestMapping_shouldDeserialiseRequest() throws Exception { OBJECT_MAPPER.readValue(METHOD_CALL_REQUEST, MethodCallMessage.class); } @Test public void testMethodReturn_shouldDeserialiseResponse() throws Exception { StringWriter writer = new StringWriter(); OBJECT_MAPPER.writeValue(writer, methodReturn); JsonNode resultMessage = OBJECT_MAPPER.readTree(writer.toString()); JsonNode readTree = resultMessage.get("result"); assertThat(readTree.get("className").toString(), equalTo("\"org.openengsb.ports.jms.TestClass\"")); assertThat(readTree.get("metaData").toString(), equalTo("{\"serviceId\":\"test\"}")); assertThat(readTree.get("type").toString(), equalTo("\"Object\"")); assertThat(readTree.get("arg").toString(), equalTo("{\"test\":\"test\"}")); } }