Commits

Arkadi Shishlov committed c2be485

Initial import

Comments (0)

Files changed (15)

-Nothing there yet.
-Waiting for Tieto bureaucratic machine to authorize open-source process.
+Hi!
+I'm please to announce the availability of open-source implementation of
+OCS/Lync compatible SIP Producer endpoint.
+The source is licensed under Apache license.
+
+http://bitbucket.org/arkadi/camel-sipe
+
+Some bits of code - Type3Message.java thats deals with NTLM signatures are
+modeled from jCIFS (LGPL v2.1+) class of the same name. But similarity ends
+here, so I put the Apache license banner on it. If you think there are
+problems with code licensing - just let me know, and I'll try to find a
+consensus.
+
+The source could be built standalone or put into Camel tree and built there
+with minor pom modifications. jCIFS library must be installed into local repo:
+http://jcifs.samba.org/src/jcifs-1.3.17.jar
+mvn install:install-file -Dfile=jcifs-1.3.17.jar -DgroupId=org.samba.jcifs \
+    -DartifactId=jcifs -Dversion=1.3.17 -Dpackaging=jar
+
+The implementation is fully thread-safe in a sense that SIP state-machine
+is protected by synchronized functions to keep things simple and
+race/deadlock-free, at least for the initial release. It does not mean the
+performance is bad. Not at all! Some highlights includes:
+1. supports sending of multiple messages to multiple users to multiple
+servers in parallel
+2. event-driven state machine caches presence information and dialogs
+3. uses Camel asynchronous processing
+4. uses EPID and GRUU MS REGISTER extension
+5. uses enhanced presence MS SUBSCRIBE extension
+6. works with OCS clustered server pool
+7. multiple endpoints could be instantiated (on different ports) to
+connect to different OCS/AD domains
+8. multiple endpoints with different URL-s may share single SIP stack
+(some coding is required to detect incompatible configurations)
+9. properly calculates SIP request/response NTLM signatures, EPID ->
++sip.instance magic is also there.
+
+Tested against OCS 2007R2 and Lync.
+
+At present, only Producer part is implemented, but it could be easily
+extended in a day or two to provide the Consumer. I just didn't get to
+the business case where the Consumer functionality would be necessary.
+There are still some internal implementation deficiencies, but I believe
+that even in the current state it could be considered for a pre-production
+deployments.
+
+So, I invite you to review and use the code with a hope that in some near
+future it will land in the official Apache Camel source tree.
+Even though you're not a Camel user, you may find it educating in case you
+wish to know how to develop with JAIN-SIP and/or how inter-operate with
+Microsoft-extended SIP.
+
+
+Kudos to Camel, JAIN-JIP, and Pidgin SIPE developers who also made this
+possible!
+
+Usage example:
+
+from("jetty:http://localhost:1112/inbound")
+    .setHeader("To", constant("lynctest"))
+    .to("sipe://cs2010.example.com:5061?"
+    + "toUser=lynctest&" // used if not set via To header
+    + "toHost=example.com&"
+    + "fromUser=lynctest2&"
+    + "fromHost=example.com&"
+    + "fromPort=5066&"
+    + "ip=eth0&"
+    + "authUserName=lynctest2&"
+    + "authPassword=***&"
+    + "authAdDomain=exampledom&"
+    + "transport=tls&"
+    + "debugLog=/tmp/jain-sip-debug.log&"
+    + "presenceList=3500,5000,15500,12500");
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+<!--  <parent>
+    <groupId>org.apache.camel</groupId>
+    <artifactId>components</artifactId>
+    <version>2.9-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>-->
+
+    <groupId>org.apache.camel</groupId>
+	<artifactId>camel-sipe</artifactId>
+<!--	<packaging>bundle</packaging>-->
+	<name>Camel :: SIPE</name>
+    <version>2.8.3</version>
+	<description>Camel SIP protocol based communication component for proprietary Office Communicator extensions</description>
+
+<!--	<properties>
+		<camel.osgi.export.pkg>
+			org.apache.camel.component.sipe.*
+		</camel.osgi.export.pkg>
+	</properties>-->
+
+    <properties>
+        <camel-version>2.8.3</camel-version>
+        <log4j-version>1.2.16</log4j-version>
+        <slf4j-version>1.6.1</slf4j-version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <repositories>
+        <repository>
+            <id>jboss.release</id>
+            <name>JBoss Maven Repository</name>
+            <url>http://repository.jboss.org/nexus/content/groups/public-jboss</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </repository>
+        <repository>
+            <id>apache.release</id>
+            <name>Apache Releases Repository</name>
+            <url>https://repository.apache.org/content/repositories/releases</url>
+        </repository>
+        <repository>
+            <id>camel-extra.release</id>
+            <name>Camel Extra Releases Repository</name>
+            <url>http://camel-extra.googlecode.com/svn/maven2/releases</url>
+        </repository>
+        <repository>
+            <id>springframework.release</id>
+            <name>Spring Framework Releases Repository</name>
+            <url>http://maven.springframework.org/release</url>
+        </repository>
+    </repositories>
+
+  	<dependencies>
+	    <dependency>
+	      	<groupId>org.apache.camel</groupId>
+	      	<artifactId>camel-core</artifactId>
+            <version>${camel-version}</version>
+	    </dependency>
+        <!--
+        <dependency>
+	      	<groupId>org.apache.servicemix.specs</groupId>
+	      	<artifactId>org.apache.servicemix.specs.jain-sip-api-1.2</artifactId>
+	      	<version>${servicemix-specs-version}</version>
+	    </dependency>
+	    <dependency>
+	      	<groupId>org.apache.servicemix.bundles</groupId>
+	      	<artifactId>org.apache.servicemix.bundles.jain-sip-ri</artifactId>
+	      	<version>${jain-sip-ri-bundle-version}</version>
+        </dependency>
+        -->
+        <dependency>
+            <groupId>javax.sdp</groupId>
+            <artifactId>nist-sdp</artifactId>
+            <version>1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.sip</groupId>
+            <artifactId>jain-sip-api</artifactId>
+            <version>1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.sip</groupId>
+            <artifactId>jain-sip-ri</artifactId>
+            <version>1.2.X-SNAPSHOT</version>
+<!--            <version>1.2.148.9</version>-->
+	    </dependency>
+        <dependency>
+            <groupId>org.samba.jcifs</groupId>
+            <artifactId>jcifs</artifactId>
+            <version>1.3.17</version>
+	    </dependency>
+
+		<!-- testing -->
+		<dependency>
+			<groupId>org.apache.camel</groupId>
+			<artifactId>camel-test</artifactId>
+            <version>${camel-version}</version>
+<!--			<scope>test</scope>-->
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+            <version>4.8.1</version>
+			<scope>test</scope>
+		</dependency>
+
+		<!-- logging -->
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-log4j12</artifactId>
+            <version>${slf4j-version}</version>
+<!--			<scope>test</scope>-->
+		</dependency>
+		<dependency>
+			<groupId>log4j</groupId>
+			<artifactId>log4j</artifactId>
+            <version>${log4j-version}</version>
+<!--			<scope>test</scope>-->
+		</dependency>
+  	</dependencies>
+
+</project>

src/main/java/org/apache/camel/component/sipe/MSUUID.java

+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.camel.component.sipe;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/* http://msdn.microsoft.com/en-us/library/dd905844(v=office.12).aspx */
+public class MSUUID {
+    private static final String namespace = "fcacfb03-8a73-46ef-91b1-e5ebeeaba4fe";
+    private byte[] uuid = new byte[16];
+
+    public MSUUID(String id) throws NumberFormatException {
+        // Jug, thanks!
+        if (id.length() != 36)
+            throw new NumberFormatException("UUID has to be represented by the standard 36-char representation");
+
+        for (int i = 0, j = 0; i < 36; ++j) {
+            // Need to bypass hyphens:
+            switch (i) {
+            case 8:
+            case 13:
+            case 18:
+            case 23:
+                if (id.charAt(i) != '-') {
+                    throw new NumberFormatException("UUID has to be represented by the standard 36-char representation");
+                }
+                ++i;
+            }
+            char c = id.charAt(i);
+
+            if (c >= '0' && c <= '9') {
+                uuid[j] = (byte) ((c - '0') << 4);
+            } else if (c >= 'a' && c <= 'f') {
+                uuid[j] = (byte) ((c - 'a' + 10) << 4);
+            } else if (c >= 'A' && c <= 'F') {
+                uuid[j] = (byte) ((c - 'A' + 10) << 4);
+            } else {
+                throw new NumberFormatException("Non-hex character '"+c+"'");
+            }
+
+            c = id.charAt(++i);
+
+            if (c >= '0' && c <= '9') {
+                uuid[j] |= (byte) (c - '0');
+            } else if (c >= 'a' && c <= 'f') {
+                uuid[j] |= (byte) (c - 'a' + 10);
+            } else if (c >= 'A' && c <= 'F') {
+                uuid[j] |= (byte) (c - 'A' + 10);
+            } else {
+                throw new NumberFormatException("Non-hex character '"+c+"'");
+            }
+            ++i;
+        }
+    }
+
+    public static String generate(String epid) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+        MSUUID uuid = new MSUUID(namespace);
+        MessageDigest hash = MessageDigest.getInstance("SHA1");
+        uuid.swap();
+        hash.update(uuid.uuid);
+        hash.update(epid.getBytes("ASCII"));
+        uuid.uuid = hash.digest(); // 20 bytes, but anyway...
+        uuid.swap();
+        uuid.mod();
+        return uuid.toString();
+    }
+
+    /* this the most critical part the MS stuff differs from all other UUID tools
+     * the time_low, time_mid, and time_hi_and_version fields are read as numbers
+     * and placed in byte array as little-endian integers, so we must swap out BE
+     * loaded data
+     */
+    private void swap () {
+        byte b;
+        // time_low
+        b = uuid[0];
+        uuid[0] = uuid[3];
+        uuid[3] = b;
+        b = uuid[1];
+        uuid[1] = uuid[2];
+        uuid[2] = b;
+        // time_mid
+        b = uuid[4];
+        uuid[4] = uuid[5];
+        uuid[5] = b;
+        // time_hi_and_version
+        b = uuid[6];
+        uuid[6] = uuid[7];
+        uuid[7] = b;
+    }
+
+    /* perform masking as specified in UUID specification */
+    private final static int INDEX_TYPE = 6;
+    private final static int INDEX_VARIATION = 8;
+    private void mod() {
+        uuid[INDEX_TYPE] &= (byte) 0x0F;
+        uuid[INDEX_TYPE] |= (byte) (5 << 4); // v5
+        uuid[INDEX_VARIATION] &= (byte) 0x3F;
+        uuid[INDEX_VARIATION] |= (byte) 0x80;
+    }
+
+    private final static String hexChars = "0123456789abcdef";
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder(36);
+        for (int i = 0; i < 16; ++i) {
+            switch (i) {
+                case 4:
+                case 6:
+                case 8:
+                case 10:
+                    sb.append('-');
+            }
+            int hex = uuid[i] & 0xFF;
+            sb.append(hexChars.charAt(hex >> 4));
+            sb.append(hexChars.charAt(hex & 0x0f));
+        }
+        return sb.toString();
+    }
+}

src/main/java/org/apache/camel/component/sipe/ProxyRouter.java

+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.camel.component.sipe;
+
+import gov.nist.javax.sip.stack.HopImpl;
+import java.util.ArrayList;
+import java.util.ListIterator;
+import javax.sip.SipStack;
+import javax.sip.address.Hop;
+import javax.sip.address.Router;
+import javax.sip.message.Request;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class ProxyRouter implements Router {
+    private static final Logger log = LoggerFactory.getLogger(ProxyRouter.class);
+
+    SipProducerListener listener;
+
+    public ProxyRouter(SipStack stack, String proxy) {
+    }
+
+    void setSipListener(SipProducerListener listener) {
+        this.listener = listener;
+    }
+
+    public Hop getOutboundProxy() {
+        if (log.isDebugEnabled())
+            log.debug("routing to " + listener.getProxyHost() + ":" + listener.getProxyPort() + " via " + listener.getTransport().toUpperCase());
+        return new HopImpl(listener.getProxyHost(), listener.getProxyPort(), listener.getTransport());
+    }
+
+    public ListIterator getNextHops(Request request) {
+        ArrayList<Hop> a = new ArrayList<Hop>(1);
+        a.add(getNextHop(request));
+        return a.listIterator();
+    }
+
+    public Hop getNextHop(Request request) {
+        return getOutboundProxy();
+    }
+}

src/main/java/org/apache/camel/component/sipe/SipComponent.java

+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.camel.component.sipe;
+
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.impl.DefaultComponent;
+
+public class SipComponent extends DefaultComponent {
+
+    @Override
+    protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
+        return new SipEndpoint(uri, remaining, this);
+    }
+}

src/main/java/org/apache/camel/component/sipe/SipEndpoint.java

+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.camel.component.sipe;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.sip.ListeningPoint;
+import javax.sip.SipFactory;
+import javax.sip.SipProvider;
+import javax.sip.SipStack;
+
+import org.apache.camel.Component;
+import org.apache.camel.Consumer;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.impl.DefaultEndpoint;
+import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class SipEndpoint extends DefaultEndpoint {
+    private static final transient Logger log = LoggerFactory.getLogger(SipEndpoint.class);
+
+    private static volatile int seq = 0;
+    private static final SipFactory sipFactory = SipFactory.getInstance();
+    static {
+        sipFactory.setPathName("gov.nist");
+    }
+    private final Map<String, SipProducerListener> listeningPointListeners = new HashMap<String, SipProducerListener>();
+
+    private String uri;
+
+    private final Properties sipStackProperties = new Properties();
+
+    // only set in instance of SipEndpoint that created Listening Point, Provider, and started the SIP stack
+    private SipStack sipStack = null;
+    private ListeningPoint listeningPoint;
+    private SipProvider provider;
+    // set in every instance of SipEndpoint
+    private SipProducerListener sipProducerListener = null;
+    // reference counter for every Producer started/stopped
+    private int refCounter = 0;
+
+    private String stackName;
+    private String transport = "tcp";
+    private String trustStore;
+    private String keyStore;
+    private String keyStorePassword;
+    private int maxForwards;
+    private boolean consumer = false;
+    private String maxMessageSize;
+    // XXX not used
+    private String contentType = "text/plain";
+    private String serverLog;
+    private String debugLog;
+    private String traceLevel = "32";
+    private String ip;
+	private String authUserName;
+	private String authPassword;
+	private String authAdDomain;
+    private final Set<String> presenceList = new HashSet<String>();
+    private String fromUser;
+    private String fromHost;
+    private int fromPort = 5060;
+    private String toUser;
+    private String toHost;
+    private String proxyHost;
+    private int proxyPort = 5060;
+
+    public SipEndpoint(String uri, String remaining, Component component) throws SocketException {
+        super(uri, component);
+        this.uri = remaining;
+
+        setStackName("CIH-" + seq++);
+        setMaxMessageSize("20480");
+        // Possible statuses of users in OCS
+        // 3500 - Online
+        // 5000 - Inactive
+        // 6500 - Busy
+        // 9500 - Do not disturb
+        // 12500 - Be right back
+        // 15500 - Away
+        // 18000 - Offline
+        setPresenceList("3500,5000,12500,15550");
+        setIp(null);
+        sipStackProperties.setProperty("javax.sip.USE_ROUTER_FOR_ALL_URIS", "true");
+        sipStackProperties.setProperty("javax.sip.ROUTER_PATH", "org.apache.camel.component.sipe.ProxyRouter");
+        //sipStackProperties.setProperty("gov.nist.javax.sip.THREAD_POOL_SIZE", "1");
+        //sipStackProperties.setProperty("gov.nist.javax.sip.TCP_POST_PARSING_THREAD_POOL_SIZE", "1");
+    }
+
+    public boolean isSingleton() {
+        return true;
+    }
+
+    // do not use atomic ref - use synchronized!
+    protected synchronized void startSipStack() throws Exception {
+        if (refCounter++ > 0)
+            return;
+
+        String user = null;
+        String host = null;
+        String port = null;
+
+        int portI = uri.indexOf(":");
+        if (portI > 0) {
+            port = uri.substring(portI+1);
+            uri = uri.substring(0, portI);
+        }
+        int atI = uri.indexOf("@");
+        if (atI > 0) {
+            user = uri.substring(0, atI);
+            uri = uri.substring(atI);
+        }
+        host = uri;
+
+        if (consumer) {
+            if (fromUser == null)
+                fromUser = user;
+            fromHost = host;
+            if (port != null)
+                fromPort = Integer.valueOf(port);
+        } else {
+            if (toUser == null)
+                toUser = user;
+            proxyHost = host;
+            if (port != null)
+                proxyPort = Integer.valueOf(port);
+            ObjectHelper.notNull(proxyHost, "Proxy Host (sipe:proxy-host:port)");
+        }
+        ObjectHelper.notNull(fromUser, "fromUser");
+        ObjectHelper.notNull(fromHost, "fromHost");
+        ObjectHelper.notNull(fromPort, "fromPort");
+
+        String instanceKey = ip + "|" + fromPort;
+        synchronized (listeningPointListeners) {
+            // TODO must check for transport tcp/tls (at least)
+            sipProducerListener = listeningPointListeners.get(instanceKey);
+            if (sipProducerListener == null) {
+                sipStack = sipFactory.createSipStack(sipStackProperties);
+                listeningPoint = sipStack.createListeningPoint(ip, fromPort, transport);
+                provider = sipStack.createSipProvider(listeningPoint);
+                sipProducerListener = new SipProducerListener(this);
+                provider.addSipListener(sipProducerListener);
+                ((ProxyRouter)sipStack.getRouter()).setSipListener(sipProducerListener);
+                listeningPointListeners.put(instanceKey, sipProducerListener);
+            }
+            // XXX sipProducerListener is initialized with SipEndpoint with different settings
+        }
+        if (sipStack != null)
+            sipStack.start();
+    }
+
+    protected synchronized void stopSipStack() throws Exception {
+        if (--refCounter > 0)
+            return;
+
+        if (sipStack == null) {
+            sipProducerListener = null;
+            return;
+        }
+
+        sipProducerListener.stopTimer();
+        sipStack.stop();
+        sipStack.deleteListeningPoint(listeningPoint);
+        provider.removeSipListener(sipProducerListener);
+        sipStack.deleteSipProvider(provider);
+        
+        sipProducerListener = null;
+        provider = null;
+        sipStack = null;
+    }
+
+    public Consumer createConsumer(Processor processor) throws Exception {
+        if (!consumer)
+            throw new UnsupportedOperationException("Endpoint is not a Consumer (set consumer=true)");
+        throw new UnsupportedOperationException("SIPE Consumer not implemented");
+        // return new SipConsumer(this, processor);
+    }
+
+    public Producer createProducer() throws Exception {
+        if (consumer)
+            throw new UnsupportedOperationException("Endpoint is a Consumer (set consumer=false)");
+        return new SipProducer(this);
+    }
+
+    private String getInetAddr(NetworkInterface ni) throws SocketException {
+        InetAddress addr = null;
+        Enumeration<InetAddress> addresses = ni.getInetAddresses();
+        while (addresses.hasMoreElements()) {
+           addr = addresses.nextElement();
+           if (addr instanceof Inet4Address)
+               break; // just take first IPv4
+        }
+        if (addr != null)
+            return addr.getHostAddress();
+        return null;
+    }
+
+    public void setIp(String ip) throws SocketException {
+        NetworkInterface ni;
+        if (ip != null) {
+            // interface by name
+            ni = NetworkInterface.getByName(ip);
+            if (ni != null) {
+                ip = getInetAddr(ni);
+                if (ip != null) {
+                    this.ip = ip;
+                    return;
+                }
+            }
+            // lets hope its IP and not mis-spelled interface name
+            this.ip = ip;
+            return;
+        }
+        Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+
+        // any other IP that looks goods for the purpose
+        while(interfaces.hasMoreElements()) {
+            ni = interfaces.nextElement();
+            if (ni.isUp() && !ni.isLoopback() && !ni.isPointToPoint() && !ni.isVirtual()) {
+                ip = getInetAddr(ni);
+                if (ip != null) {
+                    this.ip = ip;
+                    return;
+                }
+            }
+        }
+        throw new SocketException("Unable to determine host primary IP for SIP stack binding");
+    }
+
+    public String getIp() {
+        return ip;
+    }
+
+    public void setPresenceList(String presenceCodes) {
+        presenceList.clear();
+        presenceList.addAll(Arrays.asList(presenceCodes.split(",")));
+    }
+
+    public Set<String> getPresenceList() {
+        return presenceList;
+    }
+
+    public boolean isConsumer() {
+        return consumer;
+    }
+
+    public void setConsumer(boolean consumer) {
+        this.consumer = consumer;
+    }
+
+    public String getAuthAdDomain() {
+        return authAdDomain;
+    }
+
+    public void setAuthAdDomain(String authAdDomain) {
+        this.authAdDomain = authAdDomain;
+    }
+
+    public String getAuthUserName() {
+        return authUserName;
+    }
+
+    public void setAuthUserName(String authUserName) {
+        this.authUserName = authUserName;
+    }
+
+    public String getAuthPassword() {
+        return authPassword;
+    }
+
+    public void setAuthPassword(String authPassword) {
+        this.authPassword = authPassword;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public void setContentType(String contentType) {
+        this.contentType = contentType;
+    }
+
+    public String getFromHost() {
+        return fromHost;
+    }
+
+    public void setFromHost(String fromHost) {
+        this.fromHost = fromHost;
+    }
+
+    public int getFromPort() {
+        return fromPort;
+    }
+
+    public void setFromPort(int fromPort) {
+        this.fromPort = fromPort;
+    }
+
+    public String getFromUser() {
+        return fromUser;
+    }
+
+    public void setFromUser(String fromUser) {
+        this.fromUser = fromUser;
+    }
+
+    public int getMaxForwards() {
+        return maxForwards;
+    }
+
+    public void setMaxForwards(int maxForwards) {
+        this.maxForwards = maxForwards;
+    }
+
+    public String getMaxMessageSize() {
+        return maxMessageSize;
+    }
+
+    public void setMaxMessageSize(String maxMessageSize) {
+        this.maxMessageSize = maxMessageSize;
+        sipStackProperties.setProperty("gov.nist.javax.sip.MAX_MESSAGE_SIZE", maxMessageSize);
+    }
+
+    public String getDebugLog() {
+        return debugLog;
+    }
+
+    public void setDebugLog(String nistDebugLog) {
+        this.debugLog = nistDebugLog;
+        sipStackProperties.setProperty("gov.nist.javax.sip.DEBUG_LOG", nistDebugLog);
+        setTraceLevel(traceLevel);
+    }
+
+    public String getServerLog() {
+        return serverLog;
+    }
+
+    public void setServerLog(String nistServerLog) {
+        this.serverLog = nistServerLog;
+        sipStackProperties.setProperty("gov.nist.javax.sip.SERVER_LOG", nistServerLog);
+        setTraceLevel(traceLevel);
+    }
+
+    public String getTraceLevel() {
+        return traceLevel;
+    }
+
+    public void setTraceLevel(String nistTraceLevel) {
+        this.traceLevel = nistTraceLevel;
+        sipStackProperties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", nistTraceLevel);
+    }
+
+    public String getStackName() {
+        return stackName;
+    }
+
+    public void setStackName(String stackName) {
+        this.stackName = stackName;
+        sipStackProperties.setProperty("javax.sip.STACK_NAME", stackName);
+    }
+
+    public String getToHost() {
+        return toHost;
+    }
+
+    public void setToHost(String toHost) {
+        this.toHost = toHost;
+    }
+
+    public String getProxyHost() {
+        return proxyHost;
+    }
+
+    public void setProxyHost(String proxyHost) {
+        this.proxyHost = proxyHost;
+    }
+
+    public int getProxyPort() {
+        return proxyPort;
+    }
+
+    public void setProxyPort(int toPort) {
+        this.proxyPort = toPort;
+    }
+
+    public String getToUser() {
+        return toUser;
+    }
+
+    public void setToUser(String toUser) {
+        this.toUser = toUser;
+    }
+
+    public String getTransport() {
+        return transport;
+    }
+
+    public void setTransport(String transport) {
+        this.transport = transport;
+    }
+
+    public String getKeyStore() {
+        return keyStore;
+    }
+
+    // see JAIN-SIP gov.nist.core.net.SslNetworkLayer and gov.nist.javax.sip.SipStackImpl
+    public void setKeyStore(String keyStore) {
+        this.keyStore = keyStore;
+        sipStackProperties.setProperty("javax.net.ssl.keyStore", keyStore);
+    }
+
+    public String getKeyStorePassword() {
+        return keyStorePassword;
+    }
+
+    public void setKeyStorePassword(String keyStorePassword) {
+        this.keyStorePassword = keyStorePassword;
+        sipStackProperties.setProperty("javax.net.ssl.keyStorePassword", keyStore);
+    }
+
+    public String getTrustStore() {
+        return trustStore;
+    }
+
+    public void setTrustStore(String trustStore) {
+        this.trustStore = trustStore;
+        sipStackProperties.setProperty("javax.net.ssl.trustStore", keyStore);
+    }
+
+    public SipProducerListener getSipProducerListener() {
+        return sipProducerListener;
+    }
+
+    public SipProvider getProvider() {
+        return provider;
+    }
+
+    public SipStack getSipStack() {
+        return sipStack;
+    }
+}

src/main/java/org/apache/camel/component/sipe/SipPresenceIncompatible.java

+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.camel.component.sipe;
+
+/**
+ * Exception thrown when user presence status is incompatible with presenceList= configured on endpoint.
+ */
+public class SipPresenceIncompatible extends RuntimeException {
+
+    public SipPresenceIncompatible(String message) {
+        super(message);
+    }
+
+}

src/main/java/org/apache/camel/component/sipe/SipProducer.java

+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.camel.component.sipe;
+
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.AsyncProcessor;
+import org.apache.camel.Exchange;
+import org.apache.camel.ServicePoolAware;
+import org.apache.camel.impl.DefaultProducer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SipProducer extends DefaultProducer implements AsyncProcessor, ServicePoolAware {
+    private static final Logger log = LoggerFactory.getLogger(SipProducer.class);
+
+    public static final String COMPLETED = "org.apache.camel.component.sipe.SipProducer.COMPLETED";
+    public static final long TIMEOUT = 10000;
+
+    private SipEndpoint sipEndpoint;
+
+    public SipProducer(SipEndpoint sipEndpoint) {
+        super(sipEndpoint);
+        this.sipEndpoint = sipEndpoint;
+    }
+
+    // DefaultProducer doStart/Stop contains logging statements only,
+    // so the sequence of super invocation is not essential
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        sipEndpoint.startSipStack();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+        sipEndpoint.stopSipStack();
+    }
+
+    private String getTo(Exchange exchange) {
+        String to = exchange.getIn().getHeader("to", String.class);
+        if (to == null)
+            to = sipEndpoint.getToUser();
+        if (to == null)
+            throw new IllegalArgumentException(
+                    "SIP adressee not set - use setHeader(\"to\") or specify toUser= route parameter");
+        if (to.indexOf("@") == -1) {
+            if (sipEndpoint.getToHost() == null)
+                throw new IllegalArgumentException(
+                        "SIP adressee domain is not set - use user@domain TO or specify toHost= route parameter");
+            to = to + "@" + sipEndpoint.getToHost();
+        }
+        return to;
+    }
+
+    public void process(Exchange exchange) throws Exception {
+        final String body = exchange.getIn().getBody(String.class);
+        String to = getTo(exchange);
+        synchronized (body) {
+            try {
+                sipEndpoint.getSipProducerListener().sendChatMessage(exchange, to, body, new AsyncCallback() {
+                    public void done(boolean doneSync) {
+                        synchronized (body) {
+                            body.notifyAll();
+                        }
+                    }
+                });
+                long start = System.currentTimeMillis();
+                while (exchange.getProperty(COMPLETED) == null) {
+                    body.wait(TIMEOUT);
+                    if (System.currentTimeMillis() - start > TIMEOUT)
+                        throw new RuntimeException("Too much time elapsed waiting for message completion");
+                }
+                exchange.removeProperty(COMPLETED);
+                if (exchange.isFailed()) {
+                    Exception e = exchange.getException();
+                    if (e == null)
+                        throw new RuntimeException("Exchange failed for unknown reason");
+                    else
+                        throw e;
+                }
+            } catch (Exception ex) {
+                log.error("process() failed", ex);
+                throw ex;
+            }
+        }
+    }
+
+    // http://camel.apache.org/asynchronous-processing.html
+    public boolean process(Exchange exchange, AsyncCallback callback) {
+        String body = exchange.getIn().getBody(String.class);
+        String to;
+        try {
+            to = getTo(exchange);
+        } catch (Exception e) {
+            exchange.setException(e);
+            //callback.done(true); // XXX ?
+            return true;
+        }
+        try {
+            sipEndpoint.getSipProducerListener().sendChatMessage(exchange, to, body, callback);
+            return false;
+        } catch (Exception ex) {
+            log.error("async process() failed", ex);
+            exchange.setException(ex);
+            return true;
+        }
+    }
+}

src/main/java/org/apache/camel/component/sipe/SipProducerListener.java

+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.camel.component.sipe;
+
+import gov.nist.javax.sip.Utils;
+import gov.nist.javax.sip.header.WWWAuthenticate;
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.message.SIPResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.sip.ClientTransaction;
+import javax.sip.Dialog;
+import javax.sip.address.Address;
+import javax.sip.address.SipURI;
+import javax.sip.header.AuthorizationHeader;
+import javax.sip.header.CSeqHeader;
+import javax.sip.header.ProxyAuthorizationHeader;
+import javax.sip.header.ViaHeader;
+import javax.sip.DialogTerminatedEvent;
+import javax.sip.IOExceptionEvent;
+import javax.sip.InvalidArgumentException;
+import javax.sip.RequestEvent;
+import javax.sip.ResponseEvent;
+import javax.sip.ServerTransaction;
+import javax.sip.SipException;
+import javax.sip.SipFactory;
+import javax.sip.SipListener;
+import javax.sip.SipProvider;
+import javax.sip.TimeoutEvent;
+import javax.sip.TransactionTerminatedEvent;
+import javax.sip.address.AddressFactory;
+import javax.sip.header.CallIdHeader;
+import javax.sip.header.ContactHeader;
+import javax.sip.header.ContentTypeHeader;
+import javax.sip.header.ExpiresHeader;
+import javax.sip.header.FromHeader;
+import javax.sip.header.Header;
+import javax.sip.header.HeaderFactory;
+import javax.sip.header.MaxForwardsHeader;
+import javax.sip.header.SubscriptionStateHeader;
+import javax.sip.header.ToHeader;
+import javax.sip.message.Message;
+import javax.sip.message.MessageFactory;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+import jcifs.ntlmssp.Type2Message;
+import jcifs.util.Base64;
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.Exchange;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/* Thanks to JAIN-SIP and Pidgin SIPE developers! */
+
+public class SipProducerListener implements SipListener {
+    private static final Logger log = LoggerFactory.getLogger(SipProducerListener.class);
+
+    // singletons
+    private static final SipFactory sipFactory = SipFactory.getInstance();
+	private static AddressFactory addressFactory = null;
+	private static HeaderFactory headerFactory = null;
+	private static MessageFactory messageFactory = null;
+
+    // headers that never changes
+	private static MaxForwardsHeader maxForwardsHeader;
+	private static Header userAgentHeader;
+	private static Header authSuportedHeader;
+    private static Header authEventHeader;
+    private static Header authAllowEventsHeader;
+	private static Header[] subscribeHeaders;
+	private static Header[] inviteHeaders;
+	private static ContentTypeHeader defaultContentTypeHeader;
+
+    // for Via header generation
+    private static final Utils sipUtils = new Utils();
+
+    private static final int PRESENCE_CACHE_VALIDITY = 3600; // sec
+    static {
+        try {
+            addressFactory = sipFactory.createAddressFactory();
+            headerFactory = sipFactory.createHeaderFactory();
+            messageFactory = sipFactory.createMessageFactory();
+            maxForwardsHeader = headerFactory.createMaxForwardsHeader(70);
+            userAgentHeader = headerFactory.createHeader("User-Agent", getUserAgentString());
+            authSuportedHeader = headerFactory.createSupportedHeader("gruu-10, com.microsoft.msrtc.presence, adhoclist, msrtc-event-categories");
+            authEventHeader = headerFactory.createHeader("Event", "registration");
+            authAllowEventsHeader = headerFactory.createHeader("Allow-Events", "presence");
+            /* The SUBSCRIBE request uses "presence" Event package as specified in
+             * http://www.ietf.org/rfc/rfc3265.txt and http://www.ietf.org/rfc/rfc3856.txt
+             * but we're not using application/pidf+xml as specifed in
+             * http://www.ietf.org/rfc/rfc3856.txt because it only provides coarse-graned
+             * presence state: offline (closed), online (open), away, busy.
+             *
+             * With MS enhanced presence http://msdn.microsoft.com/en-us/library/dd922532(v=office.12).aspx
+             * it possible to get more states: Inactive, Do not disturb, Be right back.
+             */
+            subscribeHeaders = new Header[] {
+                    headerFactory.createHeader("Event", "presence"),
+                    headerFactory.createAcceptHeader("application", "rlmi+xml"),
+                    headerFactory.createAcceptHeader("multipart", "related"),
+                    // this is obsolete and we don't support it
+                    // headerFactory.createAcceptHeader("text", "xml+msrtc.pidf"),
+
+                    // MS enhanced presence
+                    headerFactory.createAcceptHeader("application", "msrtc-event-categories+xml"),
+
+                    // RFC3856 presence schema
+                    // headerFactory.createAcceptHeader("application", "xpidf+xml"),
+                    // headerFactory.createAcceptHeader("application", "pidf+xml"),
+
+                    // why?
+                    headerFactory.createHeader("Supported", "eventlist"),
+
+                    // BENOTIFY request does not require response
+                    // http://msdn.microsoft.com/en-us/library/cc246160(v=PROT.10).aspx
+                    headerFactory.createHeader("Supported", "ms-benotify"),
+                    headerFactory.createHeader("Proxy-Require", "ms-benotify"),
+
+                    // do not use NOTIFY piggyback on SUBSCRIBE response
+                    // http://msdn.microsoft.com/en-us/library/cc246152(v=PROT.10).aspx
+                    // headerFactory.createHeader("Supported", "ms-piggyback-first-notify"),
+
+                    // do not use batched SUBSCRIBE
+                    // http://msdn.microsoft.com/en-us/library/dd944569(v=office.12).aspx
+                    // headerFactory.createHeader("Require", "adhoclist"),
+                    // headerFactory.createHeader("Require", "categoryList"),
+                    headerFactory.createHeader("Expires", PRESENCE_CACHE_VALIDITY + "")
+                };
+            inviteHeaders = new Header[] {
+                    // do we need this?
+                    headerFactory.createHeader("Supported","ms-delayed-accept"),
+                    headerFactory.createHeader("Supported","ms-renders-gif"),
+                    /*
+                    headerFactory.createHeader("Allow", "INVITE"),
+                    headerFactory.createHeader("Allow", "BYE"),
+                    headerFactory.createHeader("Allow", "ACK"),
+                    headerFactory.createHeader("Allow", "CANCEL"),
+                    headerFactory.createHeader("Allow", "INFO"),
+                    headerFactory.createHeader("Allow", "UPDATE"),
+                    headerFactory.createHeader("Allow", "REFER"),
+                    headerFactory.createHeader("Allow", "NOTIFY"),
+                    headerFactory.createHeader("Allow", "BENOTIFY"),
+                    headerFactory.createHeader("Allow", "OPTIONS"),
+                    */
+                    headerFactory.createHeader("ms-keep-alive", "UAC;hop-hop=yes")
+                };
+            defaultContentTypeHeader = headerFactory.createContentTypeHeader("text", "plain");
+            // defaulContentTypeHeader.setParameter("charset", "UTF-8");
+        } catch (Exception ex) {
+            log.error("SipProducerListener <clinit> failure", ex);
+        }
+    }
+
+    private static String getUserAgentString() {
+        // UCCAPI/3.5.6907.37 OC/3.5.6907.37 (Microsoft Office Communicator 2007 R2)
+        String userAgent = "Camel::SIPE JAIN-SIP/1.2";
+        try {
+            Properties p = new Properties();
+            p.load(SipProducerListener.class.getResourceAsStream("/META-INF/maven/com.tieto/camel-sipe/pom.properties"));
+            String cameSipeVersion = p.getProperty("version");
+            p.clear();
+            p.load(SipProducerListener.class.getResourceAsStream("/META-INF/maven/javax.sip/jain-sip-ri/pom.properties"));
+            String jainSipVersion = p.getProperty("version", "1.2");
+            userAgent = "Camel::SIPE/" + cameSipeVersion + " JAIN-SIP/" + jainSipVersion;
+            log.info("Initializing " + userAgent);
+        } catch (Exception e) {}
+        return userAgent;
+    }
+
+    // supplied by SipEndpoint
+    private final SipEndpoint endpoint;
+    private final SipProvider provider;
+    // Endpoint settings
+    public String fromIp;
+    private int fromPort;
+    private String fromUser;
+    private String fromHost;
+    private String authUserName;
+    private String authPassword;
+    private String authAdDomain;
+    private String transport;
+    // may be modified after 301 redirect
+    private String proxyHost;
+    private int proxyPort;
+
+    private enum PeerState {
+        OUT_OF_DIALOG,
+        WAITING_FOR_INVITE_RESPONSE, IN_DIALOG //, WAITING_FOR_BYE_RESPONSE
+    }
+    private enum PresenceState {
+        UNKNOWN, WAITING_FOR_SUBSCRIBE_RESPONSE, WAITING_FOR_NOTIFY, ONLINE, OFFLINE
+    }
+    private enum ConnectionState {
+        UNAUTHORIZED, AUTH1, AUTH2, AUTH3, AUTHORIZED
+    }
+    ConnectionState state = ConnectionState.UNAUTHORIZED;
+    // TODO
+    boolean prolongedFailureMode = false;
+
+    // authorization request header caching between phases
+    private Request authRequest;
+    // fromAddress is constant for the life of Endpoint
+    private Address fromAddress;
+    // see utils.MSUUID and http://msdn.microsoft.com/en-us/library/dd905844(v=office.12).aspx
+    private final String epid;
+    private final String sipInstance;
+    // the local SIP instance GRUU, learned from auth phase 3 response
+    private ContactHeader myContactHeader;
+
+    // set after auth phase 3 is completed, used to create signature for Proxy-Authorization header
+	private String authTargetName = "unset";
+	private String authRealm = "unset";
+	private String authOpaque;
+	private byte[] sealKey;
+	private byte[] signKey;
+    // limit-rate authentication
+    private int authRetryCounter = 0;
+    private long authRetryTimestamp = 0;
+
+    // for CSeq SIP header
+	private long cSeqCounter = 0L;
+    // for Cnum NTLM signature
+	private long cnumCounter = 0L;
+
+    // SIP transaction and dialog ApplicationData to support SipProducer's state-machine
+    private long messageIdCounter = 0;
+    private class MessageHolder {
+        final long id = ++messageIdCounter;
+        final Exchange exchange;
+        final String to;
+        final String body;
+        final AsyncCallback callback;
+        final long enqueueTime = System.currentTimeMillis();
+        final Peer peer;
+        ClientTransaction tx = null;
+        //int txRetransmits = 0;
+
+        public MessageHolder(Exchange exchange, String toUser, String body, AsyncCallback callback, Peer peer) {
+            this.exchange = exchange;
+            this.to = toUser;
+            this.body = body;
+            this.callback = callback;
+            this.peer = peer;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            final MessageHolder other = (MessageHolder) obj;
+            if (this.id != other.id) {
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 3;
+            hash = 53 * hash + (int) (this.id ^ (this.id >>> 32));
+            return hash;
+        }
+    };
+    private final Deque<MessageHolder> queue = new LinkedList<MessageHolder>();
+    private final Map<Long, MessageHolder> lost = new HashMap<Long, MessageHolder>();
+    private final Timer timer = new Timer("Camel::SIPE timer", true);
+    // for keepAlive()
+    private volatile long lastPacket = 0;
+
+    private class Peer {
+        final String to;
+        PresenceState presenceState = PresenceState.UNKNOWN;
+        PeerState state = PeerState.OUT_OF_DIALOG;
+        long subscribeSentTime = 0;
+        long presenceExpiresTime = 0;
+        long inviteSentTime = 0;
+        Dialog dialog = null;
+        final List messages = new ArrayList<MessageHolder>();
+
+        public Peer(String toUser) {
+            this.to = toUser;
+        }
+    }
+    private final Map<String, Peer> peerCache = new HashMap<String, Peer>();
+
+    public SipProducerListener(SipEndpoint endpoint) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+        this.endpoint = endpoint;
+        this.provider = endpoint.getProvider();
+        this.transport = endpoint.getTransport();
+        this.fromIp = endpoint.getIp();
+        this.fromPort = endpoint.getFromPort();
+        this.fromUser = endpoint.getFromUser();
+        this.fromHost = endpoint.getFromHost();
+        this.authAdDomain = endpoint.getAuthAdDomain();
+        this.authUserName = endpoint.getAuthUserName();
+        this.authPassword = endpoint.getAuthPassword();
+        this.proxyHost = endpoint.getProxyHost();
+        this.proxyPort = endpoint.getProxyPort();
+
+        // generate instance EPID
+        this.epid = stableEpid();
+        this.sipInstance = "<urn:uuid:" + MSUUID.generate(epid) + ">";
+
+        // schedule CRLFCRLF keepalive message and Garbage Collector to handle lost messages
+        timer.schedule(
+            new TimerTask() {
+                @Override
+                public void run() {
+                    try {
+                        gcLostMessages();
+                        keepAlive();
+                    } catch (Exception ex) {
+                        log.error("Timer error", ex);
+                    }
+                }
+            }, SipProducer.TIMEOUT, SipProducer.TIMEOUT/2);
+    }
+
+    public void stopTimer() {
+        timer.cancel();
+    }
+
+    // for ProxyRouter
+    public String getProxyHost() {
+        return proxyHost;
+    }
+
+    public int getProxyPort() {
+        return proxyPort;
+    }
+
+    public String getTransport() {
+        return transport;
+    }
+
+	private static String toHex(byte[] bytes){
+        if (bytes == null)
+            return null;
+        StringBuilder result = new StringBuilder(bytes.length*2);
+        for (byte bb : bytes)
+            result.append(Integer.toString((bb & 0xff) + 0x100, 16).substring(1));
+        return result.toString();
+	}
+
+	private static final SecureRandom random = new SecureRandom();
+	public static byte[] getRandomBytes(int length){
+		byte[] rand = new byte[length];
+		random.nextBytes(rand);
+		return rand;
+	}
+
+	public static String getRandomHexStr(int length){
+		byte[] randBytes = getRandomBytes(length);
+		return toHex(randBytes);
+	}
+
+    /* Return a 10-char long EPID with 5 bytes of randomness */
+    private String stableEpid() throws NoSuchAlgorithmException, UnsupportedEncodingException {
+        String src = fromIp + "|" + fromPort + "|" + fromHost;
+        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
+        byte[] digest = sha1.digest(src.getBytes("ASCII"));
+        return toHex(digest).substring(0, 10);
+    }
+
+    private static final String debugLineSeparator = "------------------------------------------";
+
+    private void failExchange(MessageHolder message, Exception e) {
+        if (log.isInfoEnabled() && !(e instanceof SipPresenceIncompatible)) {
+            String body = message.body;
+            if (body != null && body.length() > 50)
+                body = body.substring(0, 50);
+            log.info("Exchange failed: " + message.id + " " + message.to + " " + body, e);
+        }
+        long now = System.currentTimeMillis();
+        message.exchange.setException(e);
+        Peer peer = message.peer;
+        peer.messages.remove(message);
+        if (peer.state == PeerState.WAITING_FOR_INVITE_RESPONSE
+                && peer.inviteSentTime + SipProducer.TIMEOUT/2 < now)
+            peer.state = PeerState.OUT_OF_DIALOG;
+        if((peer.presenceState == PresenceState.WAITING_FOR_SUBSCRIBE_RESPONSE || peer.presenceState == PresenceState.WAITING_FOR_NOTIFY)
+                && peer.subscribeSentTime + SipProducer.TIMEOUT/2 < now)
+            peer.presenceState = PresenceState.UNKNOWN;
+        completeExchange(message);
+    }
+
+    private void completeExchange(MessageHolder message) {
+        lost.remove(message.id);
+        message.exchange.setProperty(SipProducer.COMPLETED, true);
+        message.callback.done(false);
+    }
+
+	protected boolean startAuthentication() throws Exception {
+        if (authRetryCounter > 5) {
+            // start over from central director
+            this.proxyHost = endpoint.getProxyHost();
+            this.proxyPort = endpoint.getProxyPort();
+            authRetryCounter = 0;
+        }
+        long now = System.currentTimeMillis();
+        if (authRetryCounter > 2 && authRetryTimestamp + 10000 > now) {
+            log.warn("Rate-limiting authentication");
+            // lets somebody to wake-up us later with another message
+            return false;
+        } else {
+            if (log.isDebugEnabled())
+                log.debug("starting authentication as " + fromUser + "@" + fromHost);
+            ++authRetryCounter;
+            authRetryTimestamp = now;
+            Request request = create1AuthRequest();
+            //ClientTransaction tx = provider.getNewClientTransaction(request);
+            //tx.sendRequest();
+            provider.sendRequest(request);
+            state = ConnectionState.AUTH1;
+            return true;
+        }
+    }
+
+    /* processMessage and SipListener functions are synchronized to serialize state-machine logic */
+
+    private synchronized void processMessages() {
+        if (state == ConnectionState.UNAUTHORIZED) {
+            try {
+                if (!startAuthentication()) {
+                    Exception e = new RuntimeException("Unauthorized");
+                    for (MessageHolder msg : queue)
+                        failExchange(msg, e);
+                    queue.clear();
+                }
+            } catch (Exception ex) {
+                log.error("Unable to send authentication request", ex);
+                // lets somebody to wake-up us later with another message
+            }
+            return;
+        }
+        if (state == ConnectionState.AUTH1 || state == ConnectionState.AUTH2 || state == ConnectionState.AUTH3)
+            return;
+
+        long now = System.currentTimeMillis();
+        int size = queue.size();
+        if (size == 0)
+            log.debug("no queued SIP messages to process");
+        while (size-- > 0) {
+            MessageHolder msg = queue.removeFirst();
+            Peer peer = msg.peer;
+            try {
+                if ((peer.presenceState == PresenceState.ONLINE || peer.presenceState == PresenceState.OFFLINE) &&
+                        peer.presenceExpiresTime < now) {
+                    if (log.isDebugEnabled())
+                        log.debug(peer.to + " presence state expired");
+                    peer.presenceState = PresenceState.UNKNOWN;
+                }
+
+                if (peer.presenceState == PresenceState.UNKNOWN) {
+                    if (log.isDebugEnabled())
+                        log.debug("subscribing to " + peer.to + " presence state");
+                    Request request = createSubscribeRequest(msg.to);
+                    ClientTransaction tx = provider.getNewClientTransaction(request);
+                    tx.setApplicationData(msg);
+                    tx.sendRequest();
+                    peer.presenceState = PresenceState.WAITING_FOR_SUBSCRIBE_RESPONSE;
+                    peer.messages.add(msg);
+                    peer.subscribeSentTime = now;
+                    continue;
+                }
+                if (peer.presenceState == PresenceState.OFFLINE) {
+                    if (log.isDebugEnabled())
+                        log.debug(peer.to + " presence status does not allow message delivery");
+                    failExchange(msg, new SipPresenceIncompatible("'" + msg.to + "' user presence status does not allow message delivery"));
+                    continue;
+                }
+                if (peer.presenceState == PresenceState.WAITING_FOR_SUBSCRIBE_RESPONSE ||
+                    peer.presenceState == PresenceState.WAITING_FOR_NOTIFY) {
+                    // lets enqueue into user queue and wait
+                    if (log.isDebugEnabled())
+                        log.debug("waiting for " + peer.to + " presence status");
+                    peer.messages.add(msg);
+                    continue;
+                }
+                // peer.presenceState -> ONLINE
+                if (peer.state == PeerState.OUT_OF_DIALOG) {
+                    if (log.isDebugEnabled())
+                        log.debug(peer.to + " not in dialog, inviting");
+                    Request request = createInviteRequest(msg.to);
+                    ClientTransaction tx = provider.getNewClientTransaction(request);
+                    tx.setApplicationData(msg);
+                    tx.sendRequest();
+                    peer.state = PeerState.WAITING_FOR_INVITE_RESPONSE;
+                    peer.messages.add(msg);
+                    peer.inviteSentTime = now;
+                    continue;
+                }
+                if (peer.state == PeerState.WAITING_FOR_INVITE_RESPONSE /* ||
+                    peer.state == PeerState.WAITING_FOR_BYE_RESPONSE*/) {
+                    if (log.isDebugEnabled())
+                        log.debug("waiting for " + peer.to + " INVITE response");
+                        // log.debug("waiting for " + peer.to + " INVITE/BYE response");
+                    // lets enqueue into user queue and wait
+                    peer.messages.add(msg);
+                    continue;
+                }
+                // peer.state -> IN_DIALOG
+                if (log.isDebugEnabled())
+                    log.debug("sending MESSAGE to " + msg.to);
+                Request request = peer.dialog.createRequest(Request.MESSAGE);
+                request = createMessageRequestViaDialog(msg.to, msg.body, request);
+                ClientTransaction tx = provider.getNewClientTransaction(request);
+                tx.setApplicationData(msg);
+                peer.dialog.sendRequest(tx);
+                msg.tx = tx;
+                lastPacket = now;
+            } catch (Exception ex) {
+                failExchange(msg, ex);
+            }
+        }
+    }
+
+	public synchronized void processResponse(ResponseEvent evt) {
+        ClientTransaction tx = evt.getClientTransaction();
+        Dialog dialog = evt.getDialog();
+        Request request = null;
+		Response response = evt.getResponse();
+		int status = response.getStatusCode();
+
+        if (log.isDebugEnabled()) {
+            if (tx != null) log.debug(tx.toString());
+            if (dialog != null) log.debug(dialog.toString());
+            log.debug(status + " " + response.getReasonPhrase());
+            try {
+                if (response instanceof SIPResponse) {
+                    String content = ((SIPResponse)response).getMessageContent();
+                    if (content != null)
+                        log.debug(debugLineSeparator + "\n" + content + "\n" + debugLineSeparator);
+                }
+            } catch (Exception e) { e.printStackTrace(); }
+        }
+
+        try {
+            // XXX must match to REGISTER transaction (or no transaction at all)
+            // because we can get multiple 401 responses to in-flight (MESSAGE) requests?
+            if (state == ConnectionState.AUTH1 || state == ConnectionState.AUTH2) {
+                if (status != 401) {
+                    log.error("No state machine for auth phase 1/2 and we got " + status + " " + response.getReasonPhrase());
+                    return;
+                }
+            }
+            // authentication
+            if (state == ConnectionState.AUTH1) {
+                extractAuthRealm(response);
+                request = create2AuthRequest();
+                //ClientTransaction tx = provider.getNewClientTransaction(request);
+                state = ConnectionState.AUTH2;
+                //tx.sendRequest();
+                provider.sendRequest(request);
+
+            } else if (state == ConnectionState.AUTH2) {
+                request = create3AuthRequest(response);
+                //ClientTransaction tx = provider.getNewClientTransaction(request);
+                state = ConnectionState.AUTH3;
+                //tx.sendRequest();
+                provider.sendRequest(request);
+
+            } else if (state == ConnectionState.AUTH3) {
+                if (status == 200) {
+                    if (extractGRUU(response)) {
+                        state = ConnectionState.AUTHORIZED;
+                        authRetryCounter = 0;
+                    }
+                // http://msdn.microsoft.com/en-us/library/cc246226(v=PROT.10).aspx
+                }  else if (status == 301) { // 301 Redirect to Home Server
+                    extractHomeServer(response);
+                }  else {
+                    log.error("Authorization failed: " + status + " " + response.getReasonPhrase());
+                }
+                if (state != ConnectionState.AUTHORIZED) {
+                    state = ConnectionState.UNAUTHORIZED;
+                    if (status == 301) {
+                        startAuthentication();
+                    } else {
+                        // start over from central director
+                        this.proxyHost = endpoint.getProxyHost();
+                        this.proxyPort = endpoint.getProxyPort();
+                    }
+                } else {
+                    log.debug("re-queueing messages after authentication");
+                    for (Map.Entry<String, Peer> x : peerCache.entrySet()) {
+                        Peer peer = x.getValue();
+                        queue.addAll(peer.messages);
+                        peer.messages.clear();
+                    }
+                }
+
+            // message processing
+            } else if (state == ConnectionState.AUTHORIZED) {
+                if (status == 401) {
+                    // start over from central director
+                    this.proxyHost = endpoint.getProxyHost();
+                    this.proxyPort = endpoint.getProxyPort();
+                    state = ConnectionState.UNAUTHORIZED;
+                    // re-SUBSCRIBE just in case we missed status change NOTIFY
+                    for (Peer peer : peerCache.values())
+                        peer.presenceState = PresenceState.UNKNOWN;
+                    if (tx != null) {
+                        request = tx.getRequest();
+                        String method = request.getMethod();
+                        boolean invite = Request.INVITE.equals(method);
+                        if (invite || Request.MESSAGE.equals(method)) {
+                            MessageHolder msg = (MessageHolder) tx.getApplicationData();
+                            if (msg != null) {
+                                queue.add(msg);
+                                if (invite) {
+                                    msg.peer.state = PeerState.OUT_OF_DIALOG;
+                                    msg.peer.messages.remove(msg);
+                                }
+                            }
+                            // wait for kick from outside if no message was re-queued
+                        }
+                    }
+                } else {
+                    if (tx == null) {
+                        log.error("Response without transaction: " + status + " " + response.getReasonPhrase());
+                        return;
+                    }
+                    request = tx.getRequest();
+                    String method = request.getMethod();
+                    MessageHolder msg = (MessageHolder) tx.getApplicationData();
+                    if (msg == null) {
+                        log.error("Response without transaction's ApplicationData: " + method + " " + status + " " + response.getReasonPhrase());
+                        return;
+                    }
+                    Peer peer = msg.peer;
+
+                    if (method.equals(Request.SUBSCRIBE)) {
+                        if (peer.presenceState != PresenceState.WAITING_FOR_SUBSCRIBE_RESPONSE)
+                            log.warn("Got " + status + " " + response.getReasonPhrase() +
+                                    " in response to SUBSCRIBE but our state is " + peer.presenceState);
+                        if (status == 200) {
+                            // after refreshed or overriden subscription new NOTIFY must follow shortly
+                            // also, NOTIFY may arrive before response
+                            if (peer.presenceState == PresenceState.WAITING_FOR_SUBSCRIBE_RESPONSE)
+                                peer.presenceState = PresenceState.WAITING_FOR_NOTIFY;
+                        } else {
+                            log.warn("Got " + status + " " + response.getReasonPhrase() +
+                                    " in response to SUBSCRIBE " + peer.to);
+                            // 404 is fast path - most likely edge server does not know about the domain
+                            if (status == 404) {
+                                peer.presenceExpiresTime = System.currentTimeMillis() + 600*1000;
+                                peer.presenceState = PresenceState.OFFLINE;
+                                queue.addAll(peer.messages);
+                                peer.messages.clear();
+                            } else {
+                                peer.presenceState = PresenceState.UNKNOWN;
+                            }
+                        }
+
+                    } else if (method.equals(Request.INVITE)) {
+                        if (peer.state != PeerState.WAITING_FOR_INVITE_RESPONSE)
+                            log.warn("Got " + status + " " + response.getReasonPhrase() +
+                                    " in response to INVITE but our state is " + peer.state);
+
+                        if (status == 100 || status == 101) {
+                            // 100 Trying
+                            // 101 Progress Report ms-diagnostics: 25008;reason="Attempting to route to Primary Pool"
+                        } else if (status == 200) {
+                            if (response.getHeader("Record-Route") == null)
+                                log.error("No Record-Route in " + status + " " + response.getReasonPhrase() +
+                                    " in response to INVITE");
+                            dialog.setApplicationData(peer);
+                            request = dialog.createAck(((CSeqHeader)response.getHeader("CSeq")).getSeqNumber());
+                            request = createAckRequestViaDialog(msg.to, request);
+                            dialog.sendAck(request);
+                            peer.dialog = dialog;
+                            // re-queue messages
+                            queue.addAll(peer.messages);
+                            peer.messages.clear();
+                            peer.state = PeerState.IN_DIALOG;
+                        } else {
+                            log.warn("Got " + status + " " + response.getReasonPhrase() +
+                                    " in response to INVITE");
+                            peer.state = PeerState.OUT_OF_DIALOG;
+                        }
+                        // XXX should we ACK 4xx reject with proper auth header?
+
+                    } else if (method.equals(Request.MESSAGE)) {
+                        if (status == 200) {
+                            completeExchange(msg);
+                        // sometimes we also get 500 Stale CSeq Value from OC client
+                        // looks like MS bug
+                        } else {
+                            String exMessage = "Got " + status + " " + response.getReasonPhrase() +