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 ํค๋์ ์ฌ๋ฐ๋ฅด์ง ์์ ๊ฐ์ด ์ ๋ฌ๋๋ ๊ฒฝ์ฐ์ ๋ํด์ ์์ธ์ ํ์ ํ๊ธฐ๋ ์ด๋ ต์ต๋๋ค. ๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ ๋ณธ ๋ฌธ์ ๊ฐ ๋ค์ ๋ฐ์ํ์ ๋ ์ด๋ค ์ ๋ณด๋ก ์์ฒญ๋์๋์ง์ ๋ํ ๋ก๊ทธ๊ฐ ๊ธฐ๋ก๋์์ผ๋ฏ๋ก ์์ธ ํ์ ์ ์ํ ์ค๋ง๋ฆฌ๋ฅผ ์ฐพ์ ์ ์๋ ๋ฐฉ์์ ๋ง๋ จํ ์ ์๊ฒ ๋ฉ๋๋ค.