Skip to content

Commit 1000ad1

Browse files
author
ST47
committed
Convert fully to Flask on python3.7 rather than lighttpd, various related cleanup
1 parent 1c4234e commit 1000ad1

File tree

7 files changed

+325
-4
lines changed

7 files changed

+325
-4
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ docs/_build/
5858
.ssh/
5959
.vim/
6060
.viminfo
61-
rebuild_venv.sh
6261
replica.my.cnf
6362
service.manifest
6463
venv/
64+
venv3.5/
65+
www/python/venv/
66+
ipinfo_token
67+
GeoLite2-City*

rebuild_geolocation.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
wget -O geolite.tar.gz "PASTE_PERSONAL_DOWNLOAD_URL_FOR_GEOLITE_CITY_GZIP"
2+
tar -zxvf geolite.tar.gz
3+
rm geolite.tar.gz

rebuild_venv.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cd ~/www/python/
2+
rm -rf venv
3+
python3.7 -m virtualenv -p /usr/bin/python3.7 venv
4+
source ~/www/python/venv/bin/activate
5+
pip install -r ~/www/python/src/requirements.txt

requirements.txt

Lines changed: 0 additions & 3 deletions
This file was deleted.

start_webservice.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
webservice python3.7 start

www/python/src/app.py

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#! /usr/bin/env python
2+
import sys
3+
import six
4+
from ipwhois import IPWhois, WhoisLookupError
5+
import cgitb
6+
import os
7+
import re
8+
from six.moves import urllib
9+
import cgi
10+
import json
11+
import requests
12+
import socket
13+
import geoip2.database
14+
from flask import Flask, request
15+
16+
PROJECT = 'whois-referral'
17+
SITE = '//'+PROJECT+'.toolforge.org/'
18+
LOGDIR = '/data/project/'+PROJECT+'/logs'
19+
20+
PROVIDERS = {
21+
'ARIN': lambda x: 'http://whois.arin.net/rest/ip/' + urllib.parse.quote(x),
22+
'RIPENCC': lambda x: 'https://apps.db.ripe.net/search/query.html?searchtext=%s#resultsAnchor' % urllib.parse.quote(x),
23+
'AFRINIC': lambda x: 'http://afrinic.net/cgi-bin/whois?searchtext=' + urllib.parse.quote(x),
24+
'APNIC': lambda x: 'http://wq.apnic.net/apnic-bin/whois.pl?searchtext=' + urllib.parse.quote(x),
25+
'LACNIC': lambda x: 'http://lacnic.net/cgi-bin/lacnic/whois?lg=EN&query=' + urllib.parse.quote(x)
26+
}
27+
28+
TOOLS = {
29+
'Stalktoy': lambda x: 'https://tools.wmflabs.org/meta/stalktoy/' + x,
30+
'GlobalContribs': lambda x: 'https://tools.wmflabs.org/guc/index.php?user=%s&blocks=true' % x,
31+
'ProxyChecker': lambda x: 'https://ipcheck.toolforge.org/index.php?ip=%s' % x,
32+
'Geolocation': lambda x: 'https://whatismyipaddress.com/ip/%s' % x,
33+
}
34+
35+
geolite_file = '/data/project/'+PROJECT+'/GeoLite2-City_20201013/GeoLite2-City.mmdb'
36+
geoip_reader = None
37+
if os.path.exists(geolite_file):
38+
geoip_reader = geoip2.database.Reader(geolite_file)
39+
40+
ipinfo_file = '/data/project/'+PROJECT+'/ipinfo_token'
41+
ipinfo_token = None
42+
if os.path.exists(ipinfo_file):
43+
try:
44+
f = open(ipinfo_file)
45+
ipinfo_token = f.read().strip()
46+
except:
47+
pass
48+
49+
50+
def order_keys(x):
51+
keys = dict((y, x) for (x, y) in enumerate([
52+
'geolite2', 'geo_ipinfo',
53+
'asn_registry', 'asn_country_code', 'asn_cidr', 'query',
54+
'referral', 'nets', 'asn', 'asn_date',
55+
'name', 'description', 'address',
56+
'city', 'state', 'country', 'postal_code',
57+
'cidr', 'range', 'created', 'updated', 'handle', 'parent_handle',
58+
'ip_version', 'start_address', 'end_address',
59+
'abuse_emails', 'tech_emails', 'misc_emails']))
60+
if x in keys:
61+
return '0_%04d' % keys[x]
62+
else:
63+
return '1_%s' % x
64+
65+
66+
def lookup(ip, rdap=False):
67+
obj = IPWhois(ip)
68+
if rdap:
69+
return obj.lookup_rdap(asn_methods=['dns', 'whois', 'http'])
70+
else:
71+
try:
72+
ret = obj.lookup_whois(get_referral=True, asn_methods=['dns', 'whois', 'http'])
73+
except WhoisLookupError:
74+
ret = obj.lookup_whois(asn_methods=['dns', 'whois', 'http'])
75+
# remove some fields that clutter
76+
for x in ['raw', 'raw_referral']:
77+
ret.pop(x, None)
78+
return ret
79+
80+
81+
def format_new_lines(s):
82+
return s.replace('\n', '<br/>')
83+
84+
85+
def format_table(dct, target):
86+
if isinstance(dct, six.string_types):
87+
return format_new_lines(dct)
88+
if isinstance(dct, list):
89+
return '\n'.join(format_table(x, target) for x in dct)
90+
ret = '<div class="table-responsive"><table class="table table-condensed"><tbody>'
91+
for (k, v) in sorted(dct.items(), key=lambda x: order_keys(x[0])):
92+
if v is None or len(v) == 0 or v == 'NA' or v == 'None':
93+
if k in ('referral',):
94+
continue
95+
ret += '<tr class="text-muted"><th>%s</th><td>%s</td></tr>' % (k, v)
96+
elif isinstance(v, six.string_types):
97+
if k == 'asn_registry' and v.upper() in PROVIDERS:
98+
ret += '<tr><th>%s</th><td><a href="%s"><span class="glyphicon glyphicon-link"></span>%s</a></td></tr>' % (
99+
k, PROVIDERS[v.upper()](target), v.upper()
100+
)
101+
elif k == 'asn':
102+
ret += '<tr><th>%s</th><td><a href="https://tools.wmflabs.org/isprangefinder/hint.php?type=asn&range=%s">%s</a></td></tr>' % (
103+
k, v, v
104+
)
105+
else:
106+
ret += '<tr><th>%s</th><td>%s</td></tr>' % (
107+
k, format_new_lines(v)
108+
)
109+
else:
110+
ret += '<tr><th>%s</th><td>%s</td></tr>' % (k, format_table(v, target))
111+
ret += '</tbody></table></div>'
112+
return ret
113+
114+
115+
def format_result(result, target):
116+
return '<div class="panel panel-default">%s</div>' % format_table(result, target)
117+
118+
119+
def format_link_list(header, ls):
120+
ret = '''
121+
<div class="panel panel-default">
122+
<div class="panel-heading">%s</div>
123+
<div class="list-group">
124+
''' % header
125+
126+
for (link, title, anchor, cls) in ls:
127+
ret += '<a class="%s" href="%s" title="%s">%s</a>\n' % (
128+
' '.join(cls+['list-group-item']),
129+
link, title, anchor
130+
)
131+
ret += '</div></div>'
132+
return ret
133+
134+
135+
def format_page():
136+
ip = request.args.get('ip', '')
137+
fmt = request.args.get('format', 'html').lower()
138+
do_lookup = request.args.get('lookup', 'false').lower() != 'false'
139+
use_rdap = request.args.get('rdap', 'false').lower() != 'false'
140+
css = '''
141+
.el { display: flex; flex-direction: row; align-items: baseline; }
142+
.el-ip { flex: 0?; max-width: 70%%; overflow: hidden; text-overflow: ellipsis; padding-right: .2em; }
143+
.el-prov { flex: 1 8em; }
144+
th { font-size: small; }
145+
.link-result { -moz-user-select: all; -webkit-user-select: all; -ms-user-select: all; user-select: all; }
146+
'''
147+
148+
# remove spaces, the zero-width space and left-to-right mark
149+
if six.PY2:
150+
ip = ip.decode('utf-8')
151+
ip = re.sub('[^0-9a-f.:/]', '', ip, flags=re.I)
152+
ip = ip.strip().strip(u' \u200b\u200e')
153+
ip_arg = ip
154+
if '/' in ip:
155+
ip = ip.split('/')[0]
156+
cidr = True
157+
else:
158+
cidr = False
159+
160+
result = {}
161+
error = False
162+
if do_lookup:
163+
try:
164+
result = lookup(ip, use_rdap)
165+
except Exception as e:
166+
result = {'error': repr(e)}
167+
error = True
168+
169+
geoip_res = geoip_reader.city(ip)
170+
if geoip_res:
171+
try:
172+
result['geolite2'] = geoip_res.country.name
173+
if geoip_res.subdivisions.most_specific.name:
174+
result['geolite2'] = geoip_res.subdivisions.most_specific.name + ", " + result['geolite2']
175+
if geoip_res.city.name:
176+
result['geolite2'] = geoip_res.city.name + ", " + result['geolite2']
177+
except Exception as e:
178+
result['geolite2'] = "Unavailable: " + repr(e)
179+
180+
if ipinfo_token:
181+
ipinfo = requests.get('https://ipinfo.io/'+ip+'/json?token='+ipinfo_token)
182+
ipinfo_json = ipinfo.json()
183+
if ipinfo_json and 'error' not in ipinfo_json:
184+
result['geo_ipinfo'] = ipinfo_json['country']
185+
if 'region' in ipinfo_json:
186+
result['geo_ipinfo'] = ipinfo_json['region'] + ", " + result['geo_ipinfo']
187+
if 'city' in ipinfo_json:
188+
result['geo_ipinfo'] = ipinfo_json['city'] + ", " + result['geo_ipinfo']
189+
190+
191+
if fmt == 'json' and do_lookup:
192+
return '{}\n'.format(json.dumps(result))
193+
194+
ret = '''<!DOCTYPE HTML>
195+
<html lang="en">
196+
<head>
197+
<meta charset="utf-8">
198+
<link rel="stylesheet" href="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.min.css">
199+
<link rel="stylesheet" href="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap-theme.min.css">
200+
<title>Whois Gateway Beta</title>
201+
<style type="text/css">
202+
{css}
203+
</style>
204+
</head>
205+
<body>
206+
<div class="container">
207+
<div class="row">
208+
<div class="col-sm-5">
209+
<header><h1>Whois Gateway<span style="color: #20c997; font-size: 18px; font-weight: bold; position: relative; text-transform: uppercase; top: -3px; vertical-align: top;">BETA</span></h1></header>
210+
</div>
211+
<div class="col-sm-7"><div class="alert alert-success" role="alert">
212+
<strong>This is a beta version of the Whois Gateway operated by <a href="https://en.wikipedia.org/wiki/User:ST47">ST47</a>.</strong> It adds support for querying referral DNS servers, such as those provided by Cogent for their 38.0.0.0/8 range. This is done automatically when the provider supports it. The source code for this fork is maintained at <a href="https://github.com/wiki-ST47/whois-gateway/">GitHub</a>.
213+
</div></div>
214+
</div>
215+
216+
<div class="row">
217+
<div class="col-sm-9">
218+
219+
<form action="{site}/gateway.py" role="form">
220+
<input type="hidden" name="lookup" value="true"/>
221+
<div class="row form-group {error}">
222+
<div class="col-md-10"><div class="input-group">
223+
<label class="input-group-addon" for="ipaddress-input">IP address</label>
224+
<input type="text" name="ip" value="{ip}" id="ipaddress-input" class="form-control" {af}/>
225+
</div></div>
226+
<div class="col-md-2"><input type="submit" value="Lookup" class="btn btn-default btn-block"/></div>
227+
</div>
228+
</form>
229+
'''.format(site=SITE,
230+
css=css,
231+
ip=ip_arg,
232+
error= 'has-error' if error else '',
233+
af= 'autofocus onFocus="this.select();"' if (not do_lookup or error) else '')
234+
235+
if cidr:
236+
ret += '''<div class="alert alert-warning" role="alert">
237+
The IP address you provided included a CIDR range. The results below apply to the IP address you provided, with the CIDR range ignored. There may be other addresses in that range that are not included in this report.
238+
</div>'''
239+
240+
if do_lookup:
241+
link = 'https://%s.toolforge.org/%s/lookup' % (PROJECT, ip)
242+
hostname = None
243+
try:
244+
hostname = socket.gethostbyaddr(ip)[0]
245+
except IOError:
246+
pass
247+
ret += '''
248+
<div class="panel panel-default"><div class="panel-heading">{hostname}</div>
249+
<div class="panel-body">{table}</div></div>
250+
251+
<div class="row form-group">
252+
<div class="col-md-12"><div class="input-group">
253+
<label class="input-group-addon"><a href="{link}">Link this result</a></label>
254+
<output class="form-control link-result">{link}</output>
255+
</div></div>
256+
</div>
257+
'''.format(hostname='<strong>%s</strong>' % hostname if hostname else '<em>(No corresponding host name retrieved)</em>',
258+
table=format_table(result, ip),
259+
link=link)
260+
261+
ret += '''</div>
262+
<div class="col-sm-3">
263+
'''
264+
ret += format_link_list(
265+
'Other tools',
266+
[(q(ip),
267+
'Look up %s at %s' % (ip, name),
268+
'<small class="el-ip">%s</small><span class="el-prov"> @%s</span>' % (ip, name),
269+
['el'])
270+
for (name, q) in sorted(TOOLS.items())]
271+
)
272+
273+
ret += format_link_list(
274+
'Sources',
275+
[(q(ip),
276+
'Look up %s at %s' % (ip, name),
277+
'<small class="el-ip">%s</small><span class="el-prov"> @%s</span>' % (ip, name),
278+
['el', 'active'] if result.get('asn_registry', '').upper() == name else ['el'])
279+
for (name, q) in sorted(PROVIDERS.items())]
280+
)
281+
282+
ret += '''
283+
</div>
284+
</div>
285+
286+
<footer><div class="container">
287+
<hr>
288+
<p class="text-center text-muted">
289+
<a href="{site}">Whois Gateway</a>
290+
<small>(<a href="https://github.com/wiki-ST47/whois-gateway">source code</a>,
291+
<a href="https://github.com/whym/whois-gateway">upstream</a>,
292+
<a href="https://github.com/whym/whois-gateway#api">API</a>)</small>
293+
on <a href="https://toolforge.org">Toolforge</a> /
294+
<a href="https://github.com/wiki-ST47/whois-gateway/issues">Issues?</a>
295+
</p>
296+
</div></footer>
297+
</div>
298+
</body></html>'''.format(site=SITE)
299+
300+
return ret
301+
302+
app = Flask(__name__)
303+
@app.route('/', defaults={'path': ''})
304+
@app.route('/<path:path>')
305+
def main_route(path):
306+
return format_page()
307+

www/python/src/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ipwhois==1.2.0
2+
six
3+
nose
4+
geoip2
5+
flask

0 commit comments

Comments
 (0)