globalmap.py 22 KB

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