-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: add intrinsic support for SOCKS proxies #1966
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| if (isBrowser || opts.unixSocket) { | ||
| opts.socks = undefined | ||
| } | ||
|
|
||
| if ( | ||
| !isBrowser && | ||
| !opts.unixSocket && | ||
| opts.socks === undefined && | ||
| typeof process !== 'undefined' | ||
| ) { | ||
| opts.socks = process.env['MQTTJS_SOCKS_PROXY'] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why? Could you remove the env var here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the environment variable in order to provide an easy way to route unmodified applications that use mqtt.js through a proxy. In or case the culprit is Node-RED.
It would be definitely preferable if these had support for using a proxy themselves, but configuring a proxy is one of those networking options where it is not uncommon to offer configuration through the environment.
|
@DirtyHairy Would it be possible to test part of this? |
|
Sure, done 😏 |
|
Just checking in to see whether there is something still missing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are failing
| function fatal<T>(e: T): T { | ||
| try { | ||
| if ((e as any).code === undefined) (e as any).code = 'SOCKS' | ||
| return e | ||
| } catch { | ||
| return e | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check ErrorWithReasonCode and do something like that instead:
export class SocksError extends Error {
public code: string
public constructor(message: string) {
super(message)
this.code = 'SOCKS'
// We need to set the prototype explicitly
Object.setPrototypeOf(this, SocksError.prototype)
Object.getPrototypeOf(this).name = SocksError.name
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am fine with making that change. However, I still need to keep the function to "decorate" errors from the SOCKS client with .code so that MQTT.js will treat them as fatal (I'd rather not wrap them in another Error as that obscures the original reason).
Should I make the change anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not wrap them in another Error as that obscures the original reason
You are not wrapping them twice you just create a specific error instance for sock related errors and that is exactly what you are doing now but in a non-typed way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are not wrapping them twice you just create a specific error instance for sock related errors and that is exactly what you are doing now but in a non-typed way
I have two code paths that call fatal: one decorates newly created errors, and the other decorates an error that I received and rethrow from socksClient (socks:205). In both cases I set .code in order to keep mqtt.js from ignoring the error ( client.ts:814). The first case can be replaced with a custom error class, the second case only if I wrap the error in my custom error, that's why I went for a decorator.
BTW: why are you manipulating the prototype directly? My understanding is that this used to be necessary for ES5 (as Error always returns a fresh object if called directly, independent of the scope), but for ES2015 the spec explicitly requires Error to be subclassable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the first part cannot you do something like:
constructor(messageOrError: string | Error) {
if (messageOrError instanceof Error) {
super(messageOrError.message);
if (messageOrError.stack) {
this.stack = messageOrError.stack;
}
} else {
super(messageOrError);
}
Object.setPrototypeOf(this, SocksError.prototype);
this.name = "SocksError";
}BTW: why are you manipulating the prototype directly? My understanding is that this used to be necessary for ES5 (as Error always returns a fresh object if called directly, independent of the scope), but for ES2015 the spec explicitly requires Error to be subclassable.
From what I know:
Object.setPrototypeOf(this, SocksError.prototype): This used to be necessary in order to havee instanceof Errorreturning true, without that it used to return falseObject.getPrototypeOf(this).name = SocksError.namewithout this thenameproperty on the error thrown it'sErrorand notSocksError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the first part cannot you do something like:
Compared to that I personally prefer just setting code on the error. But, then again, this is not my project, so my personal preferences are not what counts here 😏 So, if you prefer that, I'll change the code.
From what I know:
I think you can do without that by now (in ES2015), unless you want to support compiling down to ES5:
Welcome to Node.js v23.7.0.
Type ".help" for more information.
> class FooError extends Error {name = 'FooError'}
undefined
> foo = new FooError()
FooError
at REPL2:1:7
at ContextifyScript.runInThisContext (node:vm:137:12)
at REPLServer.defaultEval (node:repl:597:22)
at bound (node:domain:433:15)
at REPLServer.runBound [as eval] (node:domain:444:12)
at REPLServer.onLine (node:repl:926:10)
at REPLServer.emit (node:events:519:35)
at REPLServer.emit (node:domain:489:12)
at [_onLine] [as _onLine] (node:internal/readline/interface:416:12)
at [_line] [as _line] (node:internal/readline/interface:887:18)
> foo.name
'FooError'
> foo instanceof FooError
true
> foo instanceof Error
true
But, same here, I can just go ahead and add the prototype mangling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
problem is that foo instanceof Error is false without the second line? Anyway keep it like it is now no problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway keep it like it is now no problem
Alright, I won't complain about that 😏
problem is that foo instanceof Error is false without the second line?
The issue is foo instanceof FooError which is false for ES5 / "old-school" class definitions (unless you set up the prototype correctly after calling Error). If you extend Error ES2015 style with class / extends the prototype chain is set up correctly. My understanding is that the spec requires Error to check new.target to see whether it is called in a ES2015 constructing context and to behave accordingly (modify the target instead of returning a new object) in this case.
If you run the code in my NodeJS session through TS in ES5 mode foo instanceof FooError is false, and foo instanceof Error is true.
|
@DirtyHairy Sorry for late reply but I totally forgot about this |
|
@DirtyHairy That's not the right approach to fix tests, I suggest you to ensure there are no leaked handlers that prevent test to exit |
|
@robertsLando I don't think the reason are leaking handlers, I think this is a node.js issue similar to nodejs/node#54163 . I got errors because of remaining work on the loop before, and these look differently. Furthermore I can't reproduce this issue locally with either |
|
But, anyway, my commit does not fix the tests either 🤷♂️ I must admit I am slightly clueless here as I can't reproduce the issue. |
|
Also, not getting this error for any other run on the CI is a pretty strong argument for something being wrong with my tests 😏 I just have no clue what is going on. |
2eacc7b to
8684fe3
Compare
|
@DirtyHairy Seems you fixed it? |
|
Na, sorry, not yet. But I've got progress: I can reproduce the fail in a Linux VM (I am on a Mac) 😏 With a reproducer I'll get that fixed. |
|
Fixed. Unfortunately, be deleting a test: I had a test that tries to assert that errors on socket stream propagate to the stream presented to MQTT.js by closing the server with RST. Unfortunately, this does not translate into an error, but into an EOF in Linux, so the test waited indefinitely. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1966 +/- ##
==========================================
+ Coverage 81.15% 81.46% +0.31%
==========================================
Files 24 25 +1
Lines 1470 1565 +95
Branches 349 366 +17
==========================================
+ Hits 1193 1275 +82
- Misses 193 199 +6
- Partials 84 91 +7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
|
@mcollina could you kindly give a check to this please? |
|
@DirtyHairy but there is no debug output, and I can't find "mqttjs:socks" in dist |
|
@wshshra What are you trying to do, and what happens, and what do you expect to happen? Can you show some code? |
my code: the socks not work,then i want debug it, but there is not debug output Finally I solved it like this |
This PR adds support for establishing TCP and TLS connections through a SOCKS proxy. Proxy support is enabled with a new
socksconfiguration parameter that takes a curl-style SOCKS URL, or by setting an environment variable (MQTTJS_SOCKS_PROXY).Supported schemes (and protocols) are
socks5://,socks5h://,socks4://andsocks4a://.