Skip to content
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

Feature Request: Support for Custom NSURLSessionConfiguration in Native WebSocket Implementation #111

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

shalom-aviv
Copy link
Contributor

@shalom-aviv shalom-aviv commented Dec 14, 2024

Reason for Changes

We encountered an issue on iOS versions earlier than iOs 17. In these versions, system-wide proxy settings are not applied to new instances of URLSessionWebSocketTask. This means that if the application relies on system-level proxy configurations, the WebSocket connections will bypass them.

To enable proxying for iOS versions below 17, proxy settings must be explicitly provided when creating the URLSession. Without this capability, developers cannot ensure that WebSocket traffic respects proxy configurations, leading to potential security and compliance issues in controlled network environments.

This limitation makes it critical to allow developers to pass a custom NSURLSessionConfiguration to the NativeWebSocket implementation.

proxyman: Capture WS/WSS from iOS

Description

Currently, the NativeWebSocket implementation uses URLSessionConfiguration.default for its NSURLSessionConfiguration. While this works for standard use cases, it becomes a limitation for advanced configurations where custom settings (e.g., proxy settings) are required. Without a way to inject a custom NSURLSessionConfiguration, developers are forced to use method swizzling, which is not ideal because:

  1. Swizzling affects all instances of NSURLSession, leading to uncontrolled side effects.
  2. It is impossible to control which parts of the codebase are impacted by these changes.

To address this, I have implemented changes that allow developers to pass a custom NSURLSessionConfiguration through the WebSocketTransport configuration. These changes are backward-compatible and provide a more flexible approach for advanced configurations.

Key Features Added:

  1. Custom NSURLSessionConfiguration Injection:
    • Developers can now pass a custom URLSessionConfiguration provider to the NativeWebSocket through the WebSocketTransport configuration.
    • Example use case: Adding proxy settings to the WebSocket connection.
  2. Backward Compatibility:
    • The default behavior remains unchanged, using URLSessionConfiguration.default if no custom provider is specified.

Code Changes:

  1. Added urlSessionConfigurationProvider to NativeWebSocket
  2. Utilized the Configuration in getOrCreateSession
  3. Backward-Compatible Changes to CentrifugeClientConfig

Extended Example, now user can specify proxy params for centrifuge client.

Risks or Considerations:

• Potential Misuse of Configuration Provider: Developers need to ensure the custom URLSessionConfiguration is well-defined, as incorrect configurations could cause unexpected behavior.

Screenshot Descriptions

Below are screenshots to assist with configuring Charles Proxy and testing proxying of WebSocket traffic for CentrifugeClient:
1. Charles Proxy Configuration:
• The Charles Proxy server is set up on the local IP address 192.168.1.80 and listens for traffic on port 8889.
• The screenshot shows the necessary settings in the Charles application to enable WebSocket proxying.
Screenshot 2024-12-14 at 11 59 50 AM
2. Captured Traffic in Charles:
• This screenshot demonstrates successful interception of WebSocket traffic between the CentrifugeClient and the Centrifugo server.
• The server hosting Centrifugo is running on 192.168.1.136 with port 8000.
Screenshot 2024-12-14 at 1 37 28 PM
3. Application Proxy Settings:
• Screenshots show the proxy settings applied directly within the application using the urlSessionConfigurationProvider configuration.

Proxy params Proxy ON
IMG_3717 IMG_3718

@FZambia
Copy link
Member

FZambia commented Dec 18, 2024

Hello @shalom-aviv

Many thanks for PR! Just letting you know I noticed it, will try to review it as soon as I can, but may take a bit more time than usual these days.

@shalom-aviv
Copy link
Contributor Author

Hello @shalom-aviv

Many thanks for PR! Just letting you know I noticed it, will try to review it as soon as I can, but may take a bit more time than usual these days.

No problem
Thanks

@shalom-aviv
Copy link
Contributor Author

@FZambia Hey
Not in this year? 😉

@FZambia
Copy link
Member

FZambia commented Jan 5, 2025

Hello, this year definitely :) Sorry, really busy working on v6 + holidays. Will come back to this very soon!

@shalom-aviv
Copy link
Contributor Author

shalom-aviv commented Jan 6, 2025

Hello, this year definitely :) Sorry, really busy working on v6 + holidays. Will come back to this very soon!

Happy New Year! 🥳
Waiting Happy New Centrifuge )))))

token: String = Defaults.token,
data: Data? = Defaults.data,
debug: Bool = Defaults.debug,
useNativeWebSocket: Bool = Defaults.webSocketTransport.isNativeWebSocket,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand right that we now have 2 different constructors here? It seems too much for the change, maybe it's possible to continue accepting useNativeWebSocket, but extend existing init with a new option to provide a configuration for Native WebSocket?

In this way the the cognitive overhead on my non-Swift brain should be less. But probably I am missing something here which prevent making it in a suggested way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first congratulate on the release of the new version of Centrifuge, and welcome back!

Thank you for your feedback! Let me explain my reasoning behind adding a second constructor.

The goal was to maintain backward compatibility with previous versions of the library. By introducing a new constructor with new parameter, we ensure that the existing codebases depending on the older constructor won't break.

Two potential approaches:

  1. Combine both configurations into a single constructor
    This would work, but it introduces non-obvious logic for handling parameters. Specifically:

    • The constructor would now have two parameters for configuring the same option (useNativeWebSocket and webSocketTransport), which could potentially conflict.
    • To determine the final behavior, users would need to dive into the library's source code. This could create confusion for those unfamiliar with the internal logic.

    Example of the combined constructor:

    public init(
        timeout: Double = Defaults.timeout,
        headers: [String: String] = Defaults.headers,
        tlsSkipVerify: Bool = Defaults.tlsSkipVerify,
        minReconnectDelay: Double = Defaults.minReconnectDelay,
        maxReconnectDelay: Double = Defaults.maxReconnectDelay,
        maxServerPingDelay: Double = Defaults.maxServerPingDelay,
        name: String = Defaults.name,
        version: String = Defaults.version,
        token: String = Defaults.token,
        data: Data? = Defaults.data,
        debug: Bool = Defaults.debug,
        useNativeWebSocket: Bool = Defaults.webSocketTransport.isNativeWebSocket,
        webSocketTransport: WebSocketTransport? = nil,
        tokenGetter: CentrifugeConnectionTokenGetter? = Defaults.tokenGetter,
        logger: CentrifugeLogger? = Defaults.logger
    ) {
        // Non-obvious resolution logic
        let webSocketTransportOldSetter: WebSocketTransport = useNativeWebSocket ? .native() : .starscream
        self.webSocketTransport = webSocketTransport ?? webSocketTransportOldSetter
        // Other initializations...
    }

    While this approach simplifies the public API (one constructor), it complicates the parameter behavior.

  2. Introduce a new constructor and mark the old one as deprecated

    • The older constructor would remain for backward compatibility but include a @available attribute with a clear deprecation message.
    • This ensures developers receive explicit guidance to migrate to the new constructor, without introducing ambiguity in parameter usage.

    Example:

    @available(*, deprecated, message: "useNativeWebSocket is deprecated. Please use webSocketTransport instead.")
    public init(
        timeout: Double = Defaults.timeout,
        headers: [String: String] = Defaults.headers,
        tlsSkipVerify: Bool = Defaults.tlsSkipVerify,
        ...
    ) {
        self.init(
            timeout: timeout,
            headers: headers,
            ...,
            webSocketTransport: useNativeWebSocket ? .native() : .starscream,
            ...
        )
    }

While the 1. option is functional, I believe the 2. approach is more user-friendly and clear for developers using the library. It allows for backward compatibility while guiding users toward the improved API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach 2. changes:
Screenshot 2025-01-17 at 18 46

Xcode will highlight warning about obsolete constructor:

Screenshot 2025-01-17 at 18 47
Screenshot 2025-01-17 at 18 48

Copy link
Member

@FZambia FZambia Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the change requires more complex way forward than possible. If we will improve constructors over deprecations we may end up with even more than two. And in my head it seems rather straightforward to have two options. In this way:

// useNativeWebSocket enables using native URLSessionWebSocketTask for WebSocket connections
// instead of our fork of Starscream v3. If you need custom configuration for URLSessionWebSocketTask
// then use urlSessionConfigurationProvider option to configure desired behaviour.
useNativeWebSocket: Bool
// urlSessionConfigurationProvider allows setting custom options for URLSessionWebSocketTask.
urlSessionConfigurationProvider: URLSessionConfigurationProvider

(probably can be improved more, but I hope the main idea here is that when enabling useNativeWebSocket we clearly say how it can be customized in a comment).

I think this does the job without introducing deprecations and maintenance overhead. For me finding a balance in SDK maintenance complexity is very important, so sometimes more simple solution seems better than more idiomatic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, update is ready. Please check it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants