---
-- Implementation of the XDMCP (X Display Manager Control Protocol) based on:
--   x http://www.xfree86.org/current/xdmcp.pdf
--
-- @author Patrik Karlsson <patrik@cqure.net>

local bin = require "bin"
local ipOps = require "ipOps"
local nmap = require "nmap"
local stdnse = require "stdnse"
local table = require "table"
_ENV = stdnse.module("xdmcp", stdnse.seeall)

-- Supported operations
OpCode = {
  BCAST_QUERY = 1,
  QUERY = 2,
  WILLING = 5,
  REQUEST = 7,
  ACCEPT = 8,
  MANAGE = 10,
}

-- Packet class
Packet = {

  -- The cdmcp header
  Header = {

    -- Creates a new instance of class
    -- @param version number containing the protocol version
    -- @param opcode number containing the opcode type
    -- @param length number containing the length of the data
    -- @return o instance of class
    new = function(self, version, opcode, length)
      local o = { version = version, opcode = opcode, length = length }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Parses data based on which a new object is instantiated
    -- @param data opaque string containing data received over the wire
    -- @return hdr instance of class
    parse = function(data)
      local pos, hdr = nil, Packet.Header:new()
      pos, hdr.version, hdr.opcode, hdr.length = bin.unpack(">SSS", data)
      return hdr
    end,

    -- Converts the instance to an opaque string
    -- @return str string containing the instance
    __tostring = function(self)
      assert(self.length, "No header length was supplied")
      return bin.pack(">SSS", self.version, self.opcode, self.length)
    end,
  },

  [OpCode.QUERY] = {

    -- Creates a new instance of class
    -- @param authnames table of strings containing authentication
    --        mechanism names.
    -- @return o instance of class
    new = function(self, authnames)
      local o = {
        header = Packet.Header:new(1, OpCode.QUERY),
        authnames = authnames or {},
      }
      o.header.length = #o.authnames + 1
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Converts the instance to an opaque string
    -- @return str string containing the instance
    __tostring = function(self)
      local data = tostring(self.header)
      data = data .. bin.pack("C", #self.authnames)
      for _, name in ipairs(self.authnames) do
        data = data .. bin.pack(">P", name)
      end
      return data
    end,

  },

  [OpCode.BCAST_QUERY] = {
    new = function(...)
      local packet = Packet[OpCode.QUERY]:new(...)
      packet.header.opcode = OpCode.BCAST_QUERY
      return packet
    end,

    __tostring = function(...)
      return Packet[OpCode.QUERY]:__tostring(...)
    end

  },

  [OpCode.WILLING] = {

    -- Creates a new instance of class
    -- @return o instance of class
    new = function(self)
      local o = {
        header = Packet.Header:new(1, OpCode.WILLING)
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Parses data based on which a new object is instantiated
    -- @param data opaque string containing data received over the wire
    -- @return hdr instance of class
    parse = function(data)
      local willing = Packet[OpCode.WILLING]:new()
      willing.header = Packet.Header.parse(data)

      local pos = 7
      pos, willing.authname, willing.hostname,
      willing.status = bin.unpack("ppp", data, pos)
      return willing
    end,

  },

  [OpCode.REQUEST] = {

    -- The connection class
    Connection = {

      IpType = {
        IPv4 = 0,
        IPv6 = 6,
      },

      -- Creates a new instance of class
      -- @param iptype number
      -- @param ip opaque string containing the ip
      -- @return o instance of class
      new = function(self, iptype, ip)
        local o = {
          iptype = iptype,
          ip = ip,
        }
        setmetatable(o, self)
        self.__index = self
        return o
      end,

    },

    -- Creates a new instance of class
    -- @param disp_no number containing the display name
    -- @param auth_name string containing the authentication name
    -- @param auth_data string containing additional authentication data
    -- @param authr_names string containing authorization mechanisms
    -- @param manf_id string containing the manufacturer id
    -- @return o instance of class
    new = function(self, disp_no, conns, auth_name, auth_data, authr_names, manf_id )
      local o = {
        header = Packet.Header:new(1, OpCode.REQUEST),
        disp_no = disp_no or 1,
        conns = conns or {},
        auth_name = auth_name or "",
        auth_data = auth_data or "",
        authr_names = authr_names or {},
        manf_id = manf_id or "",
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Adds a new connection entry
    -- @param conn instance of Connections
    addConnection = function(self, conn)
      table.insert(self.conns, conn)
    end,

    -- Adds a new authorization entry
    -- @param str string containing the name of the authorization mechanism
    addAuthrName = function(self, str)
      table.insert(self.authr_names, str)
    end,

    -- Converts the instance to an opaque string
    -- @return str string containing the instance
    __tostring = function(self)
      local data = bin.pack(">SC", self.disp_no, #self.conns)
      for _, conn in ipairs(self.conns) do
        data = data .. bin.pack(">S", conn.iptype)
      end
      data = data .. bin.pack("C", #self.conns)
      for _, conn in ipairs(self.conns) do
        data = data .. bin.pack(">P", ipOps.ip_to_str(conn.ip))
      end
      data = data .. bin.pack(">PP", self.auth_name, self.auth_data)
      data = data .. bin.pack("C", #self.authr_names)
      for _, authr in ipairs(self.authr_names) do
        data = data .. bin.pack(">P", authr)
      end
      data = data .. bin.pack(">P", self.manf_id)
      self.header.length = #data

      return tostring(self.header) .. data
    end,

  },

  [OpCode.ACCEPT] = {

    -- Creates a new instance of class
    -- @param session_id number containing the session id
    -- @param auth_name string containing the authentication name
    -- @param auth_data string containing additional authentication data
    -- @param authr_name string containing the authorization mechanism name
    -- @param authr_names string containing authorization mechanisms
    -- @return o instance of class
    new = function(self, session_id, auth_name, auth_data, authr_name, authr_data)
      local o = {
        header = Packet.Header:new(1, OpCode.ACCEPT),
        session_id = session_id,
        auth_name = auth_name,
        auth_data = auth_data,
        authr_name = authr_name,
        authr_data = authr_data,
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Parses data based on which a new object is instantiated
    -- @param data opaque string containing data received over the wire
    -- @return hdr instance of class
    parse = function(data)
      local accept = Packet[OpCode.ACCEPT]:new()
      accept.header = Packet.Header.parse(data)
      local pos = 7
      pos, accept.session_id, accept.auth_name, accept.auth_data,
      accept.authr_name, accept.authr_data = bin.unpack(">IPPPP", data, pos)
      return accept
    end,

  },

  [OpCode.MANAGE] = {

    -- Creates a new instance of class
    -- @param session_id number containing the session id
    -- @param disp_no number containing the display number
    -- @param disp_class string containing the display class
    -- @return o instance of class
    new = function(self, sess_id, disp_no, disp_class)
      local o = {
        header = Packet.Header:new(1, OpCode.MANAGE),
        session_id = sess_id,
        disp_no = disp_no,
        disp_class = disp_class or ""
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Converts the instance to an opaque string
    -- @return str string containing the instance
    __tostring = function(self)
      local data = bin.pack(">ISP", self.session_id, self.disp_no, self.disp_class)
      self.header.length = #data
      return tostring(self.header) .. data
    end,

  }

}

-- The Helper class serves as the main script interface
Helper = {

  -- Creates a new instance of Helper
  -- @param host table as received by the action method
  -- @param port table as received by the action method
  -- @param options table
  -- @return o new instance of Helper
  new = function(self, host, port, options)
    local o = {
      host = host,
      port = port,
      options = options or {},
    }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  -- "Connects" to the server (ie. creates the socket)
  -- @return status, true on success, false on failure
  connect = function(self)
    self.socket = nmap.new_socket("udp")
    self.socket:set_timeout(self.options.timeout or 10000)
    return true
  end,

  -- Creates a xdmcp session
  -- @param auth_name string containing the authentication name
  -- @param authr_name string containing the authorization mechanism name
  -- @param disp_class string containing the display class
  -- @return status true on success, false on failure
  -- @return response table or err string containing an error message
  createSession = function(self, auth_names, authr_names, disp_no)
    local info  = nmap.get_interface_info(self.host.interface)
    if ( not(info) ) then
      return false, ("Failed to get information for interface %s"):format(self.host.interface)
    end

    local req = Packet[OpCode.QUERY]:new(auth_names)
    local status, response = self:exch(req)
    if ( not(status) ) then
      return false, response
    elseif ( response.header.opcode ~= OpCode.WILLING ) then
      return false, "Received unexpected response"
    end

    local REQ = Packet[OpCode.REQUEST]
    local iptype = REQ.Connection.IpType.IPv4
    if ( nmap.address_family() == 'inet6' ) then
      iptype = REQ.Connection.IpType.IPv6
    end

    local conns = { REQ.Connection:new(iptype, info.address) }
    local req = REQ:new(disp_no, conns, nil, nil, authr_names)
    local status, response = self:exch(req)
    if ( not(status) ) then
      return false, response
    elseif ( response.header.opcode ~= OpCode.ACCEPT ) then
      return false, "Received unexpected response"
    end

    -- Sending this last manage packet doesn't make any sense as we can't
    -- set up a listening TCP server anyway. When we can, we could enable
    -- this and wait for the incoming request and retrieve X protocol info.

    -- local manage = Packet[OpCode.MANAGE]:new(response.session_id,
    --   disp_no, "MIT-unspecified")
    -- local status, response = self:exch(manage)
    -- if ( not(status) ) then
    --   return false, response
    -- end

    return true, {
      session_id = response.session_id,
      auth_name = response.auth_name,
      auth_data = response.auth_data,
      authr_name = response.authr_name,
      authr_data = response.authr_data,
    }
  end,

  send = function(self, req)
    return self.socket:sendto(self.host, self.port, tostring(req))
  end,

  recv = function(self)
    local status, data = self.socket:receive()
    if ( not(status) ) then
      return false, data
    end
    local header = Packet.Header.parse(data)
    if ( not(header) ) then
      return false, "Failed to parse xdmcp header"
    end
    if ( not(Packet[header.opcode]) ) then
      return false, ("No parser for opcode: %d"):format(header.opcode)
    end
    local resp = Packet[header.opcode].parse(data)
    if ( not(resp) ) then
      return false, "Failed to parse response"
    end
    return true, resp
  end,

  -- Sends a request to the server, receives and parses a response
  -- @param req instance of Packet
  -- @return status true on success, false on failure
  -- @return response instance of response packet
  exch = function(self, req)
    local status, err = self:send(req)
    if ( not(status) ) then
      return false, "Failed to send xdmcp request"
    end
    return self:recv()
  end,

}

return _ENV;
