em_server.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. #!/usr/bin/env ruby
  2. require 'eventmachine'
  3. require 'digest/md5'
  4. require 'fiber'
  5. require 'geoip'
  6. require 'socket'
  7. # .. allowed commands .. ROLES = CAPABILITIES
  8. # normal users have ROLE broadcast. Roles are defined on a per-user basis .. in a config.
  9. $version = "0.2EventMachineSocketServer"
  10. $debug = 0
  11. $role_commands = Hash[
  12. #noinspection RubyStringKeysInHashInspection
  13. 'everyone' => %w(PING WHO C PART),
  14. 'broadcast_admin' => %w(BC_ID BC BC_ENDCOUNT),
  15. 'broadcast' => %w(REQ_BC BC_RE),
  16. 'specbot_admin' => %w(REQ_ASSIGN REQ_UNASSIGN REQ_PING REQ_ASSIGNMENTS),
  17. 'specbot' => %w(ASSIGN_RE UNASSIGN_RE PING_RE ASSIGNMENTS_RE),
  18. ]
  19. $default_role = "everyone"
  20. # which role is talking to which role?
  21. # effectively it says: this (local) command is sent to that (remote) topic .. that certain topic is read by that user with that role.
  22. $role_dialogs = Hash[
  23. #noinspection RubyStringKeysInHashInspection
  24. 'everyone' => %w(everyone),
  25. 'broadcast_admin' => %w(broadcast),
  26. 'broadcast' => %w(broadcast_admin),
  27. 'specbot_admin' => %w(specbot),
  28. 'specbot' => %w(specbot_admin),
  29. ]
  30. $user_roles = Hash[
  31. #noinspection RubyStringKeysInHashInspection
  32. 'paul_dev_eggdrop' => %w(everyone broadcast),
  33. 'paul_eggdrop' => %w(everyone broadcast),
  34. 'paul_dev_specbot' => %w(everyone broadcast specbot),
  35. 'paul_specbot' => %w(everyone broadcast specbot),
  36. 'qw.nu' => %w(everyone broadcast),
  37. 'qw.nu_poster' => %w(everyone broadcast),
  38. 'mihawk_dev_specbot' => %w(everyone broadcast specbot),
  39. 'mihawk_specbot' => %w(everyone broadcast specbot),
  40. 'central_brain' => %w(everyone broadcast_admin specbot_admin),
  41. ]
  42. $qw_server = Hash.new
  43. def string2bytearray(text)
  44. return_array = Array.new
  45. text.each_byte{
  46. |b|
  47. return_array.push(b)
  48. }
  49. return_array
  50. end
  51. def qwstatus(iphost, ipport)
  52. udp_payload = [0xFF, 0xFF, 0xFF, 0xFF]
  53. udp_payload.concat(string2bytearray("status 23"))
  54. udp_payload.concat([0x0a]) # linefeed at the end
  55. udp_payload = udp_payload.pack("C*")
  56. #p udp_payload
  57. u2 = UDPSocket.new
  58. u2.send(udp_payload, 0, iphost, ipport)
  59. the_return = u2.recv(1500)
  60. u2.close
  61. put_log "muh #{the_return}"
  62. if the_return =~ /\W\W\W\Wn\\(.+)$/
  63. line = $1
  64. put_log "line #{line}"
  65. matches = line.scan(/(.+?)\\(.+?)(\\|$)/)
  66. the_hash = Hash.new
  67. matches.each {
  68. |k, v, _|
  69. the_hash[k] = "#{v}"
  70. }
  71. return the_hash
  72. else
  73. return false
  74. end
  75. end
  76. module HandleQwStatus
  77. attr_accessor :received
  78. def received
  79. puts "lol: #{@data}"
  80. @data
  81. end
  82. def receive_data(data)
  83. puts "lal: #{data}"
  84. @data = data
  85. self.received
  86. end
  87. end
  88. module CentralProtocolHandler
  89. @@connected_clients = Array.new
  90. @@broadcasts = Hash.new
  91. # default method that is being run on connection!
  92. def post_init # connection of someone starts here...
  93. @username = nil
  94. end
  95. ### getters
  96. def entered_username?
  97. !@username.nil? && !@username.empty? # then it's true
  98. end
  99. def username
  100. @username
  101. end
  102. def my_roles
  103. $user_roles[@username]
  104. end
  105. def my_cmds
  106. my_roles.collect {|v| $role_commands[v]}.flatten
  107. end
  108. # returns online users by default by searching through saved connections
  109. def ousers
  110. users = Array.new
  111. @@connected_clients.each {|c| users.push(c.username)}
  112. users
  113. end # of ousers
  114. ### checkers
  115. def allowed_cmd(inputmessage)
  116. my_cmds.each{|c|
  117. #print "A #{c} B #{inputmessage}\n"
  118. if inputmessage =~ /^#{c}/
  119. return true
  120. end
  121. }
  122. false
  123. end
  124. def online(user)
  125. if ousers.include? user
  126. true
  127. else
  128. false
  129. end
  130. end
  131. ### actions
  132. def put_log(msg)
  133. puts "#{Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")}: #{msg}"
  134. end
  135. # searches through our hash of saved connections and writes them a messages.
  136. def write_user(input, username = nil)
  137. if username.nil? || username.empty?
  138. username = @username
  139. end
  140. if @@connected_clients.find { |c| c.username == username }
  141. connection = @@connected_clients.find { |c| c.username == username }
  142. sometime = "#{Time.now.utc.strftime("%Y-%m-%d %H:%M:%S %z")}"
  143. line = "#{sometime}: #{input}\n"
  144. connection.send_data line
  145. end
  146. end # of write_user
  147. # searches through roles and writes those users on their connections
  148. def write_role(role, input, *exempts)
  149. #find users with role
  150. users = Array.new
  151. $user_roles.each {|k, v|
  152. if v.include? role
  153. users.push(k)
  154. end
  155. }
  156. # find users that are online and inside Array "users"
  157. lala = Array.new
  158. users.each {|v|
  159. if ousers.include? v
  160. lala.push(v)
  161. end
  162. }
  163. # now write to them
  164. lala.each {|user|
  165. if not exempts.include? user
  166. write_user(input, user)
  167. end
  168. }
  169. end # of write_role()
  170. # what happens with what input?!
  171. def inputting(input)
  172. write_user("SYS You typed: #{input}")
  173. if input =~ /^([A-Z_]+)/
  174. cmd = $1
  175. end
  176. # now we have the cmd .. or not ;)
  177. if input =~ /^[A-Z_]+ (.+)/
  178. payload = $1
  179. end
  180. # now we can use payload
  181. if allowed_cmd(cmd)
  182. ### typical system commands follow
  183. if cmd == "PING"
  184. write_user("PONG")
  185. elsif cmd == "C"
  186. if payload =~ /^(.+)$/
  187. write_role($default_role, "C #{@username}: #{$1}")
  188. else
  189. write_user("SYS Format is C <chattext>")
  190. end
  191. elsif cmd == "WHO"
  192. ousers.each {|ouser| write_user("WHO_RE #{ouser} ROLES: #{$user_roles[ouser].join(", ")}")}
  193. elsif cmd == "PART"
  194. write_user("SYS Goodbye '#{@username}'.")
  195. write_role($default_role, "PARTED User '#{@username}' just left the party.", @username)
  196. return "bye"
  197. ###now for the good stuff, broadcasting
  198. elsif cmd == "REQ_BC"
  199. # help with format .. but send the raw payload
  200. if payload =~ /^(.+),(.+),(.+),'(.+)','(.+)'$/
  201. # n f s ni txt
  202. error = 0
  203. # $&
  204. # The string matched by the last successful pattern match in this scope, or nil if the last pattern match failed. (Mnemonic: like & in some editors.) This variable is r
  205. network = $1
  206. freqname = $2
  207. source = $3
  208. nickname = $4
  209. text = $5
  210. if not network =~ /^(QWalt)|(QDEV)/
  211. write_user("SYS Network name #{network} is unknown: QWalt or QDEV allowed.")
  212. error = 1
  213. end
  214. if not freqname =~ /^(-qw-)|(-spam-)|(-dev-)/
  215. write_user("SYS Frequency name is unknown #{freqname}")
  216. error = 1
  217. end
  218. if source =~ /^(#.+)|(qw:\/\/)|(http:\/\/)/
  219. else
  220. write_user("SYS Source string is not in the form ^(#.+)|(qw:\/\/)|(http:\/\/) was: #{source}")
  221. error = 1
  222. end
  223. if not nickname =~ /^.+/
  224. write_user("SYS Nickname string is not in the form ^.+ #{nickname}")
  225. error = 1
  226. end
  227. if not text =~ /^.+/
  228. write_user("SYS Message string is not in the form ^.+ #{text}")
  229. error = 1
  230. end
  231. else # of check syntax
  232. write_user("SYS Command format is REQ_BC <network>,<frequency>,<source/channel>,'<nickname>','<message>'")
  233. error = 1
  234. end
  235. if error == 0
  236. # send REQ_BC to the corresponding role.
  237. bcid = Digest::MD5.hexdigest("%d %s %s %s %s %s" % [Time.now.utc.to_i, network, freqname, source, nickname, text])
  238. # so it only reaches the issuer of REQ_BC
  239. write_user("BC_ID #{bcid} for: #{network},#{freqname},#{source}")
  240. @@broadcasts[bcid] = @username
  241. # qw:// ip resolving
  242. if source =~ /^qw:\/\/(.+):(\d+)(.*)$/
  243. # ip address is 15 in length
  244. iphost = $1
  245. domainname = iphost # set a default
  246. ipport = $2
  247. rest = $3
  248. if iphost =~ /^\d+\.\d+\.\d+\.\d+/
  249. # find country code
  250. c = GeoIP.new('GeoIP.dat').country(iphost)[3].to_s.downcase
  251. rest.prepend " #{c}" unless c.nil? || c.empty?
  252. # get hostname from a qw status packet! perhaps there's a DNS name inside, which we can use! fixme
  253. hum = qwstatus(iphost, ipport)
  254. p hum
  255. if hum["hostname"] =~ /([\w\.]{3,}\.\w{2,4})/
  256. hostnamedns = $1
  257. ip_of_hostnamedns = Resolv.getaddress(hostnamedns)
  258. if ip_of_hostnamedns && (ip_of_hostnamedns == iphost)
  259. # ok, we take it.
  260. put_log "ok, we take #{hostnamedns} for #{iphost} here.."
  261. domainname = hostnamedns
  262. end
  263. else
  264. # resolve it to a dns name
  265. resulting_name = Resolv.getname(iphost)
  266. if resulting_name
  267. # if the resulting dns name...
  268. # .. is too long, use ip-address instead.
  269. # .. has too many dots, use ip-address instead.
  270. # .. has too many numbers, use ip-address instead.
  271. if resulting_name.length > 21 || resulting_name.split(".").size > 3 || resulting_name.scan(/\d/).size > 3
  272. put_log "cutting down host_name: #{resulting_name}, because:"
  273. put_log "#{resulting_name.length}"
  274. put_log "#{resulting_name.split(".").size}"
  275. put_log "#{resulting_name.scan(/\d/).size}"
  276. else
  277. domainname = resulting_name
  278. end
  279. end # of resolving
  280. end
  281. end # if ip is x.x.x.x
  282. source = "qw://#{domainname}:#{ipport}#{rest}"
  283. end # of if source qw://x.x.x.x:portzzz
  284. # resolve
  285. finalmessage = "BC %s %s,%s,%s,'%s','%s'" % [bcid, network, freqname, source, nickname, text]
  286. write_role("broadcast", finalmessage, @username) # write to the role with the exempt of myself.
  287. end
  288. #end of REQ_BC here..
  289. elsif cmd == "BC_RE"
  290. if payload =~ /^(.+) (.+)=(\d+),(.+)=(\d+)$/
  291. bcid = $1
  292. user_string = $2
  293. user_count = $3
  294. item_string = $4
  295. item_count = $5
  296. payload = "%s %s=%d,%s=%d" % [bcid, user_string, user_count, item_string, item_count]
  297. # according bcid it is possible to get the originating user.. so send only to his topic!
  298. if @@broadcasts[bcid]
  299. to_user = @@broadcasts[bcid]
  300. write_user("SYS Broadcast reply bcid: #{bcid} underway to #{to_user}.")
  301. write_user("#{cmd} #{payload}", to_user)
  302. else
  303. write_user("SYS Broadcast reply bcid: #{bcid} underway.")
  304. write_role("broadcast", "#{cmd} #{payload}", @username)
  305. end
  306. else # of bc_re format check
  307. put_log "SYS Format is BC_RE <bcid> <userstring>=<usercount>,<itemstring>=<itemcount>"
  308. end
  309. end
  310. else # of if allowed command
  311. write_user("SYS Command #{input} not allowed, your commands are: #{my_cmds.join(", ")}.")
  312. end # of if allowed command
  313. end # of inputting!
  314. # default method that is being run on receiving data!
  315. def receive_data(data) # connection receives a line on the server end
  316. line = data.chomp unless data.chomp.nil?
  317. if line
  318. if entered_username? # oh, it's a known user
  319. # it's alive! kill ping timer, set a new one:
  320. if @ping_timer
  321. EventMachine.cancel_timer(@ping_timer)
  322. end
  323. @ping_timer = EventMachine.add_periodic_timer 90, proc { write_user("PING alive?") }
  324. # work on input
  325. inputter = inputting(line)
  326. if inputter == "bye"
  327. close_connection
  328. end
  329. else # of username known, then it seems to be the first connect
  330. # and this line is the user name coming in!
  331. username = line
  332. if online(username)
  333. put_log "SYS User '#{username}' tried to double connect. Say bye bye."
  334. send_data "SYS Hey '#{username}', only one connection allowed. Bye.\n"
  335. EventMachine.add_timer 1, proc {
  336. write_user("PING check forced", username)
  337. close_connection
  338. }
  339. return false
  340. end
  341. if $user_roles.any? {|k| k.include? username}
  342. @username = username
  343. @@connected_clients.push(self)
  344. @ping_timer = EventMachine.add_periodic_timer 90, proc { write_user("PING alive?") }
  345. # starting a pinger
  346. put_log("SYS '#{@username}' connected. Online now: #{ousers.join(", ")}")
  347. write_user("HELLO Hi user '#{@username}'! How are you? I'm '#{$version}'")
  348. write_user("ROLES #{my_roles.join(", ")}")
  349. write_user("COMMANDS #{my_cmds.join(", ")}")
  350. write_role($default_role, "JOINED User '#{@username}' just joined the party.", @username)
  351. else
  352. put_log "SYS User '#{username}' unknown to me. Saying bye."
  353. send_data "SYS User '#{username}' unknown to me. Bye.\n"
  354. # disconnect him
  355. close_connection
  356. end
  357. end # of username known
  358. end # of line
  359. end
  360. # default method that is being run on disconnection!
  361. def unbind # a connection goes bye bye
  362. if @username
  363. @@connected_clients.delete(self)
  364. if @ping_timer
  365. EventMachine.cancel_timer(@ping_timer)
  366. end
  367. put_log "SYS '#{@username}' disconnected."
  368. end
  369. put_log "SYS Online users now: #{ousers.join(", ")}"
  370. end
  371. end
  372. def main
  373. # #run: Note that this will block current thread.
  374. EventMachine.run do
  375. EventMachine.start_server("0.0.0.0", 7337, CentralProtocolHandler)
  376. end
  377. end
  378. main