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

websocket protocol support #73

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

Conversation

twister55
Copy link

WebSocket protocol support

@incubos
Copy link
Member

incubos commented Feb 13, 2023

Copyright header is missing in all the files.

@incubos
Copy link
Member

incubos commented Feb 13, 2023

You might want to add yourself to CONTRIBUTORS.


public void handshake(Request request) throws IOException {
final String version = request.getHeader(WebSocketHeaders.VERSION);
if (!"13".equals(version)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Magic constant?

Copy link
Author

Choose a reason for hiding this comment

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

fixed

server.handleMessage(session, (BinaryMessage) message);
} else if (message instanceof CloseMessage) {
server.handleMessage(session, (CloseMessage) message);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe IllegalArgumentException in case of unexpected message?

@Override
public void close() {
try {
extensions.forEach(Extension::close);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it possible to get some exception from Extension.close and stay with some unclosed ones?

Copy link
Author

@twister55 twister55 Sep 14, 2024

Choose a reason for hiding this comment

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

fixed, thanks

return response;
} catch (VersionException e) {
Response response = new Response("426 Upgrade Required", Response.EMPTY);
response.addHeader(WebSocketHeaders.createVersionHeader(13));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Magic constant once again.

Copy link
Author

Choose a reason for hiding this comment

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

fixed

/**
* @author <a href="mailto:vadim.yelisseyev@gmail.com">Vadim Yelisseyev</a>
*/
public class HandshakeException extends RuntimeException {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it possible to extend WebSocketException here as siblings do?

Copy link
Author

Choose a reason for hiding this comment

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

I think the handshake happens before the request goes from HTTP to WebSocket state so I separated them in two different hierarchies. But I don't mind to join it :)

}

public Map<String, String> getParameters() {
return parameters;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe wrap to unmodifiableMap?

Copy link
Author

Choose a reason for hiding this comment

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

fixed

*/
public class ExtensionRequestParser {

public static List<ExtensionRequest> parse(String header) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems unit test for this parsing would be valuable.

if (serverContextTakeover) {
serverContextTakeover = false;
} else {
throw new HandshakeException("Duplicate definition of the server_no_context_takeover extension parameter");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it really a HandshakeException?

Copy link
Author

Choose a reason for hiding this comment

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

I looked all usages of HandshakeException and for now it's always threw for illegal arguments in request. I can remove it in favor of IllegalArgumentException or I can make to HandshakeException extends IllegalArgumentException. What do you think?

сс @incubos


@Override
public String toString() {
return getClass().getSimpleName() + "<" + Hex.toHex(payload) + ">";
Copy link
Collaborator

Choose a reason for hiding this comment

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

How long this payload can be? Is it useful to create such a string for a long payload?

public static final short TLS_HANDSHAKE_FAILURE = 1015;

public CloseMessage(byte[] payload) {
this(payload.length == 0 ? null : Short.reverseBytes(unsafe.getShort(payload, byteArrayOffset)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's quite hard to perceive unsafe due to static import.
JavaInternals.unsafe.getShort seems to be more understandable.


@Override
public String toString() {
return "CloseMessage<" + payload + ">";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Raw byte[].toString?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, sorry, it's short here.

Copy link
Member

@incubos incubos left a comment

Choose a reason for hiding this comment

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

There is a number of questions and comments. PR contains complex logic, so unit tests for different types of messages, serde, fragmentation, size limits, options, etc. are definitely welcome.

response.addHeader("Connection: Upgrade");
response.addHeader(WebSocketHeaders.createAcceptHeader(request));
return response;
} catch (VersionException e) {
Copy link
Member

Choose a reason for hiding this comment

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

Where can it come from?

Copy link
Author

Choose a reason for hiding this comment

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

fixed

}

protected void handleMessage(WebSocketSession session, Message<?> message) throws IOException {
if (message instanceof PingMessage) {
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to use message.opcode to replace numerous instanceofs with faster switch on enum?

Copy link
Author

Choose a reason for hiding this comment

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

yes, fixed


@Override
public void handleException(Throwable e) {
log.error(e);
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about moving logging inside if? super.handleException() deals with logging already.

Copy link
Author

Choose a reason for hiding this comment

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

sure, thanks


@Override
public String toString() {
return getClass().getSimpleName() + "<" + Hex.toHex(payload) + ">";
Copy link
Member

Choose a reason for hiding this comment

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

SimpleName.of() is generally faster and more predictable.

Copy link
Author

Choose a reason for hiding this comment

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

fixed, thanks

private final boolean fin;
private final Opcode opcode;
private int rsv;
private int payloadLength;
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need this field if we have payload.length()?

Copy link
Author

Choose a reason for hiding this comment

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

first payloadLength is read from the frame header and then the number of bytes equal to payloadLength is read from the socket, so we need to store this field separately (see FrameReader.java:65)

private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover";
private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";

private static final int RSV_BITMASK = 0b100;
Copy link
Member

Choose a reason for hiding this comment

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

A comment would be helpful.

private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";

private static final int RSV_BITMASK = 0b100;
private static final byte[] EOM_BYTES = new byte[] {0, 0, -1, -1};
Copy link
Member

Choose a reason for hiding this comment

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

A comment would be helpful.

Copy link
Author

Choose a reason for hiding this comment

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

added

break;
}
} else {
if (fin && !clientContextTakeover) {
Copy link
Member

Choose a reason for hiding this comment

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

Unit tests especially for context takeover and framing would be appreciated.


int compressedLength;
do {
compressedLength = deflater.deflate(outputBuffer, 0, outputBuffer.length, Deflater.SYNC_FLUSH);
Copy link
Member

Choose a reason for hiding this comment

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

JavaDoc says:

In the case of FULL_FLUSH or SYNC_FLUSH, if the return value is len, the space available in output buffer b, this method should be invoked again with the same flush parameter and more output space.

My concern is about a case when compressed payload doesn't fit outputBuffer.

if (protocols != null) {
for (String protocol : protocols.split(",")) {
if (config.isSupportedProtocol(protocol)) {
response.addHeader(WebSocketHeaders.PROTOCOL + protocol);
Copy link
Member

Choose a reason for hiding this comment

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

PROTOCOL header value from request is transformed into multiple headers in response -- is that the desired behavior?

Copy link
Author

Choose a reason for hiding this comment

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

there is a break on next line, so only first supported protocol will be taken

@incubos incubos self-assigned this Mar 24, 2023
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.

3 participants