PageRenderTime 40ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/sagator-1.2.3/srv/webq.py

#
Python | 307 lines | 285 code | 8 blank | 14 comment | 5 complexity | e2b7d7b4f143e7e0232ef6d2ea147063 MD5 | raw file
Possible License(s): GPL-2.0
  1. '''
  2. webq.py - An service for sagator's quarantine over HTTP.
  3. (c) 2005-2009 Jan ONDREJ (SAL) <ondrejj(at)salstar.sk>
  4. This program is free software; you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation; either version 2 of the License, or
  7. (at your option) any later version.
  8. '''
  9. import re, db, gettext, avlib
  10. from aglib import *
  11. from avir.basic import const
  12. from avlib import trans_class, _
  13. from BaseHTTPServer import HTTPServer
  14. from SimpleHTTPServer import SimpleHTTPRequestHandler
  15. from cgi import parse_qs
  16. from crypt import crypt
  17. import base64, binascii
  18. __all__ = ['webq']
  19. # load neccessary libraries for chroot
  20. crypt("sagator", '$1$sagator$')
  21. class condition:
  22. def __init__(self, WA):
  23. self.WA=WA
  24. def like(self, key, value):
  25. if not value:
  26. return None
  27. if value[:1]=='!':
  28. self.WA.append(value[1:])
  29. return key+" NOT LIKE %s"
  30. else:
  31. self.WA.append(value)
  32. return key+" LIKE %s"
  33. def date(self, key, value, dir):
  34. if not value:
  35. return None
  36. self.WA.append(value)
  37. return "%s %s %%s" % (key, dir)
  38. def eq(self, key, user):
  39. if not user:
  40. return None
  41. if user[:1]=='!':
  42. oper="!="
  43. user=user[1:]
  44. else:
  45. oper="="
  46. self.WA.append(user)
  47. return key+oper+'%s'
  48. class webq(ServiceTCPServer,HTTPServer):
  49. '''
  50. Web service for sagator's quaratine access.
  51. This service can be used to access email collected by sagator via
  52. web interface.
  53. Requirements: python-genshi-0.4 or higher
  54. Usage: webq(host='0.0.0.0', port=8008, db, log='/var/log/sagator/webq.log',
  55. scanner, userconv)
  56. Where: host is an string, which defines IP address to bind,
  57. default: 0.0.0.0
  58. port is an integer, which defines tcp port to listen, default: 8008
  59. db is a database connection. For description see Databases.txt.
  60. log is defining a log file name, by default /var/log/sagator/webq.log
  61. scanner is a scanner to use for checking (only one scanner
  62. can be used here and it must be a buffer scanner!)
  63. userconv is an array, which defines regular expression
  64. and substitution strings. Usernames from login prompt
  65. are marched against this regular expression and substitued
  66. by substitution string.
  67. It is recommended to use apache mod_proxy module to redirect standard
  68. web traffic from port 80 to webq()'s 8008. For example:
  69. ProxyPass /webq http://localhost:8008
  70. ProxyPassReverse /webq http://localhost:8008
  71. Example: See default config file for example.
  72. New in version 0.9.0.
  73. '''
  74. name='webq()'
  75. def __init__(self, host='0.0.0.0', port=8008, db=None,
  76. log='/var/log/sagator/webq.log',
  77. scanner=const(0),
  78. userconv=['^(.*)$','\\1']):
  79. self.BINDTO=(host, port)
  80. self.LOGFILE=log
  81. self.SCANNERS=[scanner]
  82. self.WORK_DIR=os.path.dirname(os.path.abspath(avlib.__file__))
  83. self.WEB_ROOT=os.path.join(self.WORK_DIR, 'srv/web')
  84. self.REQUEST_HANDLER=webq_request_handler
  85. self.REQUEST_HANDLER.ENV={
  86. 'gettext': _,
  87. 'DB': db,
  88. 'SCANNER': scanner,
  89. 'REQUESTS': {},
  90. 'REQ': {},
  91. 'LANGS': ['en_US', 'sk_SK'],
  92. 'condition': condition,
  93. 'check_passwd': self.check_passwd,
  94. 'change_passwd': self.change_passwd
  95. }
  96. from genshi.template import TemplateLoader
  97. # email parser is required to run in chroot
  98. import email
  99. email.message_from_string('') # ignore output, just load module
  100. self.REQUEST_HANDLER.load_template = TemplateLoader().load
  101. self.REQUEST_HANDLER.USERCONV_REG = re.compile(userconv[0])
  102. self.REQUEST_HANDLER.USERCONV_REPL = userconv[1]
  103. try:
  104. import cracklib
  105. except ImportError, err:
  106. try:
  107. # debian hack to load renamed cracklib
  108. import crack as cracklib
  109. except ImportError:
  110. cracklib = None
  111. debug.echo(1, "webq(): Cracklib import failed (%s), "
  112. "disabling cracklib functionality." % err)
  113. self.cracklib = cracklib
  114. if cracklib:
  115. self.cracklib_trans = trans_class('cracklib')
  116. def test_scanners(self, scanner):
  117. ServiceTCPServer.test_scanners(self, self.SCANNERS)
  118. # copy all templates
  119. import shutil
  120. try:
  121. import srv.web
  122. except ImportError:
  123. debug.echo(1, 'webq(): ERROR: webq not installed, '
  124. 'please install sagator-webq package')
  125. raise
  126. webqsource=os.path.abspath(os.path.dirname(srv.web.__file__))
  127. webqtarget=safe.fn(self.WEB_ROOT)
  128. if webqsource!=webqtarget:
  129. try:
  130. if not os.path.isdir(webqtarget):
  131. debug.echo(9, "Making webq chroot directory: ", webqtarget)
  132. os.makedirs(webqtarget)
  133. for f in os.listdir(webqsource):
  134. if os.path.isfile(os.path.join(webqsource, f)):
  135. debug.echo(9, "Copying webq file: ", f)
  136. shutil.copy(os.path.join(webqsource, f),
  137. os.path.join(webqtarget, f))
  138. except OSError, e:
  139. debug.echo(9, "OSError: %s" % e)
  140. def serve_forever(self):
  141. if safe.fn('/usr')=='/usr':
  142. os.chdir(self.WORK_DIR)
  143. sys.path.insert(0, 'srv/web')
  144. import srv.web # reimport for chroot
  145. self.sighup(0,0) # redirect logs
  146. HTTPServer.serve_forever(self)
  147. def sighup(self,sn,stack):
  148. if debug.logfile=="-":
  149. return
  150. # reopen logs
  151. os.close(1)
  152. os.open(self.LOGFILE, os.O_CREAT|os.O_WRONLY|os.O_APPEND, 0640)
  153. os.dup2(1,2) # copy to stderr fd
  154. def check_passwd(self, DB, login, old):
  155. passwd, perms, lang, showrows = DB.query(
  156. "SELECT pass,perms,lang,showrows "
  157. "FROM webaccess WHERE email=%s",
  158. [login]
  159. )[0]
  160. if passwd!=crypt(old, passwd):
  161. return dict()
  162. return dict(
  163. REMOTE_LOGIN = login,
  164. PERMS = perms,
  165. LANG = lang or "en_US",
  166. SHOW_ROWS = showrows or 50
  167. )
  168. def change_passwd(self, DB, login, old, new, retyped, lang='C'):
  169. if not new:
  170. return ""
  171. if not self.check_passwd(DB, login, old):
  172. return "Wrong password!"
  173. if self.cracklib:
  174. try:
  175. e = self.cracklib.FascistCheck(new)
  176. except ValueError, e:
  177. pass
  178. if e and e!=new:
  179. self.cracklib_trans.set_lang(lang)
  180. return _('Password %s.') % self.cracklib_trans.gettext(str(e))
  181. if new != retyped:
  182. return "New and retyped passwords does not match!"
  183. DB.execute("UPDATE webaccess SET pass=%s WHERE email=%s",
  184. [crypt(new, '$1$'+randomchars(8)), login])
  185. return "Password changed successfully."
  186. class webq_request_handler(SimpleHTTPRequestHandler):
  187. def do_POST(self):
  188. return self.send_head()
  189. def send_head(self):
  190. path = self.path
  191. i = path.rfind('?')
  192. if i>=0:
  193. path, query = path[:i], path[i+1:]
  194. else:
  195. query = ''
  196. if not path.strip('/'):
  197. path = '/index.html'
  198. scriptfile = self.translate_path("srv/web/"+path)
  199. if not os.path.exists(scriptfile):
  200. self.send_error(404, "No such template (%r)" % scriptfile)
  201. return
  202. if not os.path.isfile(scriptfile):
  203. self.send_error(403, "Template is not a plain file (%r)" % path)
  204. return
  205. ns=self.ENV.copy()
  206. if query:
  207. ns['QUERY_STRING'] = query
  208. ns['REQUESTS'].update(parse_qs(query, True))
  209. length = int(self.headers.getheader('content-length') or '0')
  210. if length > 0:
  211. data=self.rfile.read(length)
  212. ns['REQUESTS'].update(parse_qs(data, True))
  213. for key,value in ns['REQUESTS'].items():
  214. ns['REQ'][key]=value[0].decode('UTF-8')
  215. ns['CONTENT_LENGTH'] = length
  216. ns['REMOTE_ADDR'] = self.client_address[0]
  217. ns['REMOTE_USER'] = ''
  218. ns['REMOTE_LOGIN'] = ''
  219. ns['PERMS'] = ''
  220. ns['LANG'] = 'C'
  221. ns.update(self.check_auth())
  222. ns['gettext'].set_lang(ns['LANG'])
  223. if not ns.get('REMOTE_USER'):
  224. self.send_response(401, "Authorization Required")
  225. self.send_header("WWW-Authenticate", 'Basic realm="Restricted access"')
  226. self.end_headers()
  227. self.wfile.write("Authorization Required")
  228. return
  229. try:
  230. template = self.load_template(scriptfile)
  231. s = template.generate(**ns).render('xhtml')
  232. self.send_response(200, "Script output follows")
  233. self.send_header("Content-type", "text/html; charset=UTF-8")
  234. self.send_header("Content-Length", str(len(s)))
  235. self.end_headers()
  236. self.wfile.write(s)
  237. except Exception, e:
  238. import traceback
  239. s=traceback.format_exc()
  240. self.send_response(500, "Internal server error!")
  241. self.send_header("Content-type", "text/plain; charset=UTF-8")
  242. self.send_header("Content-Length", str(len(s)))
  243. self.end_headers()
  244. self.wfile.write(s)
  245. def check_auth(self):
  246. '''
  247. Check authorization request.
  248. Return value is an empty dictionary, if authorization fails or an
  249. namespace update on success.
  250. '''
  251. env={}
  252. authorization = self.headers.getheader("authorization")
  253. if authorization:
  254. authorization = authorization.split()
  255. if len(authorization) == 2:
  256. env['AUTH_TYPE'] = authorization[0]
  257. if authorization[0].lower() == "basic":
  258. try:
  259. authorization = base64.decodestring(authorization[1])
  260. except binascii.Error:
  261. pass
  262. else:
  263. authorization = authorization.split(':')
  264. if len(authorization) == 2:
  265. env.update(self.check_pass(authorization))
  266. return env
  267. def check_pass(self, auth):
  268. '''
  269. Check user password. Parameter auth is an array of:
  270. ['login','plain text password']
  271. Return value is an empty dictionary, if authorization fails,
  272. or an namespace update on success.
  273. '''
  274. try:
  275. passwd,perms,lang,showrows = self.ENV['DB'].query(
  276. "SELECT pass,perms,lang,showrows FROM webaccess WHERE email=%s",
  277. auth[:1]
  278. )[0]
  279. except IndexError:
  280. return {} # no record found for this login
  281. cryptpass = crypt(auth[1], passwd)
  282. if passwd!=cryptpass:
  283. return {}
  284. return {
  285. 'REMOTE_USER': self.USERCONV_REG.sub(self.USERCONV_REPL, auth[0]),
  286. 'REMOTE_LOGIN': auth[0],
  287. 'PERMS': perms,
  288. 'LANG': lang or "en_US",
  289. 'SHOW_ROWS': showrows or 50
  290. }