require "./spec_helper"
require "../spec_helper"
require "http/web_socket"
require "http/server"
require "random/secure"
require "../../support/fibers"
require "../../support/ssl"
require "../socket/spec_helper.cr"

private def assert_text_packet(packet, size, final = false)
  assert_packet packet, HTTP::WebSocket::Protocol::Opcode::TEXT, size, final: final
end

private def assert_binary_packet(packet, size, final = false)
  assert_packet packet, HTTP::WebSocket::Protocol::Opcode::BINARY, size, final: final
end

private def assert_ping_packet(packet, size, final = false)
  assert_packet packet, HTTP::WebSocket::Protocol::Opcode::PING, size, final: final
end

private def assert_close_packet(packet, size, final = false)
  assert_packet packet, HTTP::WebSocket::Protocol::Opcode::CLOSE, size, final: final
end

private def assert_packet(packet, opcode, size, final = false)
  packet.opcode.should eq(opcode)
  packet.size.should eq(size)
  packet.final.should eq(final)
end

private class MalformerHandler
  include HTTP::Handler

  def call(context)
    context.response.headers["Transfer-Encoding"] = "chunked"
    call_next(context)
  end
end

describe HTTP::WebSocket do
  describe "receive" do
    it "can read a small text packet" do
      data = Bytes[0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(64)
      result = ws.receive(buffer)
      assert_text_packet result, 5, final: true
      String.new(buffer[0, result.size]).should eq("Hello")
    end

    it "can read partial packets" do
      data = Bytes[0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
        0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(3)

      2.times do
        result = ws.receive(buffer)
        assert_text_packet result, 3, final: false
        String.new(buffer).should eq("Hel")

        result = ws.receive(buffer)
        assert_text_packet result, 2, final: true
        String.new(buffer[0, 2]).should eq("lo")
      end
    end

    it "can read masked text message" do
      data = Bytes[0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58,
        0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58]
      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(3)

      2.times do
        result = ws.receive(buffer)
        assert_text_packet result, 3, final: false
        String.new(buffer).should eq("Hel")

        result = ws.receive(buffer)
        assert_text_packet result, 2, final: true
        String.new(buffer[0, 2]).should eq("lo")
      end
    end

    it "can read fragmented packets" do
      data = Bytes[0x01, 0x03, 0x48, 0x65, 0x6c, 0x80, 0x02, 0x6c, 0x6f,
        0x01, 0x03, 0x48, 0x65, 0x6c, 0x80, 0x02, 0x6c, 0x6f]

      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(10)

      2.times do
        result = ws.receive(buffer)
        assert_text_packet result, 3, final: false
        String.new(buffer[0, 3]).should eq("Hel")

        result = ws.receive(buffer)
        assert_text_packet result, 2, final: true
        String.new(buffer[0, 2]).should eq("lo")
      end
    end

    it "read ping packet" do
      data = Bytes[0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(64)
      result = ws.receive(buffer)
      assert_ping_packet result, 5, final: true
      String.new(buffer[0, result.size]).should eq("Hello")
    end

    it "read ping packet in between fragmented packet" do
      data = Bytes[0x01, 0x03, 0x48, 0x65, 0x6c,
        0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
        0x80, 0x02, 0x6c, 0x6f]
      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(64)

      result = ws.receive(buffer)
      assert_text_packet result, 3, final: false
      String.new(buffer[0, 3]).should eq("Hel")

      result = ws.receive(buffer)
      assert_ping_packet result, 5, final: true
      String.new(buffer[0, result.size]).should eq("Hello")

      result = ws.receive(buffer)
      assert_text_packet result, 2, final: true
      String.new(buffer[0, 2]).should eq("lo")
    end

    it "read long packet" do
      data = File.read(datapath("websocket_longpacket.bin"))
      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(2048)

      result = ws.receive(buffer)
      assert_text_packet result, 1023, final: true
      String.new(buffer[0, 1023]).should eq("x" * 1023)
    end

    it "read very long packet" do
      data = Bytes.new(10 + 0x010000)

      header = Bytes[0x82, 127_u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
      data.copy_from(header)

      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(0x010000)

      result = ws.receive(buffer)
      assert_binary_packet result, 0x010000, final: true
    end

    it "can read a close packet" do
      data = Bytes[0x88, 0x00]
      io = IO::Memory.new(data)
      ws = HTTP::WebSocket::Protocol.new(io)

      buffer = Bytes.new(64)
      result = ws.receive(buffer)
      assert_close_packet result, 0, final: true
    end
  end

  describe "send" do
    it "sends long data with correct header" do
      big_string = "abcdefghijklmnopqrstuvwxyz" * (IO::DEFAULT_BUFFER_SIZE // 4)
      size = big_string.size
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.send(big_string)
      bytes = io.to_slice
      bytes.size.should eq(10 + size) # 2 bytes header, 8 bytes size, UInt16 + 1 bytes content
      bytes[1].should eq(127)
      received_size = 0
      8.times { |i| received_size <<= 8; received_size += bytes[2 + i] }
      received_size.should eq(size)
      size.times do |i|
        bytes[10 + i].should eq(big_string[i].ord)
      end
    end

    it "sets binary opcode if used with slice" do
      sent_bytes = uninitialized UInt8[4]

      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io, masked: true)
      ws.send(sent_bytes.to_slice)
      bytes = io.to_slice
      (bytes[0] & 0x0f).should eq(0x02)
    end
  end

  describe "stream" do
    it "sends continuous data and splits it to frames" do
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.stream do |io| # default frame size of 1024
        3.times { io.write(("a" * 512).to_slice) }
      end

      bytes = io.to_slice
      bytes.size.should eq(4 * 2 + 512 * 3) # two frames with 2 bytes header, 2 bytes size, 3 * 512 bytes content in total
      first_frame, second_frame = {bytes[0, (4 + 1024)], bytes + (4 + 1024)}
      (first_frame[0] & 0x80).should eq(0x00) # FINAL bit unset
      (first_frame[0] & 0x0f).should eq(0x02) # BINARY frame
      first_frame[1].should eq(126)           # extended size
      received_size = 0
      2.times { |i| received_size <<= 8; received_size += first_frame[2 + i] }
      received_size.should eq(1024)
      received_size.times do |i|
        bytes[4 + i].should eq('a'.ord)
      end

      (second_frame[0] & 0x80).should_not eq(0x00) # FINAL bit set
      (second_frame[0] & 0x0f).should eq(0x00)     # CONTINUATION frame
      second_frame[1].should eq(126)               # extended size
      received_size = 0
      2.times { |i| received_size <<= 8; received_size += second_frame[2 + i] }
      received_size.should eq(512)
      received_size.times do |i|
        bytes[4 + i].should eq('a'.ord)
      end
    end

    it "sends less data than the frame size if necessary" do
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.stream do |io| # default frame size of 1024
        io.write("hello world".to_slice)
      end

      bytes = io.to_slice
      bytes.size.should eq(2 + 11) # one frame with 1 byte header, 1 byte size, "hello world" bytes content in total
      first_frame = bytes
      (first_frame[0] & 0x80).should_not eq(0x00) # FINAL bit set
      (first_frame[0] & 0x0f).should eq(0x02)     # BINARY frame
      first_frame[1].should eq(11)                # non-extended size
      (bytes + 2).should eq "hello world".to_slice
    end

    it "sets opcode of first frame to binary if stream is called with binary = true" do
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.stream(binary: true) { |io| }

      bytes = io.to_slice
      (bytes[0] & 0x0f).should eq(0x02) # BINARY frame
    end

    it "should not send frame if stream is flushed" do
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.stream do |stream|
        stream.write "foo".to_slice
        stream.flush
        io.size.should eq 0
      end # flushed/sent here
      bytes = io.to_slice
      bytes.size.should eq 5 # 1 byte header, 1 byte size, 3 byte payload "foo"
      first_frame = bytes
      (first_frame[0] & 0x80).should_not eq(0x00) # FINAL bit set
      (first_frame[0] & 0x0f).should eq(0x02)     # BINARY frame
      first_frame[1].should eq(3)
    end
  end

  describe "send_masked" do
    it "sends the data with a bitmask" do
      sent_string = "hello"
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io, masked: true)
      ws.send(sent_string)
      bytes = io.to_slice
      bytes.size.should eq(11)     # 2 bytes header, 4 bytes mask, 5 bytes content
      bytes[1].bit(7).should eq(1) # For mask bit
      (bytes[1] - 128).should eq(sent_string.size)
      (bytes[2] ^ bytes[6]).should eq('h'.ord)
      (bytes[3] ^ bytes[7]).should eq('e'.ord)
      (bytes[4] ^ bytes[8]).should eq('l'.ord)
      (bytes[5] ^ bytes[9]).should eq('l'.ord)
      (bytes[2] ^ bytes[10]).should eq('o'.ord)
    end

    it "sends long data with correct header" do
      big_string = "abcdefghijklmnopqrstuvwxyz" * (IO::DEFAULT_BUFFER_SIZE // 4)
      size = big_string.size
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io, masked: true)
      ws.send(big_string)
      bytes = io.to_slice
      bytes.size.should eq(size + 14) # 2 bytes header, 8 bytes size, 4 bytes mask, UInt16::MAX + 1 bytes content
      bytes[1].bit(7).should eq(1)    # For mask bit
      (bytes[1] - 128).should eq(127)
      received_size = 0
      8.times { |i| received_size <<= 8; received_size += bytes[2 + i] }
      received_size.should eq(size)
      size.times do |i|
        (bytes[14 + i] ^ bytes[10 + (i % 4)]).should eq(big_string[i].ord)
      end
    end
  end

  describe "close" do
    it "closes with code" do
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.close(4020)
      bytes = io.to_slice
      (bytes[0] & 0x0f).should eq(0x08) # CLOSE frame
      bytes[1].should eq(0x02)          # 2 bytes code
      bytes[2].should eq(0x0f)
      bytes[3].should eq(0xb4)
    end

    it "closes with message" do
      message = "bye"
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.close(nil, message)
      bytes = io.to_slice
      (bytes[0] & 0x0f).should eq(0x08) # CLOSE frame
      bytes[1].should eq(0x05)          # 2 + message.bytesize
      bytes[2].should eq(0x03)
      bytes[3].should eq(0xe8)
      String.new(bytes[4..6]).should eq(message)
    end

    it "closes with message and code" do
      message = "4020"
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.close(4020, message)
      bytes = io.to_slice
      (bytes[0] & 0x0f).should eq(0x08) # CLOSE frame
      bytes[1].should eq(0x06)          # 2 + message.bytesize
      bytes[2].should eq(0x0f)
      bytes[3].should eq(0xb4)
      String.new(bytes[4..7]).should eq(message)
    end

    it "closes without message" do
      io = IO::Memory.new
      ws = HTTP::WebSocket::Protocol.new(io)
      ws.close
      bytes = io.to_slice
      (bytes[0] & 0x0f).should eq(0x08) # CLOSE frame
      bytes[1].should eq(0x00)
    end
  end

  each_ip_family do |family, local_address|
    it "negotiates over HTTP correctly" do
      address_chan = Channel(Socket::IPAddress).new
      close_chan = Channel({Int32, String}).new

      f = spawn do
        http_ref = nil
        ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
          ctx.request.path.should eq("/foo/bar")
          ctx.request.query_params["query"].should eq("arg")
          ctx.request.query_params["yes"].should eq("please")

          ws.on_message do |str|
            ws.send("pong #{str}")
          end

          ws.on_close do |code, message|
            http_ref.not_nil!.close
            close_chan.send({code.to_i, message})
          end
        end

        http_server = http_ref = HTTP::Server.new([ws_handler])
        address = http_server.bind_tcp(local_address, 0)
        address_chan.send(address)
        http_server.listen
      end

      listen_address = address_chan.receive
      wait_until_blocked f

      ws2 = HTTP::WebSocket.new("ws://#{listen_address}/foo/bar?query=arg&yes=please")

      random = Random::Secure.hex
      ws2.on_message do |str|
        str.should eq("pong #{random}")
        ws2.close(4020, "close message")
      end
      ws2.send(random)

      ws2.run

      code, message = close_chan.receive
      code.should eq(4020)
      message.should eq("close message")
    end

    it "negotiates over HTTPS correctly" do
      address_chan = Channel(Socket::IPAddress).new

      server_context, client_context = ssl_context_pair

      f = spawn do
        http_ref = nil
        ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
          ctx.request.path.should eq("/")

          ws.on_message do |str|
            ws.send("pong #{str}")
            ws.close
          end

          ws.on_close do
            http_ref.not_nil!.close
          end
        end

        http_server = http_ref = HTTP::Server.new([ws_handler])

        address = http_server.bind_tls(local_address, context: server_context)
        address_chan.send(address)
        http_server.listen
      end

      listen_address = address_chan.receive
      wait_until_blocked f

      ws2 = HTTP::WebSocket.new(listen_address.address, port: listen_address.port, path: "/", tls: client_context)

      random = Random::Secure.hex
      ws2.on_message do |str|
        str.should eq("pong #{random}")
      end
      ws2.send(random)

      ws2.run
    end
  end

  it "sends correct HTTP basic auth header" do
    ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
      ws.send ctx.request.headers["Authorization"]
      ws.close
    end
    http_server = HTTP::Server.new([ws_handler])
    address = http_server.bind_unused_port

    run_server(http_server) do
      client = HTTP::WebSocket.new("ws://test_username:test_password@#{address}")
      message = nil
      client.on_message do |msg|
        message = msg
      end
      client.run
      message.should eq(
        "Basic #{Base64.strict_encode("test_username:test_password")}")
    end
  end

  it "handshake fails if server does not switch protocols" do
    http_server = HTTP::Server.new do |context|
      context.response.status_code = 200
    end

    address = http_server.bind_unused_port

    run_server(http_server) do
      expect_raises(Socket::Error, "Handshake got denied. Status code was 200.") do
        HTTP::WebSocket::Protocol.new(address.address, port: address.port, path: "/")
      end
    end
  end

  it "ignores body in upgrade response (malformed)" do
    malformer = MalformerHandler.new
    ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
      ws.on_message do |str|
        ws.send(str)
      end
    end
    http_server = HTTP::Server.new([malformer, ws_handler])

    address = http_server.bind_unused_port

    run_server(http_server) do
      client = HTTP::WebSocket.new("ws://#{address}")
      message = nil
      client.on_message do |msg|
        message = msg
        client.close
      end
      client.send "hello"
      client.run
      message.should eq("hello")
    end
  end

  it "doesn't compress upgrade response body" do
    compress_handler = HTTP::CompressHandler.new
    ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
      ws.on_message do |str|
        ws.send(str)
      end
    end
    http_server = HTTP::Server.new([compress_handler, ws_handler])

    address = http_server.bind_unused_port

    run_server(http_server) do
      client = HTTP::WebSocket.new("ws://#{address}", headers: HTTP::Headers{"Accept-Encoding" => "gzip"})
      message = nil
      client.on_message do |msg|
        message = msg
        client.close
      end
      client.send "hello"
      client.run
      message.should eq("hello")
    end
  end

  describe "Sec-WebSocket-Protocol" do
    it "fails handshake if server does not include Sec-WebSocket-Protocol in response when requested" do
      http_server = HTTP::Server.new do |context|
        response = context.response
        key = context.request.headers["Sec-WebSocket-Key"]
        response.status_code = 101
        response.headers["Upgrade"] = "websocket"
        response.headers["Connection"] = "Upgrade"
        response.headers["Sec-WebSocket-Accept"] = HTTP::WebSocket::Protocol.key_challenge(key) if key
      end

      address = http_server.bind_unused_port

      run_server(http_server) do
        expect_raises(Socket::Error, "Handshake got denied. Server did not respond with Sec-WebSocket-Protocol.") do
          HTTP::WebSocket::Protocol.new(
            address.address,
            port: address.port,
            path: "/",
            protocols: ["chat"]
          )
        end
      end
    end

    it "fails handshake if server responds with non-requested Sec-WebSocket-Protocol" do
      http_server = HTTP::Server.new do |context|
        response = context.response
        key = context.request.headers["Sec-WebSocket-Key"]
        response.status_code = 101
        response.headers["Upgrade"] = "websocket"
        response.headers["Connection"] = "Upgrade"
        response.headers["Sec-WebSocket-Accept"] = HTTP::WebSocket::Protocol.key_challenge(key) if key
        response.headers["Sec-WebSocket-Protocol"] = "video"
      end

      address = http_server.bind_unused_port

      run_server(http_server) do
        expect_raises(Socket::Error, "Handshake got denied. Server responded with an invalid Sec-WebSocket-Protocol.") do
          HTTP::WebSocket::Protocol.new(
            address.address,
            port: address.port,
            path: "/",
            protocols: ["chat"]
          )
        end
      end
    end

    it "accepts handshake if server responds with one of the requested Sec-WebSocket-Protocol" do
      http_server = HTTP::Server.new do |context|
        response = context.response
        key = context.request.headers["Sec-WebSocket-Key"]
        response.status_code = 101
        response.headers["Upgrade"] = "websocket"
        response.headers["Connection"] = "Upgrade"
        response.headers["Sec-WebSocket-Accept"] = HTTP::WebSocket::Protocol.key_challenge(key) if key
        response.headers["Sec-WebSocket-Protocol"] = "chat"
      end

      address = http_server.bind_unused_port

      run_server(http_server) do
        ws = HTTP::WebSocket::Protocol.new(
          address.address,
          port: address.port,
          path: "/",
          protocols: ["chat", "video"]
        )
        ws.protocol.should eq("chat")
      end
    end

    it "accepts handshake if neither provided nor responded Sec-WebSocket-Protocol" do
      http_server = HTTP::Server.new do |context|
        response = context.response
        key = context.request.headers["Sec-WebSocket-Key"]
        response.status_code = 101
        response.headers["Upgrade"] = "websocket"
        response.headers["Connection"] = "Upgrade"
        response.headers["Sec-WebSocket-Accept"] = HTTP::WebSocket::Protocol.key_challenge(key) if key
      end

      address = http_server.bind_unused_port

      run_server(http_server) do
        HTTP::WebSocket::Protocol.new(
          address.address,
          port: address.port,
          path: "/"
        )
      end
    end
  end

  describe "handshake fails if server does not verify Sec-WebSocket-Key" do
    it "Sec-WebSocket-Accept missing" do
      http_server = HTTP::Server.new do |context|
        response = context.response
        response.status_code = 101
        response.headers["Upgrade"] = "websocket"
        response.headers["Connection"] = "Upgrade"
      end

      address = http_server.bind_unused_port

      run_server(http_server) do
        expect_raises(Socket::Error, "Handshake got denied. Server did not verify WebSocket challenge.") do
          HTTP::WebSocket::Protocol.new(address.address, port: address.port, path: "/")
        end
      end
    end

    it "Sec-WebSocket-Accept incorrect" do
      http_server = HTTP::Server.new do |context|
        response = context.response
        response.status_code = 101
        response.headers["Upgrade"] = "websocket"
        response.headers["Connection"] = "Upgrade"
        response.headers["Sec-WebSocket-Accept"] = "foobar"
      end

      address = http_server.bind_unused_port

      run_server(http_server) do
        expect_raises(Socket::Error, "Handshake got denied. Server did not verify WebSocket challenge.") do
          HTTP::WebSocket::Protocol.new(address.address, port: address.port, path: "/")
        end
      end
    end
  end

  typeof(HTTP::WebSocket.new(URI.parse("ws://localhost")))
  typeof(HTTP::WebSocket.new("localhost", "/"))
  typeof(HTTP::WebSocket.new("ws://localhost"))
  typeof(HTTP::WebSocket.new(URI.parse("ws://localhost"), headers: HTTP::Headers{"X-TEST_HEADER" => "some-text"}))
end

private def integration_setup(&)
  bin_ch = Channel(Bytes).new
  txt_ch = Channel(String).new
  ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
    ws.on_binary { |bytes| bin_ch.send bytes }
    ws.on_message { |bytes| txt_ch.send bytes }
  end
  server = HTTP::Server.new [ws_handler]
  address = server.bind_unused_port
  spawn server.listen
  wsoc = HTTP::WebSocket.new("http://#{address}")

  yield wsoc, bin_ch, txt_ch
ensure
  server.close if server
end

describe "Websocket integration tests" do
  # default frame size is 1024, but be explicit here in case the default changes in the future

  it "streams less than the buffer frame size" do
    integration_setup do |wsoc, bin_ch, _|
      bytes = "hello test world".to_slice
      wsoc.stream(frame_size: 1024, &.write(bytes))
      received = bin_ch.receive
      received.should eq bytes
    end
  end

  it "streams single messages more than the buffer frame size" do
    integration_setup do |wsoc, bin_ch, _|
      bytes = ("hello test world" * 80).to_slice
      bytes.size.should be > 1024
      wsoc.stream(frame_size: 1024, &.write(bytes))
      received = bin_ch.receive
      received.should eq bytes
    end
  end

  it "streams single messages made up of multiple parts that eventually become more than the buffer frame size" do
    integration_setup do |wsoc, bin_ch, _|
      bytes = "hello test world".to_slice
      wsoc.stream(frame_size: 1024) { |io| 80.times { io.write bytes } }
      received = bin_ch.receive
      received.size.should be > 1024
      received.should eq ("hello test world" * 80).to_slice
    end
  end

  it "sends single text messages" do
    integration_setup do |wsoc, _, txt_ch|
      wsoc.send "hello text"
      wsoc.send "hello again"
      txt_ch.receive.should eq "hello text"
      txt_ch.receive.should eq "hello again"
    end
  end
end
