IRE Muds - ATCP

Posted by Flelm on Mon 11 Sep 2006 11:18 AM — 33 posts, 116,414 views.

#0
Basically, I was wondering if its possible to get MUSHclient to hook into TELNET sub-option negotiation in order to pull, in my case, information from the various IRE MUDs.

Here is the document on ATCP: http://www.ironrealms.com/rapture/manual/files/FeatATCP-txt.html

I have no problem in doing the coding gruntwork, if someone can point me in the initial direction. I assume I'd need to (at least) put together a dll file.

Thanks in advance.
Russia #1
Last I checked, which was again just now, Mushclient resists you messing with options negotiation. For example, the following (IRE docs messed up the numerical codes btw):


function OnPluginPacketReceived(packet) 
  if string.find(packet, "\255\251\200") then
     Send("\255\253\200")
  end
end


Results in Mushclient sending 255 255 253 200 + newline, therefore escaping your attempt at an option reply. Then it proceeds to refuse that 200 option. So unless Nick adds a callback to specifically hook into this sequence it seems impossible.

#2
That would answer my question in the short term.

Immediately after posting that I had done a little more research and had figured out if I could only figure out how to send that packet I could filter out the rest of the incoming messages.

Anyone else know of a way to do this? Nick?
USA #3
Heh, I've been looking for that information for some time now, thanks for the link.
Russia #4
Erm, also worthy of mention is the fact that the last time I successfully negotiated ATCP, which was fairly long ago, there was a catch. Not sure if it still exists, but back then the server sent an authentication code after a couple of minutes, and after my failing to confirm it stopped sending any extra info.

But they might've given up on that since then, I think there was some custom-coded proxy that claimed to successfully use ATCP.
#5
I know of the proxy, and it does successfully use ATCP.
Australia Forum Administrator #6
At present MUSHclient assumes that when you do world.Send or any of its variants you are trying to send a command to the MUD, and that command will be terminated by a newline. If it didn't make that assumption everyone would have to do things like this:

Send ("north\n")

I suppose it would be reasonably easy to add a "SendPacket" function, which bypasses all the normal tests and plugin callbacks (eg. OnPluginSend and so on) and simply sends the packet.
USA #7
Go Nick :D
Australia Forum Administrator #8
OK, added SendPkt to version 3.81.
Russia #9
Would that callback work though? I am not an expert on how telnet option negotiation works, but wouldn't Mushclient sending a DON'T after a plugin sends a DO for an option present a problem?
Australia Forum Administrator #10
Well it actually responds WONT - I just tested with the packet debug.

However all you have to do is amend the incoming packet to get rid of the IAC DO ATCP from the packet, and then using SendPkt you reply IAC WILL ATCP and you should be fine.

So, I think this change is all that is needed to use the ATCP feature.
Australia Forum Administrator #11
Quote:

Results in Mushclient sending 255 255 253 200 + newline ...


I didn't notice initially your point that the IAC got doubled (as well as a newline added). The new SendPkt function fixes both of those problems, it does not double the IAC character, and does not append newlines.
Russia #12
Nevermind - found a way.
Amended on Sun 15 Oct 2006 06:57 AM by Ked
USA #13
Has anyone got a working version of this they could post?
Russia #14
This is what I have so far, still haven't decided what I want to do with the data though:


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<muclient>
<plugin
   name="ATCP"
   author="Keldar"
   id="a2a6350d9144c09a8a7c4636"
   language="Lua"
   purpose="Enabling the ATCP protocol"
   date_written="2006-10-15"
   requires="3.81"
   version="1.0"
   >
<description trim="y">
<![CDATA[
sdf
]]>
</description>

</plugin>


<!--  Get our standard constants -->

<include name="constants.lua"/>

<!--  Script  -->


<script>
<![CDATA[
require "tprint"

codes = {
IAC_WILL_ATCP = "\255\251\200",
IAC_WONT_ATCP = "\255\252\200",
IAC_DO_ATCP = "\255\253\200",
IAC_DONT_ATCP = "\255\254\200",
IAC_SB_ATCP = "\255\250\200",
IAC_SE = "\255\240"
}

leftovers = nil

function OnPluginPacketReceived(packet) 
  if string.find(packet, codes.IAC_WILL_ATCP) then
    SendPkt(codes.IAC_DO_ATCP)
    Note("TEST")
    return string.gsub(packet, "(.-)" .. codes.IAC_WILL_ATCP .. "(.-)", "%1%2")
  end
  local atcp = parseATCP(packet)
  --tprint(atcp)
  return packet
end


function parseATCP(packet)
  local packet = packet
  if leftovers then
    packet = leftovers .. packet
  end
  local atcp = {}
  for msg in string.gmatch(packet, codes.IAC_SB_ATCP .. ".-" .. codes.IAC_SE) do
    table.insert(atcp, msg)
  end
  leftovers = string.match(packet, "(\255\250?\200?[^" .. codes.IAC_SE .. "]-)$")
  return atcp
end

]]>
</script>


<!--  Plugin help  -->

<aliases>
  <alias
   script="OnHelp"
   match="ATCP:help"
   enabled="y"
  >
  </alias>
</aliases>

<script>
<![CDATA[
function OnHelp ()
  world.Note (world.GetPluginInfo (world.GetPluginID (), 3))
end
]]>
</script> 

</muclient>
Australia Forum Administrator #15
You can do the finding, replacing, and sending in a simpler way.


Rather than:


if string.find(packet, codes.IAC_WILL_ATCP) then
    SendPkt(codes.IAC_DO_ATCP)
    Note("TEST")
    return string.gsub(packet, "(.-)" .. codes.IAC_WILL_ATCP .. "(.-)", "%1%2")
  end


Try this:


function f (s)
  Note ("TEST")
  SendPkt(codes.IAC_DO_ATCP)
  return ""  
end -- f

packet = string.gsub (packet, codes.IAC_WILL_ATCP, f)


You can use string.gsub to call a function, and return a value, so this lets you send the packet, and replace the IAC_DO_ATCP with an empty string, all in one operation.

You can inline the function to save having to give it a name:


packet = string.gsub (packet, codes.IAC_WILL_ATCP, 
          function (s)
            SendPkt(codes.IAC_DO_ATCP)
            return ""  
          end -- function
       )

USA #16
Thanks! I've been testing this, and the only codes I'm getting are for room name and room exits (which already seems to me to be most useful for mapping). I'm very interested in retrieving information on current maximum health as well, and I'm pretty sure ATCP can provide this. Would I be correct in thinking that this must probably be enabled explicitly. If this is the case, is it possible to find out these codes.

I'm thinking about decompiling the IRE java client, hacking it to run through my own proxy server so I can see what packets it sends to the server. But, that's also waaay more work than I want to do...

-Tsunami
USA #17
Hmm, I also just got this:

==================================================================
You are using an unsupported version of the client software.
Please update by forcing a reload of the page and reconnect.
==================================================================

I looked through the packet debug, and offhand, can't find any stray packets that could have asked for a version verification or anything.
Russia #18
Yep, this is still behaving as it used to - not very well. I wonder how that proxy mentioned earlier does it, and whether it gets anything beyond room name and exits.

I've looked through an Ethereal dump of Nexus' interactions with the server and here's the general outline:

1. Upon connection the client sends: IAC DO ATCP + IAC SB ATCP + a list of options + IAC SE

2. The server responds with IAC WILL EOR + IAC WILL ATCP + IAC WILL ?(hex 56)

3. The client sends: IAC SB ATCP + "login name password" + IAC SE + IAC SB ATCP + "file get nexus-settings Settings" + IAC SE + IAC SB ATCP + "file get nexus-reflexes Reflexes" + IAC SE

4. Server responds: IAC SB ATCP + "Auth.Request CH.garbagestring" + IAC Se

5. Client sends: IAC DO EOR + IAC DO ATCP

6. Server sends the initial login screen and starts sending ATCP data

7. Client sends: IAC SB ATCP + "auth -715 Nexus 3.0.9" + IAC SE

So there's clearly an authentication procedure involved, although I haven't yet tried simply reproducing the exact things that Nexus sends to the server. So far, I've done it the responsible way: identifying the client as "Muclient 3.81", sending "1" only for those options that I might make use of, etc. The result was an absence of any auth requests and any data beyond room name and exits. In other words, this will require more experimentation.

Russia #19
An update - it appears that I misread the logs and that was the cause of all the problems.

So far, I've managed to get Char.Vitals, Room.Brief, and Room.Exits messages to work consistently without any authentication or pretending to be the Nexus client. I'll post the plugin after a bit more testing.
USA #20
If you ever did finish this, could you post? Thanks.
Russia #21
I suppose I could post what I have so far, though I've found an authentication routine in that proxy from Imperian that I want to try out,
and there's still a weird issue with parsing - it sometimes screws up in some unknown to me way,
resulting in prompt triggers not matching. There's also the problem with the "unsupported client" message,
but I am hoping that solving the authentication puzzle will remove that problem also.


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<muclient>
<plugin
   name="ATCP"
   author="Keldar"
   id="a2a6350d9144c09a8a7c4636"
   language="Lua"
   purpose="Enabling the ATCP protocol"
   date_written="2006-10-15"
   requires="3.81"
   version="1.0"
   >
<description trim="y">
<![CDATA[
sdf
]]>
</description>

</plugin>

<variables>
<variable name="NexusVer">3.0.0</variable>
</variables>


<!--  Get our standard constants -->

<include name="constants.lua"/>

<!--  Script  -->


<script>
<![CDATA[
require "tprint"

client_id =  "Muclient 3.8.1"--"Nexus 3.0.9" --

nexus_opts = {
{"hello", client_id},
--{"auth", "1"},
--{"composer", "1"},
--{"keepalive", "1"},
--{"char_name", "1"},
--{"filestore", "1"},
--{"topvote", "1"},
{"char_vitals", "1"},
{"room_brief", "1"},
{"room_exits", "1"},
--{"mediapak", "1"},
--{"wiz", "1"}
}


codes = {
IAC_WILL_ATCP = "\255\251\200",
IAC_WONT_ATCP = "\255\252\200",
IAC_DO_ATCP = "\255\253\200",
IAC_DONT_ATCP = "\255\254\200",
IAC_SB_ATCP = "\255\250\200",
IAC_SE = "\255\240",
IAC_DO_EOR = "\255\253\025",
IAC_WILL_EOR = "\255\251\025",
IAC_GA = "\255\249"
}

leftovers = nil

PAT = "^(.-)" .. codes.IAC_SB_ATCP .. "(.-)" .. codes.IAC_SE .. "(.-)$"
UNSUP_MSG = "\r\n\r\n==================================================================\r\n   You are using an unsupported version of the client software.\r\n\r\n   Please update by forcing a reload of the page and reconnect.\r\n==================================================================\r\n\r\n"

function SendATCP(msg)
  SendPkt(codes.IAC_SB_ATCP .. msg .. codes.IAC_SE)
end

function OnPluginConnect()
  local msg = ""
  for _,v in ipairs(nexus_opts) do
    msg = msg .. v[1] .. " " .. v[2] .. "\10"
  end 
  msg = string.sub(msg, 1, -2)
  print(msg)
  SendPkt(codes.IAC_DO_ATCP .. codes.IAC_SB_ATCP .. msg .. codes.IAC_SE)
end

function OnPluginPacketReceived(packet)
  if string.find(packet, codes.IAC_WILL_ATCP) then
    Note("IAC WILL ATCP")
    packet = string.gsub(packet, "(.-)" .. codes.IAC_WILL_ATCP .. "(.-)", "%1%2")
  end
  local s,e = string.find(packet, UNSUP_MSG, 1, true)
  if s then
    Note("Unsupported client message gagged.")
    return string.sub(packet, e+1)
  end
  if string.find(packet, codes.IAC_WILL_EOR) then
    Note("IAC WILL EOR")
    packet = string.gsub(packet, "(.-)" .. codes.IAC_WILL_EOR .. "(.-)", "%1%2")    
  end
  if string.find(packet, "Enter an option or enter your character's name.") then
    Note("Login:")
    
    -- If auth option is specified then this is where we send the name and password
    --
  end
  local packet, atcp, parsed = parseATCP(packet)
  if atcp[1] and string.find(atcp[1], "^Auth.Request CH") then
    SendPkt(codes.IAC_DO_EOR .. codes.IAC_DO_ATCP)
  end
  --tprint(atcp)
  --print(parsed["Char.Vitals"])
  return packet
end


function parseATCP(packet)
  local packet = packet
  local s,e,prev,msg,nxt, asplit
  local atcp,parsed = {}, {}
  if leftovers then
    s,e,msg = string.find(leftovers .. packet, "^" .. codes.IAC_SB_ATCP .. "(.-)" .. codes.IAC_SE)
    if s then
      asplit = utils.split(msg, "\10")
      parsed[asplit[1] ] = asplit[2]
      table.insert(atcp, msg)
      packet = string.sub(leftovers .. packet, e+1)
    end
    leftovers = nil
  end
  
  local new_packet = ""
  local lastend, lastnxt
  local pat = PAT
  s,e,prev,msg,nxt = string.find(packet, pat)
  while s do
    new_packet = new_packet .. prev
    lastend = e
    lastnxt = nxt
    asplit = utils.split(msg, "\10")
    parsed[asplit[1] ] = asplit[2]
    table.insert(atcp, msg)

    s,e,prev,msg,nxt = string.find(lastnxt, pat)
  end
  
  if not lastnxt then
    lastnxt = packet
  end
  s,e,prev,msg = string.find(lastnxt, "^(.-)(\255\250?\200?.*)")
  if s then 
    leftovers = msg
    new_packet = new_packet .. prev
  else
    new_packet = new_packet .. lastnxt
  end
  
  if #new_packet > 0 then
    packet = new_packet
  end
  
  return packet, atcp, parsed
end



]]>
</script>


<!--  Plugin help  -->

<aliases>
  <alias
   script="OnHelp"
   match="ATCP:help"
   enabled="y"
  >
  </alias>
</aliases>

<script>
<![CDATA[
function OnHelp ()
  world.Note (world.GetPluginInfo (world.GetPluginID (), 3))
end
]]>
</script> 

</muclient>
Amended on Sun 22 Oct 2006 06:23 AM by Ked
Russia #22
This should work better, I reworked the parsing to be less confusing:


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<muclient>
<plugin
   name="ATCP"
   author="Keldar"
   id="a2a6350d9144c09a8a7c4636"
   language="Lua"
   purpose="Enabling the ATCP protocol"
   date_written="2006-10-15"
   requires="3.81"
   version="1.0"
   >
<description trim="y">
<![CDATA[
sdf
]]>
</description>

</plugin>

<variables>
<variable name="NexusVer">3.0.0</variable>
</variables>


<!--  Get our standard constants -->

<include name="constants.lua"/>

<!--  Script  -->


<script>
<![CDATA[
require "tprint"

client_id =  "Muclient 3.8.1"--"Nexus 3.0.9" --

nexus_opts = {
{"hello", client_id},
--{"auth", "1"},
--{"composer", "1"},
--{"keepalive", "1"},
--{"char_name", "1"},
--{"filestore", "1"},
--{"topvote", "1"},
{"char_vitals", "1"},
{"room_brief", "1"},
{"room_exits", "1"},
--{"mediapak", "1"},
--{"wiz", "1"}
}


codes = {
IAC_WILL_ATCP = "\255\251\200",
IAC_WONT_ATCP = "\255\252\200",
IAC_DO_ATCP = "\255\253\200",
IAC_DONT_ATCP = "\255\254\200",
IAC_SB_ATCP = "\255\250\200",
IAC_SE = "\255\240",
IAC_DO_EOR = "\255\253\025",
IAC_WILL_EOR = "\255\251\025",
IAC_GA = "\255\249"
}

leftovers = ""

PAT = "^(.-)" .. codes.IAC_SB_ATCP .. "(.-)" .. codes.IAC_SE .. "(.-)$"
UNSUP_MSG = "\r\n\r\n==================================================================\r\n   You are using an unsupported version of the client software.\r\n\r\n   Please update by forcing a reload of the page and reconnect.\r\n==================================================================\r\n\r\n"

function SendATCP(msg)
  SendPkt(codes.IAC_SB_ATCP .. msg .. codes.IAC_SE)
end

function OnPluginConnect()
  local msg = ""
  for _,v in ipairs(nexus_opts) do
    msg = msg .. v[1] .. " " .. v[2] .. "\10"
  end 
  msg = string.sub(msg, 1, -2)
  print(msg)
  SendPkt(codes.IAC_DO_ATCP .. codes.IAC_SB_ATCP .. msg .. codes.IAC_SE)
end

function OnPluginPacketReceived(packet)
  if string.find(packet, codes.IAC_WILL_ATCP) then
    Note("IAC WILL ATCP")
    packet = string.gsub(packet, "(.-)" .. codes.IAC_WILL_ATCP .. "(.-)", "%1%2")
  end
  local s,e = string.find(packet, UNSUP_MSG, 1, true)
  if s then
    Note("Unsupported client message gagged.")
    return string.sub(packet, e+1)
  end
  if string.find(packet, codes.IAC_WILL_EOR) then
    Note("IAC WILL EOR")
    packet = string.gsub(packet, "(.-)" .. codes.IAC_WILL_EOR .. "(.-)", "%1%2")    
  end
  if string.find(packet, "Enter an option or enter your character's name.") then
    Note("Login:")
    
    -- If auth option is specified then this is where we send the name and password
    --
  end
  local packet, atcp, parsed = parseATCP(packet)
  if atcp[1] and string.find(atcp[1], "^Auth.Request CH") then
    SendPkt(codes.IAC_DO_EOR .. codes.IAC_DO_ATCP)
  end
  --tprint(atcp)
  --print(parsed["Char.Vitals"])
  return packet
end


function parseATCP(packet)
  local packet = packet
  local s,e,prev,msg,nxt, asplit
  local atcp,parsed = {}, {}
  if #leftovers > 0 then
    s,e,msg = string.find(leftovers .. packet, "^" .. codes.IAC_SB_ATCP .. "(.-)" .. codes.IAC_SE)
    if s then
      asplit = utils.split(msg, "\10")
      parsed[asplit[1] ] = asplit[2]
      table.insert(atcp, msg)
      packet = string.sub(leftovers .. packet, e+1)
    end
    leftovers = ""
  end
  
  local new_packet = ""
  local pat2 = codes.IAC_SB_ATCP .. "(.-)" .. codes.IAC_SE
  new_packet = string.gsub(packet, pat2, function(msg)
    asplit = utils.split(msg, "\10")
    parsed[asplit[1] ] = asplit[2]
    table.insert(atcp, msg)
    return ""
  end)
  
  s,e,msg = string.find(new_packet, "(\255\250?\200?.*)")
  if s and string.find(string.sub(msg, 2, 2), "[^\251\252\253\254\240]") then
      leftovers = msg
      new_packet = string.sub(new_packet, 1, s-1)
  end
  
  if #new_packet > 0 then
    packet = new_packet
  end
  
  return packet, atcp, parsed
end



]]>
</script>


<!--  Plugin help  -->

<aliases>
  <alias
   script="OnHelp"
   match="ATCP:help"
   enabled="y"
  >
  </alias>
</aliases>

<script>
<![CDATA[
function OnHelp ()
  world.Note (world.GetPluginInfo (world.GetPluginID (), 3))
end
]]>
</script> 

</muclient>



[EDIT] More bugs.
Amended on Sun 22 Oct 2006 09:01 AM by Ked
Russia #23
Ok, here's another version. This one authenticates the connection using the method found in Whyte's Imperian Modular Trigger System proxy (http://sf.net/projects/imts).
The authentication works, however there's a quirk: Rapture will not send the authentication request if the client sends anything at all prior to enabling ATCP
(IAC DO ATCP + options).

What this means is that if you use auto-login or have any plugins that send commands immediately upon connection then
authentication will fail, due to it never getting requested. To solve this problem, this plugin suppresses and
buffers any commands until it has had a chance to negotiate ATCP, once that is done - it resends all buffered commands.

Still nothing useful is done with incoming ATCP data, but at least the general
system seems to mostly work now.


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<muclient>
<plugin
   name="ATCP"
   author="Keldar"
   id="a2a6350d9144c09a8a7c4636"
   language="Lua"
   purpose="Enabling the ATCP protocol"
   date_written="2006-10-15"
   requires="3.81"
   version="1.1"
   >
<description trim="y">
<![CDATA[
sdf
]]>
</description>

</plugin>

<triggers>
  <trigger
   enabled="y"
   lines_to_match="7"
   match="\n\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\n   You are using an unsupported version of the client software\.\n\n   Please update by forcing a reload of the page and reconnect\.\n\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\=\n\Z"
   multi_line="y"
   name="unsup_message"
   regexp="y"
   script="GagUnsup"
   sequence="100"
  >
  </trigger>
</triggers>



<!--  Get our standard constants -->

<include name="constants.lua"/>

<!--  Script  -->


<script>
<![CDATA[
require "tprint"

client_id =  "Muclient 3.8.1"--"Nexus 3.0.9" --

nexus_opts = {
{"hello", client_id},
{"auth", "1"},
--{"composer", "1"},
--{"keepalive", "1"},
{"char_name", "1"},
--{"filestore", "1"},
--{"topvote", "1"},
{"char_vitals", "1"},
{"room_brief", "1"},
{"room_exits", "1"},
--{"mediapak", "1"},
--{"wiz", "1"}
}


codes = {
IAC_WILL_ATCP = "\255\251\200",
IAC_WONT_ATCP = "\255\252\200",
IAC_DO_ATCP = "\255\253\200",
IAC_DONT_ATCP = "\255\254\200",
IAC_SB_ATCP = "\255\250\200",
IAC_SE = "\255\240",
IAC_DO_EOR = "\255\253\025",
IAC_WILL_EOR = "\255\251\025",
IAC_GA = "\255\249"
}

leftovers = ""

PAT = codes.IAC_SB_ATCP .. "(.-)" .. codes.IAC_SE

function SendATCP(msg)
  SendPkt(codes.IAC_SB_ATCP .. msg .. codes.IAC_SE)
end

connected = false
send_buffer = {}
function OnPluginSend(data)
  if connected then
    return true
  else
    table.insert(send_buffer, data)
    return false
  end
    
end

function OnPluginConnect()
  local msg = ""
  for _,v in ipairs(nexus_opts) do
    msg = msg .. v[1] .. " " .. v[2] .. "\10"
  end 
  msg = string.sub(msg, 1, -2)
  print(msg)
  SendPkt(codes.IAC_DO_ATCP .. codes.IAC_SB_ATCP .. msg .. codes.IAC_SE)
  connected = true
  for _,comm in ipairs(send_buffer) do
    Send(comm)
  end
  send_buffer = {}
end

function OnPluginPacketReceived(packet)
  if string.find(packet, codes.IAC_WILL_ATCP) then
    Note("IAC WILL ATCP")
    packet = string.gsub(packet, "(.-)" .. codes.IAC_WILL_ATCP .. "(.-)", "%1%2")
  end
  if string.find(packet, codes.IAC_WILL_EOR) then
    Note("IAC WILL EOR")
    packet = string.gsub(packet, "(.-)" .. codes.IAC_WILL_EOR .. "(.-)", "%1%2")    
  end
  local packet, atcp, parsed = parseATCP(packet)
  if parsed["Auth.Request CH"] then
    SendATCP("auth " .. tostring(atcp_auth(parsed["Auth.Request CH"])) .. " " .. client_id)
  end
  if parsed["Auth.Request ON"] then
    Note("Authorization accepted")
  end
  --tprint(atcp)
  --print(parsed["Char.Vitals"])
  return packet
end


function parseATCP(packet)
  local packet = packet
  local s,e,prev,msg,nxt, asplit
  local atcp,parsed = {}, {}
  if #leftovers > 0 then
    s,e,msg = string.find(leftovers .. packet, "^" .. codes.IAC_SB_ATCP .. "(.-)" .. codes.IAC_SE)
    if s then
      asplit = utils.split(msg, "\10")
      parsed[asplit[1] ] = asplit[2]
      table.insert(atcp, msg)
      packet = string.sub(leftovers .. packet, e+1)
    end
    leftovers = ""
  end
  
  local new_packet = ""
  local pat2 = PAT
  new_packet = string.gsub(packet, pat2, function(msg)
    asplit = utils.split(msg, "\10")
    parsed[asplit[1] ] = asplit[2] or ""
    table.insert(atcp, msg)
    return ""
  end)
  
  s,e,msg = string.find(new_packet, "(\255\250?\200?.*)")
  if s and string.find(string.sub(msg, 2, 2), "[^\251\252\253\254\240]") then
      leftovers = msg
      new_packet = string.sub(new_packet, 1, s-1)
  end
  
  packet = new_packet
 
  
  return packet, atcp, parsed
end

function GagUnsup(n,o,w)
  DeleteLines(6)
end


function atcp_auth(seed)
  local a,i = 17, 0
  local n
  
  for letter in string.gmatch(seed, ".") do
    n = string.byte(letter) - 96
    
    if math.fmod(i, 2) == 0 then
      a = a + n * (bit.bor(i, 13))
    else
      a = a - n * ( bit.bor(i, 11))
    end
    
    i = i + 1
  end
  
  return a
end

]]>
</script>


<!--  Plugin help  -->

<aliases>
  <alias
   script="OnHelp"
   match="ATCP:help"
   enabled="y"
  >
  </alias>
</aliases>

<script>
<![CDATA[
function OnHelp ()
  world.Note (world.GetPluginInfo (world.GetPluginID (), 3))
end
]]>
</script> 

</muclient>

Amended on Sun 22 Oct 2006 10:53 AM by Ked
Russia #24
And to summarize: the (hopefully) final version of this plugin is up at my site: http://www.freewebs.com/keldar/atcp.htm
USA #25
I recently tackled ATCP again, using Ked's plugin as a basis for creating my own plugin. However, while Keldar's works as it should for the most part, mine only works for the initial login. After looking at the packets sent to the server, it becomes obvious what's happening, but not why.

Here are the packets Ked's (modified) plugin sends/recieves:


Sent: IAC DO ATCP + IAC SB ATCP + options + IAC SE
Sent: IAC DO ATCP + IAC SB ATCP + login info + IAC SE
Received: IAC WILL EOR + IAC WILL ATCP + IAC WILL COMPRESS2

**
Sent: IAC DO COMPRESS2
**

Received: IAC SB ATCP + "Auth.Request CH.auth" + IAC SE
Sent: IAC SB ATCP + "auth num client" + IAC SE
Received: IAC WILL ATCP
Received: IAC SB ATCP + atcpinfo + IAC SE

**
Received: IAC SB COMPRESS2 + IAC SE
**


And here's what my version sends/recieves:


Sent: IAC DO ATCP + IAC SB ATCP + options + IAC SE
Sent: IAC DO ATCP + IAC SB ATCP + login info + IAC SE
Received: IAC WILL EOR + IAC WILL ATCP + IAC WILL COMPRESS2
Received: IAC SB ATCP + "Auth.Request CH.auth" + IAC SE
Sent: IAC SB ATCP + "auth num client" + IAC SE
Received: IAC WILL ATCP
Received: IAC SB ATCP + atcpinfo + IAC SE

**
Sent: IAC DONT ATCP
**


For comparison, here is what the Nexus java client sends/receives:


Sent: IAC DO ATCP + IAC SB ATCP + options + IAC SE
Sent: IAC SB ATCP + login info + IAC SE
Sent: IAC SB ATCP + nexus settings + IAC SE
Received: IAC WILL EOR + IAC WILL ATCP + IAC WILL COMPRESS2
Received: IAC SB ATCP + "Auth.Request CH.auth" + IAC SE
Sent: IAC SB ATCP + "auth num client" + IAC SE
Sent: IAC SB ATCP + java information + IAC SE
Received: IAC SB ATCP + "Auth.Request ON" + IAC SE
Received: IAC SB ATCP + "Client.Ping" + IAC SE
Sent: IAC SB ATCP + "ping info" + IAC SE
Received: IAC SB ATCP + atcpinfo + IAC SE


After my version sends the IAC DONT ATCP request, I assume the server complies, since no more ATCP messages are forthcoming. I'm assuming that hex 56 (decimal 86) is the COMPRESS2 option, which is for MCCPv2, correct?

So why does MUSHclient never reply to the MCCP negotiation when my plugin is in use, and, more importantly, where is the IAC DONT ATCP in my plugin coming from? The code in my plugin cannot physically send an IAC DONT ATCP as far as I can see, meaning that it comes from MUSHclient? And why for mine then, but not Ked's? In the full packet debug, the starred packets are the only bytes in the entire log that differ between when my plugin was used and Ked's was used.

Anything y'all can figure out would be much appreciated.
Australia Forum Administrator #26
There is no code in MUSHclient that sends DONT ATCP, so I have to assume there is a coding error in your plugin. Without seeing it, it is hard to say what.

If you are using Lua, you could make a debugging version of SendPkt, that will show if you are doing it in your plugin. Here is an example:


do
  local Old_SendPkt = SendPkt  -- save original
  function SendPkt (what)  -- make new version
    print ("Sending Packet: " .. utils.tohex (what))
    local t = debug.getinfo (2, "ln")
    print ("Called from", t.name, "at line", t.currentline)
    Old_SendPkt (what)  -- call original
  end -- revised SendPkt
end -- local block


You could put that in the plugin initialization. That will show exactly what you are sending, in hex. Then you could add an extra argument to the new SendPkt (which the original would ignore) which tells you where you are calling it from.

[EDIT]

Edited to add debug.getinfo - that shows the function name, and line number in that function, that SendPkt is called from.

For example:


Sending Packet: 626C61680A
Called from mytest at line 16

Amended on Mon 07 May 2007 01:53 AM by Nick Gammon
USA #27
Tested again, with those modifications, and once again, its not my plugin that's sending it. My plugin shows up as sending 3 packets, none of which contain IAC DONT ATCP. The IAC DONT ATCP still shows up on the MUSHclient debug, but is not printed as coming from my plugin. I went through all instances of SendPkt in my plugin, all pad the information so that it would have to be longer. The packet with IAC DONT ATCP is just:

Sent  packet: 4 (3 bytes)
...                ff fe c8


There are no other plugins installed, MC version 3.84, XPsp2, etc.

It's not possible that MC receives the IAC WILL ATCP, doesn't recognize what ATCP is supposed to be and sends IAC DONT ATCP in return?

Thanks!
Australia Forum Administrator #28
Well, it was a nice debug attempt. :)

You are right, in response to any IAC WILL x, if MUSHclient doesn't recognise x it responds IAC DONT x.

In addition, in response to any IAC DO x, if MUSHclient doesn't recognise x it responds IAC WONT x.


I have to assume that your incoming packet handling is not correctly recognising the ATCP, and thus letting it through, and then MUSHclient responds to it.

Quote:

I'm assuming that hex 56 (decimal 86) is the COMPRESS2 option, which is for MCCPv2, correct?


From the source code:


#define TELOPT_COMPRESS 85   // telet negotiation code for starting compression v1
#define TELOPT_COMPRESS2 86  // telet negotiation code for starting compression v2

USA #29
Ah, I hadn't thought that I might be letting it slip through. I'll check, but that's prolly it. Thanks!
USA #30
And, one more thing. I've been looking around a bit, but I can't understand what role the EOR code plays in general Telnet negotiation. I am ready for enlightenment. Thanks!
Australia Forum Administrator #31
One approach is to do something like this:


replacements = {
   bam = "box",
   bad = "abc",
   bat = "xyz", 
   }

s = "I see bam bam bad bat"
s2 = string.gsub (s, "ba.", replacements)

print (s2)  --> I see box box abc xyz


I assume you are looking for something like IAC DO x, so you could put the IAC DO into the search string and a "." for the 3rd character. Then make a replacements table, like I did above.
USA #32
I now have a working version that handles ATCP negotiations and etc. There are two main problems with Ked's version. 1) His authentication function is wrong (looks like he mixed up some variables) 2) He doesn't parse sub-negotiation options entirely correctly. The code for my version follows. It's cut from part of a larger plugin, so there are some functions and variables that are not defined here. They are all rather self-explanatory however.

The plugin screws up rather quickly when MCCP is active, so it suppresses IAC_WILL_COMPRESS2.

ATCP information is stored in the atcp.values table.


atcp = {}

--code adapted from Ked's ATCP script, which can be found at:
--<http://www.freewebs.com/keldar/atcp.htm>

atcp.IAC				= '\255'
atcp.WILL				= '\251'
atcp.WONT				= '\252'
atcp.ATCP				= '\200'

atcp.IAC_SE				= '\255\240'
atcp.IAC_SB_ATCP		= '\255\250\200'
atcp.IAC_WILL_ATCP		= '\255\251\200'
atcp.IAC_WONT_ATCP		= '\255\252\200'
atcp.IAC_DO_ATCP		= '\255\253\200'
atcp.IAC_DONT_ATCP		= '\255\254\200'

atcp.IAC_WILL_COMPRESS2	= '\255\251\086'

atcp.enabled		= false
atcp.leftovers		= false
atcp.values			= {}

atcp.options		= {
	{name='hello',			value=config.atcp_id},
	{name='auth',			value='1'},
	{name='composer',		value='1'},
	{name='char_name',		value='1'},
	{name='topvote',		value='1'},
	{name='char_vitals',	value='1'},
	{name='room_brief',		value='1'},
	{name='room_exits',		value='1'}
}
	
--mediapak
--wiz
--filestore
--keepalive

--ATCP FUNCTIONS

function atcp.init()
	world.SendPkt(atcp.IAC_DO_ATCP)

	local msg = false
	for _,option in ipairs(atcp.options) do
		if(not msg) then
			msg = option.name .. ' ' .. option.value
		else
			msg = msg .. '\010' .. option.name .. ' ' .. option.value
		end
	end
	
	world.SendPkt(atcp.IAC_SB_ATCP .. msg .. atcp.IAC_SE .. atcp.IAC_SB_ATCP .. 'login ' .. config.login .. ' ' .. config.password .. atcp.IAC_SE)
end
function atcp.close()
	world.SendPkt(atcp.IAC_WONT_ATCP)
end

function atcp.authenticate(seed)
	local a = 17
  
	for i=0,string.len(seed) - 1 do		
		if(math.fmod(i,2) == 0) then
			a = a + (string.byte(seed,i + 1) - 96) * (bit.bor(i,13))
		else
			a = a - (string.byte(seed,i + 1) - 96) * (bit.bor(i,11))
		end
	end

	return a
end

function atcp.retrieve_info(msg,capture)
	local ar = {}
	local d1 = string.find(msg,' ',0,true) or -2
	local d2 = string.find(msg,'\010',0,true) or d1 + 1
	
	if(d2 == -1) then
		capture[msg] = {}
		atcp.values[msg] = {}
		return capture
	elseif(d1 < 0) then
		d1 = d2 + 1
	end
	
	local d
	if(d1 < d2) then
		ar = utils.split(msg,' ',2)
		d = d1
	else
		ar = utils.split(msg,'\010',2)
		d = d2
	end
	
	local name = string.sub(msg,0,d - 1)
	capture[name] = utils.split(string.sub(msg,d + 1),'\010')
	atcp.values[name] = capture[name]
	
	return capture
end

--PARSE FUNCTION

function atcp.parse(packet)
	local parsed = {}
	
  	if(atcp.leftovers) then
  		packet = atcp.leftovers .. packet
  		atcp.leftovers = false
	end
	
	--disable mccp
	packet = string.gsub(packet, atcp.IAC_WILL_COMPRESS2, '')
	
	packet = string.gsub(packet, atcp.IAC .. '(.)' .. atcp.ATCP, 
		function(code)
			if(code == atcp.WILL) then
				if(not atcp.enabled) then
					atcp.enabled = true
					atcp.init()
					
					if(CTRL_DEBUG) then
						system.DebugNote('ATCP PROTOCOL ON.')
					end
				end
				
				return ''
			 elseif(code == atcp.WONT) then
				if(atcp.enabled) then
					atcp.enabled = false
					atcp.close()
					
					if(CTRL_DEBUG) then
						system.DebugNote('ATCP PROTOCOL OFF.')
					end
				end
				
				return ''
			end
			
			return nil
		end)
	
	packet = string.gsub(packet, '' .. atcp.IAC_SB_ATCP .. '(.-)' .. atcp.IAC_SE, 
		function(msg)
			parsed = atcp.retrieve_info(msg,parsed)
			return ''
		end)
	
	local s,e,msg = string.find(packet, '(\255[^\255]*)$')    --"(\255\250?\200?.*[^\255])")
	if(s) then
		if((#msg == 1) or ((#msg > 1) and string.find(msg, "^\255\250")) or ((#msg > 2) and string.find(msg, "^\255\250\200"))) then
			atcp.leftovers = msg
		end
	end
	
	return packet,parsed
end

--PLUGIN CALLBACKS

function OnPluginPacketReceived(packet)
  	local packet,parsed = atcp.parse(packet)
  	
  	if(parsed['Auth.Request']) then  		
  		if(parsed['Auth.Request'][1] == 'CH') then
	  		world.SendPkt(atcp.IAC_SB_ATCP .. 'auth ' .. atcp.authenticate(parsed['Auth.Request'][2]) .. ' ' .. config.atcp_id .. atcp.IAC_SE)
	  		
	  		if(CTRL_DEBUG) then
	  			system.DebugNote('ATCP AUTHORIZATION SENT.')
	  		end
	  	elseif(parsed['Auth.Request'][1] == 'OFF') then
    		system.WarningNote('ATCP AUTHORIZATION FAILED.')
   		elseif(parsed['Auth.Request'][1] == 'ON' and CTRL_DEBUG) then
    		system.DebugNote('ATCP AUTHORIZATION ACCEPTED.')
    	end
	end
	
	--'Auth.Request'
	--'Char.Name'
	--'Char.Vitals'
	--'Client.Compose'
	--'Client.GoodBye'
	--'Client.JavaEnv'
	--'Client.VoteReminder'
	--'Room.Exits'
	--'Room.Brief'

	return packet
end