eclipse / paho.mqtt.java

Eclipse Paho Java MQTT client library. Paho is an Eclipse IoT project.
https://eclipse.org/paho
Other
2.11k stars 883 forks source link

Feature request: Native proxy support without providing custom SocketFactory #575

Open a-zink opened 6 years ago

a-zink commented 6 years ago

The client allows to set a custom SocketFactory which allows the connection to be established over a proxy. However this is quite cumbersome. Using a proxy should be an easy-to-use feature of the client because this is a quite common use-case. Especially for mqtt over websockets, which is already supported.

There are basically two use-cases for mqtt over websockets (also see introduction chapter in amqp over websockets which is a related topic)

As this is the Java mqtt client, browser support is not the target use-case for websockets. Hence I argue that the only reason for using the websocket feature is network restrictions. However this feature is somehow incomplete without native proxy support.

Also see https://github.com/eclipse/paho.mqtt.java/issues/573 https://github.com/eclipse/paho.mqtt.java/pull/319 (Only addresses socks proxy) https://github.com/eclipse/paho.mqtt.java/issues/419 (Seems to be a proxy auth issue. I would say this is out of scope, as workarounds like cntlm exist)

dasAnderl commented 5 years ago

@a-zink im trying hard currently to make mqtt client work behind a proxy. we are in the company network scenario. the proxy is a web proxy. what i do on my localhost to emulate the server environment is:

using a proxy from https://free-proxy-list.net 62.99.67.216:8080 then i create an entry in /etc/hosts -> 0.0.0.0 to block name resolving for the mqtt broker.

in the mqtt client connect options i then use a custom ssl socket factory (see below) locally this works great and im able to connect to the client though the proxy on the company server i get: Unable to connect to server createSocket from SslTunnelFactory is not even called. to me this looks like the client in making some connection to the broker before even using the custom sockets.

btw im also using mqttConnectOptions.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);

does the broker always(by default) support mqtt over websockets or has this feature to be enabled?

For any help i would be more than grateful, as i am seriously running out of ideas here

currently i am using the following custom ssl socket factory (modified from the link):

`/*

AndrewJudson commented 5 years ago

I'm also interested in a solution to this. I am currently blocked on using paho due to this

AndrewJudson commented 5 years ago

@dasAnderl did you ever figure this out? I have been trying your solution and was able to perform SSL handshake with the server, but then paho gave me java.net.SocketException: Already connected

a-zink commented 5 years ago

@dasAnderl @AndrewJudson please see my comment https://github.com/eclipse/paho.mqtt.java/issues/573#issuecomment-484773525

a-zink commented 5 years ago

@dasAnderl

does the broker always(by default) support mqtt over websockets or has this feature to be enabled?

Yes, the broker of course needs to support websockets.

dasAnderl commented 3 years ago

@a-zink sorry andreas for my very late reply it worked for us with the custom socketfactory and java below 1.8.0_261-b25 and mqtt version 1.2.0 later mqtt versions do not seem to support the custom ssl factory anymore #706 -> java.net.SocketException: Unconnected sockets not implemented

here the working custom factory in kotlin:

@Suppress("MagicNumber")
internal object MqttClientSocketFactory {
    @JvmStatic
    operator fun get(certPair: X509CertPair, brokerUrl: String): SSLSocketFactory {
        return when (CfgProxy.useProxy) {
            true -> socketFactoryForProxy(brokerUrl, certPair)
                .also { log.info("using proxy socket factory") }
            false -> socketFactory(certPair)
        }
    }

    private fun socketFactory(certPair: X509CertPair): SSLSocketFactory {
        val keyManagerCsa = keyManagerVkms(certPair)
        return sslCtxVkmsAws(keyManagerCsa).socketFactory
    }

    private fun socketFactoryForProxy(brokerUrl: String, certPair: X509CertPair): SSLSocketFactory {
        val hostName = getHostName(brokerUrl)
        val keyManagerCsa = keyManagerVkms(certPair)
        val delegate = sslCtxVkmsAws(keyManagerCsa).socketFactory
        return object : SSLSocketFactory() {

            override fun getDefaultCipherSuites(): Array<String> {
                return delegate.defaultCipherSuites
            }

            override fun getSupportedCipherSuites(): Array<String> {
                return delegate.supportedCipherSuites
            }

            @Throws(IOException::class)
            override fun createSocket(proxySocket: Socket, host: String, port: Int, autoClose: Boolean): Socket {
                doTunnelHandshake(proxySocket, hostName, 8883)
                return delegate.createSocket(proxySocket, hostName, 8883, autoClose)
            }

            // PAHO DOES NOT USE
            @Throws(IOException::class, UnknownHostException::class)
            override fun createSocket(s: String, i: Int): Socket? {
                return null
            }

            // PAHO DOES NOT USE
            @Throws(IOException::class, UnknownHostException::class)
            override fun createSocket(s: String, i: Int, inetAddress: InetAddress, i1: Int): Socket? {
                return null
            }

            // PAHO DOES NOT USE
            @Throws(IOException::class)
            override fun createSocket(inetAddress: InetAddress, i: Int): Socket? {
                return null
            }

            // PAHO DOES NOT USE
            @Throws(IOException::class)
            override fun createSocket(inetAddress: InetAddress, i: Int, inetAddress1: InetAddress, i1: Int): Socket? {
                return null
            }
        }
    }

    private fun sslCtxVkmsAws(keyManagerVkms: KeyManager) =
        SSLContextBuilder()
            .build()
            .apply {
                init(
                    arrayOf(keyManagerVkms),
                    arrayOf(acceptAllTrustManager()),
                    SecureRandom()
                )
            }

    private fun keyManagerVkms(certPair: X509CertPair): KeyManager {
        val certKeys = certPair.keyPair
        val selfSignedChain = certPair.certs
        return getKeyManagers("glcs_self_signed", certKeys, selfSignedChain)
    }

    @Throws(IOException::class)
    private fun doTunnelHandshake(tunnel: Socket, host: String, port: Int) {
        val msg = ("CONNECT " + host + ":" + port + " HTTP/1.0\n" +
                "User-Agent: " +
                System.getProperty("http.agent") +
                "\r\n\r\n")
        val out = tunnel.getOutputStream()
        val b: ByteArray
        b = try { /*
             * We really do want ASCII7 -- the http protocol doesn't change
             * with locale.
             */
            msg.toByteArray(charset("ASCII7"))
        } catch (ignored: UnsupportedEncodingException) { /*
             * If ASCII7 isn't there, something serious is wrong, but
             * Paranoia Is Good (tm)
             */
            msg.toByteArray()
        }
        out.write(b)
        out.flush()
        /*
         * We need to store the reply so we can create a detailed
         * error message to the user.
         */
        val reply = ByteArray(200)
        var replyLen = 0
        var newlinesSeen = 0
        var headerDone = false /* Done on first newline */
        val `in` = tunnel.getInputStream()
        while (newlinesSeen < 2) {
            val i = `in`.read()
            if (i < 0) {
                throw IOException("Unexpected EOF from proxy")
            }
            if (i == '\n'.toInt()) {
                headerDone = true
                ++newlinesSeen
            } else if (i != '\r'.toInt()) {
                newlinesSeen = 0
                if (!headerDone && replyLen < reply.size) {
                    reply[replyLen++] = i.toByte()
                }
            }
        }
    }

    private fun getHostName(brokerUrl: String): String {
        return brokerUrl
            .replace("ssl://", "")
            .replace("https://", "")
            .replace("http://", "")
            .also {
                if (it.contains(":") || it.contains("/")) {
                    throw AssertionError("the hostname should not contain any protocol or path: $it ")
                }
                log.info("using hostname $it")
            }
    }
}
ramki519 commented 1 year ago

Is this feature supported yet? This is a blocking issue for many.

onkariwaligunje commented 9 months ago

Hello, Has anyone got solution to this problem. I am also facing issue while establishing connection through http proxy. It gives ERROR-MqttAgent Error message: Connection to the Mqtt Failed due to MqttException (0) - java.net.SocketTimeoutException.

I tried the solution given by dasAnderl, but I am getting java.net.SocketException: Already connected

I am currently blocked due to this issue, any help is greatly appreciated, Thanks!

onkariwaligunje commented 9 months ago

https://github.com/eclipse/paho.mqtt.java/pull/1010 The changes in this PR provides proxy support. I tried building code and tested it and it does establishes the connection over a http proxy