# Copyright (C) 2003, 2004 Konstantin Korikov
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import os
import pty
import Queue
import time
import random
import signal
import re
import select
import commands
import sys
import chestnut_dialer
from chestnut_dialer import _
from chestnut_dialer import debug_msg
import chestnut_dialer.config
def _uni2ascii(s):
return s.encode("ascii", "replace")
class Connection:
STOP_RESON_NORMAL_TERM = 0
STOP_RESON_UNKNOWN = 1
STOP_RESON_NO_DIALTONE = 2
STOP_RESON_BUSY = 3
STOP_RESON_NO_CARRIER = 4
STOP_RESON_AUTH_FAILED = 5
account = None
text_queue = None
active = None
connected = None
stop_reson = None
pppd_exit_code = 0
speed = None
start_time = None
linkname = None
script_noterm_tpl = """
ABORT "%(error_resp)s"
ABORT "%(busy_resp)s"
ABORT "%(nocarrier_resp)s"
ABORT "%(nodialtone_resp)s"
ABORT "%(noanswer_resp)s"
TIMEOUT %(modem_timeout)s
'' "%(init_cmd)s"
"%(init_resp)s" "%(init2_cmd)s"
"%(init_resp)s" "%(vol_cmd)s"
"%(vol_resp)s" "%(dial_cmd)s%(dial_prefix)s%(phone_number)s"
TIMEOUT %(dial_timeout)s
"%(connect_resp)s" '\\c'
"%(modem_term)s" '\\c'
"""
script_term_tpl = """
ABORT "%(error_resp)s"
ABORT "%(busy_resp)s"
ABORT "%(nocarrier_resp)s"
ABORT "%(nodialtone_resp)s"
ABORT "%(noanswer_resp)s"
ABORT 'Invalid Login'
ABORT 'Login incorrect'
TIMEOUT %(modem_timeout)s
'' "%(init_cmd)s"
"%(init_resp)s" "%(init2_cmd)s"
"%(init_resp)s" "%(vol_cmd)s"
"%(vol_resp)s" "%(dial_cmd)s%(dial_prefix)s%(phone_number)s"
TIMEOUT %(dial_timeout)s
"%(connect_resp)s" ''
'%(chat_login_prompt)s--%(chat_login_prompt)s' '%(user)s'
'%(chat_passwd_prompt)s' '%(passwd)s'
TIMEOUT %(ppp_timeout)s
'~--' ''
"""
script_auto_tpl = """
import sys
sys.path.append('%(path)s')
from chestnut_dialer.chat import Chat
chat = Chat()
r = chat.chat('-e',
'ABORT', '%(error_resp)s',
'ABORT', '%(busy_resp)s',
'ABORT', '%(nocarrier_resp)s',
'ABORT', '%(nodialtone_resp)s',
'ABORT', '%(noanswer_resp)s',
'TIMEOUT', '%(modem_timeout)s',
'', '%(init_cmd)s',
'%(init_resp)s', '%(init2_cmd)s',
'%(init_resp)s', '%(vol_cmd)s',
'%(vol_resp)s', '%(dial_cmd)s%(dial_prefix)s%(phone_number)s',
'TIMEOUT', '%(dial_timeout)s',
'%(connect_resp)s', '',
'%(modem_term)s', '')
if r != 0: sys.exit(r)
r = chat.chat('-e',
'ABORT', '%(error_resp)s',
'TIMEOUT', '%(ppp_timeout)s',
'~--', '')
if r != 0:
r = chat.chat('-e',
'ABORT', '%(error_resp)s',
'ABORT', 'Invalid Login',
'ABORT', 'Login incorrect',
'TIMEOUT', '%(prompt_timeout)s'
'%(chat_login_prompt)s--%(chat_login_prompt)s', '%(user)s',
'%(chat_passwd_prompt)s', '@%(passwd_file)s',
'TIMEOUT', '%(ppp_timeout)s',
'~--', '')
sys.exit(r)
"""
script_callback_answer_tpl = """
ABORT "%(error_resp)s"
TIMEOUT %(modem_timeout)s
'' "%(callback_init_cmd)s"
"%(init_resp)s" "%(callback_init2_cmd)s"
"%(init_resp)s" "%(vol_cmd)s"
"%(vol_resp)s" ''
TIMEOUT %(dial_timeout)s
SAY '\\nWaiting for incoming call...'
"%(ring_resp)s" "%(answer_cmd)s"
"%(connect_resp)s" ''
TIMEOUT %(ppp_timeout)s
'~--' ''
"""
script_dialin_tpl = """
ABORT "%(error_resp)s"
ABORT "%(nocarrier_resp)s"
ABORT "%(nodialtone_resp)s"
TIMEOUT %(modem_timeout)s
'' "%(init_cmd)s"
"%(init_resp)s" "%(init2_cmd)s"
"%(init_resp)s" "%(vol_cmd)s"
"%(vol_resp)s" ''
TIMEOUT 3600
SAY '\\nWaiting for incoming call...'
"%(ring_resp)s" "%(answer_cmd)s"
TIMEOUT %(dial_timeout)s
"%(connect_resp)s" '\\c'
"%(modem_term)s" '\\c'
"""
_linkname_re = re.compile(r'Using interface (.*)', re.I)
_auth_failed_re = re.compile(
r'(authentication failed|Invalid Login|Login incorrect)', re.I)
_ifconfig_info_re = [
re.compile(r'^\s*inet addr:(?P<local_ip>\d+\.\d+\.\d+\.\d+)\s+' +
r'P-t-P:(?P<remote_ip>\d+\.\d+\.\d+\.\d+)\s+' +
r'Mask:(?P<netmask>\d+\.\d+\.\d+\.\d+)', re.M | re.I),
re.compile(r'^\s*RX (?P<rx>packets.*)$', re.M),
re.compile(r'^\s*TX (?P<tx>packets.*)$', re.M),
re.compile(r'^\s*RX bytes:(?P<rx_bytes>\d+ \(.*?\))\s+' +
r'TX bytes:(?P<tx_bytes>\d+ \(.*?\))', re.M | re.I)]
def __init__(self, account, text_queue,
pppd_cbcp_with_auto_answer = 0,
write_dns_to_resolv_conf = 1):
self.account = account
self.text_queue = text_queue
self._pppd_cbcp_with_auto_answer = pppd_cbcp_with_auto_answer
self._write_dns_to_resolv_conf = write_dns_to_resolv_conf
def __del__(self):
self.stop()
def start(self):
self.stop()
self.stop_reson = None
account = self.account
self._phone_num_index = -1
self._attempt_count = account['redial_attempts']
if not account['auth_type']:
account['auth_type'] = "pap/chap"
if len(account['phone_numbers']) == 0:
account['phone_numbers'] = ['NONUMBER']
self._callback_answer_mode = 0
self._connection_init()
def _make_temp_file(self, perm = 0600):
file_name = os.tempnam()
f = None
try:
f = open(file_name, "w")
f.close()
except IOError:
debug_msg(_("cannot create temp file '%s'") % file_name, 1)
file_name = None
else:
try:
os.chmod(file_name, perm)
f = open(file_name, "w")
except OSError:
debug_msg(_("cannot chagne file permitions on '%s'") % file_name, 2)
return (file_name, f)
def _connection_init(self):
account = self.account
if not self._callback_answer_mode:
self._phone_num_index = (self._phone_num_index + 1) % len(account['phone_numbers'])
if self._attempt_count >= 0: self._attempt_count -= 1
self.speed = None
self.linkname = None
self._chat_temp_file = None
self._passwd_temp_file = None
self._passwdfdr = None
# init common ppp options
po = []
if account["device"]:
po += [_uni2ascii(account["device"])]
if account["speed"]:
po += [_uni2ascii(unicode(account["speed"]))]
if account['lock_device']: po += ["lock"]
if account['modem']: po += ["modem"]
else: po += ["local"]
if account['flow_control'] != 'no-change':
po += [_uni2ascii(account['flow_control'])]
po += ["asyncmap", "00000000"]
if account['default_route']: po += ["defaultroute"]
if not len(account['dns_servers']): po += ["usepeerdns"]
po += ["mtu", str(account["mtu"])]
po += ["mru", str(account["mru"])]
if account["ip"] or account["remote"]:
po += [_uni2ascii(account["ip"] + ":" + account["remote"])]
if account["mask"]: po += ["netmask", _uni2ascii(account['mask'])]
po += ["ipparam", "ppp", "nodetach", "logfd", "1"]
# routine to escape chat parameters
def escape_quotes(s):
"Escape unescaped quotes"
ret, i = ("", 0)
while i < len(s):
if s[i] == '\\':
ret += s[i:i+2]
i += 1
elif s[i] in '"\'':
ret += '\\' + s[i]
else:
ret += s[i]
i += 1
return ret
escape_param = escape_quotes
# chat templete
chat_tpl = ""
# determine what chat templete to use and
# check if we need to escape chat parameters
if self._callback_answer_mode:
chat_tpl = self.script_callback_answer_tpl
elif account['use_script'] == 'predef-noterm':
chat_tpl = self.script_noterm_tpl
elif account['use_script'] == 'predef-auto':
escape_param = lambda s: escape_quotes(s).replace("\\", "\\\\").replace("'", "\\'")
chat_tpl = self.script_auto_tpl
elif account['use_script'] == 'predef-term':
chat_tpl = self.script_term_tpl
elif account['use_script'] == 'custom':
chat_tpl = account['chat_script']
elif account['use_script'] == 'dialin':
chat_tpl = self.script_dialin_tpl
# init chat parameters
chat_params = {}
if chat_tpl != "":
for p in account.keys():
chat_params[p] = escape_param(_uni2ascii(unicode(account[p])))
chat_params.update({"vol_cmd": escape_param(
_uni2ascii(account[("mute_cmd", "low_vol_cmd",
"max_vol_cmd")[account["volume_setting"]]]))})
if not self._callback_answer_mode:
chat_params.update({"path": chestnut_dialer.config.path})
chat_params.update({"phone_number":
escape_param(_uni2ascii(
account['phone_numbers'][self._phone_num_index]))})
# init authentication parameters
if account['remotename'] != '':
po += ["remotename", _uni2ascii(account['remotename'])]
if account['auth_type'] == 'pap/chap':
po += ["user", _uni2ascii(account["user"])]
if account['use_passwordfd']:
self._passwdfdr, passwdfdw = os.pipe()
os.write(passwdfdw, _uni2ascii(account["passwd"]))
os.close(passwdfdw)
po += ["call", "chestnut-dialer",
"passwordfd", str(self._passwdfdr)]
# chack if we need to write password to temporary file
if (chat_tpl != "" and account['use_script'] == 'predef-auto' and
not self._callback_answer_mode):
self._passwd_temp_file, f = self._make_temp_file(0600)
if f:
f.write(_uni2ascii(account["passwd"]))
f.close()
chat_params.update({"passwd_file": self._passwd_temp_file})
# write chat to the temporary file
if chat_tpl != "":
self._chat_temp_file, f = self._make_temp_file(0600)
if f:
f.write(chat_tpl % chat_params)
f.close()
# init connect command
if self._chat_temp_file:
if (account['use_script'] == 'predef-auto' and
not self._callback_answer_mode):
po += ["connect",
chestnut_dialer.config.python + " " + self._chat_temp_file]
else:
po += ["connect", "%s -t %d -e -f %s" %
(chestnut_dialer.config.chat,
account["dial_timeout"], self._chat_temp_file)]
if (account["callback"] and
not self._callback_answer_mode):
po += ["callback", _uni2ascii(account['callback_phone_number'])]
# init user's ppp options
s = _uni2ascii(account['ppp_options'])
i = 0; a = ""
try:
while 1:
while s[i].isspace(): i += 1
if s[i] == '"':
i += 1
while 1:
if s[i] == "\\":
i += 1
if s[i] in ('"', "\\"): a += s[i]
else: a += "\\" + s[i]
elif s[i] == '"': break
else: a += s[i]
i += 1
elif s[i] == "'":
i += 1
while s[i] != "'":
a += s[i]; i += 1
else:
while not s[i].isspace():
a += s[i]; i += 1
po += [a]; a = ""; i += 1
except IndexError:
if a != "": po += [a]
debug_msg(_("PPP options: %s") % str(po), 3)
self._pppd, self._pppd_pty = pty.fork()
if self._pppd < 0:
debug_msg(_("fork() failed"), 1)
elif self._pppd == 0:
os.execv(chestnut_dialer.config.pppd, ["pppd"] + po)
debug_msg(_("pppd pid %d") % self._pppd, 3)
self._no_dialtone = None
self._busy = None
self._no_carrier = None
self._auth_failed = None
self._speed_re = re.compile(
re.escape(_uni2ascii(account['connect_resp'])) +
r'\s+(\d+)(.*)')
self._interface_up = 0
self._pppd_chars = ""
self.active = 1
self.connected = 0
self._resolv_dns = []
def _is_pppd_live(self):
try:
pid, code = os.waitpid(self._pppd, os.WNOHANG)
except OSError: return 0
if pid != 0 and pid == self._pppd:
self.pppd_exit_code = code / 256
return 0
return 1
def iteration(self):
account = self.account
if not self._interface_up:
pppd_kiled = 0
buf = ""
t = time.time()
while time.time() - t < 0.1:
if select.select([self._pppd_pty], [], [], 0.02)[0]:
try: buf += os.read(self._pppd_pty, 1)
except OSError: break
else:
break
if buf == "" and not self._is_pppd_live(): pppd_kiled = 1
if len(buf):
buf = buf.replace("\r", "")
self.text_queue.put(buf)
buf = self._pppd_chars + buf
lines = buf.split("\n")
self._pppd_chars = lines[-1]
lines[-1:] = []
for s in lines:
if not self.speed:
m = self._speed_re.match(s)
if m:
if int(m.group(1)) < account['min_speed']:
debug_msg(_("speed < minimum speed, reconnecting"), 3)
os.kill(self._pppd, signal.SIGTERM)
t = time.time()
while self._is_pppd_live():
if time.time() - t > 3:
try: os.kill(self._pppd, signal.SIGKILL)
except OSError: pass
break
time.sleep(0.1)
else:
self.speed = m.group(1) + m.group(2)
continue
if not self._no_dialtone and s == account['nodialtone_resp']:
self._no_dialtone = 1
continue
if not self._busy and s == account['busy_resp']:
self._busy = 1
continue
if not self._no_carrier and s == account['nocarrier_resp']:
self._no_carrier = 1
continue
if not self._auth_failed and self._auth_failed_re.search(s):
self._auth_failed = 1
continue
if not self.linkname:
m = self._linkname_re.match(s)
if m:
self.linkname = m.group(1)
continue
if self.linkname and re.search(r'^[ \t]+UP',
commands.getoutput(
"%s %s 2> /dev/null" % (chestnut_dialer.config.ifconfig, self.linkname)), re.M):
self._interface_up = 1
self.start_time = int(time.time())
debug_msg(_("connected"), 3)
self._callback_answer_mode = 0
if self._write_dns_to_resolv_conf:
if account['dns_servers']:
self._resolv_dns = map(lambda s: _uni2ascii(s),
account['dns_servers'])
else:
try:
f = open(chestnut_dialer.config.ppp_resolv_conf)
except IOError:
debug_msg(_("cannot open '%s' for reading") %
chestnut_dialer.config.ppp_resolv_conf, 2)
else:
r = re.compile(r'nameserver\s+((\d+\.){3}\d+).*')
for s in f.readlines():
m = r.match(s)
if m:
self._resolv_dns.append(m.group(1))
f.close()
if self._resolv_dns:
debug_msg(_("writing to resolv.conf"), 3)
try:
f = open(chestnut_dialer.config.resolv_conf, "a")
except IOError:
debug_msg(_("cannot open '%s' for appending") %
chestnut_dialer.config.resolv_conf, 2)
else:
f.write("\n")
for dns in self._resolv_dns:
f.write("nameserver %s # %s TEMP ENTRY\n" %
(dns, chestnut_dialer.program_name.upper()))
f.close()
self.connected = 1
if account['connect_program']:
os.system(account['connect_program'])
if not pppd_kiled: return
if self._chat_temp_file:
debug_msg(_("removing chat temp file"), 3)
os.unlink(self._chat_temp_file)
self._chat_temp_file = None
if self._passwd_temp_file:
debug_msg(_("removing password temp file"), 3)
os.unlink(self._passwd_temp_file)
self._passwd_temp_file = None
if self._is_pppd_live(): return
os.close(self._pppd_pty)
if self._passwdfdr:
try: os.close(self._passwdfdr)
except OSError: pass
debug_msg(_("disconnected, pppd exit code %d") %
self.pppd_exit_code, 3)
if (self.pppd_exit_code == 14 and
account['callback'] and
not self._pppd_cbcp_with_auto_answer and
self.stop_reson == None and
not self._callback_answer_mode):
debug_msg(_("going into callback answer mode"), 3)
self._callback_answer_mode = 1
self._connection_init()
return
self._callback_answer_mode = 0
if self._write_dns_to_resolv_conf and self._resolv_dns:
debug_msg(_("restoring resolv.conf"), 3)
try:
f = open(chestnut_dialer.config.resolv_conf, "r+")
except IOError:
debug_msg(_("cannot open '%s' for reading and writing") %
chestnut_dialer.config.resolv_conf, 2)
else:
txt = f.read()
txt = re.sub(r'(?m)^.*# %s TEMP ENTRY$' % chestnut_dialer.program_name.upper(), "", txt)
txt = re.sub(r'\n+', "\n", txt)
f.seek(0)
f.truncate(0)
f.write(txt)
f.close()
if self.connected and account['disconnect_program']:
os.system(account['disconnect_program'])
if self.stop_reson == None:
self.stop_reson = (self._no_dialtone and self.STOP_RESON_NO_DIALTONE
) or (self._busy and self.STOP_RESON_BUSY
) or (self._no_carrier and self.STOP_RESON_NO_CARRIER
) or ((self._auth_failed or
self.pppd_exit_code in (11, 19)) and self.STOP_RESON_AUTH_FAILED
) or self.STOP_RESON_UNKNOWN
if (self.stop_reson != self.STOP_RESON_NORMAL_TERM and
self.pppd_exit_code != 2):
if (self.stop_reson != self.STOP_RESON_NO_DIALTONE or
'no-dialtone' in account['redial_if']) and (
self.stop_reson != self.STOP_RESON_AUTH_FAILED or
'auth-fail' in account['redial_if']):
if ((not self.connected and self._attempt_count != 0) or
(self.connected and account['redial_auto'])):
self.connected = 0
self.stop_reson = None
self._connection_init()
return
self.connected = 0
self.active = 0
def stop(self):
if not self.is_active(): return
self.stop_reson = self.STOP_RESON_NORMAL_TERM
os.kill(self._pppd, signal.SIGTERM)
second_sig = 0
loop_count = 0
while self.active:
time.sleep(0.1)
if loop_count > 25:
if not second_sig:
try: os.kill(self._pppd, signal.SIGKILL)
except OSError: pass
second_sig = 1
else:
if loop_count > 80:
debug_msg(_("unable to kill pppd"), 1)
break
self.iteration()
loop_count += 1
def is_active(self):
return self.active
def is_connected(self):
return self.connected
def get_stop_reson(self):
return self.stop_reson
def get_pppd_exit_code(self):
return self.pppd_exit_code
def get_info(self):
t = int(time.time()) - self.start_time
info = {
'account_name': self.account['name'],
'speed': self.speed,
'time': "%02d:%02d:%02d" % (t / 3600, (t / 60) % 60, t % 60),
'interface': self.linkname}
output = commands.getoutput(chestnut_dialer.config.ifconfig +
" " + self.linkname)
for r in self._ifconfig_info_re:
m = r.search(output)
if m:
grps = m.groupdict("");
for g in grps.keys():
info.update({g:grps[g]})
return info
|