Merge lp:~cjwatson/launchpad/geoip-ip-address-handling into lp:launchpad
- geoip-ip-address-handling
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 19025 |
Proposed branch: | lp:~cjwatson/launchpad/geoip-ip-address-handling |
Merge into: | lp:launchpad |
Diff against target: |
507 lines (+88/-189) 8 files modified
lib/lp/answers/browser/tests/views.txt (+6/-6) lib/lp/answers/stories/question-search-multiple-languages.txt (+17/-4) lib/lp/services/geoip/configure.zcml (+1/-6) lib/lp/services/geoip/doc/geoip.txt (+26/-70) lib/lp/services/geoip/helpers.py (+25/-18) lib/lp/services/geoip/interfaces.py (+3/-19) lib/lp/services/geoip/model.py (+9/-66) setup.py (+1/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/geoip-ip-address-handling |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+369729@code.launchpad.net |
Commit message
Revamp GeoIP IP address handling.
Description of the change
This removes the weird special case mapping private addresses to South Africa, and switches to the ipaddress module rather than ad-hoc private IP address detection, theoretically allowing us to handle IPv6 addresses too (although real IPv6 support requires switching to GeoIP2 data).
The ipaddress module was already in our virtualenv as an indirect dependency.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/answers/browser/tests/views.txt' | |||
2 | --- lib/lp/answers/browser/tests/views.txt 2018-12-07 13:24:51 +0000 | |||
3 | +++ lib/lp/answers/browser/tests/views.txt 2019-07-04 18:55:19 +0000 | |||
4 | @@ -573,14 +573,14 @@ | |||
5 | 573 | will contain languages from the HTTP request, or the most likely | 573 | will contain languages from the HTTP request, or the most likely |
6 | 574 | interesting languages based on GeoIP information. | 574 | interesting languages based on GeoIP information. |
7 | 575 | 575 | ||
12 | 576 | For example, if the user doesn't log in and their browser is configured to | 576 | For example, if the user doesn't log in, their browser is configured to |
13 | 577 | accept Brazilian Portuguese, the vocabulary will contain the languages | 577 | accept Brazilian Portuguese, and their request appears to come from a South |
14 | 578 | spoken in South Africa (because the 127.0.0.1 IP address is mapped to | 578 | African IP address, the vocabulary will contain the languages spoken in |
15 | 579 | South Africa in the tests). | 579 | South Africa. |
16 | 580 | 580 | ||
17 | 581 | >>> login(ANONYMOUS) | 581 | >>> login(ANONYMOUS) |
18 | 582 | >>> request = LaunchpadTestRequest( | 582 | >>> request = LaunchpadTestRequest( |
20 | 583 | ... HTTP_ACCEPT_LANGUAGE='pt_BR') | 583 | ... HTTP_ACCEPT_LANGUAGE='pt_BR', REMOTE_ADDR='196.36.161.227') |
21 | 584 | >>> from lp.answers.browser.question import ( | 584 | >>> from lp.answers.browser.question import ( |
22 | 585 | ... QuestionLanguageVocabularyFactory) | 585 | ... QuestionLanguageVocabularyFactory) |
23 | 586 | >>> view = getMultiAdapter((firefox, request), name='+addticket') | 586 | >>> view = getMultiAdapter((firefox, request), name='+addticket') |
24 | @@ -676,7 +676,7 @@ | |||
25 | 676 | database. | 676 | database. |
26 | 677 | 677 | ||
27 | 678 | >>> request = LaunchpadTestRequest( | 678 | >>> request = LaunchpadTestRequest( |
29 | 679 | ... HTTP_ACCEPT_LANGUAGE='fr, en_CA') | 679 | ... HTTP_ACCEPT_LANGUAGE='fr, en_CA', REMOTE_ADDR='196.36.161.227') |
30 | 680 | 680 | ||
31 | 681 | >>> login(ANONYMOUS) | 681 | >>> login(ANONYMOUS) |
32 | 682 | >>> view = UserSupportLanguagesView(None, request) | 682 | >>> view = UserSupportLanguagesView(None, request) |
33 | 683 | 683 | ||
34 | === modified file 'lib/lp/answers/stories/question-search-multiple-languages.txt' | |||
35 | --- lib/lp/answers/stories/question-search-multiple-languages.txt 2019-05-22 14:57:45 +0000 | |||
36 | +++ lib/lp/answers/stories/question-search-multiple-languages.txt 2019-07-04 18:55:19 +0000 | |||
37 | @@ -3,6 +3,12 @@ | |||
38 | 3 | By default, only questions written in English or one of the user | 3 | By default, only questions written in English or one of the user |
39 | 4 | preferred languages are listed and searched. | 4 | preferred languages are listed and searched. |
40 | 5 | 5 | ||
41 | 6 | Make the test browsers look like they're coming from an arbitrary South | ||
42 | 7 | African IP address, since we'll use that later. | ||
43 | 8 | |||
44 | 9 | >>> anon_browser.addHeader('X_FORWARDED_FOR', '196.36.161.227') | ||
45 | 10 | >>> user_browser.addHeader('X_FORWARDED_FOR', '196.36.161.227') | ||
46 | 11 | |||
47 | 6 | 12 | ||
48 | 7 | == Anonymous searching == | 13 | == Anonymous searching == |
49 | 8 | 14 | ||
50 | @@ -29,10 +35,9 @@ | |||
51 | 29 | ... print(question.find('a').renderContents()) | 35 | ... print(question.find('a').renderContents()) |
52 | 30 | Installation failed | 36 | Installation failed |
53 | 31 | 37 | ||
58 | 32 | The questions match the languages inferred from by GeoIP | 38 | The questions match the languages inferred from GeoIP (South Africa in |
59 | 33 | 127.0.0.1 is mapped to South Africa in the test suite. The language | 39 | this case). The language control shows the intersection of the user's |
60 | 34 | control shows the intersection of the user's languages and the | 40 | languages and the languages of the target's questions. In this example: |
57 | 35 | languages of the target's questions. In this example: | ||
61 | 36 | set(['af', 'en', 'st', 'xh', 'zu']) & set(['en', 'es']) == set(en). | 41 | set(['af', 'en', 'st', 'xh', 'zu']) & set(['en', 'es']) == set(en). |
62 | 37 | 42 | ||
63 | 38 | >>> sorted(anon_browser.getControl(name='field.language').options) | 43 | >>> sorted(anon_browser.getControl(name='field.language').options) |
64 | @@ -113,6 +118,10 @@ | |||
65 | 113 | anonymous user makes a request from a GeoIP that has no languages | 118 | anonymous user makes a request from a GeoIP that has no languages |
66 | 114 | mapped, we assume they speak the default language of English. | 119 | mapped, we assume they speak the default language of English. |
67 | 115 | 120 | ||
68 | 121 | >>> for pos, (key, _) in enumerate(anon_browser.mech_browser.addheaders): | ||
69 | 122 | ... if key == 'X_FORWARDED_FOR': | ||
70 | 123 | ... del anon_browser.mech_browser.addheaders[pos] | ||
71 | 124 | ... break | ||
72 | 116 | >>> anon_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1') | 125 | >>> anon_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1') |
73 | 117 | >>> anon_browser.open( | 126 | >>> anon_browser.open( |
74 | 118 | ... 'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions') | 127 | ... 'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions') |
75 | @@ -166,6 +175,10 @@ | |||
76 | 166 | When the project languages are just English, and the user speaks | 175 | When the project languages are just English, and the user speaks |
77 | 167 | that language, we do not show the language controls. | 176 | that language, we do not show the language controls. |
78 | 168 | 177 | ||
79 | 178 | >>> for pos, (key, _) in enumerate(user_browser.mech_browser.addheaders): | ||
80 | 179 | ... if key == 'X_FORWARDED_FOR': | ||
81 | 180 | ... del user_browser.mech_browser.addheaders[pos] | ||
82 | 181 | ... break | ||
83 | 169 | >>> user_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1') | 182 | >>> user_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1') |
84 | 170 | >>> user_browser.open( | 183 | >>> user_browser.open( |
85 | 171 | ... 'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions') | 184 | ... 'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions') |
86 | 172 | 185 | ||
87 | === modified file 'lib/lp/services/geoip/configure.zcml' | |||
88 | --- lib/lp/services/geoip/configure.zcml 2010-10-25 05:33:20 +0000 | |||
89 | +++ lib/lp/services/geoip/configure.zcml 2019-07-04 18:55:19 +0000 | |||
90 | @@ -1,4 +1,4 @@ | |||
92 | 1 | <!-- Copyright 2009-2010 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
93 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
94 | 3 | --> | 3 | --> |
95 | 4 | 4 | ||
96 | @@ -34,9 +34,4 @@ | |||
97 | 34 | factory="lp.services.geoip.helpers.request_country" | 34 | factory="lp.services.geoip.helpers.request_country" |
98 | 35 | provides="lp.services.worlddata.interfaces.country.ICountry" /> | 35 | provides="lp.services.worlddata.interfaces.country.ICountry" /> |
99 | 36 | 36 | ||
100 | 37 | <adapter | ||
101 | 38 | for="zope.publisher.interfaces.browser.IBrowserRequest" | ||
102 | 39 | factory="lp.services.geoip.model.GeoIPRequest" | ||
103 | 40 | provides="lp.services.geoip.interfaces.IGeoIPRecord" /> | ||
104 | 41 | |||
105 | 42 | </configure> | 37 | </configure> |
106 | 43 | 38 | ||
107 | === modified file 'lib/lp/services/geoip/doc/geoip.txt' | |||
108 | --- lib/lp/services/geoip/doc/geoip.txt 2016-01-26 15:47:37 +0000 | |||
109 | +++ lib/lp/services/geoip/doc/geoip.txt 2019-07-04 18:55:19 +0000 | |||
110 | @@ -15,26 +15,30 @@ | |||
111 | 15 | u'Brazil' | 15 | u'Brazil' |
112 | 16 | 16 | ||
113 | 17 | When running tests the IP address will start with '127.', and GeoIP | 17 | When running tests the IP address will start with '127.', and GeoIP |
116 | 18 | would, obviously, fail to find the country for, so we use a South | 18 | would, obviously, fail to find the country for that, so we return None. |
115 | 19 | African IP address in that case. | ||
117 | 20 | 19 | ||
120 | 21 | >>> geoip.getCountryByAddr('127.0.0.88').name | 20 | >>> print(geoip.getCountryByAddr('127.0.0.88')) |
121 | 22 | u'South Africa' | 21 | None |
122 | 23 | 22 | ||
123 | 24 | We do the same trick for any IP addresses on private networks. | 23 | We do the same trick for any IP addresses on private networks. |
124 | 25 | 24 | ||
137 | 26 | >>> geoip.getCountryByAddr('10.0.0.88').name | 25 | >>> print(geoip.getCountryByAddr('10.0.0.88')) |
138 | 27 | u'South Africa' | 26 | None |
139 | 28 | 27 | ||
140 | 29 | >>> geoip.getCountryByAddr('192.168.0.7').name | 28 | >>> print(geoip.getCountryByAddr('192.168.0.7')) |
141 | 30 | u'South Africa' | 29 | None |
142 | 31 | 30 | ||
143 | 32 | >>> geoip.getCountryByAddr('172.16.0.1').name | 31 | >>> print(geoip.getCountryByAddr('172.16.0.1')) |
144 | 33 | u'South Africa' | 32 | None |
145 | 34 | 33 | ||
146 | 35 | IGeoIP also provides a getRecordByAddress() method, which returns a | 34 | >>> print(geoip.getCountryByAddr('::1')) |
147 | 36 | GeoIPRecord object, containing a bunch more information about that IP's | 35 | None |
148 | 37 | location. | 36 | |
149 | 37 | >>> print(geoip.getCountryByAddr('fc00::1')) | ||
150 | 38 | None | ||
151 | 39 | |||
152 | 40 | IGeoIP also provides a getRecordByAddress() method, which returns a dict | ||
153 | 41 | containing a bunch more information about that IP's location. | ||
154 | 38 | 42 | ||
155 | 39 | >>> record = geoip.getRecordByAddress('201.13.165.145') | 43 | >>> record = geoip.getRecordByAddress('201.13.165.145') |
156 | 40 | >>> for key, value in sorted(record.items()): | 44 | >>> for key, value in sorted(record.items()): |
157 | @@ -52,61 +56,13 @@ | |||
158 | 52 | region_name: ... | 56 | region_name: ... |
159 | 53 | time_zone: ... | 57 | time_zone: ... |
160 | 54 | 58 | ||
180 | 55 | And again we'll use a South African IP if the address starts with '127.'. | 59 | And again we'll return None if the address is private. |
181 | 56 | 60 | ||
182 | 57 | >>> record = geoip.getRecordByAddress('127.0.0.1') | 61 | >>> print(geoip.getRecordByAddress('127.0.0.1')) |
183 | 58 | >>> for key, value in sorted(record.items()): | 62 | None |
184 | 59 | ... print "%s: %s" % (key, value) | 63 | |
185 | 60 | area_code: ... | 64 | If it can't find a GeoIP record for the given IP address, it will return |
167 | 61 | city: ... | ||
168 | 62 | country_code: ZA | ||
169 | 63 | country_code3: ZAF | ||
170 | 64 | country_name: South Africa | ||
171 | 65 | dma_code: ... | ||
172 | 66 | latitude: ... | ||
173 | 67 | longitude: ... | ||
174 | 68 | postal_code: ... | ||
175 | 69 | region: ... | ||
176 | 70 | region_name: ... | ||
177 | 71 | time_zone: ... | ||
178 | 72 | |||
179 | 73 | If it can't find a GeoIPRecord for the given IP address, it will return | ||
186 | 74 | None. | 65 | None. |
187 | 75 | 66 | ||
188 | 76 | >>> print geoip.getRecordByAddress('255.255.255.255') | 67 | >>> print geoip.getRecordByAddress('255.255.255.255') |
189 | 77 | None | 68 | None |
190 | 78 | |||
191 | 79 | We also have GeoIPRequest, which adapts an IBrowserRequest into an | ||
192 | 80 | IGeoIPRecord, providing the latitude, longitude and time zone of the | ||
193 | 81 | request's originating IP address. | ||
194 | 82 | |||
195 | 83 | >>> from lp.services.webapp.servers import LaunchpadTestRequest | ||
196 | 84 | >>> from lp.services.geoip.interfaces import IGeoIPRecord | ||
197 | 85 | >>> request = LaunchpadTestRequest() | ||
198 | 86 | |||
199 | 87 | Since our request won't have an originating IP address, we'll use that | ||
200 | 88 | same South African IP address. | ||
201 | 89 | |||
202 | 90 | >>> from lp.services.geoip.helpers import ipaddress_from_request | ||
203 | 91 | >>> print ipaddress_from_request(request) | ||
204 | 92 | None | ||
205 | 93 | >>> geoip_request = IGeoIPRecord(request) | ||
206 | 94 | >>> geoip_request.latitude | ||
207 | 95 | -33... | ||
208 | 96 | >>> geoip_request.longitude | ||
209 | 97 | 18... | ||
210 | 98 | >>> geoip_request.time_zone | ||
211 | 99 | 'Africa/Johannesburg' | ||
212 | 100 | |||
213 | 101 | If the request had an originating IP address, though, it'd be used when | ||
214 | 102 | we adapted it into an IGeoIPRecord. | ||
215 | 103 | |||
216 | 104 | >>> request = LaunchpadTestRequest( | ||
217 | 105 | ... environ={'REMOTE_ADDR': '201.13.165.145'}) | ||
218 | 106 | >>> geoip_request = IGeoIPRecord(request) | ||
219 | 107 | >>> geoip_request.latitude | ||
220 | 108 | -23... | ||
221 | 109 | >>> geoip_request.longitude | ||
222 | 110 | -45... | ||
223 | 111 | >>> geoip_request.time_zone in ('Brazil/Acre', 'America/Sao_Paulo') | ||
224 | 112 | True | ||
225 | 113 | 69 | ||
226 | === modified file 'lib/lp/services/geoip/helpers.py' | |||
227 | --- lib/lp/services/geoip/helpers.py 2012-01-06 11:08:30 +0000 | |||
228 | +++ lib/lp/services/geoip/helpers.py 2019-07-04 18:55:19 +0000 | |||
229 | @@ -1,10 +1,10 @@ | |||
231 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
232 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
233 | 3 | 3 | ||
234 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
235 | 5 | 5 | ||
238 | 6 | import re | 6 | import ipaddress |
239 | 7 | 7 | import six | |
240 | 8 | from zope.component import getUtility | 8 | from zope.component import getUtility |
241 | 9 | 9 | ||
242 | 10 | from lp.services.geoip.interfaces import IGeoIP | 10 | from lp.services.geoip.interfaces import IGeoIP |
243 | @@ -12,6 +12,7 @@ | |||
244 | 12 | 12 | ||
245 | 13 | __all__ = [ | 13 | __all__ = [ |
246 | 14 | 'request_country', | 14 | 'request_country', |
247 | 15 | 'ipaddress_is_global', | ||
248 | 15 | 'ipaddress_from_request', | 16 | 'ipaddress_from_request', |
249 | 16 | ] | 17 | ] |
250 | 17 | 18 | ||
251 | @@ -25,39 +26,48 @@ | |||
252 | 25 | This information is not reliable and trivially spoofable - use it only | 26 | This information is not reliable and trivially spoofable - use it only |
253 | 26 | for selecting sane defaults. | 27 | for selecting sane defaults. |
254 | 27 | """ | 28 | """ |
258 | 28 | ipaddress = ipaddress_from_request(request) | 29 | ip_address = ipaddress_from_request(request) |
259 | 29 | if ipaddress is not None: | 30 | if ip_address is not None: |
260 | 30 | return getUtility(IGeoIP).getCountryByAddr(ipaddress) | 31 | return getUtility(IGeoIP).getCountryByAddr(ip_address) |
261 | 31 | return None | 32 | return None |
262 | 32 | 33 | ||
263 | 33 | 34 | ||
265 | 34 | _ipaddr_re = re.compile('\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?') | 35 | def ipaddress_is_global(addr): |
266 | 36 | """Return True iff the IP address is on a global public network.""" | ||
267 | 37 | try: | ||
268 | 38 | return ipaddress.ip_address(six.ensure_text(addr)).is_global | ||
269 | 39 | except ValueError: | ||
270 | 40 | return False | ||
271 | 35 | 41 | ||
272 | 36 | 42 | ||
273 | 37 | def ipaddress_from_request(request): | 43 | def ipaddress_from_request(request): |
274 | 38 | """Determine the IP address for this request. | 44 | """Determine the IP address for this request. |
275 | 39 | 45 | ||
277 | 40 | Returns None if the IP address cannot be determined or is localhost. | 46 | Returns None if the IP address cannot be determined or is not on a |
278 | 47 | global public network. | ||
279 | 41 | 48 | ||
280 | 42 | The remote IP address is determined by the X-Forwarded-For: header, | 49 | The remote IP address is determined by the X-Forwarded-For: header, |
281 | 43 | or failing that, the REMOTE_ADDR CGI environment variable. | 50 | or failing that, the REMOTE_ADDR CGI environment variable. |
282 | 44 | 51 | ||
283 | 45 | Because this information is unreliable and trivially spoofable, we | 52 | Because this information is unreliable and trivially spoofable, we |
286 | 46 | don't bother to do much error checking to ensure the IP address is at all | 53 | don't bother to do much error checking to ensure the IP address is at |
287 | 47 | valid. | 54 | all valid, beyond what the ipaddress module gives us. |
288 | 48 | 55 | ||
289 | 49 | >>> google = '66.102.7.104' | ||
290 | 50 | >>> ipaddress_from_request({'REMOTE_ADDR': '1.1.1.1'}) | 56 | >>> ipaddress_from_request({'REMOTE_ADDR': '1.1.1.1'}) |
291 | 51 | '1.1.1.1' | 57 | '1.1.1.1' |
292 | 52 | >>> ipaddress_from_request({ | 58 | >>> ipaddress_from_request({ |
294 | 53 | ... 'HTTP_X_FORWARDED_FOR': '666.666.666.666', | 59 | ... 'HTTP_X_FORWARDED_FOR': '66.66.66.66', |
295 | 54 | ... 'REMOTE_ADDR': '1.1.1.1' | 60 | ... 'REMOTE_ADDR': '1.1.1.1' |
296 | 55 | ... }) | 61 | ... }) |
298 | 56 | '666.666.666.666' | 62 | '66.66.66.66' |
299 | 57 | >>> ipaddress_from_request({'HTTP_X_FORWARDED_FOR': | 63 | >>> ipaddress_from_request({'HTTP_X_FORWARDED_FOR': |
300 | 58 | ... 'localhost, 127.0.0.1, 255.255.255.255,1.1.1.1' | 64 | ... 'localhost, 127.0.0.1, 255.255.255.255,1.1.1.1' |
301 | 59 | ... }) | 65 | ... }) |
303 | 60 | '255.255.255.255' | 66 | '1.1.1.1' |
304 | 67 | >>> ipaddress_from_request({ | ||
305 | 68 | ... 'HTTP_X_FORWARDED_FOR': 'nonsense', | ||
306 | 69 | ... 'REMOTE_ADDR': '1.1.1.1' | ||
307 | 70 | ... }) | ||
308 | 61 | """ | 71 | """ |
309 | 62 | ipaddresses = request.get('HTTP_X_FORWARDED_FOR') | 72 | ipaddresses = request.get('HTTP_X_FORWARDED_FOR') |
310 | 63 | 73 | ||
311 | @@ -70,10 +80,7 @@ | |||
312 | 70 | # We actually get a comma separated list of addresses. We need to throw | 80 | # We actually get a comma separated list of addresses. We need to throw |
313 | 71 | # away the obvious duds, such as loopback addresses | 81 | # away the obvious duds, such as loopback addresses |
314 | 72 | ipaddresses = [addr.strip() for addr in ipaddresses.split(',')] | 82 | ipaddresses = [addr.strip() for addr in ipaddresses.split(',')] |
319 | 73 | ipaddresses = [ | 83 | ipaddresses = [addr for addr in ipaddresses if ipaddress_is_global(addr)] |
316 | 74 | addr for addr in ipaddresses | ||
317 | 75 | if not (addr.startswith('127.') | ||
318 | 76 | or _ipaddr_re.search(addr) is None)] | ||
320 | 77 | 84 | ||
321 | 78 | if ipaddresses: | 85 | if ipaddresses: |
322 | 79 | # If we have more than one, have a guess. | 86 | # If we have more than one, have a guess. |
323 | 80 | 87 | ||
324 | === modified file 'lib/lp/services/geoip/interfaces.py' | |||
325 | --- lib/lp/services/geoip/interfaces.py 2013-01-07 02:40:55 +0000 | |||
326 | +++ lib/lp/services/geoip/interfaces.py 2019-07-04 18:55:19 +0000 | |||
327 | @@ -1,15 +1,11 @@ | |||
329 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
330 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
331 | 3 | 3 | ||
336 | 4 | from zope.interface import ( | 4 | from zope.interface import Interface |
333 | 5 | Attribute, | ||
334 | 6 | Interface, | ||
335 | 7 | ) | ||
337 | 8 | 5 | ||
338 | 9 | 6 | ||
339 | 10 | __all__ = [ | 7 | __all__ = [ |
340 | 11 | 'IGeoIP', | 8 | 'IGeoIP', |
341 | 12 | 'IGeoIPRecord', | ||
342 | 13 | 'IRequestLocalLanguages', | 9 | 'IRequestLocalLanguages', |
343 | 14 | 'IRequestPreferredLanguages', | 10 | 'IRequestPreferredLanguages', |
344 | 15 | ] | 11 | ] |
345 | @@ -19,7 +15,7 @@ | |||
346 | 19 | """The GeoIP utility, which represents the GeoIP database.""" | 15 | """The GeoIP utility, which represents the GeoIP database.""" |
347 | 20 | 16 | ||
348 | 21 | def getRecordByAddress(ip_address): | 17 | def getRecordByAddress(ip_address): |
350 | 22 | """Return the IGeoIPRecord for the given IP address, or None.""" | 18 | """Return the GeoIP record for the given IP address, or None.""" |
351 | 23 | 19 | ||
352 | 24 | def getCountryByAddr(ip_address): | 20 | def getCountryByAddr(ip_address): |
353 | 25 | """Find and return an ICountry based on the given IP address. | 21 | """Find and return an ICountry based on the given IP address. |
354 | @@ -29,18 +25,6 @@ | |||
355 | 29 | """ | 25 | """ |
356 | 30 | 26 | ||
357 | 31 | 27 | ||
358 | 32 | class IGeoIPRecord(Interface): | ||
359 | 33 | """A single record in the GeoIP database. | ||
360 | 34 | |||
361 | 35 | A GeoIP record gathers together all of the relevant information for a | ||
362 | 36 | single IP address or machine name in the DNS. | ||
363 | 37 | """ | ||
364 | 38 | |||
365 | 39 | latitude = Attribute("The geographic latitude, in real degrees.") | ||
366 | 40 | latitude = Attribute("The geographic longitude, in real degrees.") | ||
367 | 41 | time_zone = Attribute("The time zone.") | ||
368 | 42 | |||
369 | 43 | |||
370 | 44 | class IRequestLocalLanguages(Interface): | 28 | class IRequestLocalLanguages(Interface): |
371 | 45 | 29 | ||
372 | 46 | def getLocalLanguages(): | 30 | def getLocalLanguages(): |
373 | 47 | 31 | ||
374 | === modified file 'lib/lp/services/geoip/model.py' | |||
375 | --- lib/lp/services/geoip/model.py 2015-07-08 16:05:11 +0000 | |||
376 | +++ lib/lp/services/geoip/model.py 2019-07-04 18:55:19 +0000 | |||
377 | @@ -1,9 +1,8 @@ | |||
379 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
380 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
381 | 3 | 3 | ||
382 | 4 | __all__ = [ | 4 | __all__ = [ |
383 | 5 | 'GeoIP', | 5 | 'GeoIP', |
384 | 6 | 'GeoIPRequest', | ||
385 | 7 | 'RequestLocalLanguages', | 6 | 'RequestLocalLanguages', |
386 | 8 | 'RequestPreferredLanguages', | 7 | 'RequestPreferredLanguages', |
387 | 9 | ] | 8 | ] |
388 | @@ -16,10 +15,12 @@ | |||
389 | 16 | from zope.interface import implementer | 15 | from zope.interface import implementer |
390 | 17 | 16 | ||
391 | 18 | from lp.services.config import config | 17 | from lp.services.config import config |
393 | 19 | from lp.services.geoip.helpers import ipaddress_from_request | 18 | from lp.services.geoip.helpers import ( |
394 | 19 | ipaddress_from_request, | ||
395 | 20 | ipaddress_is_global, | ||
396 | 21 | ) | ||
397 | 20 | from lp.services.geoip.interfaces import ( | 22 | from lp.services.geoip.interfaces import ( |
398 | 21 | IGeoIP, | 23 | IGeoIP, |
399 | 22 | IGeoIPRecord, | ||
400 | 23 | IRequestLocalLanguages, | 24 | IRequestLocalLanguages, |
401 | 24 | IRequestPreferredLanguages, | 25 | IRequestPreferredLanguages, |
402 | 25 | ) | 26 | ) |
403 | @@ -42,7 +43,8 @@ | |||
404 | 42 | 43 | ||
405 | 43 | def getRecordByAddress(self, ip_address): | 44 | def getRecordByAddress(self, ip_address): |
406 | 44 | """See `IGeoIP`.""" | 45 | """See `IGeoIP`.""" |
408 | 45 | ip_address = ensure_address_is_not_private(ip_address) | 46 | if not ipaddress_is_global(ip_address): |
409 | 47 | return None | ||
410 | 46 | try: | 48 | try: |
411 | 47 | return self._gi.record_by_addr(ip_address) | 49 | return self._gi.record_by_addr(ip_address) |
412 | 48 | except SystemError: | 50 | except SystemError: |
413 | @@ -53,7 +55,8 @@ | |||
414 | 53 | 55 | ||
415 | 54 | def getCountryByAddr(self, ip_address): | 56 | def getCountryByAddr(self, ip_address): |
416 | 55 | """See `IGeoIP`.""" | 57 | """See `IGeoIP`.""" |
418 | 56 | ip_address = ensure_address_is_not_private(ip_address) | 58 | if not ipaddress_is_global(ip_address): |
419 | 59 | return None | ||
420 | 57 | geoip_record = self.getRecordByAddress(ip_address) | 60 | geoip_record = self.getRecordByAddress(ip_address) |
421 | 58 | if geoip_record is None: | 61 | if geoip_record is None: |
422 | 59 | return None | 62 | return None |
423 | @@ -68,44 +71,6 @@ | |||
424 | 68 | return country | 71 | return country |
425 | 69 | 72 | ||
426 | 70 | 73 | ||
427 | 71 | @implementer(IGeoIPRecord) | ||
428 | 72 | class GeoIPRequest: | ||
429 | 73 | """An adapter for a BrowserRequest into an IGeoIPRecord.""" | ||
430 | 74 | |||
431 | 75 | def __init__(self, request): | ||
432 | 76 | self.request = request | ||
433 | 77 | ip_address = ipaddress_from_request(self.request) | ||
434 | 78 | if ip_address is None: | ||
435 | 79 | # This happens during page testing, when the REMOTE_ADDR is not | ||
436 | 80 | # set by Zope. | ||
437 | 81 | ip_address = '127.0.0.1' | ||
438 | 82 | ip_address = ensure_address_is_not_private(ip_address) | ||
439 | 83 | self.ip_address = ip_address | ||
440 | 84 | self.geoip_record = getUtility(IGeoIP).getRecordByAddress( | ||
441 | 85 | self.ip_address) | ||
442 | 86 | |||
443 | 87 | @property | ||
444 | 88 | def latitude(self): | ||
445 | 89 | """See `IGeoIPRecord`.""" | ||
446 | 90 | if self.geoip_record is None: | ||
447 | 91 | return None | ||
448 | 92 | return self.geoip_record['latitude'] | ||
449 | 93 | |||
450 | 94 | @property | ||
451 | 95 | def longitude(self): | ||
452 | 96 | """See `IGeoIPRecord`.""" | ||
453 | 97 | if self.geoip_record is None: | ||
454 | 98 | return None | ||
455 | 99 | return self.geoip_record['longitude'] | ||
456 | 100 | |||
457 | 101 | @property | ||
458 | 102 | def time_zone(self): | ||
459 | 103 | """See `IGeoIPRecord`.""" | ||
460 | 104 | if self.geoip_record is None: | ||
461 | 105 | return None | ||
462 | 106 | return self.geoip_record['time_zone'] | ||
463 | 107 | |||
464 | 108 | |||
465 | 109 | @implementer(IRequestLocalLanguages) | 74 | @implementer(IRequestLocalLanguages) |
466 | 110 | class RequestLocalLanguages(object): | 75 | class RequestLocalLanguages(object): |
467 | 111 | 76 | ||
468 | @@ -168,27 +133,5 @@ | |||
469 | 168 | return sorted(languages, key=lambda x: x.englishname) | 133 | return sorted(languages, key=lambda x: x.englishname) |
470 | 169 | 134 | ||
471 | 170 | 135 | ||
472 | 171 | def ensure_address_is_not_private(ip_address): | ||
473 | 172 | """Return the given IP address if it doesn't start with '127.'. | ||
474 | 173 | |||
475 | 174 | If it does start with '127.' then we return a South African IP address. | ||
476 | 175 | Notice that we have no specific reason for using a South African IP | ||
477 | 176 | address here -- we could have used any other non-private IP address. | ||
478 | 177 | """ | ||
479 | 178 | private_prefixes = ( | ||
480 | 179 | '127.', | ||
481 | 180 | '192.168.', | ||
482 | 181 | '172.16.', | ||
483 | 182 | '10.', | ||
484 | 183 | ) | ||
485 | 184 | |||
486 | 185 | for prefix in private_prefixes: | ||
487 | 186 | if ip_address.startswith(prefix): | ||
488 | 187 | # This is an arbitrary South African IP which was handy at the | ||
489 | 188 | # time of writing; it's not special in any way. | ||
490 | 189 | return '196.36.161.227' | ||
491 | 190 | return ip_address | ||
492 | 191 | |||
493 | 192 | |||
494 | 193 | class NoGeoIPDatabaseFound(Exception): | 136 | class NoGeoIPDatabaseFound(Exception): |
495 | 194 | """No GeoIP database was found.""" | 137 | """No GeoIP database was found.""" |
496 | 195 | 138 | ||
497 | === modified file 'setup.py' | |||
498 | --- setup.py 2019-06-18 16:52:56 +0000 | |||
499 | +++ setup.py 2019-07-04 18:55:19 +0000 | |||
500 | @@ -164,6 +164,7 @@ | |||
501 | 164 | 'gunicorn[gthread]', | 164 | 'gunicorn[gthread]', |
502 | 165 | 'html5browser', | 165 | 'html5browser', |
503 | 166 | 'importlib-resources', | 166 | 'importlib-resources', |
504 | 167 | 'ipaddress', | ||
505 | 167 | 'ipython', | 168 | 'ipython', |
506 | 168 | 'jsautobuild', | 169 | 'jsautobuild', |
507 | 169 | 'launchpad-buildd', | 170 | 'launchpad-buildd', |