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.

404 lines
15 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  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, 'values': values}
  245. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  246. cmd = render_template_string(self.a1, **args)
  247. if not self.a2:
  248. proc = subprocess.Popen(cmd, shell=True)
  249. return 'forked'
  250. else:
  251. try:
  252. return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
  253. except subprocess.CalledProcessError as e:
  254. return e.output
  255. def act_udp(self, path, headers, data, values, response):
  256. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  257. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  258. dest = render_template_string(self.a1, **args)
  259. packet = render_template_string(self.a2, **args)
  260. encoding = render_template_string(self.a3, **args)
  261. try:
  262. if encoding in (u'hex', u'base64'):
  263. packet = packet.decode(encoding)
  264. elif encoding == 'input':
  265. packet = str(data)
  266. elif encoding == 'json':
  267. packet = jdumps(data) # XXX HACKS
  268. else:
  269. packet = packet.encode(encoding)
  270. except Exception as e:
  271. return 'failed to encode packet: ' + str(e)
  272. host, _, port = dest.partition(':')
  273. if not _:
  274. return 'illegal specification: no port in destination'
  275. try:
  276. port = int(port)
  277. except ValueError:
  278. return 'illegal port value: ' + port
  279. if port < 0 or port > 65535:
  280. return 'illegal port value: ' + str(port)
  281. try:
  282. res = socket.getaddrinfo(host, port)
  283. except socket.gaierror:
  284. return 'bad hostname:' + host
  285. for fam, tp, proto, canon, addr in res:
  286. if tp == socket.SOCK_DGRAM:
  287. try:
  288. if fam == socket.AF_INET:
  289. so.sendto(packet, addr)
  290. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  291. elif fam == socket.AF_INET6:
  292. so6.sendto(packet, addr)
  293. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  294. except Exception:
  295. pass
  296. return 'no good address family found'
  297. def act_tcp(self, path, headers, data, values, response):
  298. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  299. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  300. dest = render_template_string(self.a1, **args)
  301. packet = render_template_string(self.a2, **args)
  302. encoding = render_template_string(self.a3, **args)
  303. try:
  304. if encoding in (u'hex', u'base64'):
  305. packet = packet.decode(encoding)
  306. elif encoding == 'input':
  307. packet = str(data)
  308. elif encoding == 'json':
  309. packet = jdumps(data) # XXX HACKS
  310. else:
  311. packet = packet.encode(encoding)
  312. except Exception as e:
  313. return 'failed to encode packet: ' + str(e)
  314. host, _, port = dest.partition(':')
  315. if not _:
  316. return 'illegal specification: no port in destination'
  317. try:
  318. port = int(port)
  319. except ValueError:
  320. return 'illegal port value: ' + port
  321. if port < 0 or port > 65535:
  322. return 'illegal port value: ' + str(port)
  323. try:
  324. res = socket.getaddrinfo(host, port)
  325. except socket.gaierror:
  326. return 'bad hostname:' + host
  327. so = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  328. so.settimeout(0.1)
  329. so6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  330. so6.settimeout(0.1)
  331. for fam, tp, proto, canon, addr in res:
  332. if tp == socket.SOCK_STREAM:
  333. try:
  334. if fam == socket.AF_INET:
  335. so.connect(addr)
  336. so.send(packet)
  337. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  338. elif fam == socket.AF_INET6:
  339. so6.connect(addr)
  340. so6.send(packet)
  341. return 'sent to {}: {}'.format(addr, packet.encode('hex'))
  342. except Exception:
  343. pass
  344. return 'no good address family found'
  345. def act_set_response(self, path, headers, data, values, response):
  346. args = {'path': path, 'headers': headers, 'data': data, 'values': values}
  347. args['response'] = {'data': response.get_data(), 'headers': response.headers}
  348. content = render_template_string(self.a1, **args)
  349. content_type = render_template_string(self.a2, **args)
  350. response.set_data(content)
  351. response.headers['Content-type'] = content_type
  352. return 'response set to "' + content_type + '":\n' + content