This is Pluto, the webhook server.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

model.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import os, sqlite3, json, urllib2, ssl, urllib, time, subprocess, socket
  2. from flask import render_template_string
  3. from util import *
  4. import secrets
  5. #db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'pluto.db'), check_same_thread = False)
  6. db = sqlite3.connect('/var/www/pluto/pluto.db', check_same_thread = False)
  7. cur = db.cursor()
  8. so = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  9. so.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
  10. so6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
  11. so6.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
  12. class DBError(Exception):
  13. pass
  14. class NoSuchEntity(DBError):
  15. pass
  16. class TooManyEntities(DBError):
  17. pass
  18. class DBObject(object):
  19. __FIELDS__ = ()
  20. __DEFAULTS__ = {}
  21. __TABLE__ = ''
  22. __TYPES__ = {}
  23. AUTO_COMMIT = True
  24. def __init__(self, rowid, *data):
  25. self.rowid = rowid
  26. for idx, field in enumerate(self.__FIELDS__):
  27. default = self.__DEFAULTS__.get(field)
  28. if idx < len(data):
  29. setattr(self, field, data[idx])
  30. else:
  31. setattr(self, field, default)
  32. @classmethod
  33. def create_table(cls):
  34. cur.execute('CREATE TABLE IF NOT EXISTS %(table)s (%(columns)s)'%\
  35. {'table': cls.__TABLE__,
  36. 'columns': ', '.join('%s%s'%(field, ' '+cls.__TYPES__[field] if field in cls.__TYPES__ else '') for field in cls.__FIELDS__)}
  37. )
  38. @classmethod
  39. def create(cls, *data):
  40. row = list(data)
  41. for field in cls.__FIELDS__[len(data):]:
  42. row.append(cls.__DEFAULTS__[field])
  43. cur.execute('INSERT INTO %(table)s VALUES (%(fields)s)'%{
  44. 'table': cls.__TABLE__,
  45. 'fields': ', '.join(['?'] * len(cls.__FIELDS__))
  46. }, row)
  47. if cls.AUTO_COMMIT:
  48. db.commit()
  49. return cls(cur.lastrowid, *row)
  50. def delete(self):
  51. cur.execute('DELETE FROM %(table)s WHERE ROWID=?'%{'table': self.__TABLE__}, (self.rowid,))
  52. if self.AUTO_COMMIT:
  53. db.commit()
  54. def update(self):
  55. cur.execute('UPDATE %(table)s SET %(fields)s WHERE ROWID=?'%{
  56. 'table': self.__TABLE__,
  57. 'fields': ', '.join('%s=?'%(field,) for field in self.__FIELDS__)
  58. }, tuple(getattr(self, field) for field in self.__FIELDS__) + (self.rowid,))
  59. if self.AUTO_COMMIT:
  60. db.commit()
  61. @classmethod
  62. def get(cls, **criteria):
  63. pairs = criteria.items()
  64. keys = [pair[0] for pair in pairs]
  65. values = [pair[1] for pair in pairs]
  66. cur.execute('SELECT ROWID, %(fields)s FROM %(table)s WHERE %(criteria)s'%{
  67. 'table': cls.__TABLE__,
  68. 'fields': ', '.join(cls.__FIELDS__),
  69. 'criteria': ' and '.join('%s=?'%(k,) for k in keys),
  70. }, values)
  71. return [cls(*row) for row in cur]
  72. @classmethod
  73. def all(cls):
  74. cur.execute('SELECT ROWID, %(fields)s FROM %(table)s'%{
  75. 'table': cls.__TABLE__,
  76. 'fields': ', '.join(cls.__FIELDS__),
  77. })
  78. return [cls(*row) for row in cur]
  79. @classmethod
  80. def sorted(cls, by, limit=None):
  81. cur.execute('SELECT ROWID, %(fields)s FROM %(table)s ORDER BY %(by)s %(limit)s'%{
  82. 'table': cls.__TABLE__,
  83. 'fields': ', '.join(cls.__FIELDS__),
  84. 'by': by,
  85. 'limit': ('' if limit is None else 'LIMIT %d'%(limit,)),
  86. })
  87. return [cls(*row) for row in cur]
  88. @classmethod
  89. def get_one(cls, **criteria):
  90. res = cls.get(**criteria)
  91. if len(res) < 1:
  92. raise NoSuchEntity(cls, criteria)
  93. elif len(res) > 1:
  94. raise TooManyEntities(cls, criteria)
  95. return res[0]
  96. def __repr__(self):
  97. return '<%(cls)s(%(table)s %(row)d %(items)s'%{
  98. 'table': self.__TABLE__,
  99. 'cls': type(self).__name__,
  100. 'row': self.rowid,
  101. 'items': ' '.join('%s=%r'%(field, getattr(self, field)) for field in self.__FIELDS__),
  102. }
  103. class Log(DBObject):
  104. __TABLE__ = 'log'
  105. __FIELDS__ = ('time', 'path', 'headers', 'data', 'hooks')
  106. @classmethod
  107. def most_recent(cls, n=None):
  108. return cls.sorted('time DESC', n)
  109. class DebugLog(DBObject):
  110. __TABLE__ = 'debuglog'
  111. __FIELDS__ = ('time', 'path', 'headers', 'data', 'value', 'hook', 'cond', 'act', 'success', 'message')
  112. @classmethod
  113. def most_recent(cls, n=None):
  114. return cls.sorted('time DESC', n)
  115. class Hook(DBObject):
  116. __TABLE__ = 'hooks'
  117. __FIELDS__ = ('name', 'author', 'disabled', 'debugged')
  118. __DEFAULTS__ = {
  119. 'disabled': 0,
  120. 'debugged': 0,
  121. }
  122. def trigger(self, path, headers, data, values, response):
  123. if self.disabled:
  124. return False
  125. conditions = Condition.for_hook(self)
  126. actions = Action.for_hook(self)
  127. for condition in conditions:
  128. result, msg = condition.test_select(path, headers, data, values, response)
  129. if self.debugged:
  130. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), jdumps(values), self.rowid, condition.rowid, None, result, msg)
  131. if not result:
  132. break
  133. else:
  134. for act in actions:
  135. result = act.actuate(path, headers, data, values, response)
  136. if self.debugged:
  137. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), jdumps(values), self.rowid, None, act.rowid, None, result)
  138. if self.debugged:
  139. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), jdumps(values), self.rowid, None, None, True, None)
  140. return True
  141. if self.debugged:
  142. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), jdumps(values), self.rowid, None, None, False, None)
  143. return False
  144. class Condition(DBObject):
  145. __TABLE__ = 'conditions'
  146. __FIELDS__ = ('hook', 'selector', 's1', 's2', 's3', 'test', 't1', 't2', 't3', 'invert')
  147. @classmethod
  148. def for_hook(cls, hook):
  149. return cls.get(hook=hook.rowid)
  150. def get_hook(self):
  151. return Hook.get_one(rowid=self.hook)
  152. def select(self, path, headers, data, values, response):
  153. return getattr(self, 'select_' + self.selector, self.no_select)(path, headers, data, values, response)
  154. def no_select(self, path, headers, data, values, response):
  155. print 'No selector found for', self.selector
  156. return None
  157. def select_header(self, path, headers, data, values, response):
  158. return headers.get(self.s1, '')
  159. def select_JSON(self, path, headers, data, values, response):
  160. if not isinstance(data, dict):
  161. return False
  162. cur = data
  163. for part in self.s1.split('.'):
  164. cur = cur.get(part)
  165. if cur is None:
  166. return False
  167. return str(cur)
  168. def select_path(self, path, headers, data, values, response):
  169. return path
  170. def select_value(self, path, headers, data, values, response):
  171. print values
  172. print self.s1
  173. print values.get(self.s1, '')
  174. return values.get(self.s1, '')
  175. def test_value(self, val):
  176. try:
  177. result = getattr(self, 'test_' + self.test, self.no_test)(val)
  178. except (ValueError, TypeError) as e:
  179. result = (False, "Error: " + str(e))
  180. if self.invert:
  181. result = (not result[0], result[1])
  182. return result
  183. def no_test(self, val):
  184. return False, "No valid test by that name"
  185. def test_equal(self, val):
  186. return str(val) == self.t1, "Compare: %r == %r" % (val, self.t1)
  187. def test_inrange(self, val):
  188. return float(self.t1) <= float(val) <= float(self.t2), "Compare %r <= %r <= %r" % (float(self.t1), float(val), float(self.t2))
  189. def test_truthy(self, val):
  190. return bool(val), "Test: %r" %(val,)
  191. def test_contains(self, val):
  192. return self.t1 in val, "Compare: %r in %r" % (self.t1, val)
  193. def test_select(self, path, headers, data, values, response):
  194. return self.test_value(self.select(path, headers, data, values, response))
  195. class Action(DBObject):
  196. __TABLE__ = 'actions'
  197. __FIELDS__ = ('hook', 'action', 'a1', 'a2', 'a3')
  198. GITLAB_API = 'https://gitlab.cosi.clarkson.edu/api/v3/'
  199. GITLAB_TOKEN = secrets.GITLAB_TOKEN
  200. PROTO = ssl.PROTOCOL_TLSv1_2
  201. @classmethod
  202. def for_hook(cls, hook):
  203. return cls.get(hook=hook.rowid)
  204. def get_hook(self):
  205. return Hook.get_one(rowid=self.hook)
  206. def actuate(self, path, headers, data, values, response):
  207. try:
  208. return getattr(self, 'act_' + self.action, self.no_act)(path, headers, data, values, response)
  209. except (ValueError, TypeError):
  210. pass
  211. def no_act(self, path, headers, data, values, response):
  212. return 'INTERNAL ERROR: ACTION NOT FOUND'
  213. def act_post(self, path, headers, data, values, response):
  214. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  215. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  216. url = render_template_string(self.a1, **args)
  217. postdata = render_template_string(self.a2, **args)
  218. headers = json.loads(render_template_string(self.a3, **args))
  219. print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
  220. req = urllib2.Request(url, postdata, headers)
  221. ctxt = ssl.SSLContext(self.PROTO)
  222. result = urllib2.urlopen(req, context=ctxt)
  223. out = result.read()
  224. #out = None
  225. print 'Complete, got', repr(out)
  226. return out
  227. def act_gitlab(self, path, headers, data, values, response):
  228. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  229. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  230. url = self.GITLAB_API + render_template_string(self.a1, **args)
  231. params = json.loads(render_template_string(self.a2, **args))
  232. headers = json.loads(render_template_string(self.a3, **args))
  233. headers.update({'PRIVATE-TOKEN': self.GITLAB_TOKEN})
  234. postdata = urllib.urlencode(params)
  235. print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
  236. req = urllib2.Request(url, postdata, headers)
  237. ctxt = ssl.SSLContext(self.PROTO)
  238. result = urllib2.urlopen(req, context=ctxt)
  239. out = result.read()
  240. #out = None
  241. print 'Complete, got', repr(out)
  242. return out
  243. def act_system(self, path, headers, data, values, response):
  244. args = {'path': path, 'headers': headers, 'data': data}
  245. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  246. args['escaped_values'] = {k: v.replace('\\', '\\\\').replace('\'', '\\\'') for k, v in
  247. values.items()}
  248. cmd = render_template_string(self.a1, **args)
  249. print "dbg: ", cmd
  250. if not self.a2:
  251. proc = subprocess.Popen(['bash', '-c', cmd])
  252. return 'forked'
  253. else:
  254. try:
  255. return subprocess.check_output(['bash', '-c', cmd], stderr=subprocess.STDOUT)
  256. except subprocess.CalledProcessError as e:
  257. return e.output
  258. def act_udp(self, path, headers, data, values, response):
  259. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  260. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  261. dest = render_template_string(self.a1, **args)
  262. packet = render_template_string(self.a2, **args)
  263. encoding = render_template_string(self.a3, **args)
  264. try:
  265. if encoding in (u'hex', u'base64'):
  266. packet = packet.decode(encoding)
  267. elif encoding == 'input':
  268. packet = str(data)
  269. elif encoding == 'json':
  270. packet = jdumps(data) # XXX HACKS
  271. else:
  272. packet = packet.encode(encoding)
  273. except Exception as e:
  274. return 'failed to encode packet: ' + str(e)
  275. host, _, port = dest.partition(':')
  276. if not _:
  277. return 'illegal specification: no port in destination'
  278. try:
  279. port = int(port)
  280. except ValueError:
  281. return 'illegal port value: ' + port
  282. if port < 0 or port > 65535:
  283. return 'illegal port value: ' + str(port)
  284. try:
  285. res = socket.getaddrinfo(host, port)
  286. except socket.gaierror:
  287. return 'bad hostname:' + host
  288. for fam, tp, proto, canon, addr in res:
  289. if tp == socket.SOCK_DGRAM:
  290. try:
  291. if fam == socket.AF_INET:
  292. so.sendto(packet, addr)
  293. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  294. elif fam == socket.AF_INET6:
  295. so6.sendto(packet, addr)
  296. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  297. except Exception:
  298. pass
  299. return 'no good address family found'
  300. def act_tcp(self, path, headers, data, values, response):
  301. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  302. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  303. dest = render_template_string(self.a1, **args)
  304. packet = render_template_string(self.a2, **args)
  305. encoding = render_template_string(self.a3, **args)
  306. try:
  307. if encoding in (u'hex', u'base64'):
  308. packet = packet.decode(encoding)
  309. elif encoding == 'input':
  310. packet = str(data)
  311. elif encoding == 'json':
  312. packet = jdumps(data) # XXX HACKS
  313. else:
  314. packet = packet.encode(encoding)
  315. except Exception as e:
  316. return 'failed to encode packet: ' + str(e)
  317. host, _, port = dest.partition(':')
  318. if not _:
  319. return 'illegal specification: no port in destination'
  320. try:
  321. port = int(port)
  322. except ValueError:
  323. return 'illegal port value: ' + port
  324. if port < 0 or port > 65535:
  325. return 'illegal port value: ' + str(port)
  326. try:
  327. res = socket.getaddrinfo(host, port)
  328. except socket.gaierror:
  329. return 'bad hostname:' + host
  330. so = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  331. so.settimeout(0.1)
  332. so6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  333. so6.settimeout(0.1)
  334. for fam, tp, proto, canon, addr in res:
  335. if tp == socket.SOCK_STREAM:
  336. try:
  337. if fam == socket.AF_INET:
  338. so.connect(addr)
  339. so.send(packet)
  340. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  341. elif fam == socket.AF_INET6:
  342. so6.connect(addr)
  343. so6.send(packet)
  344. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  345. except Exception:
  346. pass
  347. return 'no good address family found'
  348. def act_set_response(self, path, headers, data, values, response):
  349. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  350. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  351. content = render_template_string(self.a1, **args)
  352. content_type = render_template_string(self.a2, **args)
  353. response.set_data(content)
  354. response.headers['Content-type'] = content_type
  355. return 'response set to "' + content_type + '":\n' + content