New plugin that finds out where an IP or domain is located (by looking up the IP...
[rbot] / data / rbot / plugins / iplookup.rb
1 #################################################################\r
2 # IP Lookup Plugin\r
3 # ----------------------------\r
4 # by Chris Gahan (chris@ill-logic.com)\r
5 #\r
6 # Purpose:\r
7 # ------------------\r
8 # Lets you lookup the owner and their address for any IP address\r
9 # or IRC user.\r
10 #\r
11 #################################################################\r
12 \r
13 require 'socket'\r
14 require 'resolv'\r
15 \r
16 #################################################################\r
17 ## ARIN Whois module...\r
18 ##\r
19 \r
20 module ArinWhois\r
21 \r
22   class Chunk < Hash\r
23     def customer?\r
24       keys.grep(/^(City|Address|StateProv|(Org|Cust)Name)$/).any?\r
25     end\r
26     \r
27     def network?\r
28       keys.grep(/^(CIDR|NetHandle|Parent)$/).any?\r
29     end\r
30 \r
31     def contact?\r
32       keys.grep(/^(R|Org)(Tech|Abuse)(Handle|Name|Phone|Email)$/).any?\r
33     end\r
34     \r
35     def valid?\r
36       customer? or network? or contact?\r
37     end\r
38     \r
39     def owner\r
40       self[keys.grep(/^(Org|Cust)Name$/).first]\r
41     end\r
42     \r
43     def location\r
44       "#{self['City']}, #{self['StateProv']}, #{self['Country']}"\r
45     end\r
46     \r
47     def address\r
48       "#{self['Address']}, #{location}  #{self['PostalCode']}"\r
49     end\r
50     \r
51   end\r
52 \r
53   class ArinWhoisParser\r
54     \r
55     def initialize(data)\r
56       @data = data\r
57     end\r
58     \r
59     def split_array_at(a, &block)\r
60       return a unless a.any?\r
61       a = a.to_a\r
62       \r
63       results = []\r
64       last_cutpoint = 0\r
65       \r
66       a.each_with_index do |el,i|\r
67         if block.call(el)\r
68           unless i == 0\r
69             results << a[last_cutpoint...i]\r
70             last_cutpoint = i\r
71           end\r
72         end\r
73       end\r
74       \r
75       if last_cutpoint < a.size or last_cutpoint == 0\r
76         results << a[last_cutpoint..-1]\r
77       end\r
78       \r
79       results\r
80     end\r
81     \r
82     # Whois output format\r
83     # ------------------------\r
84     # Owner info block:\r
85     #   {Org,Cust}Name\r
86     #   Address\r
87     #   City\r
88     #   StateProv\r
89     #   PostalCode\r
90     #   Country (2-digit)\r
91     #\r
92     # Network Information:\r
93     #   CIDR (69.195.25.0/25)\r
94     #   NetHandle (NET-72-14-192-0-1)\r
95     #   Parent (NET-72-0-0-0-0)\r
96     #\r
97     # Contacts:\r
98     #   ({R,Org}{Tech,Abuse}{Handle,Name,Phone,Email})*\r
99     \r
100     def parse_chunks\r
101       return if @data =~ /^No match found /\r
102       chunks = @data.gsub(/^# ARIN WHOIS database, last updated.+/m, '').scan(/(([^\n]+\n)+\n)/m)\r
103       chunks.map do |chunk|\r
104         result = Chunk.new\r
105         \r
106         chunk[0].scan(/([A-Za-z]+?):(.*)/).each do |tuple|\r
107           #puts tuple.inspect\r
108           result[tuple[0]] = tuple[1].strip\r
109         end\r
110         \r
111         result\r
112       end\r
113     end\r
114     \r
115     \r
116     def get_parsed_data\r
117       return unless chunks = parse_chunks\r
118       \r
119       results = split_array_at(parse_chunks) {|chunk|chunk.customer?}\r
120       results.map do |chunks|\r
121         {\r
122           :customer => chunks.select{|x|x.customer?}[0],\r
123           :net      => chunks.select{|x|x.network?}[0],\r
124           :contacts => chunks.select{|x|x.contact?}\r
125         }\r
126       end\r
127     end\r
128     \r
129     # Return a hash with :customer, :net, and :contacts info filled in.\r
130     def get_most_specific_owner\r
131       return unless datas = get_parsed_data\r
132       \r
133       datas_with_bitmasks = datas.map do |data| \r
134         bitmask = data[:net]['CIDR'].split('/')[1].to_i\r
135         [bitmask, data]\r
136       end\r
137       #datas_with_bitmasks.sort.each{|x|puts x[0]}\r
138       winner = datas_with_bitmasks.sort[-1][1]\r
139     end\r
140 \r
141   end # of class ArinWhoisParser\r
142 \r
143 module_function\r
144 \r
145   def raw_whois(query_string, host)\r
146     s = TCPsocket.open(host, 43)\r
147     s.write(query_string+"\n")\r
148     ret = s.read\r
149     s.close\r
150     return ret\r
151   end\r
152 \r
153   def lookup(ip)\r
154     data = raw_whois("+#{ip}", 'whois.arin.net')\r
155     arin = ArinWhoisParser.new data\r
156     arin.get_most_specific_owner\r
157   end\r
158   \r
159   def lookup_location(ip)\r
160     result = lookup(ip)\r
161     result[:customer].location\r
162   end\r
163   \r
164   def lookup_address(ip)\r
165     result = lookup(ip)\r
166     result[:customer].address\r
167   end\r
168 \r
169   def lookup_info(ip)\r
170     if result = lookup(ip)\r
171       "#{result[:net]['CIDR']} => #{result[:customer].owner} (#{result[:customer].address})"\r
172     else\r
173       "Address not found."\r
174     end\r
175   end\r
176 \r
177 end\r
178 \r
179 \r
180 \r
181 #################################################################\r
182 ## The Plugin\r
183 ##\r
184 \r
185 class IPLookupPlugin < Plugin\r
186   def help(plugin, topic="")
187     "iplookup [ip address / domain name] => lookup info about the owner of the IP address from the ARIN whois database"
188   end\r
189   \r
190   def iplookup(m, params)\r
191     reply = ""\r
192     if params[:domain]\r
193       ip = Resolv.getaddress(params[:domain])\r
194       reply += "(#{params[:domain]} = #{ip}) "\r
195     else\r
196       ip = params[:ip]\r
197     end\r
198     \r
199     reply += ArinWhois.lookup_info(ip)\r
200     m.reply reply\r
201   end\r
202 \r
203   def userip(m, params)\r
204     #users = @channels[m.channel].users\r
205     #m.reply "users = #{users.inspect}"\r
206     #m.reply @bot.sendq("WHO #{params[:user]}")\r
207   end\r
208   \r
209 end\r
210 \r
211 plugin = IPLookupPlugin.new
212 plugin.map 'iplookup :ip', :action => 'iplookup', :requirements => {:ip => /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/}\r
213 plugin.map 'iplookup :domain', :action => 'iplookup', :requirements => {:domain => /^[a-z0-9\.\-]{4,255}$/i}\r
214 plugin.map 'userip :user', :action => 'userip', :requirements => {:user => /\w+/}
215 \r
216 \r
217 if __FILE__ == $0\r
218   include ArinWhois\r
219   data = open('whoiscgm.txt').read\r
220   c = ArinWhoisParser.new data\r
221   puts c.get_parsed_data.inspect\r
222 end