globalmap.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-"
  3. # vim: set expandtab tabstop=4 shiftwidth=4:
  4. """
  5. This file is part of the XSSer project, https://xsser.03c8.net
  6. Copyright (c) 2010/2020 | psy <epsylon@riseup.net>
  7. xsser is free software; you can redistribute it and/or modify it under
  8. the terms of the GNU General Public License as published by the Free
  9. Software Foundation version 3 of the License.
  10. xsser is distributed in the hope that it will be useful, but WITHOUT ANY
  11. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  13. details.
  14. You should have received a copy of the GNU General Public License along
  15. with xsser; if not, write to the Free Software Foundation, Inc., 51
  16. Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  17. """
  18. import os
  19. from pathlib import Path
  20. import gi
  21. gi.require_version('Gtk', '3.0')
  22. gi.require_version('Gdk', '3.0')
  23. gi.require_version('PangoCairo', '1.0')
  24. from gi.repository import Gtk as gtk
  25. from gi.repository import Gdk as gdk
  26. gdk.threads_init()
  27. from gi.repository import GObject as gobject
  28. from gi.repository import PangoCairo as pangocairo
  29. from gi.repository import GdkPixbuf
  30. from core.reporter import XSSerReporter
  31. from core.curlcontrol import Curl
  32. from collections import defaultdict
  33. from threading import Thread
  34. import traceback
  35. import urllib.request, urllib.parse, urllib.error
  36. import math
  37. import cairo
  38. import cairocffi
  39. import gzip
  40. import time
  41. from PIL import Image
  42. import array
  43. import GeoIP
  44. class PointType(object):
  45. checked = 15
  46. success = 10
  47. failed = 5
  48. crawled = 0
  49. crashsite = -1
  50. crash_color = [0.1,0.1,0.1]
  51. checked_color = [0,0.8,0.8]
  52. failed_color = [0.8,0.0,0.0]
  53. success_color = [0.0,0.0,0.8]
  54. crawl_color = [0.0,0.0,0.0]
  55. def gtkcol(col):
  56. return [int(col[0]*65535),int(col[1]*65535),int(col[2]*65535)]
  57. class MapPoint(object):
  58. def __init__(self, lat, lng, ptype, size, text): # 0, 5, 10, 15, 20 -> 20==checked
  59. self.latitude = lat
  60. self.longitude = lng
  61. self.size = size
  62. self.text = text
  63. self.reports = defaultdict(list)
  64. self.reports[ptype].append(text)
  65. self.type = ptype
  66. if ptype == PointType.crawled:
  67. self.color = crawl_color
  68. elif ptype == PointType.failed:
  69. self.color = failed_color
  70. elif ptype == PointType.success:
  71. self.color = success_color
  72. elif ptype == PointType.checked:
  73. self.color = checked_color
  74. else:
  75. self.color = crawl_color
  76. self.gtkcolor = gtkcol(self.color)
  77. def add_reports(self, report_type, reports):
  78. for report_type in set(reports.keys() + self.reports.keys()):
  79. self.reports[report_type].extend(reports[report_type])
  80. class CrashSite(MapPoint):
  81. def __init__(self, lat, lng, size, desturl):
  82. MapPoint.__init__(self, lat, lng, PointType.crashsite, size, desturl)
  83. class DownloadThread(Thread):
  84. def __init__(self, geomap, parent):
  85. Thread.__init__(self)
  86. self.daemon = True
  87. self._map = geomap
  88. self._parent = parent
  89. def run(self):
  90. geo_db_path = self._map.get_geodb_path()
  91. def reportfunc(current, blocksize, filesize):
  92. percent = min(float(current)/(filesize/float(blocksize)),1.0)
  93. self._parent.report_state('downloading map', percent)
  94. if not os.path.exists(os.path.dirname(geo_db_path)):
  95. os.makedirs(os.path.dirname(geo_db_path))
  96. self._parent.report_state('getting city database', 0.0)
  97. try:
  98. urllib.request.urlretrieve('https://xsser.03c8.net/map/GeoLite2-City.dat.gz',
  99. geo_db_path+'.gz', reportfunc)
  100. except:
  101. try:
  102. urllib.request.urlretrieve('https://xsser.sf.net/map/GeoLite2-City.dat.gz',
  103. geo_db_path+'.gz', reportfunc)
  104. except:
  105. self._parent.report_state('error downloading map', 0.0)
  106. self._map.geomap_failed()
  107. else:
  108. self._parent.report_state('map downloaded (restart XSSer!!!!)', 0.0)
  109. f_in = gzip.open(geo_db_path+'.gz', 'rb')
  110. f_out = open(geo_db_path, 'wb')
  111. f_out.write(f_in.read())
  112. f_in.close()
  113. print('deleting gzipped file')
  114. os.remove(geo_db_path+'.gz')
  115. self._map.geomap_ready()
  116. class GlobalMap(gtk.DrawingArea, XSSerReporter):
  117. def __init__(self, parent, pixbuf, onattack=False):
  118. gtk.DrawingArea.__init__(self)
  119. geo_db_path = self.get_geodb_path()
  120. self._parent = parent
  121. self._pixbuf = pixbuf
  122. self._cache_geo = {}
  123. self.geo = None
  124. self._onattack = onattack
  125. if not os.path.exists(geo_db_path):
  126. self._t = DownloadThread(self, parent)
  127. self._t.start()
  128. else:
  129. self.finish_init()
  130. def geomap_ready(self):
  131. gdk.threads_enter()
  132. gobject.timeout_add(0, self.finish_init)
  133. gdk.threads_leave()
  134. def geomap_failed(self):
  135. gdk.threads_enter()
  136. gobject.timeout_add(0, self.failed_init)
  137. gdk.threads_leave()
  138. def failed_init(self):
  139. if hasattr(self, '_t'):
  140. self._t.join()
  141. delattr(self, '_t')
  142. def finish_init(self):
  143. if hasattr(self, '_t'):
  144. self._t.join()
  145. delattr(self, '_t')
  146. parent = self._parent
  147. geo_db_path = self.get_geodb_path()
  148. Geo = GeoIP.open(geo_db_path, GeoIP.GEOIP_STANDARD)
  149. self.geo = Geo
  150. self.set_has_tooltip(True)
  151. self._max_points = 200
  152. self._lasttime = 0.0
  153. self.context = None
  154. self.mapcontext = None
  155. self._mappixbuf = None
  156. self._selected = []
  157. self._current_text = ["", 0.0]
  158. self._stats = [0,0,0,0,0,0,0]
  159. self.width = self._pixbuf.get_width()
  160. self.height = self._pixbuf.get_height()
  161. self._min_x = 0
  162. self._max_x = self.width
  163. self._drawn_points = []
  164. self._lines = []
  165. self._frozenlines = []
  166. self._points = []
  167. self._crosses = []
  168. self.connect('draw', self.expose)
  169. self.connect("query-tooltip", self.on_query_tooltip)
  170. self.queue_draw()
  171. if not self._onattack:
  172. self.add_test_points()
  173. def get_geodb_path(self):
  174. ownpath = os.path.dirname(os.path.dirname(__file__))
  175. gtkpath = os.path.join(ownpath, 'gtk')
  176. if os.path.exists(os.path.join(gtkpath, 'map/GeoIP.dat')):
  177. return os.path.join(gtkpath, 'map/GeoIP.dat')
  178. else:
  179. home = str(Path.home())
  180. return os.path.join(home, '.xsser', 'GeoIP.dat')
  181. def find_points(self, x, y, distance=9.0):
  182. points = []
  183. self._selected = []
  184. for idx, point in enumerate(self._drawn_points):
  185. d_x = x-point[0]
  186. d_y = y-point[1]
  187. if d_y*d_y+d_x*d_x < distance:
  188. self._points[point[2]].size = 4.0
  189. points.append(self._points[point[2]])
  190. self._selected.append(point[2])
  191. if points:
  192. rect = gdk.Rectangle()
  193. self.queue_draw()
  194. return points
  195. def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
  196. if not self.geo:
  197. return False
  198. points = self.find_points(x, y)
  199. if points:
  200. text = ""
  201. success = []
  202. finalsuccess = []
  203. failures = []
  204. crawls = []
  205. for point in points:
  206. finalsuccess.extend(point.reports[PointType.checked])
  207. success.extend(point.reports[PointType.success])
  208. failures.extend(point.reports[PointType.failed])
  209. crawls.extend(point.reports[PointType.crawled])
  210. if finalsuccess:
  211. text += "<b>browser checked sucesses:</b>\n"
  212. text += "\n".join(map(lambda s: gobject.markup_escape_text(s), finalsuccess))
  213. if failures or success:
  214. text += "\n"
  215. if success:
  216. text += "<b>sucesses:</b>\n"
  217. text += "\n".join(map(lambda s: gobject.markup_escape_text(s), success))
  218. if failures:
  219. text += "\n"
  220. if failures:
  221. text += "<b>failures:</b>\n"
  222. text += "\n".join(map(lambda s: gobject.markup_escape_text(s), failures))
  223. if crawls and not failures and not success:
  224. text += "<b>crawls:</b>\n"
  225. text += "\n".join(map(lambda s: gobject.markup_escape_text(s), crawls))
  226. tooltip.set_markup(str(text))
  227. return True
  228. return False
  229. def add_test_points(self):
  230. self.add_point(0.0, 0.0)
  231. self.add_point(0.0, 5.0)
  232. self.add_point(0.0, 10.0)
  233. self.add_point(0.0, 15.0)
  234. self.add_point(5.0, 0.0)
  235. self.add_point(10.0, 0.0)
  236. self.add_point(15.0, 0.0)
  237. def clear(self):
  238. self._points = []
  239. self._lines = []
  240. self.mapcontext = None
  241. self._frozenlines = []
  242. self._crosses = []
  243. self._stats = [0,0,0,0,0,0,0]
  244. def expose(self, widget, cr):
  245. if not self.mapcontext:
  246. self._mappixbuf = self._pixbuf.copy()
  247. self.mapsurface = cairo.ImageSurface.create_from_png('gtk/images/world.png')
  248. self.mapcontext = cairo.Context(self.mapsurface)
  249. self.context = cr
  250. self.draw_frozen_lines()
  251. self.context.set_source_surface(self.mapsurface)
  252. self.context.rectangle(self._min_x, self._max_x, self.width, self.height)
  253. self.context.fill()
  254. self.context.rectangle(self._min_x, self._max_x, self.width, self.height)
  255. self.context.fill()
  256. self.context.paint()
  257. self.context.set_source_rgb(0,0,0)
  258. self._min_x = 5 # we have the scale at the left for now
  259. self._max_x = 0
  260. if self.geo:
  261. self.draw(self.context)
  262. return False
  263. def add_point(self, lng, lat, point_type=PointType.crawled, desturl="testpoint"):
  264. map_point = MapPoint(lat, lng, point_type, 5.0, desturl)
  265. map_point.x, map_point.y = self.plot_point(lat, lng)
  266. self._points.append(map_point)
  267. def add_cross(self, lng, lat, col=[0,0,0], desturl="testpoint"):
  268. for a in self._crosses:
  269. if a.latitude == lat and a.longitude == lng:
  270. return
  271. crash_site = CrashSite(lat, lng, 5.0, desturl)
  272. crash_site.x, crash_site.y = self.plot_point(lat, lng)
  273. self.adjust_bounds(crash_site.x, crash_site.y)
  274. self._crosses.append(crash_site)
  275. self.queue_redraw()
  276. def insert_point(self, lng, lat, col=[0,0,0], desturl="testpoint"):
  277. self._points.insert(0, MapPoint(lat, lng, point_type, 5.0, desturl))
  278. def _preprocess_points(self):
  279. newpoints = defaultdict(list)
  280. for point in self._points:
  281. key = (point.latitude, point.longitude)
  282. newpoints[key].append(point)
  283. self._points = []
  284. for points in newpoints.values():
  285. win_type = points[0]
  286. win_size = points[0]
  287. for point in points[1:]:
  288. if point.type > win_type.type:
  289. win_type = point
  290. if point.size > win_type.size:
  291. win_size = point
  292. self._points.append(win_type)
  293. if win_type != win_size:
  294. self._points.append(win_size)
  295. for point in points:
  296. if not point in [win_size, win_type]:
  297. win_type.add_reports(point.type, point.reports)
  298. if len(self._points) > self._max_points:
  299. self._points = self._points[:self._max_points]
  300. def draw_frozen_lines(self):
  301. for line in self._lines[len(self._frozenlines):]:
  302. if line[4] <= 0.5:
  303. self.draw_line(self.mapcontext, line)
  304. self._frozenlines.append(line)
  305. def draw(self, context, failures=True):
  306. self._preprocess_points()
  307. if self._lasttime == 0:
  308. self._lasttime = time.time()-0.04
  309. currtime = time.time()
  310. timepassed = currtime - self._lasttime
  311. redraw = False
  312. if failures:
  313. self._drawn_points = []
  314. for cross in reversed(self._crosses):
  315. if cross.size > 0.1:
  316. cross.size -= timepassed*2
  317. else:
  318. self._crosses.remove(cross)
  319. if cross.size > 0.1:
  320. redraw = True
  321. self.draw_cross(cross)
  322. for line in reversed(self._lines[len(self._frozenlines):]):
  323. if line[4] > 0.5:
  324. line[4] -= timepassed*2
  325. if line[4] > 0.5:
  326. redraw = True
  327. self.draw_line(self.context, line)
  328. for idx, point in enumerate(self._points):
  329. if point.type >= PointType.success:
  330. if failures:
  331. continue
  332. else:
  333. if not failures:
  334. continue
  335. if point.size > 1.0 and not idx in self._selected:
  336. point.size -= timepassed*2
  337. redraw = True
  338. elif point.size < 1.0:
  339. point.size = 1.0
  340. self.draw_point(point)
  341. x = point.x
  342. y = point.y
  343. self.adjust_bounds(x, y)
  344. self._drawn_points.append([x, y, idx])
  345. stat_f = 1.0
  346. if failures:
  347. mp = self._max_points
  348. self.draw_bar((-45,-160,crawl_color,(self._stats[0]%mp)*stat_f))
  349. self.draw_bar((-45,-155,failed_color,(self._stats[1]%mp)*stat_f))
  350. self.draw_bar((-45,-150,success_color,(self._stats[2]%mp)*stat_f))
  351. self.draw_bar((-45,-145,checked_color,(self._stats[3]%mp)*stat_f))
  352. if int(self._stats[0] / mp):
  353. self.draw_bar((-46,-160,crawl_color,-2-(self._stats[0]/mp)*stat_f))
  354. if int(self._stats[1] / mp):
  355. self.draw_bar((-46,-155,failed_color,-2-(self._stats[1]/mp)*stat_f))
  356. if int(self._stats[2] / mp):
  357. self.draw_bar((-46,-150,success_color,-2-(self._stats[2]/mp)*stat_f))
  358. if int(self._stats[3] / mp):
  359. self.draw_bar((-46,-145,checked_color,-2-(self._stats[3]/mp)*stat_f))
  360. self.draw(context, False)
  361. else:
  362. if self._current_text[1] > 0.0:
  363. self.draw_text(100, self.height-50, self._current_text[0])
  364. self._current_text[1] -= timepassed*4
  365. self._lasttime = currtime
  366. if redraw:
  367. self.queue_redraw()
  368. def adjust_bounds(self, x, y):
  369. if x-20 < self._min_x:
  370. self._min_x = x-20
  371. elif x+20 > self._max_x:
  372. self._max_x = x+20
  373. def draw_text(self, x, y, text):
  374. self.context.save()
  375. self.context.move_to(x, y)
  376. v = (5.0-self._current_text[1])/5.0
  377. self.context.scale(0.1+max(v, 1.0), 0.1+max(v, 1.0))
  378. self.context.set_source_rgb(*gtkcol((v,)*3))
  379. u = urllib.parse.urlparse(text)
  380. self.context.show_text(u.netloc)
  381. self.context.restore()
  382. def draw_bar(self, point):
  383. if point[3]:
  384. self.context.save()
  385. x, y = self.plot_point(point[0], point[1])
  386. self.context.set_source_rgb(*point[2])
  387. self.context.rectangle(x, y, 5, -(2.0+point[3]))
  388. self.context.fill()
  389. self.context.restore()
  390. return x, y
  391. def draw_line(self, context, line):
  392. if line[4]:
  393. context.save()
  394. x, y = self.plot_point(line[0], line[1])
  395. x2, y2 = self.plot_point(line[2], line[3])
  396. self.adjust_bounds(x, y)
  397. self.adjust_bounds(x2, y2)
  398. context.set_line_width(1.0)
  399. context.set_source_rgba(0.0, 0.0, 0.0, float(line[4])/5.0)
  400. context.move_to(x, y)
  401. context.rel_line_to(x2-x, y2-y)
  402. context.stroke()
  403. context.restore()
  404. def draw_point(self, point):
  405. if point.size:
  406. self.context.save()
  407. self.context.set_source_rgb(*point.gtkcolor)
  408. self.context.translate(point.x, point.y)
  409. self.context.arc(0.0, 0.0, 2.4*point.size, 0, 2*math.pi)
  410. self.context.close_path()
  411. self.context.fill()
  412. self.context.restore()
  413. def draw_cross(self, point):
  414. if point.size:
  415. self.context.save()
  416. self.context.translate(point.x, point.y)
  417. self.context.rotate(point.size)
  418. self.context.set_line_width(0.8*point.size)
  419. self.context.set_source_rgb(*point.gtkcolor)
  420. self.context.move_to(-3*point.size, -3*point.size)
  421. self.context.rel_line_to(6*point.size, 6*point.size)
  422. self.context.stroke()
  423. self.context.move_to(-3*point.size, +3*point.size)
  424. self.context.rel_line_to(6*point.size, -6*point.size)
  425. self.context.stroke()
  426. self.context.restore()
  427. def get_latlon_fromurl(self, url):
  428. parsed_url = urlparse.urlparse(url)
  429. split_netloc = parsed_url.netloc.split(":")
  430. if len(split_netloc) == 2:
  431. server_name, port = split_netloc
  432. else:
  433. server_name = parsed_url.netloc
  434. port = None
  435. if server_name in self._cache_geo:
  436. return self._cache_geo[server_name]
  437. Geodata = self.geo.record_by_name(server_name)
  438. if Geodata:
  439. country_name = Geodata['country_name']
  440. longitude = Geodata['longitude']
  441. latitude = Geodata['latitude']
  442. self._cache_geo[server_name] = (latitude, longitude)
  443. return latitude, longitude
  444. def start_attack(self):
  445. self.clear()
  446. def queue_redraw(self):
  447. rect = gdk.Rectangle()
  448. self.queue_draw()
  449. def mosquito_crashed(self, dest_url, reason):
  450. self._current_text = [dest_url, 5.0]
  451. self._stats[4] += 1
  452. try:
  453. lat, lon = self.get_latlon_fromurl(dest_url)
  454. except:
  455. return
  456. self.add_cross(lon, lat, crash_color, dest_url)
  457. gdk.threads_enter()
  458. self.queue_redraw()
  459. gdk.threads_leave()
  460. def add_checked(self, dest_url):
  461. self._current_text = [dest_url, 5.0]
  462. self._stats[3] += 1
  463. try:
  464. lat, lon = self.get_latlon_fromurl(dest_url)
  465. except:
  466. return
  467. self.add_point(lon, lat, PointType.checked, dest_url)
  468. gdk.threads_enter()
  469. self.queue_redraw()
  470. gdk.threads_leave()
  471. def add_success(self, dest_url):
  472. self._current_text = [dest_url, 5.0]
  473. self._stats[2] += 1
  474. try:
  475. lat, lon = self.get_latlon_fromurl(dest_url)
  476. except:
  477. return
  478. self.add_point(lon, lat, PointType.success, dest_url)
  479. gdk.threads_enter()
  480. self.queue_redraw()
  481. gdk.threads_leave()
  482. def add_failure(self, dest_url):
  483. self._current_text = [dest_url, 5.0]
  484. self._stats[1] += 1
  485. try:
  486. lat, lon = self.get_latlon_fromurl(dest_url)
  487. except:
  488. return
  489. self.add_point(lon, lat, PointType.failed, dest_url)
  490. gdk.threads_enter()
  491. self.queue_redraw()
  492. gdk.threads_leave()
  493. def add_link(self, orig_url, dest_url):
  494. try:
  495. lat, lon = self.get_latlon_fromurl(orig_url)
  496. except:
  497. return
  498. try:
  499. d_lat, d_lon = self.get_latlon_fromurl(dest_url)
  500. except:
  501. return
  502. if lat == d_lat and lon == d_lon:
  503. return
  504. for a in self._lines:
  505. if a[0] == lat and a[1] == lon and a[2] == d_lat and a[3] == d_lon:
  506. return
  507. self._lines.append([lat, lon, d_lat, d_lon, 0.5])
  508. def start_crawl(self, dest_url):
  509. self._current_text = [dest_url, 5.0]
  510. self._stats[0] += 1
  511. try:
  512. lat, lon = self.get_latlon_fromurl(dest_url)
  513. except:
  514. return
  515. self.add_point(lon, lat, PointType.crawled, dest_url)
  516. gdk.threads_enter()
  517. self.queue_redraw()
  518. gdk.threads_leave()
  519. def plot_point_mercator(self, lat, lng):
  520. longitude_shift = -23
  521. map_width = self.width
  522. map_height = self.height
  523. y_pos = -1
  524. x = int((map_width * (180.0 + lng) / 360.0) + longitude_shift) % map_width
  525. lat = lat * math.pi / 180; # convert from degrees to radians
  526. y = math.log(math.tan((lat/2.0) + (math.pi/4.0)))
  527. y = (map_height / 2.0) - (map_width * y / (2.0*math.pi)) + y_pos
  528. return x, y
  529. def plot_point_mercatormiller(self, lat, lng):
  530. longitude_shift = 0
  531. map_width = self.width
  532. map_height = self.height
  533. y_pos = 70
  534. x = int((map_width * (180.0 + lng) / 360.0) + longitude_shift) % map_width
  535. lat = lat * math.pi / 180.0; # convert from degrees to radians
  536. y = 1.25*math.log(math.tan((lat/2.5) + (math.pi/4.0)))
  537. y = (map_height / 2.0) - (map_width * y / (2.0*math.pi)) + y_pos
  538. return x, y
  539. def plot_point_equirectangular(self, lat, lng):
  540. longitude_shift = -23
  541. map_width = self.width
  542. map_height = self.height
  543. y_pos = 0
  544. magic_factor = 1.1
  545. x = int((map_width * (180.0 + lng) / 360.0) + longitude_shift) % map_width
  546. y = int((map_height / 2.0) - int((map_height * (lat) / 180.0)*magic_factor))
  547. return x,y
  548. def plot_point(self, lat, lng):
  549. x, y = self.plot_point_equirectangular(lat, lng)
  550. if x-20 < self._min_x:
  551. self._min_x = x-20
  552. if x+20 > self._max_x:
  553. self._max_x = x+20
  554. return x, y