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 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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', '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, 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 = condition.test_select(path, headers, data, response)
  129. if self.debugged:
  130. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, condition.rowid, None, result, None)
  131. if not result:
  132. break
  133. else:
  134. for act in actions:
  135. result = act.actuate(path, headers, data, response)
  136. if self.debugged:
  137. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, act.rowid, None, result)
  138. if self.debugged:
  139. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, None, True, None)
  140. return True
  141. if self.debugged:
  142. DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), 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, response):
  153. return getattr(self, 'select_' + self.selector, self.no_select)(path, headers, data, response)
  154. def no_select(self, path, headers, data, response):
  155. return None
  156. def select_header(self, path, headers, data, response):
  157. return headers.get(self.s1, '')
  158. def select_JSON(self, path, headers, data, response):
  159. if not isinstance(data, dict):
  160. return False
  161. cur = data
  162. for part in self.s1.split('.'):
  163. cur = cur.get(part)
  164. if cur is None:
  165. return False
  166. return str(cur)
  167. def select_path(self, path, headers, data, response):
  168. return path
  169. def test_value(self, val):
  170. try:
  171. result = getattr(self, 'test_' + self.test, self.no_test)(val)
  172. except (ValueError, TypeError):
  173. result = False
  174. if self.invert:
  175. result = not result
  176. return result
  177. def no_test(self, val):
  178. return False
  179. def test_equal(self, val):
  180. return str(val) == self.t1
  181. def test_inrange(self, val):
  182. return float(self.t1) <= float(val) <= float(self.t2)
  183. def test_truthy(self, val):
  184. return bool(val)
  185. def test_contains(self, val):
  186. return self.t1 in val
  187. def test_select(self, path, headers, data, response):
  188. return self.test_value(self.select(path, headers, data, response))
  189. class Action(DBObject):
  190. __TABLE__ = 'actions'
  191. __FIELDS__ = ('hook', 'action', 'a1', 'a2', 'a3')
  192. GITLAB_API = 'https://gitlab.cosi.clarkson.edu/api/v3/'
  193. GITLAB_TOKEN = secrets.GITLAB_TOKEN
  194. PROTO = ssl.PROTOCOL_TLSv1_2
  195. @classmethod
  196. def for_hook(cls, hook):
  197. return cls.get(hook=hook.rowid)
  198. def get_hook(self):
  199. return Hook.get_one(rowid=self.hook)
  200. def actuate(self, path, headers, data, response):
  201. try:
  202. return getattr(self, 'act_' + self.action, self.no_act)(path, headers, data, response)
  203. except (ValueError, TypeError):
  204. pass
  205. def no_act(self, path, headers, data, response):
  206. return 'INTERNAL ERROR: ACTION NOT FOUND'
  207. def act_post(self, path, headers, data, response):
  208. args = {'path': path, 'headers': headers, 'data': data}
  209. url = render_template_string(self.a1, **args)
  210. postdata = render_template_string(self.a2, **args)
  211. headers = json.loads(render_template_string(self.a3, **args))
  212. print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
  213. req = urllib2.Request(url, postdata, headers)
  214. ctxt = ssl.SSLContext(self.PROTO)
  215. result = urllib2.urlopen(req, context=ctxt)
  216. out = result.read()
  217. #out = None
  218. print 'Complete, got', repr(out)
  219. return out
  220. def act_gitlab(self, path, headers, data, response):
  221. args = {'path': path, 'headers': headers, 'data': data}
  222. url = self.GITLAB_API + render_template_string(self.a1, **args)
  223. params = json.loads(render_template_string(self.a2, **args))
  224. headers = json.loads(render_template_string(self.a3, **args))
  225. headers.update({'PRIVATE-TOKEN': self.GITLAB_TOKEN})
  226. postdata = urllib.urlencode(params)
  227. print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
  228. req = urllib2.Request(url, postdata, headers)
  229. ctxt = ssl.SSLContext(self.PROTO)
  230. result = urllib2.urlopen(req, context=ctxt)
  231. out = result.read()
  232. #out = None
  233. print 'Complete, got', repr(out)
  234. return out
  235. def act_system(self, path, headers, data, response):
  236. args = {'path': path, 'headers': headers, 'data': data}
  237. cmd = render_template_string(self.a1, **args)
  238. if not self.a2:
  239. proc = subprocess.Popen(cmd, shell=True)
  240. return 'forked'
  241. else:
  242. try:
  243. return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
  244. except subprocess.CalledProcessError as e:
  245. return e.output
  246. def act_udp(self, path, headers, data, response):
  247. args = {'path': path, 'headers': headers, 'data': data}
  248. dest = render_template_string(self.a1, **args)
  249. packet = render_template_string(self.a2, **args)
  250. encoding = render_template_string(self.a3, **args)
  251. try:
  252. if encoding in (u'hex', u'base64'):
  253. packet = packet.decode(encoding)
  254. elif encoding == 'input':
  255. packet = str(data)
  256. elif encoding == 'json':
  257. packet = jdumps(data) # XXX HACKS
  258. else:
  259. packet = packet.encode(encoding)
  260. except Exception as e:
  261. return 'failed to encode packet: ' + str(e)
  262. host, _, port = dest.partition(':')
  263. if not _:
  264. return 'illegal specification: no port in destination'
  265. try:
  266. port = int(port)
  267. except ValueError:
  268. return 'illegal port value: ' + port
  269. if port < 0 or port > 65535:
  270. return 'illegal port value: ' + str(port)
  271. try:
  272. res = socket.getaddrinfo(host, port)
  273. except socket.gaierror:
  274. return 'bad hostname:' + host
  275. for fam, tp, proto, canon, addr in res:
  276. if tp == socket.SOCK_DGRAM:
  277. try:
  278. if fam == socket.AF_INET:
  279. so.sendto(packet, addr)
  280. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  281. elif fam == socket.AF_INET6:
  282. so6.sendto(packet, addr)
  283. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  284. except Exception:
  285. pass
  286. return 'no good address family found'
  287. def act_tcp(self, path, headers, data, response):
  288. args = {'path': path, 'headers': headers, 'data': data}
  289. dest = render_template_string(self.a1, **args)
  290. packet = render_template_string(self.a2, **args)
  291. encoding = render_template_string(self.a3, **args)
  292. try:
  293. if encoding in (u'hex', u'base64'):
  294. packet = packet.decode(encoding)
  295. elif encoding == 'input':
  296. packet = str(data)
  297. elif encoding == 'json':
  298. packet = jdumps(data) # XXX HACKS
  299. else:
  300. packet = packet.encode(encoding)
  301. except Exception as e:
  302. return 'failed to encode packet: ' + str(e)
  303. host, _, port = dest.partition(':')
  304. if not _:
  305. return 'illegal specification: no port in destination'
  306. try:
  307. port = int(port)
  308. except ValueError:
  309. return 'illegal port value: ' + port
  310. if port < 0 or port > 65535:
  311. return 'illegal port value: ' + str(port)
  312. try:
  313. res = socket.getaddrinfo(host, port)
  314. except socket.gaierror:
  315. return 'bad hostname:' + host
  316. so = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  317. so.settimeout(0.1)
  318. so6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  319. so6.settimeout(0.1)
  320. for fam, tp, proto, canon, addr in res:
  321. if tp == socket.SOCK_STREAM:
  322. try:
  323. if fam == socket.AF_INET:
  324. so.connect(addr)
  325. so.send(packet)
  326. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  327. elif fam == socket.AF_INET6:
  328. so6.connect(addr)
  329. so6.send(packet)
  330. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  331. except Exception:
  332. pass
  333. return 'no good address family found'
  334. def act_set_response(self, path, headers, data, response):
  335. args = {'path': path, 'headers': headers, 'data': data}
  336. content = render_template_string(self.a1, **args)
  337. content_type = render_template_string(self.a2, **args)
  338. response.set_data(content)
  339. response.headers['Content-type'] = content_type
  340. return 'response set to "' + content_type + '":\n' + content