| 1 | ## common/nslookup.py |
|---|
| 2 | ## |
|---|
| 3 | ## Copyright (C) 2006 Dimitur Kirov <dkirov@gmail.com> |
|---|
| 4 | ## |
|---|
| 5 | ## This program is free software; you can redistribute it and/or modify |
|---|
| 6 | ## it under the terms of the GNU General Public License as published |
|---|
| 7 | ## by the Free Software Foundation; version 2 only. |
|---|
| 8 | ## |
|---|
| 9 | ## This program is distributed in the hope that it will be useful, |
|---|
| 10 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 11 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 12 | ## GNU General Public License for more details. |
|---|
| 13 | ## |
|---|
| 14 | |
|---|
| 15 | import sys |
|---|
| 16 | import os |
|---|
| 17 | import re |
|---|
| 18 | |
|---|
| 19 | from xmpp.idlequeue import * |
|---|
| 20 | |
|---|
| 21 | if os.name == 'nt': |
|---|
| 22 | from subprocess import * # python24 only. we ask this for Windows |
|---|
| 23 | elif os.name == 'posix': |
|---|
| 24 | import fcntl |
|---|
| 25 | |
|---|
| 26 | # it is good to check validity of arguments, when calling system commands |
|---|
| 27 | ns_type_pattern = re.compile('^[a-z]+$') |
|---|
| 28 | |
|---|
| 29 | # match srv host_name |
|---|
| 30 | host_pattern = re.compile('^[a-z0-9\-._]*[a-z0-9]\.[a-z]{2,}$') |
|---|
| 31 | |
|---|
| 32 | class Resolver: |
|---|
| 33 | def __init__(self, idlequeue): |
|---|
| 34 | self.idlequeue = idlequeue |
|---|
| 35 | # dict {host : list of srv records} |
|---|
| 36 | self.resolved_hosts = {} |
|---|
| 37 | # dict {host : list of callbacks} |
|---|
| 38 | self.handlers = {} |
|---|
| 39 | |
|---|
| 40 | def parse_srv_result(self, fqdn, result): |
|---|
| 41 | ''' parse the output of nslookup command and return list of |
|---|
| 42 | properties: 'host', 'port','weight', 'priority' corresponding to the found |
|---|
| 43 | srv hosts ''' |
|---|
| 44 | if os.name == 'nt': |
|---|
| 45 | return self._parse_srv_result_nt(fqdn, result) |
|---|
| 46 | elif os.name == 'posix': |
|---|
| 47 | return self._parse_srv_result_posix(fqdn, result) |
|---|
| 48 | |
|---|
| 49 | def _parse_srv_result_nt(self, fqdn, result): |
|---|
| 50 | # output from win32 nslookup command |
|---|
| 51 | if not result: |
|---|
| 52 | return [] |
|---|
| 53 | hosts = [] |
|---|
| 54 | lines = result.replace('\r','').split('\n') |
|---|
| 55 | current_host = None |
|---|
| 56 | for line in lines: |
|---|
| 57 | line = line.lstrip() |
|---|
| 58 | if line == '': |
|---|
| 59 | continue |
|---|
| 60 | if line.startswith(fqdn): |
|---|
| 61 | rest = line[len(fqdn):] |
|---|
| 62 | if rest.find('service') > -1: |
|---|
| 63 | current_host = {} |
|---|
| 64 | elif isinstance(current_host, dict): |
|---|
| 65 | res = line.strip().split('=') |
|---|
| 66 | if len(res) != 2: |
|---|
| 67 | if len(current_host) == 4: |
|---|
| 68 | hosts.append(current_host) |
|---|
| 69 | current_host = None |
|---|
| 70 | continue |
|---|
| 71 | prop_type = res[0].strip() |
|---|
| 72 | prop_value = res[1].strip() |
|---|
| 73 | if prop_type.find('prio') > -1: |
|---|
| 74 | try: |
|---|
| 75 | current_host['prio'] = int(prop_value) |
|---|
| 76 | except ValueError: |
|---|
| 77 | continue |
|---|
| 78 | elif prop_type.find('weight') > -1: |
|---|
| 79 | try: |
|---|
| 80 | current_host['weight'] = int(prop_value) |
|---|
| 81 | except ValueError: |
|---|
| 82 | continue |
|---|
| 83 | elif prop_type.find('port') > -1: |
|---|
| 84 | try: |
|---|
| 85 | current_host['port'] = int(prop_value) |
|---|
| 86 | except ValueError: |
|---|
| 87 | continue |
|---|
| 88 | elif prop_type.find('host') > -1: |
|---|
| 89 | # strip '.' at the end of hostname |
|---|
| 90 | if prop_value[-1] == '.': |
|---|
| 91 | prop_value = prop_value[:-1] |
|---|
| 92 | current_host['host'] = prop_value |
|---|
| 93 | if len(current_host) == 4: |
|---|
| 94 | hosts.append(current_host) |
|---|
| 95 | current_host = None |
|---|
| 96 | return hosts |
|---|
| 97 | |
|---|
| 98 | def _parse_srv_result_posix(self, fqdn, result): |
|---|
| 99 | # typical output of bind-tools nslookup command: |
|---|
| 100 | # _xmpp-client._tcp.jabber.org service = 30 30 5222 jabber.org. |
|---|
| 101 | if not result: |
|---|
| 102 | return [] |
|---|
| 103 | hosts = [] |
|---|
| 104 | lines = result.split('\n') |
|---|
| 105 | for line in lines: |
|---|
| 106 | if line == '': |
|---|
| 107 | continue |
|---|
| 108 | if line.startswith(fqdn): |
|---|
| 109 | rest = line[len(fqdn):].split('=') |
|---|
| 110 | if len(rest) != 2: |
|---|
| 111 | continue |
|---|
| 112 | answer_type, props_str = rest |
|---|
| 113 | if answer_type.strip() != 'service': |
|---|
| 114 | continue |
|---|
| 115 | props = props_str.strip().split(' ') |
|---|
| 116 | if len(props) < 4: |
|---|
| 117 | continue |
|---|
| 118 | prio, weight, port, host = props[-4:] |
|---|
| 119 | if host[-1] == '.': |
|---|
| 120 | host = host[:-1] |
|---|
| 121 | try: |
|---|
| 122 | prio = int(prio) |
|---|
| 123 | weight = int(weight) |
|---|
| 124 | port = int(port) |
|---|
| 125 | except ValueError: |
|---|
| 126 | continue |
|---|
| 127 | hosts.append({'host': host, 'port': port,'weight': weight, |
|---|
| 128 | 'prio': prio}) |
|---|
| 129 | return hosts |
|---|
| 130 | |
|---|
| 131 | def _on_ready(self, host, result): |
|---|
| 132 | # nslookup finished, parse the result and call the handlers |
|---|
| 133 | result_list = self.parse_srv_result(host, result) |
|---|
| 134 | |
|---|
| 135 | # practically it is impossible to be the opposite, but who knows :) |
|---|
| 136 | if not self.resolved_hosts.has_key(host): |
|---|
| 137 | self.resolved_hosts[host] = result_list |
|---|
| 138 | if self.handlers.has_key(host): |
|---|
| 139 | for callback in self.handlers[host]: |
|---|
| 140 | callback(host, result_list) |
|---|
| 141 | del(self.handlers[host]) |
|---|
| 142 | |
|---|
| 143 | def start_resolve(self, host): |
|---|
| 144 | ''' spawn new nslookup process and start waiting for results ''' |
|---|
| 145 | ns = NsLookup(self._on_ready, host) |
|---|
| 146 | ns.set_idlequeue(self.idlequeue) |
|---|
| 147 | ns.commandtimeout = 10 |
|---|
| 148 | ns.start() |
|---|
| 149 | |
|---|
| 150 | def resolve(self, host, on_ready): |
|---|
| 151 | if not host: |
|---|
| 152 | # empty host, return empty list of srv records |
|---|
| 153 | on_ready([]) |
|---|
| 154 | return |
|---|
| 155 | if self.resolved_hosts.has_key(host): |
|---|
| 156 | # host is already resolved, return cached values |
|---|
| 157 | on_ready(host, self.resolved_hosts[host]) |
|---|
| 158 | return |
|---|
| 159 | if self.handlers.has_key(host): |
|---|
| 160 | # host is about to be resolved by another connection, |
|---|
| 161 | # attach our callback |
|---|
| 162 | self.handlers[host].append(on_ready) |
|---|
| 163 | else: |
|---|
| 164 | # host has never been resolved, start now |
|---|
| 165 | self.handlers[host] = [on_ready] |
|---|
| 166 | self.start_resolve(host) |
|---|
| 167 | |
|---|
| 168 | # TODO: move IdleCommand class in other file, maybe helpers ? |
|---|
| 169 | class IdleCommand(IdleObject): |
|---|
| 170 | def __init__(self, on_result): |
|---|
| 171 | # how long (sec.) to wait for result ( 0 - forever ) |
|---|
| 172 | # it is a class var, instead of a constant and we can override it. |
|---|
| 173 | self.commandtimeout = 0 |
|---|
| 174 | # when we have some kind of result (valid, ot not) we call this handler |
|---|
| 175 | self.result_handler = on_result |
|---|
| 176 | # if it is True, we can safetely execute the command |
|---|
| 177 | self.canexecute = True |
|---|
| 178 | self.idlequeue = None |
|---|
| 179 | self.result ='' |
|---|
| 180 | |
|---|
| 181 | def set_idlequeue(self, idlequeue): |
|---|
| 182 | self.idlequeue = idlequeue |
|---|
| 183 | |
|---|
| 184 | def _return_result(self): |
|---|
| 185 | if self.result_handler: |
|---|
| 186 | self.result_handler(self.result) |
|---|
| 187 | self.result_handler = None |
|---|
| 188 | |
|---|
| 189 | def _compose_command_args(self): |
|---|
| 190 | return ['echo', 'da'] |
|---|
| 191 | |
|---|
| 192 | def _compose_command_line(self): |
|---|
| 193 | ''' return one line representation of command and its arguments ''' |
|---|
| 194 | return reduce(lambda left, right: left + ' ' + right, self._compose_command_args()) |
|---|
| 195 | |
|---|
| 196 | def wait_child(self): |
|---|
| 197 | if self.pipe.poll() is None: |
|---|
| 198 | # result timeout |
|---|
| 199 | if self.endtime < self.idlequeue.current_time(): |
|---|
| 200 | self._return_result() |
|---|
| 201 | self.pipe.stdout.close() |
|---|
| 202 | self.pipe.stdin.close() |
|---|
| 203 | else: |
|---|
| 204 | # child is still active, continue to wait |
|---|
| 205 | self.idlequeue.set_alarm(self.wait_child, 0.1) |
|---|
| 206 | else: |
|---|
| 207 | # child has quit |
|---|
| 208 | self.result = self.pipe.stdout.read() |
|---|
| 209 | self._return_result() |
|---|
| 210 | self.pipe.stdout.close() |
|---|
| 211 | self.pipe.stdin.close() |
|---|
| 212 | def start(self): |
|---|
| 213 | if not self.canexecute: |
|---|
| 214 | self.result = '' |
|---|
| 215 | self._return_result() |
|---|
| 216 | return |
|---|
| 217 | if os.name == 'nt': |
|---|
| 218 | self._start_nt() |
|---|
| 219 | elif os.name == 'posix': |
|---|
| 220 | self._start_posix() |
|---|
| 221 | |
|---|
| 222 | def _start_nt(self): |
|---|
| 223 | # if gajim is started from noninteraactive shells stdin is closed and |
|---|
| 224 | # cannot be forwarded, so we have to keep it open |
|---|
| 225 | self.pipe = Popen(self._compose_command_args(), stdout=PIPE, |
|---|
| 226 | bufsize = 1024, shell = True, stderr = STDOUT, stdin = PIPE) |
|---|
| 227 | if self.commandtimeout >= 0: |
|---|
| 228 | self.endtime = self.idlequeue.current_time() + self.commandtimeout |
|---|
| 229 | self.idlequeue.set_alarm(self.wait_child, 0.1) |
|---|
| 230 | |
|---|
| 231 | def _start_posix(self): |
|---|
| 232 | self.pipe = os.popen(self._compose_command_line()) |
|---|
| 233 | self.fd = self.pipe.fileno() |
|---|
| 234 | fcntl.fcntl(self.pipe, fcntl.F_SETFL, os.O_NONBLOCK) |
|---|
| 235 | self.idlequeue.plug_idle(self, False, True) |
|---|
| 236 | if self.commandtimeout >= 0: |
|---|
| 237 | self.idlequeue.set_read_timeout(self.fd, self.commandtimeout) |
|---|
| 238 | |
|---|
| 239 | def end(self): |
|---|
| 240 | self.idlequeue.unplug_idle(self.fd) |
|---|
| 241 | try: |
|---|
| 242 | self.pipe.close() |
|---|
| 243 | except: |
|---|
| 244 | pass |
|---|
| 245 | |
|---|
| 246 | def pollend(self): |
|---|
| 247 | self.idlequeue.remove_timeout(self.fd) |
|---|
| 248 | self.end() |
|---|
| 249 | self._return_result() |
|---|
| 250 | |
|---|
| 251 | def pollin(self): |
|---|
| 252 | try: |
|---|
| 253 | res = self.pipe.read() |
|---|
| 254 | except Exception, e: |
|---|
| 255 | res = '' |
|---|
| 256 | if res == '': |
|---|
| 257 | return self.pollend() |
|---|
| 258 | else: |
|---|
| 259 | self.result += res |
|---|
| 260 | |
|---|
| 261 | def read_timeout(self): |
|---|
| 262 | self.end() |
|---|
| 263 | self._return_result() |
|---|
| 264 | |
|---|
| 265 | class NsLookup(IdleCommand): |
|---|
| 266 | def __init__(self, on_result, host='_xmpp-client', type = 'srv'): |
|---|
| 267 | IdleCommand.__init__(self, on_result) |
|---|
| 268 | self.commandtimeout = 10 |
|---|
| 269 | self.host = host.lower() |
|---|
| 270 | self.type = type.lower() |
|---|
| 271 | if not host_pattern.match(self.host): |
|---|
| 272 | # invalid host name |
|---|
| 273 | print >> sys.stderr, 'Invalid host: %s' % self.host |
|---|
| 274 | self.canexecute = False |
|---|
| 275 | return |
|---|
| 276 | if not ns_type_pattern.match(self.type): |
|---|
| 277 | print >> sys.stderr, 'Invalid querytype: %s' % self.type |
|---|
| 278 | self.canexecute = False |
|---|
| 279 | return |
|---|
| 280 | |
|---|
| 281 | def _compose_command_args(self): |
|---|
| 282 | return ['nslookup', '-type=' + self.type , self.host] |
|---|
| 283 | |
|---|
| 284 | def _return_result(self): |
|---|
| 285 | if self.result_handler: |
|---|
| 286 | self.result_handler(self.host, self.result) |
|---|
| 287 | self.result_handler = None |
|---|
| 288 | |
|---|
| 289 | # below lines is on how to use API and assist in testing |
|---|
| 290 | if __name__ == '__main__': |
|---|
| 291 | if os.name == 'posix': |
|---|
| 292 | idlequeue = IdleQueue() |
|---|
| 293 | elif os.name == 'nt': |
|---|
| 294 | idlequeue = SelectIdleQueue() |
|---|
| 295 | # testing Resolver class |
|---|
| 296 | import gobject |
|---|
| 297 | import gtk |
|---|
| 298 | |
|---|
| 299 | resolver = Resolver(idlequeue) |
|---|
| 300 | |
|---|
| 301 | def clicked(widget): |
|---|
| 302 | global resolver |
|---|
| 303 | host = text_view.get_text() |
|---|
| 304 | def on_result(host, result_array): |
|---|
| 305 | print 'Result:\n' + repr(result_array) |
|---|
| 306 | resolver.resolve(host, on_result) |
|---|
| 307 | win = gtk.Window() |
|---|
| 308 | win.set_border_width(6) |
|---|
| 309 | text_view = gtk.Entry() |
|---|
| 310 | text_view.set_text('_xmpp-client._tcp.jabber.org') |
|---|
| 311 | hbox = gtk.HBox() |
|---|
| 312 | hbox.set_spacing(3) |
|---|
| 313 | but = gtk.Button(' Lookup SRV ') |
|---|
| 314 | hbox.pack_start(text_view, 5) |
|---|
| 315 | hbox.pack_start(but, 0) |
|---|
| 316 | but.connect('clicked', clicked) |
|---|
| 317 | win.add(hbox) |
|---|
| 318 | win.show_all() |
|---|
| 319 | gobject.timeout_add(200, idlequeue.process) |
|---|
| 320 | gtk.main() |
|---|