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

Adding Per Message Compression Extensions from RFC-7692. #142

Closed
wants to merge 23 commits into from
Closed

Adding Per Message Compression Extensions from RFC-7692. #142

wants to merge 23 commits into from

Conversation

jhoughjr
Copy link
Collaborator

@jhoughjr jhoughjr commented Jul 5, 2023

My first PR is PMCE support for WebSocket server and client.

There is an accompanying PR going to Vapor to support the changes to the WebSocketUpgrader to enable conditional use of PMCE.

I've tried to comment everything as descriptive as possible but without knowledge of the RFC, the code may not make obvious sense.

The changes are mostly encapsulated in the new PMCE class and its members.
Websocket's main changes are an optional PMCE which is Sendable and some optional delegation to it if configured and enabled for use in the send and handle methods.

WebSocketClient.Configuration also has an optional PMCE so the client can configure its side.
The public API is mainly through converting DeflateConfigs <> HTTPHeaders.
ie if you want to serve pace web sockets, create the desire DeflateConfig and grab its headers.
Pass them out of shouldUpgrade. As a client, do the same and pass them into the connect override with the ClientConfiguration.

I have some test apps and examples in another repo and am contiuning to write documentation.
I have tested client against a node.js , a go and a vapor/websocketkit (this) implementation of PMCE servers.
I have tested the vapor/websocket-kit server against the vapor/websocket-kit client, and a web browser client, all of which works, but I'm sure its not perfect yet.

I believe MahdiBM has some experience with this issue as well as adamfowler and may be useful ion review.
I can answer any questions in discord.

Copy link
Member

@0xTim 0xTim left a comment

Choose a reason for hiding this comment

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

Some quick initial notes on this - the PR needs tidying up to start with, there's extra whitespace added, commented out code, removed tests etc

The main issue is adding a dependency on something that is not yet 1.0.0 - this is a blocker as we can't guarantee API breaking changes. This will need to be tagged a 1.0.0 before we can consider the PR (or inline the code we need)

@koraykoska
Copy link

@0xTim You are not serious, are you? As much as I like Vapor and use it every day, websocket-kit has had multiple breaking changes in the last years. Most recently, one of the latest releases (with the Sendable stuff) broke literally every single app I had using websocket-kit directly, within a minor version. And I can prove it if you need to.

There is literally no QA going on in any of the Vapor repositories. I trust @adam-fowler with compress-nio in a pre-1.0 version 10000% more than any of the Vapor repositories, especially websocket-kit, to not introduce any breaking changes.

Copying over code will very surely introduce another weak point in a barely maintained repository. I and surely many others would much rather depend on a maintained version of code doing a crucial part of what's being implemented in this PR.

If you really insist, why not simply version-lock it? Or reduce to bug-fix releases only? And then manually update the dependency if it makes (probably non-breaking) minor bumps.

This PR is very good and feature-complete. It even takes care of edge cases covered in the RFC. It introduces a long-awaited feature that is a crucial part of any real websocket library in any language. Vapor being the de-facto library used for Swift backends, you are the only one of any languages I know without websocket compression support. I would put priority on getting this merged ASAP.

A thorough code review will follow.

@Joannis
Copy link
Member

Joannis commented Jul 6, 2023

Also want to mention this exists
image

Copy link
Member

@Joannis Joannis left a comment

Choose a reason for hiding this comment

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

Code quality should really be improved, especially logging. And tests need to be added. I think this is a great start for a great addition. I'll have to read up on PMCE, but I think that the implementation is definitely a great addition and pretty well implemented.

}

@preconcurrency public func onText(_ callback: @Sendable @escaping (WebSocket, String) -> ()) {
print("\(self.onTextCallback._eventLoop)")
Copy link
Member

Choose a reason for hiding this comment

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

Print statements must be removed

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

PMCE RFC is somewhat obtuse. I'd be very surprised if it were perfectly implemented currently but it has worked against 4 different servers in my lab tests. These are all good suggestions and ive addressed most of them I think.

self.channel.writeAndFlush(frame, promise: promise)
}
//using compression
if pmce != nil && pmce!.enabled {
Copy link
Member

Choose a reason for hiding this comment

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

Please if let unwrap this, instead of force unwrapping

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done.

}
}
catch {
print("websocket-kit: \(error)")
Copy link
Member

Choose a reason for hiding this comment

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

Get rid of this print statement, and add some error handling. Of course, you can keep logging in here through Logger

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Im not sure what error handling there would be other than logging the underlying error in the catch.

}
else if frame.rsv1 && pmce == nil {
if pmce?.logging ?? false {
print("websocket-kit: PMCE: received compressed frame without PMCE configured! Closing per RFC-7692. You could have a configuration issue.")
Copy link
Member

Choose a reason for hiding this comment

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

Use swift-log

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That would mean adding a dep to websocket kit for it I assume. I had looked into it possibly but thought it kinda out of scope and thought if ppl wanted logger there they would have one by now and maybe there was a reason websocket had no logger.

Choose a reason for hiding this comment

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

How is logging handled right now in websocket-kit? Simply no logging or with prints? If it's no logging I would keep it that way and make a separate PR eventually adding proper logging and the dependency.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It was no logging. I asked Tim and he agreed it shold be added. I can't make another fork though so I just did it on this PR. It was kind of a pain point to debug it without logging so I added what I could after asking around for a while. But logging has been added via swift log to websocket and pmce classes with a boxed bool to enable with a defeat of false. that way you can turn it on for real time issues without needed a fork to add it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Logging is added now on main.

self.maxFrameSize = maxFrameSize
self.minNonFinalFragmentSize = 0
self.maxAccumulatedFrameCount = Int.max
self.maxAccumulatedFrameSize = Int.max
Copy link
Member

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 max frame sizes should ever be set to .max implicitly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that might be a good catch. Not sure where I saw that from, thought I pulled it from elsewhere in the codebase.
Do you know what default should be?

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a specific requirement. But I tend to require the API consumer to think about this. In our case - Vapor itself

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I feel like I copied it from elsewhere, but could have pulled it from somehwere... I just looked. I got it from the pre existing TLS init I was basing adding pmce arg on public init( tlsConfiguration: TLSConfiguration? = nil, maxFrameSize: Int = 1 << 14 ) so I was using vapor as the guide as far as I could tell.
I do see ur point of the caller should set that but I see no way for them to pass it on init.
Seeing as it's all sendable now, shouldn't those be loop bound to be mutable? That would prevent them from chasing it after the fact.
Im surprised it doesnt warn me. as it did on WebSocket having mutable state while being sendable. am confused.

override func setUp() async throws {
fflush(stdout)
}

override func tearDown() {
try! self.elg.syncShutdownGracefully()
Copy link
Member

Choose a reason for hiding this comment

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

Not a fan of force unwrapped optionals. And try!. You can make tearDown() throwing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think those were there originally I dont recall writing them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ive removed the !.s I can find. Not sure what to do about the ones some other maintainer left in there.
I can try if its important but feel it could unravel things maybe.

Copy link
Member

Choose a reason for hiding this comment

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

If another user did that, I don't think that's your responsibility to fix it here :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I dont mind fixing them, but without understanding the test I cant say my change would still be a valid test. I did that in the cases I did change, like with the URL, but didnt want to go down the rabbit hole like a cowboy. Someone should do it I think. Since I already fixed deprecations without asking it's kinda six of one half dozen of another.

@@ -279,42 +287,6 @@ final class WebSocketKitTests: XCTestCase {
try server.close(mode: .all).wait()
}

func testLocally() throws {
Copy link
Member

Choose a reason for hiding this comment

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

Why was this removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had thought it was something I had added to test locally. It hangs forever the only test that didnt ever pass .

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just adding what I mentioned in discord that I removed it because it was skipped anyway and mistakenly thought had made it due to its vague name as I do that when drafting a test. Especially since not skipped it would hang forever.
Adding it back just adds a skipped taste is the takeaway I htink.

@@ -479,7 +451,9 @@ final class WebSocketKitTests: XCTestCase {
}

func testBadURLInWebsocketConnect() async throws {
XCTAssertThrowsError(try WebSocket.connect(to: "%w", on: self.elg, onUpgrade: { _ in }).wait()) {
// %w seems to now get to NIO and it attempts to connect to localhost:80 ... empty string makes the test pass
XCTAssertThrowsError(try WebSocket.connect(to: "", on: self.elg, onUpgrade: { _ in }).wait()) {
Copy link
Member

Choose a reason for hiding this comment

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

Again, let's make sure we don't change tests just to get things passing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The test is no longer valid and thus did not pass when it in fact should have passed because it wasnt testing what it said it was testing. it expects %w to return nil not a URL if I recall. As a QA engineer I take tests pretty seriously.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'd add I tested this with a temporary test of URL init with both params and discerning what the test was testing, from that result and the test expectations. So I changed the setup to satisfy the expectation that existed prior.
I was surprised %w returned a URL and not nil.

var elg: EventLoopGroup!


// compression usage tests
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this old (commented) testing code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I just noticed the tests I added to websocket kit dont seem to be here ....!?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should be fixed now.

import NIOConcurrencyHelpers

public final class WebSocket: Sendable {
enum PeerType: Sendable {

public enum PeerType: Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't need to be public

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it's actually needed to be public but I'll check once more. Seems it wa a couple days ago that I changed it so might not have the same api.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I just checked and it needs to be public because types outside Websocket need it.
internal doesnt suffice because an inter needs it. I really think this should be like surface protein for a websocket. It's a perfectly fine enum and is useful for the classes around websocket to support its use.

@Joannis
Copy link
Member

Joannis commented Jul 6, 2023

As per this comment you could consider integration testing with other implementations

@codecov-commenter
Copy link

codecov-commenter commented Jul 6, 2023

Codecov Report

Merging #142 (a6ef521) into main (ffe0055) will decrease coverage by 19.74%.
The diff coverage is 41.09%.

❗ Current head a6ef521 differs from pull request most recent head 756216d. Consider uploading reports for the commit 756216d to get more accurate results

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #142       +/-   ##
===========================================
- Coverage   82.85%   63.11%   -19.74%     
===========================================
  Files           6        7        +1     
  Lines         659     1231      +572     
===========================================
+ Hits          546      777      +231     
- Misses        113      454      +341     
Impacted Files Coverage Δ
Sources/WebSocketKit/WebSocket.swift 70.52% <37.62%> (-18.52%) ⬇️
Sources/WebSocketKit/PMCE.swift 39.63% <39.63%> (ø)
Sources/WebSocketKit/WebSocketClient.swift 88.83% <51.72%> (-6.52%) ⬇️
Sources/WebSocketKit/WebSocketHandler.swift 58.44% <52.00%> (-4.06%) ⬇️
...urces/WebSocketKit/HTTPUpgradeRequestHandler.swift 73.77% <100.00%> (+1.84%) ⬆️

... and 1 file with indirect coverage changes

@jhoughjr
Copy link
Collaborator Author

jhoughjr commented Jul 6, 2023

The coverage seems odd to drop so much. I only removed a skipped test and added some that werent there. I don't understand that result.

@adam-fowler
Copy link

@0xTim You are not serious, are you?

@0xTim is right here to question relying on a package that hasn't hit v1.0. CompressNIO has had a number of breaking changes in the last few releases. Although many of those have been to add support needed by both this PR and the equivalent in hummingbird-websocket.

To make things easier I will move compress-nio to v1.0 in the next week or so.

@jhoughjr
Copy link
Collaborator Author

jhoughjr commented Jul 7, 2023

@adam-fowler Thanks a ton. Your quick responsiveness and guidance has been instrumental in getting this done on time for me.

@adam-fowler
Copy link

@jhoughjr I just released compress-nio v1.0 so you can update to that and it won't be an issue anymore

@koraykoska
Copy link

Thank you @adam-fowler

@jhoughjr
Copy link
Collaborator Author

Thought I pushed yesterday, seemed today it didnt go so pushed it now.

@jhoughjr jhoughjr closed this by deleting the head repository Jul 15, 2023
@0xTim
Copy link
Member

0xTim commented Jul 17, 2023

Hey @jhoughjr any particular reason this was closed? With the recent changes and compression 1.0 this was getting into a good shape

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.

None yet

6 participants