|
| 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 | + |
0 commit comments