/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
package stdx.net.http
import std.collection.ArrayList
import std.sync.{AtomicBool, Mutex}
import stdx.log.Logger
import stdx.encoding.url.URL
import stdx.crypto.common.*
import stdx.encoding.base64.{toBase64String, fromBase64String}
// GUID is used to generate Sec-WebSocket-Accept.
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
// the size of each fragment's payload when send large message.
const FRAMESIZE = 4 * 1024
// the limit of each frame payload length is 20M, to prevent the DOS attack
const MAX_FRAME_PAYLOAD_LENGTH = 20 * 1024 * 1024
@FastNative
foreign func DYN_SHA1(d: CPointer<UInt8>, n: Int32, md: CPointer<UInt8>, msg: CPointer<DynMsg>): CPointer<UInt8>
foreign func MallocDynMsg(): CPointer<DynMsg>
foreign func FreeDynMsg(dynMsgPtr: CPointer<DynMsg>): Unit
@C
struct DynMsg {
var found = true
var funcName = CPointer<UInt8>()
}
/**
* websocket extensions are not supported yet
*/
public class WebSocket {
let conn: WebSocketConn
let _logger: Logger = mutexLogger()
var _subProtocol: String = ""
let isClient: Bool
let writeMutex = Mutex()
let readMutex = Mutex()
let isClosed = AtomicBool(false)
let isSentCloseFrame = AtomicBool(false)
init(conn: WebSocketConn, subProtocol: String, isClient: Bool) {
this.conn = conn
this._subProtocol = subProtocol
this.isClient = isClient
}
/*
* the subProtocol of the webSocket
*/
public prop subProtocol: String {
get() {
_subProtocol
}
}
/**
* logger
* setting logger.level will take effect immediately.
* NOTE: logger should be thread-safe
*/
public prop logger: Logger {
get() {
_logger
}
}
/**
* read a frame, block to get frame.
* if websocket receives a malformed fame, it will send
* an error code to the peer end and close the connection,
* and throws a WebSocketException.
*
* @return received websocketFrame
*
* @throws WebSocketException if the frame is malformed,
* or received unexpected frame on http/2.0 layer
* or if the conn is closed.
* @throws SocketException if failed to read data.
* @throws ConnectionException if conn is closed by peer.
*/
public func read(): WebSocketFrame {
if (isClosed.load()) {
throw WebSocketException("The connection is closed.")
}
let frame: WebSocketFrame
synchronized(readMutex) {
// read 16 bits
let bytes = Array<UInt8>(2, repeat: 0)
readExact(bytes, "frame header")
frame = toWebSocketFrameFromFirstTwoBytes(bytes)
// client --> server must mask
// server --> client must not mask
// if the data is being sent by the client, the frames must be masked.
// RFC 6455 6.1.5.
// a server must not mask any frames that it sends to the client.
// RFC 6455 5.1.
if ((isClient && frame.mask) || (!isClient && !frame.mask)) {
failTheWebSocketConnection(WebSocketStatusCode.PROTOCOL_ERROR, "receiving an invalid mask message")
}
// if a nonzero value is received and none of the negotiated extensions
// defines the meaning of such a nonzero value, the receiving endpoint
// must _fail the websocket connection_.
// RFC 6455 5.2.
if (frame.rsv1 || frame.rsv2 || frame.rsv3) {
failTheWebSocketConnection(WebSocketStatusCode.PROTOCOL_ERROR,
"extensions not supported yet, rsv must be 0")
}
// if an unknown opcode is received, the receiving endpoint
// must _fail the websocket connection_.
// RFC 6455 5.2.
if (frame.frameType == UnknownWebFrame) {
failTheWebSocketConnection(WebSocketStatusCode.UNSUPPORTED_DATA,
"receiving a message with invalid frame type")
}
updatePayloadLen(frame)
// read masking-key
if (frame.mask) {
readExact(frame.maskingKey, "masking key")
}
// control frames cannot be fragmented and must have
// a payload length of 125 bytes or less.
match (frame.frameType) {
case PingWebFrame | PongWebFrame | CloseWebFrame =>
if (frame.payloadLength > 125) {
failTheWebSocketConnection(WebSocketStatusCode.PROTOCOL_ERROR,
"receiving a control frame has a payload length more than 125 bytes")
}
if (!frame.fin) {
failTheWebSocketConnection(WebSocketStatusCode.PROTOCOL_ERROR,
"receiving a control frame that is fragmented")
}
case _ => ()
}
readFramePayload(frame)
}
return frame
}
private func readExact(byteArray: Array<UInt8>, fieldName: String): Unit {
let len = conn.readRaw(byteArray)
if (len != byteArray.size) {
throw ConnectionException("Connection closed while reading websocket ${fieldName}.")
}
}
private func updatePayloadLen(frame: WebSocketFrame): Unit {
// read extended payload length.
// match 7 bit payload len
match (frame.payloadLen) {
// if 126, the following 2 bytes interpreted as a
// 16-bit unsigned integer are the payload length.
// RFC 6455 5.2.
case 126 =>
let extendedPayloadLength = Array<UInt8>(2, repeat: 0)
readExact(extendedPayloadLength, "extended payload length")
frame.payloadLength = toInt64(extendedPayloadLength)
// if 127, the following 8 bytes interpreted as a
// 64-bit unsigned integer are the payload length.
// RFC 6455 5.2.
case 127 =>
let extendedPayloadLength = Array<UInt8>(8, repeat: 0)
readExact(extendedPayloadLength, "extended payload length")
frame.payloadLength = toInt64(extendedPayloadLength)
case _ => frame.payloadLength = Int64(frame.payloadLen)
}
}
private func readFramePayload(frame: WebSocketFrame): Unit {
if (frame.payloadLength != 0) {
checkFramePayloadLimit(frame.payloadLength)
let payloadData = Array<UInt8>(frame.payloadLength, repeat: 0)
readExact(payloadData, "payload")
// server must remove masking for data frames received from a client.
frame._payload = if (!isClient) {
maskOrUnmask(frame.maskingKey, payloadData)
} else {
payloadData
}
}
}
// cjlint-ignore -start !G.OTH.03
/**
* Implementations that have implementation- and/or platform-specific
* limitations regarding the frame size or total message size after
* reassembly from multiple frames MUST protect themselves against
* exceeding those limits
* https://www.rfc-editor.org/rfc/rfc6455.html#section-10.4
*
* 1009 indicates that an endpoint is terminating the connection
* because it has received a message that is too big for it to
* process.
*/
// cjlint-ignore -end
private func checkFramePayloadLimit(payloadLength: Int64) {
if (payloadLength >= MAX_FRAME_PAYLOAD_LENGTH) {
failTheWebSocketConnection(WebSocketStatusCode.MESSAGE_TOO_BIG,
"payload length too large, unexpected value: ${payloadLength}")
}
}
/**
* write a message, block to write message
*
* @param frameType the type of the message
* @param byteArray message to be sent
* @param frameSize the frame size of each fragmented data frame
*
* @throws WebSocketException if the frame is malformed
* or if send data frames after the close frame is sent
* or WebSocketException if the conn is closed.
* @throws SocketException if failed to write data.
*/
public func write(frameType: WebSocketFrameType, byteArray: Array<UInt8>, frameSize!: Int64 = FRAMESIZE): Unit {
if (isClosed.load()) {
throw WebSocketException("The connection is closed.")
}
match (frameType) {
// control frames cannot be fragmented and must have a payload length of 125 bytes or less.
// an unfragmented message consists of a single frame with th FIN bit set and an opcode other than 0.
// unfragment
// FIN = 1, Opcode != 0
case PingWebFrame | PongWebFrame =>
if (byteArray.size > 125) {
throw WebSocketException("All control frames must have a payload length of 125 bytes or less.")
}
writeFrame(true, frameType, byteArray, isClient)
case CloseWebFrame =>
match {
// the first two bytes of the body must be 2-byte unsigned integer.
case byteArray.size == 0 => writeFrame(true, CloseWebFrame, byteArray, isClient)
case byteArray.size == 1 => throw WebSocketException("Invalid close payload.")
case byteArray.size <= 125 =>
let status = toUInt16(byteArray[..2])
// valid status code ranges defined in
// RFC 6455 7.4.2.
// status must between 1000 and 4999,
// 1005, 1006, 1015 are reserved values and must not be
// set as status code in a Close control frame by an endpoint.
if (status >= 5000 || status < 1000 || status == 1005 || status == 1006 || status == 1015) {
throw WebSocketException("Invalid close status code.")
}
writeFrame(true, CloseWebFrame, byteArray, isClient)
case _ => throw WebSocketException(
"All control frames must have a payload length of 125 bytes or less.")
}
isSentCloseFrame.store(true)
// a fragmented message consists of a single frame with the FIN bit
// clear and an opcode other than 0, followed by zero or more frames
// with the FIN bit clear and the opcode set to 0, and terminated by
// a single frame with the FIN bit set and an opcode of 0.
case TextWebFrame | BinaryWebFrame =>
// must not send any more data frames after sending a Close frame
// RFC 6455 5.1.1.
if (isSentCloseFrame.load()) {
throw WebSocketException("No more data frames can be sent after sending a Close frame.")
}
if (frameSize <= 0) {
throw WebSocketException("FrameSize must > 0.")
}
// unfragment
// FIN = 1, Opcode != 0
if (byteArray.size <= frameSize) {
writeFrame(true, frameType, byteArray, isClient)
return
}
// fragment
// first frame: FIN = 0, Opcode != 0
writeFrame(false, frameType, byteArray.slice(0, frameSize), isClient)
var sendLen = frameSize
// intermediate frame: FIN = 0, Opcode = 0
while (sendLen + frameSize < byteArray.size) {
writeFrame(false, ContinuationWebFrame, byteArray.slice(sendLen, frameSize), isClient)
sendLen += frameSize
}
// last frame: FIN = 1, Opcode = 0
writeFrame(true, ContinuationWebFrame, byteArray.slice(sendLen, (byteArray.size - sendLen)), isClient)
case _ => throw WebSocketException("Invalid frame type, the type must be Text, Binary, Close, Ping, Pong.")
}
}
private func writeFrame(fin: Bool, frameType: WebSocketFrameType, byteArray: Array<UInt8>, isClient: Bool) {
synchronized(writeMutex) {
let frameBytesExceptPayload = toWebSocketFrameBytesExceptPayload(fin, frameType, byteArray.size, isClient)
conn.writeRaw(frameBytesExceptPayload)
if (byteArray.isEmpty()) {
return
}
// client must mask data frames sent to server.
let payload = if (isClient) {
// last 4 bytes is maskingKey.
maskOrUnmask(frameBytesExceptPayload[frameBytesExceptPayload.size - 4..], byteArray)
} else {
byteArray
}
conn.writeRaw(payload)
}
}
/**
* write a close frame
*
* @param status close status code
* @param reason the websocket connection close reason
* defined as the UTF-8-encoded data
*
* @throws WebSocketException if the frame is malformed
* or if the conn is closed.
* @throws SocketException if failed to write data.
*/
public func writeCloseFrame(status!: ?UInt16 = None, reason!: String = ""): Unit {
if (isClosed.load()) {
throw WebSocketException("The connection is closed.")
}
// status none means the Close frame contains no body.
let statusCode = match (status) {
case None =>
writeFrame(true, CloseWebFrame, Array<UInt8>(), isClient)
isSentCloseFrame.store(true)
return
case Some(value) => value
}
// status must between 1000 and 4999,
// 1005, 1006, 1015 are reserved values and must not be set as status code in a Close control frame by an endpoint.
if (statusCode >= 5000 || statusCode < 1000 || statusCode == 1005 || statusCode == 1006 || statusCode == 1015) {
throw WebSocketException("Invalid status code.")
}
// the first two bytes of the body must be 2-byte unsigned integer,
// and the Close frame must have a payload length of 125 bytes or less.
if (reason.size > 123) {
throw WebSocketException("All control frames must have a payload length of 125 bytes or less.")
}
// the first two bytes of the body must be 2-byte unsigned integer (in network byte order).
let statusArr = fromUInt16(statusCode)
// the body may contain UTF-8-encoded data with value reason.
// RFC 6455 5.5.1.
let reasonArr = unsafe { reason.rawData() }
let payload = Array<UInt8>(2 + reasonArr.size, repeat: 0)
statusArr.copyTo(payload, 0, 0, 2)
reasonArr.copyTo(payload, 0, 2, reasonArr.size)
writeFrame(true, CloseWebFrame, payload, isClient)
isSentCloseFrame.store(true)
}
/**
* write a ping frame
*
* @param byteArray payload to be sent
*
* @throws WebSocketException if the frame is malformed
* or if the conn is closed.
* @throws SocketException if failed to write data.
*/
public func writePingFrame(byteArray: Array<UInt8>): Unit {
write(PingWebFrame, byteArray)
}
/**
* write a pong frame
*
* @param byteArray payload to be sent
*
* @throws WebSocketException if the frame is malformed
* or if the conn is closed.
* @throws SocketException if failed to write data.
*/
public func writePongFrame(byteArray: Array<UInt8>): Unit {
write(PongWebFrame, byteArray)
}
/**
* close the websocket connection
* RFC 6455 7.1.2.
*/
public func closeConn(): Unit {
if (isClosed.load()) {
return
}
conn.close()
isClosed.store(true)
}
/**
* if the websocket connection is established prior to the point where the
* endpoint is required to _fail the websocket connection_.
* the endpoint should send a Close frame with an appropriate status code
* before proceeding to close the websocket connection.
* RFC 6455 7.1.7.
*/
private func failTheWebSocketConnection(status: UInt16, message: String) {
// send a Close frame and close connection
writeCloseFrame(status: status)
closeConn()
// throw WebSocketException
throw WebSocketException("The websocket connection is failed, since ${message}." )
}
/**
* upgrade from server
*
* @param ctx conveys upgrade request and responseBuilder
* @param subProtocols subProtocols supported by the server, the default value is empty,
* indicating that no subProtocol is supported.
* @param origins origins supported by the server, the default value is empty,
* indicating that the server accept handshakes from all origins.
* @param userFunc user-defined callback, where param is the upgrade request received from client,
* returned value is response header, which will be sent to client as part of handshake
* response after some checking, e.g. server side can send some cookie, authentication header to client.
* the default value is a func which returns a empty HttpHeader
*
* @return websocket instance
*
* @throws SocketException, if failed to read request or send response
* @throws WebSocketException, if handshake failed, including check request header failed,
* run userFunc failed, check response headers returned by userFunc failed, run checkOrigin failed.
*/
public static func upgradeFromServer(ctx: HttpContext, subProtocols!: ArrayList<String> = ArrayList<String>(),
origins!: ArrayList<String> = ArrayList<String>(),
userFunc!: (HttpRequest) -> HttpHeaders = {_: HttpRequest => HttpHeaders()}): WebSocket {
synchronized(ctx.writerMtx) {
if (ctx.upgraded) {
throw WebSocketException("Upgrade to websocket failed, the connection has been upgraded.")
}
if (ctx.responseFlushedByUser) {
throw WebSocketException("Upgrade to websocket failed, a response has been sent to the client.")
}
if (ctx.responded) {
throw WebSocketException("Already responded.")
}
let responseHeader = userFunc(ctx.request)
return match (ctx.httpConn) {
// server1_1
case httpConn: HttpEngineConn1 =>
// reading the Client's Opening Handshake
let webSocketKey = parseUpgradeRequest1(ctx)
let subProtocol = parseUpgradeRequestCommon(ctx, origins, subProtocols)
// sending the Server's Opening Handshake
let acceptValue = generateAcceptValue(webSocketKey)
replyUpgradeResponse1(httpConn, subProtocol, acceptValue, responseHeader)
// extract the conn and construct websocket
let websocketConn = WebSocketConn1(httpConn.conn)
ctx.upgraded = true
WebSocket(websocketConn, subProtocol, false)
// server2_0
case httpConn: HttpEngineConn2 =>
// reading the Client's Opening Handshake
if (httpConn.upgrade != "websocket") {
responseAndThrows(ctx, HttpStatusCode.STATUS_BAD_REQUEST,
"the upgrade request to websocket on http/2.0 must be a CONNECT request")
}
let subProtocol = parseUpgradeRequestCommon(ctx, origins, subProtocols)
// sending the Server's Opening Handshake
replyUpgradeResponse2(httpConn, subProtocol, responseHeader)
let websocketConn = WebSocketConn2(httpConn)
ctx.upgraded = true
WebSocket(websocketConn, subProtocol, false)
case _ => throw WebSocketException("Only HTTP/1.1 or HTTP/2.0 to WebSocket upgrade is supported.")
}
}
}
/**
* upgrade from client
*
* @param client the client from which to send a websocket upgrade request
* @param url the target url
* @param subProtocols the subProtocols the client wishes to speak, ordered by preference. the default value is empty.
* @param headers the upgrade request headers, such as cookie, origin.
*
* @return websocket instance
* @return httpHeader in the response
*
* @throws SocketException, if failed to send
* @throws HttpException, if some thing wrong is http request
* @throws WebSocketException, if handshake failed, including get conn from pool failed, check response from server failed.
*/
public static func upgradeFromClient(client: Client, url: URL, version!: Protocol = HTTP1_1,
subProtocols!: ArrayList<String> = ArrayList<String>(), headers!: HttpHeaders = HttpHeaders()): (WebSocket,
HttpHeaders) {
// a client opens a connection and sends a handshake
// only HTTP/1.1 and HTTP/2.0 are supported to upgrade to WebSocket.
match (version) {
case HTTP1_1 | HTTP2_0 => ()
case _ => throw WebSocketException("Only the upgrade from HTTP1_1 and HTTP2_0 to WebSocket is supported.")
}
// generate sec-websocket-key
let webSocketKey = generateKey()
// generate Sec-WebSocket-Accept
let acceptValue = generateAcceptValue(webSocketKey)
let upgradeRequest = constructUpgradeRequest(url, webSocketKey, subProtocols, headers, version)
// once the client's opening handshake has been sent,
// the client must wait for a response from the server
// before sending any further data.
if (client.isClosed.load()) {
throw WebSocketException("This client has already closed.")
}
let resp = client.doRequest(upgradeRequest)
let subProtocol = validateUpgradeResponse(resp, acceptValue, subProtocols)
// extract the conn and construct websocket
// client1_1
let conn = match (version) {
case HTTP1_1 =>
let connNode = match (resp.connNode.getOrThrow() as ConnNode) {
case Some(v) => v
case None => throw WebSocketException("Get connection failed.")
}
WebSocketConn1(connNode.h1Engine.extractFromConnInUse(connNode))
case HTTP2_0 => (resp.body as WebSocketConn) ?? throw HttpException("H2 body broken.")
case _ => throw WebSocketException("Not supported protocol.")
}
let websocket = WebSocket(conn, subProtocol, true)
return (websocket, resp.headers)
}
}
/**
* the handshake consists of an HTTP Upgrade request,
* along with a list of required and optional header fields
* RFC 6455 4.1.
*/
func constructUpgradeRequest(url: URL, webSocketKey: String, subProtocols: ArrayList<String>, headers: HttpHeaders,
version: Protocol): HttpRequest {
// the websocket protocol defines two URI schemes,
// ws: the default port for ws is 80,
// wss: the secure scheme, the default port for wss is 443
// RFC 6455 3.
let upgradeUrl = match (url.scheme) {
case "ws" => url.replace(scheme: "http")
case "wss" => url.replace(scheme: "https")
case _ => throw WebSocketException(
"Upgrade to websocket failed, invalid URL scheme, the scheme must be ws or wss.")
}
if (url.hostName.isEmpty()) {
throw WebSocketException("Upgrade to websocket failed, no host in request URL.")
}
// send an opening handshake, and read the server’s handshake in response.
// the handshake consists of an HTTP Upgrade request,
// along with a list of required and optional header fields.
// RFC 6455 4.1.
let upgradeRequestBuilder = HttpRequestBuilder().url(upgradeUrl)
if (version == HTTP2_0) {
upgradeRequestBuilder.version(version).connect()
upgradeRequestBuilder.headers.map.add(Str(":protocol"), HeaderValue("websocket"))
} else {
upgradeRequestBuilder
.version(version)
// the method of the request must be GET
// RFC 6455 4.1.2.
.method("GET")
// must contain an Upgrade header field
// whose value must include "websocket"
// RFC 6455 4.1.5.
.header("upgrade", "websocket")
// must contain a Connection header field
// whose value must include "Upgrade"
// RFC 6455 4.1.6.
.header("connection", "Upgrade")
}
upgradeRequestBuilder
// must include a header field with the name sec-websocket-key
// RFC 6455 4.1.7.
.header("sec-websocket-key", webSocketKey)
// must include a header field with the name sec-websocket-version
// the value of this header must be 13
// RFC 6455 4.1.9.
.header("sec-websocket-version", "13")
// may include a header field with a name sec-websocket-protocol
// If present, this value indicates one or more comma-separated subprotocol
// the client wishes to speak, ordered by preference.
// RFC 6455 4.1.10.
if (!subProtocols.isEmpty()) {
upgradeRequestBuilder.headers.set("sec-websocket-protocol", subProtocols[0])
for (i in 1..subProtocols.size) {
upgradeRequestBuilder.headers.add("sec-websocket-protocol", subProtocols[i])
}
}
// may include any other header fields.
// websocket extensions not supported yet
// RFC 6455 4.1.11.
if (!headers.isEmpty()) {
if (!headers.get("sec-websocket-extensions").isEmpty()) {
throw WebSocketException("Upgrade to websocket failed, websocket extensions not supported yet.")
}
upgradeRequestBuilder.headers.addAll(headers)
}
return upgradeRequestBuilder.build()
}
/**
* validate the response
* the client must validate the server's response as follows:
* RFC 6455 4.1.
*/
func validateUpgradeResponse(resp: HttpResponse, acceptValue: String, subProtocols: ArrayList<String>): String {
// version-related check
match (resp.version) {
// the status code received from the server should be 101
// connection still maintained by Client.
case HTTP1_1 =>
if (resp.status != HttpStatusCode.STATUS_SWITCHING_PROTOCOLS) {
// conn is not returned to HttpEngine1, should be disconnected
failTheWebSocketConnection(resp, "the status code should be 101, but received ${resp.status}")
}
// if the response lacks an Upgrade header field or the Upgrade header field
// contains a value that is not an ASCII case-insensitive match for the value "websocket",
// the client must _fail the websocket connection_.
let upgradeValues = resp.headers.getInternal("upgrade") ?? failTheWebSocketConnection(resp,
"the handshake response lacks Upgrade: websocket header field")
if (!(upgradeValues |> splitValuesByComma |> "websocket".caseInsensitiveMatchAll)) {
failTheWebSocketConnection(resp, "the handshake response has a wrong Upgrade header field")
}
// if the response lacks an Connection header field or the Upgrade header field
// doesn't contain a token that is not an ASCII case-insensitive match for the value "upgrade",
// the client must _fail the websocket connection_.
if (!(resp.headers.get("connection") |> splitValuesByComma |> "upgrade".caseInsensitiveMatchOne)) {
failTheWebSocketConnection(resp, "the handshake response lacks Connection: Upgrade header field")
}
// if the response lacks a Sec-WebSocket-Accept header field or the Sec-WebSocket-Accept header field
// contains a value other than the base64-encoded SHA-1 of the concatenation of the sec-websocket-key
// (as a string, not base64-decoded) with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
// but ignoring any leading and trailing whitespace,
// the client must _fail the websocket connection_.
let acceptValues = resp.headers.getInternal("sec-websocket-accept") ?? failTheWebSocketConnection(resp,
"the handshake response lacks Sec-Websocket-Accept header field")
if (!(acceptValues |> splitValuesByComma |> acceptValue.matchAll)) {
failTheWebSocketConnection(resp, "the handshake response has a wrong accept string")
}
case HTTP2_0 =>
if (resp.status != HttpStatusCode.STATUS_OK) {
failTheWebSocketConnection(resp, "the status code should be 200, but received ${resp}")
}
// an HTTP/1.1 upgrade request but receive an HTTP/1.0 response
case _ => failTheWebSocketConnection(resp,
"wrong http version ${resp.version} which cannot be upgraded to websocket")
}
// websocket extensions not supported yet
if (!resp.headers.get("sec-websocket-extensions").isEmpty()) {
failTheWebSocketConnection(resp, "websocket extensions not supported yet")
}
// if the response includes a sec-websocket-protocol header field and this header field indicates
// the use of a subprotocol that was not present in the client’s handshake
// (the server has indicated a subprotocol not requested by the client),
// the client must _fail the websocket connection_
let subProtocol = match (resp.headers.getInternal("sec-websocket-protocol")) {
case None => ""
case Some(values) =>
let subs = splitValuesByComma(values)
if (subs.size != 1) {
failTheWebSocketConnection(resp, "the handshake response must have one or null subprotocol")
}
if (!subProtocols.contains(subs[0].trimAscii())) {
failTheWebSocketConnection(resp, "the handshake response has wrong subprotocol")
} else {
subs[0]
}
}
return subProtocol
}
/**
* parse the upgrade request.
* the server must parse at least part of this handshake in order to
* obtain the necessary information to generate the server part of the handshake.
* RFC 6455 4.2.1.
*/
func parseUpgradeRequest1(ctx: HttpContext): String {
// an HTTP/1.1 or higher GET request.
if (ctx.request.version == HTTP1_0) {
responseAndThrows(ctx, HttpStatusCode.STATUS_BAD_REQUEST,
"the upgrade request to websocket must be an HTTP/1.1 or higher request")
}
if (ctx.request.method != "GET") {
responseAndThrows(ctx, HttpStatusCode.STATUS_BAD_REQUEST,
"the upgrade request to websocket must be a GET request")
}
// Host header is checked when read request.
// an Upgrade header field containing the value "websocket", case-insensitive.
if (!(ctx.request.headers.get("upgrade") |> splitValuesByComma |> "websocket".caseInsensitiveMatchOne)) {
responseAndThrows(ctx, HttpStatusCode.STATUS_BAD_REQUEST,
"the upgrade request lacks Upgrade: websocket header field")
}
// a Connection header field that include the token "Upgrade", case-insensitive.
if (!(ctx.request.headers.get("connection") |> splitValuesByComma |> "upgrade".caseInsensitiveMatchOne)) {
responseAndThrows(ctx, HttpStatusCode.STATUS_BAD_REQUEST,
"the upgrade request lacks Connection: Upgrade header field")
}
// a sec-websocket-key header field with a base64-encoded value that,
// when decoded, is 16 bytes in length.
let keyValues = ctx.request.headers.getInternal("sec-websocket-key") ?? responseAndThrows(ctx, // cjlint-ignore !G.EXP.03
HttpStatusCode.STATUS_BAD_REQUEST, "the upgrade request lacks sec-websocket-key header field")
if (!keyValues.isSingle() || ((fromBase64String(keyValues.single)?.size ?? 0) != 16)) {
responseAndThrows(ctx, HttpStatusCode.STATUS_BAD_REQUEST,
"the upgrade request's sec-websocket-key header value is invalid")
}
return keyValues.single
}
func parseUpgradeRequestCommon(ctx: HttpContext, origins: ArrayList<String>, subProtocols: ArrayList<String>): String {
// optionally
// an Origin header field in the client's handshake indicates the origin of the script establishing the connection.
// the server may use this information as part of a determination of whether to accept the incoming connection.
// if the server does not validate the origin (origins.isEmpty() == true), it will accept connections from anywhere.
if (origins.isEmpty()) {
httpLogWarn(mutexLogger(), "[WebSocket#upgradeFromServer] No origin restrictions configured. " +
"This enables Cross-Site WebSocket Hijacking (CSWSH) vulnerability. " +
"Consider setting allowed origins explicitly.")
} else {
let originValues = ctx.request.headers.getInternal("origin") ?? responseAndThrows(ctx, // cjlint-ignore !G.EXP.03
HttpStatusCode.STATUS_FORBIDDEN, "the upgrade request lacks Origin field")
if (!originValues.isSingle() || !origins.contains(originValues.single)) {
responseAndThrows(ctx, HttpStatusCode.STATUS_FORBIDDEN,
"the upgrade request's origin is not allowed by server")
}
}
// optionally
// a sec-websocket-protocol header field, with a list of values indicating which protocols the client would like to
// speak, ordered by preference
var subProtocol: String = ""
if (!subProtocols.isEmpty()) {
let protocolValues = ctx.request.headers.get("sec-websocket-protocol") |> splitValuesByComma
for (protocol in protocolValues) {
if (subProtocols.contains(protocol)) {
subProtocol = protocol
break
}
}
}
// a sec-websocket-version header field, with a value of 13
let versionValues = ctx.request.headers.getInternal("sec-websocket-version") ?? responseAndThrows(ctx, // cjlint-ignore !G.EXP.03
HttpStatusCode.STATUS_BAD_REQUEST, "the upgrade request lacks sec-websocket-version header field")
if (!versionValues.isSingle() || versionValues.single != "13") {
responseAndThrows(ctx, HttpStatusCode.STATUS_UPGRADE_REQUIRED,
"the upgrade request's sec-websocket-version must be 13")
}
// websocket extensions are not supported yet
return subProtocol
}
/**
* if the server chooses to accept the incoming connection,
* it must reply with a valid HTTP response indicating the following
* RFC 6455 4.2.2.5.
*/
func replyUpgradeResponse1(conn: HttpEngineConn1, subProtocol: String, acceptValue: String, responseHeader: HttpHeaders) {
let responseBuilder = HttpResponseBuilder()
// a status-line with a 101 response code
// RFC 6455 4.2.2.5.1.
.status(HttpStatusCode.STATUS_SWITCHING_PROTOCOLS)
// an Upgrade header field with value "websocket"
// RFC 6455 4.2.2.5.2.
.header("upgrade", "websocket")
// a Connection header field with value "Upgrade"
// RFC 6455 4.2.2.5.3.
.header("connection", "upgrade")
// a Sec-WebSocket-Accept header field
// RFC 6455 4.2.2.5.4.
.header("sec-websocket-accept", acceptValue)
// optionally
// a sec-websocket-protocol header field
// RFC 6455 4.2.2.5.4.
if (!subProtocol.isEmpty()) {
responseBuilder.header("sec-websocket-protocol", subProtocol)
}
// other headers such as set-cookie
// RFC 6455 1.3.
responseBuilder.addHeaders(responseHeader)
conn.writeResponse(responseBuilder.build())
}
func replyUpgradeResponse2(conn: HttpEngineConn2, subProtocol: String, responseHeader: HttpHeaders) {
let headers = HttpHeaders()
if (!subProtocol.isEmpty()) {
headers.add("sec-websocket-protocol", subProtocol)
}
headers.addAll(responseHeader)
conn.writeHeader(HttpStatusCode.STATUS_OK, headers, streamEnd: false)
}
/**
* the value of this header field must be a nonce consisting of
* a randomly selected 16-byte value that has been base64-encoded.
* RFC 6455 4.1.7.
*/
func generateKey(): String {
let r = Array<Byte>(16, repeat: 0)
getGlobalCryptoKit().getRandomGen().nextBytes(r)
return toBase64String(r)
}
/**
* to do so, the client must _close the websocket connection_,
* and may report the problem to the user.
* to _close the websocket connection_,
* an endpoint closes the underlying TCP connection
* RFC 6455 7.1.7.
*/
func failTheWebSocketConnection(resp: HttpResponse, message: String) {
// extract the conn and close
// client1_1
match (resp.connNode) {
case Some(v) =>
if (let Some(node) <- (v as ConnNode)) {
node.closeConn()
}
case None => ()
}
// throw WebSocketException
throw WebSocketException("Upgrade to websocket failed, ${message}." )
}
func responseAndThrows(ctx: HttpContext, status: UInt16, message: String) {
let respBuilder = HttpResponseBuilder().status(status).body(message)
if (status == HttpStatusCode.STATUS_UPGRADE_REQUIRED) {
respBuilder.header("sec-websocket-version", "13")
}
match (ctx.httpConn) {
case httpConn: HttpEngineConn1 =>
respBuilder.header("connection", "close")
httpConn.writeResponse(respBuilder.build())
case httpConn: HttpEngineConn2 =>
httpConn.writeResponse(respBuilder)
httpConn.responded = true
case _ => ()
}
ctx.upgraded = true
throw WebSocketException(message)
}
/**
* the value of Sec-WebSocket-Accept header field is constructed by concatenating key with the string
* "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this concatenated value to obtain
* a 20-byte value and base64-encoding this 20-byte hash
* RFC 6455 4.2.5.5.4.
*/
func generateAcceptValue(key: String): String {
let mdArray: Array<UInt8> = Array<UInt8>(20, repeat: 0)
let data = unsafe { (key + GUID).rawData() }
unsafe {
let d: CPointerHandle<UInt8> = acquireArrayRawData(data)
let md: CPointerHandle<UInt8>
try {
md = acquireArrayRawData(mdArray)
} catch (e: Exception) {
releaseArrayRawData(d)
throw e
}
let dynMsgPtr = MallocDynMsg()
if (dynMsgPtr.isNull()) {
releaseArrayRawData(d)
releaseArrayRawData(md)
throw WebSocketException("Malloc failed.")
}
try {
DYN_SHA1(d.pointer, Int32(data.size), md.pointer, dynMsgPtr)
if (!dynMsgPtr.read().found) {
let funcName = CString(dynMsgPtr.read().funcName).toString()
throw WebSocketException("Can not load openssl library or function ${funcName}.")
}
} finally {
FreeDynMsg(dynMsgPtr)
releaseArrayRawData(d)
releaseArrayRawData(md)
}
}
return toBase64String(mdArray)
}
/**
* in network byte order
*/
@OverflowWrapping
func fromInt64(number: Int64, size: Int64): Array<UInt8> {
let bytes = Array<UInt8>(size, repeat: 0)
for (i in 0..size) {
bytes[i] = UInt8(number >> ((size - 1 - i) * 8))
}
return bytes
}
@OverflowWrapping
func fromUInt16(number: UInt16): Array<UInt8> {
let bytes = Array<UInt8>(2, repeat: 0)
bytes[0] = UInt8(number >> 8)
bytes[1] = UInt8(number)
return bytes
}
/**
* bytes.size <= 8
*/
func toInt64(bytes: Array<UInt8>): Int64 {
var int = 0
for (byte in bytes) {
int = int << 8
int = int | Int64(byte)
}
return int & 0X7FFF_FFFF_FFFF_FFFF
}
func toUInt16(bytes: Array<UInt8>): UInt16 {
var int: UInt16 = 0
for (byte in bytes) {
int = int << 8
int = int | UInt16(byte)
}
return int
}
/**
* the algorithm used to convert masked data into unmasked data, or vice versa
* the same algorithm applies regardless of the direction of the translation,
* e.g., the same steps are applied to mask the data as to unmask the data.
* j = i MOD 4
* transformed-octet-i = original-octet-i XOR masking-key-octet-j
* RFC 6455 5.3.
*/
func maskOrUnmask(maskingKey: Array<UInt8>, bytes: Array<UInt8>): Array<UInt8> {
let masked = Array<UInt8>(bytes.size, repeat: 0)
for (i in 0..bytes.size) {
masked[i] = bytes[i] ^ maskingKey[i % 4]
}
return masked
}