[HTML5] WebSocket 서버의 handshake
blocking socket을 기준으로 함.
발췌[HTML5] WebSocket 서버의 handshake
WebSocket protocol 문서중 서버의 handshake에 필요한 내용을 발로 번역함.
원문: http://www.whatwg.org/specs/web-socket-protocol/
아래 내용은 클라이언트에서 최초 접속 후 open status 로 가기 위한 handshake 과정이다.
handshake가 완료되고 open된 상태 이후에 나머지 law data 송수신은 IRC 프로토콜을 차용하든 직접 규약을 정하든,
서비스별 WebSocket 서버와 클라이언트가 정의한 subprotocol에 의한 통신을 하면 됨.
6.1 Reading the client's opening handshake
앞으로 문서에서 예제로 사용할 클라이언트 handshake 데이터는 아래 블록에 있는 데이터를 기준으로 한다.
Line 1
- 첫번째 라인은 항상 'GET' + 공백문자(U+0020) 로 시작해야 하며 모든 데이터는 UTF-8 인코딩이어야 한다.
- 공백문자(U+0020)를 기준으로 토큰을 나눈다고 가정했을 때,
- 두번째 토큰인 /echo 는 resource이다. 시작은 항상 슬래쉬(/ = U+002F)이며 resource에 포함될 수 있는 문자의 범위는 U+0021 ~ U+007E 이다.
- 세번째 토큰인 HTTP/1.1 은 무시해도 좋다.
- 라인의 끝은 항상 CRLF(U+000D U+000A) 이다.
Line 2 ~
두번째 라인부터는 필드를 뜻한다.
- 모든 필드는 CRLF로 끝난다.
- 모든 문자는 UTF-8 인코딩이어야 한다.
- 모든 필드는 '이름: 값'의 형태를 지닌다.
- 이름: U+0021 ~ U+0039, U+003B ~ U+007E
- U+003A (:) + U+0020 (공백문자)
- 값은 빈값이어도 상관없다.
- 2개의 연속된 CRLF는 모든 필드의 끝을 의미한다.
필드설명
- 필드명은 대소문자를 구분하지 않는다.
- 아래 나열된 필드 외에 필요하다고 판단되는 필드가 추가될 수 있다. 예를 들어 인증을 위해 "Cookie" 등이 포함될 수 있으며, 이런 필드들은 HTTP headers와 동일한 규약을 따른다.
|Upgrade| - handshake의 불변값. 항상 "WebSocket" 이라는 값을 가진다. - 무시해도 상관은 없지만 만약 이 값이 없거나 "WebSocket" 이 아닌 다른 값이면 cross-protocol attack 이라고 간주하고 WebSocket 접속과정을 중지한다.
|Connection| - handshake의 불변값. 항상 "Upgrade" 라는 값을 가진다.
- 무시해도 상관은 없지만 만약 이 값이 없거나 "Upgrade" 가 아닌 다른 값이면 cross-protocol attack 이라고 간주하고 접속과정을 중지한다.
|Host| - 클라이언트가 WebSocket으로 접속을 시도하는 hostname. 만약 virtual hosting 등으로 한 서버에서 여러 host를 서비스하고 있을 경우 각 host에 따라 다른 처리를 하기 위해 필요하다.
- 무시해도 상관은 없지만 만약 이 값이 없거나 서버에서 서비스하고 있는 hostname이 아닐 경우 cross-protocol attack 이나 DNS rebiding attack 이라고 간주하고 접속을 중지한다.
|Origin| - WebSocket 접속을 요청한 페이지의 scheme, hostname, port(만약 기본포트가 아닐 경우에만 포함). WebSocket 서버에서 요청 페이지에 따라 다른 처리방식을 각각 결정하기 위해 필요한 정보이다.
- 무시해도 상관은 없지만 만약 이 값이 없거나 서버에서 관리하고 있는 origin이 아닐 경우 cross-protocol attack 이나 cross-site scripting attack 이라고 간주하고 접속을 중지한다.
|Sec-WebSocket-Protocol| - 클라이언트가 요청하는 여러 서브프로토콜을 의미한다. 공백문자로 구분되며 순서에 따라 우선권이 부여된다. 서버에서 여러 프로토콜 혹은 프로토콜 버전을 나눠서 서비스할 경우 필요한 정보이다.
- 무시해도 상관은 없지만 만약 서버에서 서비스하고 있는 프로토콜 값이 아닐 경우 integrity errors를 피하기 위해 WebSocket 접속을 중지한다. (만약 서버에서 특정 서브프로토콜만을 지원한다면 이 값이 빈 값이어도 WebSocket 접속을 중지한다)
|Sec-WebSocket-Key1|
|Sec-WebSocket-Key2|
- 서버의 handshake 를 위한 정보를 계산하기 위해 필요한 값이다.
6.2 Sending the server's opening handshake
클라이언트에서 WebSocket 접속요청이 올 경우 서버는 아래의 순서를 따른다.
1. 만약 서버가 암호화를 지원한다면 TLS handshake 를 수행한다. 만약 실패하면 연결을 닫는다. 성공한다면 server handshake를 포함한 모든 통신은 암호화 터널을 이용해야 한다. [RFC2246]
2. 다음 정보들을 확보한다.
/host/ - WebSocket 서버의 hostname 혹은 IP Address. 필요하다면 hostname은 반드시 punycode로 인코딩되어야 한다. 서버가 virtual hosting 서비스를 하고 있을 경우 /host/ 값은 반드시 클라이언트의 handshake의 Host 필드값에서 뽑아낸 값이어야 한다. /host/ 값은 반드시 소문자여야 한다.
/port/ - 서버 접속 포트 번호
/resource_name/ - 서버에서 제공되는 서비스의 식별자. 만약 서버가 여러 서비스를 하고 있다면 /resource_name/ 값은 반드시 클라이언트의 handshake 데이터에서 뽑아낸 값이어야 한다.
/secure_flag/ - 암호화 연결이나 서버에서 암호화되어야 한다고 판단될 경우 True로, 그렇지 않으면 False로 세팅.
/origin/ - 서버로의 연결을 호출한 URL의 ASCII 소문자로 변환된 문자열. 만약 서버가 여러 origin에 대한 요청에 응답할 경우 이 값은 클라이언트의 handshake 데이터의 "Origin" 필드에서 추출된 값이어야 한다.
/subprotocol/
- null 이거나, 서버에서 사용하는 subprotocol의 문자열 값. 만약 서버가 여러 subprotocol을 지원한다면 이 값은 클라이언트의 handshake 데이터의 "Sec-WebSocket-Protocol" 에서 추출된 값이어야 한다. 해당 값이 없으면 null 로 간주하지만, 빈 문자열은 null 과 다르다고 간주한다.
/key_1/ - 클라이언트 handshake 데이터의 "Sec-WebSocket-Key1" 필드값.
/key_2/ - 클라이언트 handshake 데이터의 "Sec-WebSocket-Key2" 필드값.
/key_3/ - 클라이언트 handshake 데이터에서 2개의 CRLF 다음에 있는 8바이트.
3. /location/ = /host/ + /port/ + /resource_name/ + /secure_flag/
4. /key-number_N/ 은 /key_N/ 에서 숫자(U+0030 ~ U+0039)만 뽑아낸 값. 10개의 숫자가 나와야 하며 나머지 문자는 무시된다.
만약 /key-number_N/ 값이 4,294,967,295 를 넘으면 공격이라고 판단하고 접속을 중지한다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면,
/key-number_1/ 은 3,626,341,780 이고 /key-number_2/ 는 1,799,227,390 이다.
덧붙여 /key_3/ 은 "WjN}|M(6" 이다. = 0x57 0x6A 0x4E 0x7D 0x7C 0x4D 0x28 0x36
5. /spaces_N/ 은 /key_N/ 에 있는 공백문자(U+0020)의 갯수이다.
만약 /spaces_N/ 이 0 이거나 12 보다 크면 cross-protocol attack 으로 간주하고 접속을 중지한다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면,
/spaces_1/ 은 4 이고, /spaces_2/ 는 10 이다.
6. /key-number_N/ 이 /spaces_N/ 의 배수가 아니라면 접속을 중지한다.
/key-number_N/ % /spaces_N/ == 0
7. /part_N/ 은 /key-number_N/ 을 /spaces_N/ 으로 나눈 값이다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면,
/part_1/ 은 3,626,341,780 나누기 4 = 906,585,445 이고,
/part_2/ 는 1,799,227,390 나구니 10 = 179,922,739 이다.
8. /challenge/ 는 big-endian unsigned 32-bit integer 값으로 표현된 /part_1/ 값과 big-endian unsigned 32-bit integer 값으로 표현된 /part_2/ 값과 /key_3/ 값의 연결된 값이다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면 /challenge/ 는 다음 16 바이트의 값이 된다.
0x36 0x09 0x65 0x65 0x0A 0xB9 0x67 0x33 0x57 0x6A 0x4E 0x7D 0x7C 0x4D 0x28 0x36
9. /response/ 는 /challenge/ 값을 big-endian 128bit 문자열로 만든 값의 MD5 값이다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면 /resource/ 는 다음 16바이트의 값이 된다.
0x6E 0x60 0x39 0x65 0x42 0x6B 0x39 0x7A 0x24 0x52 0x38 0x70 0x4F 0x74 0x56 0x62
(UTF-8 문자열로는 다음처럼 표기된다. "n`9eBk9z$R8pOtVb")
10. UTF-8 로 인코딩 된 다음 라인을 CRLF를 붙여 보낸다. (send)
이 라인은 필요에 따라 변형할 수 있지만, Status-Code 는 101 이어야 하며, 기타 HTTP의 특징적인 Status-Line 값은 매칭되어야 한다.
11. 아래의 필드를 클라이언트에 보낸다. 모든 필드는 UTF-8 로 인코딩 된 '필드명: 필드값' 의 형태로 보내야 하며 각 항목에 대한 규약은 다음과 같다.
보내는 필드의 정해진 순서는 없다.
필드명 => 대소문자 구분없는 ASCII 문자열로 이루어져야 한다. 포함되어야 할 필드명은 아래에 나열한다.
콜론(:) + 공백문자 => U+003A U+0020
필드값 => CRLF로 끝나야 한다.
아래는 필드명들에 대한 설명이다.
|Upgrade| - 이 값은 반드시 "WebSocket" 이어야 한다.
|Connection| - 이 값은 반드시 "Upgrade" 이어야 한다.
|Sec-WebSocket-Location| - 이 값은 반드시 /location/ 값이어야 한다.
|Sec-WebSocket-Origin| - 이 값은 반드시 /origin/ 값이어야 한다.
|Sec-WebSocket-Protocol|
- 이 값은 /subprotocol/ 이 null 이 아닐 경우에만 포함시키도록 한다. 만약 이 값이 클라이언트에 보낼 handshake 데이터에 포함된다면 null 이어서는 안된다. 만약 이 값이 클라이언트에 보낼 handshake 데이터에 포함된다면 /subprotocl/ 값이어야 한다.
선택적으로 "Set-Cookie", "Set-Cookie2" 등의 HTTP 헤더에 사용되는 쿠키관련 필드를 포함할 수 있다.
12. 2개의 CRLF를 보낸다.
13. /response/ 값을 보낸다.
여기까지가 서버 handshake의 완성이다.
서버가 연결중지 없이 여기까지의 과정을 완료하고, 클라이언트가 WebSocket 연결을 실패하지 않는다면, 연결은 생성되며 서버는 데이터를 주고 받을 수 있다.
원문: http://www.whatwg.org/specs/web-socket-protocol/
아래 내용은 클라이언트에서 최초 접속 후 open status 로 가기 위한 handshake 과정이다.
handshake가 완료되고 open된 상태 이후에 나머지 law data 송수신은 IRC 프로토콜을 차용하든 직접 규약을 정하든,
서비스별 WebSocket 서버와 클라이언트가 정의한 subprotocol에 의한 통신을 하면 됨.
6.1 Reading the client's opening handshake
앞으로 문서에서 예제로 사용할 클라이언트 handshake 데이터는 아래 블록에 있는 데이터를 기준으로 한다.
GET /echo HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: localhost:8888
Origin: http://example.com
Sec-WebSocket-Key1: 3e6b263 4 17 80
Sec-WebSocket-Key2: 17 9 G`ZD9 2 2b 7X 3 /r90
WjN}|M(6
Upgrade: WebSocket
Connection: Upgrade
Host: localhost:8888
Origin: http://example.com
Sec-WebSocket-Key1: 3e6b263 4 17 80
Sec-WebSocket-Key2: 17 9 G`ZD9 2 2b 7X 3 /r90
WjN}|M(6
Line 1
- 첫번째 라인은 항상 'GET' + 공백문자(U+0020) 로 시작해야 하며 모든 데이터는 UTF-8 인코딩이어야 한다.
- 공백문자(U+0020)를 기준으로 토큰을 나눈다고 가정했을 때,
- 두번째 토큰인 /echo 는 resource이다. 시작은 항상 슬래쉬(/ = U+002F)이며 resource에 포함될 수 있는 문자의 범위는 U+0021 ~ U+007E 이다.
- 세번째 토큰인 HTTP/1.1 은 무시해도 좋다.
- 라인의 끝은 항상 CRLF(U+000D U+000A) 이다.
Line 2 ~
두번째 라인부터는 필드를 뜻한다.
- 모든 필드는 CRLF로 끝난다.
- 모든 문자는 UTF-8 인코딩이어야 한다.
- 모든 필드는 '이름: 값'의 형태를 지닌다.
- 이름: U+0021 ~ U+0039, U+003B ~ U+007E
- U+003A (:) + U+0020 (공백문자)
- 값은 빈값이어도 상관없다.
- 2개의 연속된 CRLF는 모든 필드의 끝을 의미한다.
필드설명
- 필드명은 대소문자를 구분하지 않는다.
- 아래 나열된 필드 외에 필요하다고 판단되는 필드가 추가될 수 있다. 예를 들어 인증을 위해 "Cookie" 등이 포함될 수 있으며, 이런 필드들은 HTTP headers와 동일한 규약을 따른다.
|Upgrade| - handshake의 불변값. 항상 "WebSocket" 이라는 값을 가진다. - 무시해도 상관은 없지만 만약 이 값이 없거나 "WebSocket" 이 아닌 다른 값이면 cross-protocol attack 이라고 간주하고 WebSocket 접속과정을 중지한다.
|Connection| - handshake의 불변값. 항상 "Upgrade" 라는 값을 가진다.
- 무시해도 상관은 없지만 만약 이 값이 없거나 "Upgrade" 가 아닌 다른 값이면 cross-protocol attack 이라고 간주하고 접속과정을 중지한다.
|Host| - 클라이언트가 WebSocket으로 접속을 시도하는 hostname. 만약 virtual hosting 등으로 한 서버에서 여러 host를 서비스하고 있을 경우 각 host에 따라 다른 처리를 하기 위해 필요하다.
- 무시해도 상관은 없지만 만약 이 값이 없거나 서버에서 서비스하고 있는 hostname이 아닐 경우 cross-protocol attack 이나 DNS rebiding attack 이라고 간주하고 접속을 중지한다.
|Origin| - WebSocket 접속을 요청한 페이지의 scheme, hostname, port(만약 기본포트가 아닐 경우에만 포함). WebSocket 서버에서 요청 페이지에 따라 다른 처리방식을 각각 결정하기 위해 필요한 정보이다.
- 무시해도 상관은 없지만 만약 이 값이 없거나 서버에서 관리하고 있는 origin이 아닐 경우 cross-protocol attack 이나 cross-site scripting attack 이라고 간주하고 접속을 중지한다.
|Sec-WebSocket-Protocol| - 클라이언트가 요청하는 여러 서브프로토콜을 의미한다. 공백문자로 구분되며 순서에 따라 우선권이 부여된다. 서버에서 여러 프로토콜 혹은 프로토콜 버전을 나눠서 서비스할 경우 필요한 정보이다.
- 무시해도 상관은 없지만 만약 서버에서 서비스하고 있는 프로토콜 값이 아닐 경우 integrity errors를 피하기 위해 WebSocket 접속을 중지한다. (만약 서버에서 특정 서브프로토콜만을 지원한다면 이 값이 빈 값이어도 WebSocket 접속을 중지한다)
|Sec-WebSocket-Key1|
|Sec-WebSocket-Key2|
- 서버의 handshake 를 위한 정보를 계산하기 위해 필요한 값이다.
6.2 Sending the server's opening handshake
클라이언트에서 WebSocket 접속요청이 올 경우 서버는 아래의 순서를 따른다.
1. 만약 서버가 암호화를 지원한다면 TLS handshake 를 수행한다. 만약 실패하면 연결을 닫는다. 성공한다면 server handshake를 포함한 모든 통신은 암호화 터널을 이용해야 한다. [RFC2246]
2. 다음 정보들을 확보한다.
/host/ - WebSocket 서버의 hostname 혹은 IP Address. 필요하다면 hostname은 반드시 punycode로 인코딩되어야 한다. 서버가 virtual hosting 서비스를 하고 있을 경우 /host/ 값은 반드시 클라이언트의 handshake의 Host 필드값에서 뽑아낸 값이어야 한다. /host/ 값은 반드시 소문자여야 한다.
/port/ - 서버 접속 포트 번호
/resource_name/ - 서버에서 제공되는 서비스의 식별자. 만약 서버가 여러 서비스를 하고 있다면 /resource_name/ 값은 반드시 클라이언트의 handshake 데이터에서 뽑아낸 값이어야 한다.
/secure_flag/ - 암호화 연결이나 서버에서 암호화되어야 한다고 판단될 경우 True로, 그렇지 않으면 False로 세팅.
/origin/ - 서버로의 연결을 호출한 URL의 ASCII 소문자로 변환된 문자열. 만약 서버가 여러 origin에 대한 요청에 응답할 경우 이 값은 클라이언트의 handshake 데이터의 "Origin" 필드에서 추출된 값이어야 한다.
/subprotocol/
- null 이거나, 서버에서 사용하는 subprotocol의 문자열 값. 만약 서버가 여러 subprotocol을 지원한다면 이 값은 클라이언트의 handshake 데이터의 "Sec-WebSocket-Protocol" 에서 추출된 값이어야 한다. 해당 값이 없으면 null 로 간주하지만, 빈 문자열은 null 과 다르다고 간주한다.
/key_1/ - 클라이언트 handshake 데이터의 "Sec-WebSocket-Key1" 필드값.
/key_2/ - 클라이언트 handshake 데이터의 "Sec-WebSocket-Key2" 필드값.
/key_3/ - 클라이언트 handshake 데이터에서 2개의 CRLF 다음에 있는 8바이트.
3. /location/ = /host/ + /port/ + /resource_name/ + /secure_flag/
4. /key-number_N/ 은 /key_N/ 에서 숫자(U+0030 ~ U+0039)만 뽑아낸 값. 10개의 숫자가 나와야 하며 나머지 문자는 무시된다.
만약 /key-number_N/ 값이 4,294,967,295 를 넘으면 공격이라고 판단하고 접속을 중지한다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면,
/key-number_1/ 은 3,626,341,780 이고 /key-number_2/ 는 1,799,227,390 이다.
덧붙여 /key_3/ 은 "WjN}|M(6" 이다. = 0x57 0x6A 0x4E 0x7D 0x7C 0x4D 0x28 0x36
5. /spaces_N/ 은 /key_N/ 에 있는 공백문자(U+0020)의 갯수이다.
만약 /spaces_N/ 이 0 이거나 12 보다 크면 cross-protocol attack 으로 간주하고 접속을 중지한다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면,
/spaces_1/ 은 4 이고, /spaces_2/ 는 10 이다.
6. /key-number_N/ 이 /spaces_N/ 의 배수가 아니라면 접속을 중지한다.
/key-number_N/ % /spaces_N/ == 0
7. /part_N/ 은 /key-number_N/ 을 /spaces_N/ 으로 나눈 값이다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면,
/part_1/ 은 3,626,341,780 나누기 4 = 906,585,445 이고,
/part_2/ 는 1,799,227,390 나구니 10 = 179,922,739 이다.
8. /challenge/ 는 big-endian unsigned 32-bit integer 값으로 표현된 /part_1/ 값과 big-endian unsigned 32-bit integer 값으로 표현된 /part_2/ 값과 /key_3/ 값의 연결된 값이다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면 /challenge/ 는 다음 16 바이트의 값이 된다.
0x36 0x09 0x65 0x65 0x0A 0xB9 0x67 0x33 0x57 0x6A 0x4E 0x7D 0x7C 0x4D 0x28 0x36
9. /response/ 는 /challenge/ 값을 big-endian 128bit 문자열로 만든 값의 MD5 값이다.
최상위에 표시한 클라이언트 handshake 데이터를 예로 들면 /resource/ 는 다음 16바이트의 값이 된다.
0x6E 0x60 0x39 0x65 0x42 0x6B 0x39 0x7A 0x24 0x52 0x38 0x70 0x4F 0x74 0x56 0x62
(UTF-8 문자열로는 다음처럼 표기된다. "n`9eBk9z$R8pOtVb")
10. UTF-8 로 인코딩 된 다음 라인을 CRLF를 붙여 보낸다. (send)
HTTP/1.1 101 WebSocket Protocol Handshake
이 라인은 필요에 따라 변형할 수 있지만, Status-Code 는 101 이어야 하며, 기타 HTTP의 특징적인 Status-Line 값은 매칭되어야 한다.
11. 아래의 필드를 클라이언트에 보낸다. 모든 필드는 UTF-8 로 인코딩 된 '필드명: 필드값' 의 형태로 보내야 하며 각 항목에 대한 규약은 다음과 같다.
보내는 필드의 정해진 순서는 없다.
필드명 => 대소문자 구분없는 ASCII 문자열로 이루어져야 한다. 포함되어야 할 필드명은 아래에 나열한다.
콜론(:) + 공백문자 => U+003A U+0020
필드값 => CRLF로 끝나야 한다.
아래는 필드명들에 대한 설명이다.
|Upgrade| - 이 값은 반드시 "WebSocket" 이어야 한다.
|Connection| - 이 값은 반드시 "Upgrade" 이어야 한다.
|Sec-WebSocket-Location| - 이 값은 반드시 /location/ 값이어야 한다.
|Sec-WebSocket-Origin| - 이 값은 반드시 /origin/ 값이어야 한다.
|Sec-WebSocket-Protocol|
- 이 값은 /subprotocol/ 이 null 이 아닐 경우에만 포함시키도록 한다. 만약 이 값이 클라이언트에 보낼 handshake 데이터에 포함된다면 null 이어서는 안된다. 만약 이 값이 클라이언트에 보낼 handshake 데이터에 포함된다면 /subprotocl/ 값이어야 한다.
선택적으로 "Set-Cookie", "Set-Cookie2" 등의 HTTP 헤더에 사용되는 쿠키관련 필드를 포함할 수 있다.
12. 2개의 CRLF를 보낸다.
13. /response/ 값을 보낸다.
여기까지가 서버 handshake의 완성이다.
서버가 연결중지 없이 여기까지의 과정을 완료하고, 클라이언트가 WebSocket 연결을 실패하지 않는다면, 연결은 생성되며 서버는 데이터를 주고 받을 수 있다.
댓글