
# Python WebSocket library with support for "wss://" encryption.
# Copyright 2011 Joel Martin
# Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
# 
# Supports following protocol versions:
#     - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
#     - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
#     - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10

require 'gserver'
require 'openssl'
require 'stringio'
require 'digest/md5'
require 'digest/sha1'
require 'base64'

unless OpenSSL::SSL::SSLSocket.instance_methods.index("read_nonblock")
  module OpenSSL
    module SSL
      class SSLSocket
        alias :read_nonblock :readpartial
      end
    end
  end
end

class EClose < Exception
end

class WebSocketServer < GServer
  @@Buffer_size = 65536

  #
  # WebSocket constants
  #
  @@Server_handshake_hixie = "HTTP/1.1 101 Web Socket Protocol Handshake\r
Upgrade: WebSocket\r
Connection: Upgrade\r
%sWebSocket-Origin: %s\r
%sWebSocket-Location: %s://%s%s\r
"

  @@Server_handshake_hybi = "HTTP/1.1 101 Switching Protocols\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Accept: %s\r
"
  @@GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"


  def initialize(opts)
    vmsg "in WebSocketServer.initialize"
    port = opts['listen_port']
    host = opts['listen_host'] || GServer::DEFAULT_HOST

    super(port, host)
    msg opts.inspect
    if opts['server_cert']
      msg "creating ssl context"
      @sslContext = OpenSSL::SSL::SSLContext.new
      @sslContext.cert = OpenSSL::X509::Certificate.new(File.open(opts['server_cert']))
      @sslContext.key = OpenSSL::PKey::RSA.new(File.open(opts['server_key']))
      @sslContext.ca_file = opts['server_cert']
      @sslContext.verify_mode = OpenSSL::SSL::VERIFY_NONE
      @sslContext.verify_depth = 0
    end

    @@client_id = 0  # Track client number total on class

    @verbose = opts['verbose']
    @opts = opts
  end
  
  def serve(io)
    @@client_id += 1
    msg self.inspect
    if @sslContext
      msg "Enabling SSL context"
      ssl = OpenSSL::SSL::SSLSocket.new(io, @sslContext)
      #ssl.sync_close = true
      #ssl.sync = true
      msg "SSL accepting"
      ssl.accept
      io = ssl # replace the unencrypted handle with the encrypted one
    end
    
    msg "initializing thread"

    # Initialize per thread state
    t = Thread.current
    t[:my_client_id] = @@client_id
    t[:send_parts] = []
    t[:recv_part] = nil
    t[:base64] = nil

    puts "in serve, client: #{t[:my_client_id].inspect}"

    begin
      t[:client] = do_handshake(io)
      new_websocket_client(t[:client])
    rescue EClose => e
      msg "Client closed: #{e.message}"
      return
    rescue Exception => e
      msg "Uncaught exception: #{e.message}"
      msg "Trace: #{e.backtrace}"
      return
    end

    msg "Client disconnected"
  end

  #
  # WebSocketServer logging/output functions
  #
  def traffic(token)
    if @verbose then print token; STDOUT.flush; end
  end

  def msg(m)
    printf("% 3d: %s\n", Thread.current[:my_client_id] || 0, m)
  end

  def vmsg(m)
    if @verbose then msg(m) end
  end

  #
  # WebSocketServer general support routines
  #
  def gen_md5(h)
    key1 = h['sec-websocket-key1']
    key2 = h['sec-websocket-key2']
    key3 = h['key3']
    spaces1 = key1.count(" ")
    spaces2 = key2.count(" ")
    num1 = key1.scan(/[0-9]/).join('').to_i / spaces1
    num2 = key2.scan(/[0-9]/).join('').to_i / spaces2

    return Digest::MD5.digest([num1, num2, key3].pack('NNa8'))
  end

  def unmask(buf, hlen, length)
    pstart = hlen + 4
    mask = buf[hlen...hlen+4].each_byte.map{|b|b}
    data = buf[pstart...pstart+length]
    #data = data.bytes.zip(mask.bytes.cycle(length)).map { |d,m| d^m }
    i=-1
    data = data.each_byte.map{|b| i+=1; (b ^ mask[i % 4]).chr}.join("")
    return data
  end

  def encode_hybi(buf, opcode, base64=false)
    if base64
      buf = Base64.encode64(buf).gsub(/\n/, '')
    end

    b1 = 0x80 | (opcode & 0x0f) # FIN + opcode
    payload_len = buf.length
    if payload_len <= 125
      header = [b1, payload_len].pack('CC')
    elsif payload_len > 125 && payload_len < 65536
      header = [b1, 126, payload_len].pack('CCn')
    elsif payload_len >= 65536
      header = [b1, 127, payload_len >> 32,
                         payload_len & 0xffffffff].pack('CCNN')
    end

    return [header + buf, header.length, 0]
  end

  def decode_hybi(buf, base64=false)
    f = {'fin'          => 0,
         'opcode'       => 0,
         'hlen'         => 2,
         'length'       => 0,
         'payload'      => nil,
         'left'         => 0,
         'close_code'   => nil,
         'close_reason' => nil}

    blen = buf.length
    f['left'] = blen

    if blen < f['hlen'] then return f end # incomplete frame

    b1, b2 = buf.unpack('CC')
    f['opcode'] = b1 & 0x0f
    f['fin']    = (b1 & 0x80) >> 7
    has_mask    = (b2 & 0x80) >> 7

    f['length'] = b2 & 0x7f

    if f['length'] == 126
      f['hlen'] = 4
      if blen < f['hlen'] then return f end # incomplete frame
      f['length'] = buf.unpack('xxn')[0]
    elsif f['length'] == 127
      f['hlen'] = 10
      if blen < f['hlen'] then return f end # incomplete frame
      top, bottom = buf.unpack('xxNN')
      f['length'] = (top << 32) & bottom
    end

    full_len = f['hlen'] + has_mask * 4 + f['length']

    if blen < full_len then return f end # incomplete frame

    # number of bytes that are part of the next frame(s)
    f['left'] = blen - full_len

    if has_mask > 0
      f['payload'] = unmask(buf, f['hlen'], f['length'])
    else
      f['payload'] = buf[f['hlen']...full_len]
    end

    if base64 and [1, 2].include?(f['opcode'])
      f['payload'] = Base64.decode64(f['payload'])
    end

    # close frame
    if f['opcode'] == 0x08
      if f['length'] >= 2
        f['close_code'] = f['payload'].unpack('n')
      end
      if f['length'] > 3
        f['close_reason'] = f['payload'][2...f['payload'].length]
      end
    end

    return f
  end

  def encode_hixie(buf)
    return ["\x00" + Base64.encode64(buf).gsub(/\n/, '') + "\xff", 1, 1]
  end

  def decode_hixie(buf)
    last = buf.index("\377")
    return {'payload' => Base64.decode64(buf[1...last]),
            'hlen' => 1,
            'length' => last - 1,
            'left' => buf.length - (last + 1)}
  end

  def send_frames(bufs)
    t = Thread.current
    if bufs.length > 0
      encbuf = ""
      bufs.each do |buf|
        if t[:version].start_with?("hybi")
          if t[:base64]
            encbuf, lenhead, lentail = encode_hybi(
              buf, opcode=1, base64=true)
          else
            encbuf, lenhead, lentail = encode_hybi(
              buf, opcode=2, base64=false)
          end
        else
          encbuf, lenhead, lentail = encode_hixie(buf)
        end
      
        t[:send_parts] << encbuf
      end

    end

    while t[:send_parts].length > 0
      buf = t[:send_parts].shift
      sent = t[:client].write(buf)

      if sent == buf.length
        traffic "<"
      else
        traffic "<."
        t[:send_parts].unshift(buf[sent...buf.length])
      end
    end

    return t[:send_parts].length
  end

  # Receive and decode Websocket frames
  # Returns: [bufs_list, closed_string]
  def recv_frames()
    t = Thread.current
    closed = false
    bufs = []

    buf = t[:client].read_nonblock(@@Buffer_size)

    if buf.length == 0
      return bufs, "Client closed abrubtly"
    end

    if t[:recv_part]
      buf = t[:recv_part] + buf
      t[:recv_part] = nil
    end

    while buf.length > 0
      if t[:version].start_with?("hybi")
        frame = decode_hybi(buf, base64=t[:base64])

        if frame['payload'] == nil
          traffic "}."
          if frame['left'] > 0
            t[:recv_part] = buf[-frame['left']...buf.length]
          end
          break
        else
          if frame['opcode'] == 0x8
            closed = "Client closed, reason: %s - %s" % [
              frame['close_code'], frame['close_reason']]
            break
          end
        end
      else
        if buf[0...2] == "\xff\x00"
          closed = "Client sent orderly close frame"
          break
        elsif buf[0...2] == "\x00\xff"
          buf = buf[2...buf.length]
          continue # No-op frame
        elsif buf.count("\xff") == 0
          # Partial frame
          traffic "}."
          t[:recv_part] = buf
          break
        end

        frame = decode_hixie(buf)
      end

      #msg "Receive frame: #{frame.inspect}"

      traffic "}"

      bufs << frame['payload']

      if frame['left'] > 0
        buf = buf[-frame['left']...buf.length]
      else
        buf = ''
      end
    end

    return bufs, closed
  end


  def send_close(code=nil, reason='')
    t = Thread.current
    if t[:version].start_with?("hybi")
      msg = ''
      if code
        msg = [reason.length, code].pack("na8")
      end

      buf, lenh, lent = encode_hybi(msg, opcode=0x08, base64=false)
      t[:client].write(buf)
    elsif t[:version] == "hixie-76"
      buf = "\xff\x00"
      t[:client].write(buf)
    end
  end

  def do_handshake(sock)
  
    t = Thread.current
    stype = ""

    if !IO.select([sock], nil, nil, 3)
      raise EClose, "ignoring socket not ready"
    end

    handshake = ""
    msg "About to read from sock [#{sock.inspect}]"
    handshake = sock.read_nonblock(1024)
    msg "Handshake [#{handshake.inspect}]"

    if handshake == nil or handshake == ""
      raise(EClose, "ignoring empty handshake")
    else
      stype = "Plain non-SSL (ws://)"
      scheme = "ws"
      if sock.class == OpenSSL::SSL::SSLSocket
        stype = "SSL (wss://)"
        scheme = "wss"
      end
      retsock = sock
    end

    h = t[:headers] = {}
    hlines = handshake.split("\r\n")
    req_split = hlines.shift.match(/^(\w+) (\/[^\s]*) HTTP\/1\.1$/) 
    t[:path] = req_split[2].strip
    hlines.each do |hline|
      break if hline == ""
      hsplit = hline.match(/^([^:]+):\s*(.+)$/)
      h[hsplit[1].strip.downcase] = hsplit[2]
    end
    puts "Headers: #{h.inspect}"

    unless h.has_key?('upgrade') &&
       h['upgrade'].downcase == 'websocket'
      raise EClose, "Non-WebSocket connection"
    end

    protocols = h.fetch("sec-websocket-protocol", h["websocket-protocol"])
    ver = h.fetch('sec-websocket-version', nil)

    if ver
      # HyBi/IETF vesrion of the protocol

      # HyBi 07 reports version 7
      # HyBi 08 - 12 report version 8
      # HyBi 13 and up report version 13
      if ['7', '8', '13'].include?(ver)
        t[:version] = "hybi-%02d" % [ver.to_i]
      else
        raise EClose, "Unsupported protocol version %s" % [ver]
      end

      # choose binary if client supports it
      if protocols.include?('binary')
        t[:base64] = false
      elsif protocols.include?('base64')
        t[:base64] = true
      else
        raise EClose, "Client must support 'binary' or 'base64' sub-protocol"
      end

      key = h['sec-websocket-key']

      # Generate the hash value for the accpet header
      accept = Base64.encode64(
        Digest::SHA1.digest(key + @@GUID)).gsub(/\n/, '')

      response = @@Server_handshake_hybi % [accept]

      if t[:base64]
        response += "Sec-WebSocket-Protocol: base64\r\n"
      else
        response += "Sec-WebSocket-Protocol: binary\r\n"
      end
      response += "\r\n"

    else
      # Hixie vesrion of the protocol (75 or 76)
      body = handshake.match(/\r\n\r\n(........)/)
      if body
        h['key3'] = body[1]
        trailer = gen_md5(h)
        pre = "Sec-"
        t[:version] = "hixie-76"
      else
        trailer = ""
        pre = ""
        t[:version] = "hixie-75"
      end
      
      # base64 required for Hixie since payload is only UTF-8
      t[:base64] = true

      response = @@Server_handshake_hixie % [pre, h['origin'], pre,
        "ws", h['host'], t[:path]]

      if protocols && protocols.include?('base64')
        response += "%sWebSocket-Protocol: base64\r\n" % [pre]
      else
        msg "Warning: client does not report 'base64' protocol support"
      end

      response += "\r\n" + trailer
    end

    msg "%s WebSocket connection" % [stype]
    msg "Version %s, base64: '%s'" % [t[:version], t[:base64]]
    if t[:path] then msg "Path: '%s'" % [t[:path]] end

    #puts "sending reponse #{response.inspect}"
    retsock.write(response)

    # Return the WebSocket socket which may be SSL wrapped
    return retsock
  end

end

# vim: sw=2
