mscdex / ssh2

SSH2 client and server modules written in pure JavaScript for node.js
MIT License
5.52k stars 664 forks source link

Node.js socks5 proxy with forwarding for SOAP requests #794

Closed scorsi closed 4 years ago

scorsi commented 5 years ago

Hello,

I am trying to reproduce this ssh command with node.js : ssh -D 1080 -N user@host -I key.pem, to use this tunnel to execute SOAP requests.

My node.js implementation of SOAP API using port 1080 is as follow (and works with the ssh command launched) :

const
    Agent = require('socks5-https-client/lib/Agent'),
    request = require('request'),
    soap = require('strong-soap').soap,
    url = 'https://example.com/wsdl'; //path to wsdl

soap.createClient(url, {
    request: request.defaults({
        agentClass: Agent,
        agentOptions: {
            socksPort: 1080
        }
    })
}, function (err, client) {
    if (err) console.log(err)
    let description = client.describe()

    console.log(JSON.stringify(description.ExampleService.ExamplePort.example_method))

    client['example_method']({ /* PAYLOAD */ }, function (err, result, envelope, soapHeader) {
        if (err) console.log(err)
        else console.log(JSON.stringify(result))
    })
})

And everything works as expected like this, whether client.describe() and client.example_method().

The issue comes when trying to create the socks proxy with node. Full (censored) code below :

const Agent = require('socks5-https-client/lib/Agent');
const Server = require('socksv5/lib/server').Server;
const socksAuth = require('socksv5/lib/auth/None');
const Client = require('ssh2').Client;
const soap = require('strong-soap').soap;
const request = require('request');
const fs = require('fs');

const config = {
    localProxy: {
        host: 'localhost',
        port: 1080
    }
};

const conn = new Client();
const server = new Server((info, accept, deny) => {
    conn.on('ready', () => {
        conn.forwardOut(
            info.srcAddr, info.srcPort,
            info.dstAddr, info.dstPort,
            (err, stream) => {
                if (err) {
                    conn.end();
                    return deny();
                }

                const clientSocket = accept(true);
                if (clientSocket) {
                    stream.pipe(clientSocket).pipe(stream).on('close', () => {
                        conn.end();
                    });
                } else {
                    conn.end();
                }
            })
    }).on('error', () => {
        deny()
    }).connect({
        host: 'sshhostname',
        port: 22,
        username: 'sshusername',
        privateKey: fs.readFileSync('./sshkey.pem'),
        debug: (s) => console.info(s)
    });
}).listen(config.localProxy.port, config.localProxy.host, () => {
    const stop = (err) => {
        if (err) console.error(err);
        conn.end();
        server.close();
    };
    try {
        soap.createClient(
            'https://somesoaphost/wsdl', {
                request: request.defaults({
                    agentClass: Agent,
                    agentOptions: {
                        socksHost: config.localProxy.host,
                        socksPort: config.localProxy.port
                    }
                })
            }, (err, client) => {
                if (err) {
                    stop(err);
                    return;
                }

                client['example_method']({
                    // Some payload
                }, (err, result) => {
                    if (err) stop(err);
                    else console.log(JSON.stringify(result));
                });
            });
    } catch (e) {
        stop(e);
    } finally {
    }
}).useAuth(socksAuth());

The description of the method example_method is well printed, so I'm sure the tunneling works (because methods can't work without it, as there is an IP restriction which accept only requests from the host server). The error comes when calling client.example_method(). The output is :

Error: SOCKS connection failed. Connection not allowed by ruleset.
    at Socket.<anonymous> (xxxxx/node_modules/socks5-client/lib/Socket.js:246:25)
    at Object.onceWrapper (events.js:285:13)
    at Socket.emit (events.js:197:13)
    at addChunk (_stream_readable.js:288:12)
    at readableAddChunk (_stream_readable.js:269:11)
    at Socket.Readable.push (_stream_readable.js:224:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:145:17)

Any idea of what is missing ? Thanks !

mscdex commented 5 years ago

Do you really need SOCKS here or are you just trying to tunnel in-process SOAP requests over SSH? If it's the latter, the code can be greatly simplified.

scorsi commented 5 years ago

The SOAP server checks IP and our server hasn't IP attached (it's a Heroku serveur with Heroku domain name, we can't changed it since it's quite old and a lot of clients requests that domain name), it's why we need to use SSH tunnel to send request to the SOAP server though a server with validated IP attached.

Do you know if there's any other possible solution to achieve that without using SOCKS ?

mscdex commented 5 years ago

What I meant was, is it only your node process that's making the SOAP requests?

scorsi commented 5 years ago

Yep, our Heroku server (with NodeJS) have to make the request. It's on a API call that the SOAP request is sent.

mscdex commented 5 years ago

Well, in the next ssh2 release there will be http(s).Agent implementations you can use to tunnel your requests instead of having to use SOCKS.

scorsi commented 5 years ago

Thanks, I'll look at this and come back to you in the month. :)