Tracing Handshake Websocket With Undertow
Handshake failed due to invalid Upgrade header: null
λ³Έ κΈμ μμ κ°μ μΉ μμΌ μ°κ²° μμ μ ν리μΌμ΄μ μ€λ₯ λ‘κ·Έκ° λ°μν 건μ λν κ΄λ ¨ λ΄μ©μ κΈ°λ‘νκΈ° μν κ² μ λλ€. μ΄ μ€λ₯ λ‘κ·Έλ μ€νλ§ μΉ μμΌ λͺ¨λμμ DefaultHandshakeHandlerλ₯Ό ν΅ν΄ νΈλμμ΄ν¬λ₯Ό μννλ κ³Όμ μμ μ¬λ°λ₯΄μ§ μμ μΉ μμΌ μ°κ²°μ λν΄ μ€λ₯ λ‘κ·Έλ‘ κΈ°λ‘νλλ‘ λμ΄μλλ° Upgrade ν€λμ μ¬λ°λ₯΄μ§ μμ κ°μ΄ μ λ¬λμλ€λ μλ―Έμ λλ€.
Connection: Upgrade
Upgrade: websocket
μΌλ°μ μΌλ‘ μΉ μμΌ μ°κ²°μ Protocol upgrade mechanismμΌλ‘ HTTP ν΅μ μ λν΄ μ»€λ₯μ μ μ ννλ κ³Όμ μ κ±°μΉκ² λλλ° μλ²μμλ μΉμμΌ μλν¬μΈνΈμ ν΄λΉνλ μμ²μ λν΄μλ Upgrade ν€λλ₯Ό νμΈνκ³ websocket μ΄ μ λ¬λμλμ§λ₯Ό νμΈν©λλ€.
μΉμμΌ νλ‘μ
μμ€ν μ΄ λμνλ μΈνλΌ νκ²½μ AWS ν΄λΌμ°λ μλΉμ€λ‘ ꡬμ±λμ΄ μμ΅λλ€. μΌλ°μ μΌλ‘ λ‘λλ°Έλ°μ±μ μν΄μ μ¬μ©νλ Elastic Load Balancing κΈ°λ₯μ λ°λ₯΄λ©΄ ALB, NLB λͺ¨λ μΉμμΌμ μ§μνλ€κ³ λμ΄μμΌλ―λ‘ μΉμμΌ μ°κ²°μ μ νμ μΈ νκ²½μ μλλλ€. νμ¬ μ‘°μ§μμ νλ«νΌμΌλ‘μ¨ μ 곡νλ νκ²½μ EC ν€ κΈ°λ°μ μΈμ¦μμ μ μ½μ¬νμΌλ‘ NLBλ₯Ό μ¬μ©ν΄μ TCP νλ‘μλ₯Ό μννκ³ SSL μ€νλ‘λλ Nginxμμ μνν ν μ ν리μΌμ΄μ μλ²λ‘ νΈλν½μ΄ μ λ¬λλ ꡬ쑰μ λλ€.
λ€λ§, μ λ¬Έμ κ° λ°μνλ€κ³ μλ΄λ νΉμ κ³ κ°μ΄ μ§μ ꡬμ±νλ νκ²½μμλ ELB λ 벨μμ SSL μ€νλ‘λλ₯Ό μνν νμ νΈλν½λ§ Nginxλ‘ μ λ¬λμ΄ μ ν리μΌμ΄μ μλ²λ‘ νλ‘μλλ―λ‘ μ½κ°μ μμ²μ΄ μ λ¬λλ λ°©μμ΄ λ€λ¦ λλ€. λ°λΌμ, ELB λ 벨μμ νΈλν½μ μ λ¬νλ κ³Όμ μμ Upgrade ν€λκ° μ μ€λ κ°λ₯μ±λ μμ¬ν΄λ³Ό μ μμ΅λλ€.
κ° νκ²½μ Nginxμλ Upgarde ν€λμ λν νλ‘μ ꡬμ±μ λ°λΌ Upgarde ν€λ κ°μ μ ν리μΌμ΄μ μΌλ‘ μ λ¬λλλ‘ $http_upgrade λ³μκ° μ€μ λμ΄ μμ΅λλ€.
νμμμ
μΌλ°μ μΌλ‘ AWS ν΄λΌμ°λ νκ²½μμ ELBλ₯Ό μ¬μ©ν λ μΉμμΌ μ°κ²° λ¬Έμ κ° λ°μν μ μμ΅λλ€. μ΄λ ELB λ‘λλ°Έλ°μ μμ±μ idle_timeout.timeout_seconds κΈ°λ³Έκ°μ΄ 60μ΄ μ΄μ΄μ λ°μνλ λΆλΆμ λν΄μλ νμμμμ 90μ΄λ‘ μ€μ λμ΄ μΉ μμΌ μ°κ²°μ λν λΆλΆμ μ μμ μΌλ‘ μ μ§νλ μνμ λλ€.
ν΄λΌμ΄μΈνΈμ μ΄κΈ° μ°κ²°μ μ μΈνκ³ λ μλ²μμ 1λΆλ§λ€ μ€μΌμ€μ μν΄ μ΄λ ν λ©μμ§λ₯Ό μ λ¬νλλ° μλ²κ° μ λ¬νλ νμ΄λ° μ 60μ΄ μ΄λ΄μ μ λ¬λλ νΈλν½μ΄ μλ€κ³ νλ¨λμ΄ μ°κ²°μ΄ ν΄μ§λ μ μμ΅λλ€.
HTTP2
μΌλ°μ μΈ μΉ μμ²μ HTTP2λ‘ μ°κ²°λ μ μλλ‘ μ§μνκ³ μλλ° μΉμμΌμ λν μ°κ²°μ λν΄μλ HTTP 1.1μ μ κ·Έλ μ΄λ 맀컀λμ¦μ μ¬μ©ν΄μΌ ν©λλ€.
curl --include \
--no-buffer \
--http1.1 \
--location \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Host: example.com" \
--header "Origin: https://example.com" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
https://example.com/websocket/server/sessionid/websocket
κ·Έλ°λ° μ cURL λͺ λ Ήμ΄μμ βhttp1.1 μ΅μ μ μ μΈνλ©΄ Upgarde ν€λμ websocketμ μ λ¬λμ§ μλ μν©μ΄ λ°μν¨μ νμΈνμμ΅λλ€. κ²°κ΅ μΌλ°μ μΈ λΈλΌμ°μ λ₯Ό ν΅ν μμ²μ΄ μλλΌ νΉμ ν΄λΌμ΄μΈνΈκ° μ§μ μΉμμΌ μ°κ²°μ μλν κ°λ₯μ±λ μλ€λ μ΄μΌκΈ° μ λλ€.
wscat λλ postman μΌλ‘λ μΉ μμΌ μ°κ²°μ μλν΄λ³΄μμ§λ§ μ μμ μΌλ‘ μ°κ²°λ¨μ νμΈν μ μμμ΅λλ€.
μ€νλ§ μΉ μμΌ
μμ€ν μ μ₯μμλ μ¬λ°λ₯΄μ§ μμ μΉμμΌ μ°κ²°μ΄ μμ²λλ λΆλΆμ΄λ―λ‘ ν΄λΉ μ€λ₯ λ‘κ·Έλ§μΌλ‘λ μ΄λ»κ² μμ²λμλκ°λ₯Ό κ²ν ν μ μλ λ°©μμ΄ μμ΅λλ€. μ€νλ§ μΉ μμΌ λͺ¨λμμ μμ² μ 보λ₯Ό κΈ°λ‘ν μ μλ λ°©μμ μ°Ύμ보λλ‘ ν©μλ€.
RequestUpgradeStrategy
RequestUpgradeStrategyλ HTTP μμ²μ λν΄μ μΉ μμΌ μ°κ²°λ‘ μ κ·Έλ μ΄λνκΈ° μν μ λ΅μ΄λ©° μΈλν μ°λ₯Ό μ¬μ©νκ³ μλ€λ©΄ UndertowRequestUpgradeStrategyκ° μ¬μ©λ©λλ€.
public class CustomRequestUpgradeStrategy extends UndertowRequestUpgradeStrategy {
@Override
protected void upgradeInternal(ServerHttpRequest request, ServerHttpResponse response, String selectedProtocol, List<Extension> selectedExtensions, Endpoint endpoint) throws HandshakeFailureException {
// NOTE: νΈλμμ΄ν¬ κ³Όμ μμ κ²μ¦λ μμ²μ λν΄μ μ
κ·Έλ μ΄λλ₯Ό μννλ€.
super.upgradeInternal(request, response, selectedProtocol, selectedExtensions, endpoint);
}
}
μ μ²λΌ μ κ·Έλ μ΄λλ₯Ό μννλ κ³Όμ μμ μμ²κ³Ό μλ΅μ λν΄ λΆκ° μ²λ¦¬λ₯Ό μνν μ μμ΅λλ€λ§ DefaultHandshakeHandler λΌλ ν΄λμ€μμ upgradeInternal ν¨μκ° νΈμΆλλ μμΉλ₯Ό μ΄ν΄λ³΄λ©΄ μΉ μμΌ μ°κ²° μμ²μ λν΄μ κ²μ¦μ μννκ³ λμ λ§μ§λ§μ upgradeInternal ν¨μκ° νΈμΆλλ―λ‘ λ³Έ λ¬Έμ κ° λ°μνμλλ μμ² μ 보λ₯Ό νμ ν μ μμ΅λλ€.
DefaultHandshakeHandler
DefaultHandshakeHandlerλ μ€νλ§ μΉ μμΌ λͺ¨λμμ κΈ°λ³Έμ μΌλ‘ μ¬μ©νλ μΉ μμΌ μ°κ²°μ μννλ νΈλ€λ¬λ‘ HandshakeHandlerλ‘ λ±λ‘λ λΉμ΄ μλ€λ©΄ λ΄λΆμ μΌλ‘ DefaultHandshakeHandlerλ₯Ό μμ±νμ¬ μ¬μ©νλλ‘ λμ΄μμ΅λλ€. μμ RequestUpgradeStrategyλ₯Ό μ΄μ©ν μ μλ μ΄μ λ₯Ό νμΈνκΈ° μν΄ νΈλμμ΄ν¬λ₯Ό μννλ μ½λλ₯Ό μ΄ν΄λ³΄λλ‘ νκ² μ΅λλ€.
public static class AbstractHandshakeHandler implements HandshakeHandler {
@Override
public final boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws HandshakeFailureException {
WebSocketHttpHeaders headers = new WebSocketHttpHeaders(request.getHeaders());
if (logger.isTraceEnabled()) {
logger.trace("Processing request " + request.getURI() + " with headers=" + headers);
}
try {
if (HttpMethod.GET != request.getMethod()) {
response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
response.getHeaders().setAllow(Collections.singleton(HttpMethod.GET));
if (logger.isErrorEnabled()) {
logger.error("Handshake failed due to unexpected HTTP method: " + request.getMethod());
}
return false;
}
if (!"WebSocket".equalsIgnoreCase(headers.getUpgrade())) {
handleInvalidUpgradeHeader(request, response);
return false;
}
if (!headers.getConnection().contains("Upgrade") && !headers.getConnection().contains("upgrade")) {
handleInvalidConnectHeader(request, response);
return false;
}
if (!isWebSocketVersionSupported(headers)) {
handleWebSocketVersionNotSupported(request, response);
return false;
}
if (!isValidOrigin(request)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return false;
}
String wsKey = headers.getSecWebSocketKey();
if (wsKey == null) {
if (logger.isErrorEnabled()) {
logger.error("Missing \"Sec-WebSocket-Key\" header");
}
response.setStatusCode(HttpStatus.BAD_REQUEST);
return false;
}
}
catch (IOException ex) {
throw new HandshakeFailureException(
"Response update failed during upgrade to WebSocket: " + request.getURI(), ex);
}
String subProtocol = selectProtocol(headers.getSecWebSocketProtocol(), wsHandler);
List<WebSocketExtension> requested = headers.getSecWebSocketExtensions();
List<WebSocketExtension> supported = this.requestUpgradeStrategy.getSupportedExtensions(request);
List<WebSocketExtension> extensions = filterRequestedExtensions(request, requested, supported);
Principal user = determineUser(request, wsHandler, attributes);
if (logger.isTraceEnabled()) {
logger.trace("Upgrading to WebSocket, subProtocol=" + subProtocol + ", extensions=" + extensions);
}
this.requestUpgradeStrategy.upgrade(request, response, subProtocol, extensions, user, wsHandler, attributes);
return true;
}
}
AbstractHandshakeHandler ν΄λμ€μ λν΄ λ‘κ·Έ λ 벨μ Traceλ‘ μ€μ νλ©΄ μμ² μ 보λ₯Ό λ‘κ·Έλ‘ κΈ°λ‘ν μ μμ§λ§ λͺ¨λ μμ²μ λν΄μ κΈ°λ‘νλ―λ‘ λ¬Έμ κ° λ°μνμλλ§ μμ² μ 보λ₯Ό λ¨κΈΈ μ μμ΅λλ€.
AbstractHandshakeHandler.handleInvalidUpgradeHeader
μμ doHandshake ν¨μλ₯Ό μ΄ν΄λ³Έ κ²°κ³Ό Upgrade ν€λμ μ¬λ°λ₯΄μ§ μμ κ°μ΄ μ λ¬λλ κ²½μ°μλ handleInvalidUpgradeHeader ν¨μλ₯Ό νΈμΆνλ κ²μ νμΈν μ μμ΅λλ€. μ΄μ μ°λ¦¬λ handleInvalidUpgradeHeader ν¨μλ₯Ό μ€λ²λΌμ΄λ νμ¬ μμ² μ 보λ₯Ό νμΈνκ³ μ€λ₯ λ‘κ·Έλ‘ κΈ°λ‘ν μ μλ μμΉλ₯Ό μκ² λμμ΅λλ€.
@Slf4j
public class CustomHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected void handleInvalidUpgradeHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
// NOTE: Upgrade ν€λμ μ¬λ°λ₯΄μ§ μμ κ°μ΄ μ λ¬λμμλ νΈμΆλλ€.
log.error("Method: {}, URI: {}, Principal: {}, Headers: {}", request.getMethodValue(), request.getURI(), request.getPrincipal(), request.getHeaders());
super.handleInvalidUpgradeHeader(request, response);
}
}
μ ν리μΌμ΄μ λ 벨μμλ Upgrade ν€λμ μ¬λ°λ₯΄μ§ μμ κ°μ΄ μ λ¬λλ κ²½μ°μ λν΄μ μμΈμ νμ νκΈ°λ μ΄λ ΅μ΅λλ€. κ·ΈλΌμλ λΆκ΅¬νκ³ λ³Έ λ¬Έμ κ° λ€μ λ°μνμ λ μ΄λ€ μ λ³΄λ‘ μμ²λμλμ§μ λν λ‘κ·Έκ° κΈ°λ‘λμμΌλ―λ‘ μμΈ νμ μ μν μ€λ§λ¦¬λ₯Ό μ°Ύμ μ μλ λ°©μμ λ§λ ¨ν μ μκ² λ©λλ€.