]>
git.ipfire.org Git - location/libloc.git/blob - src/python/location-downloader.in
2 ###############################################################################
4 # libloc - A library to determine the location of someone on the Internet #
6 # Copyright (C) 2019 IPFire Development Team <info@ipfire.org> #
8 # This library is free software; you can redistribute it and/or #
9 # modify it under the terms of the GNU Lesser General Public #
10 # License as published by the Free Software Foundation; either #
11 # version 2.1 of the License, or (at your option) any later version. #
13 # This library is distributed in the hope that it will be useful, #
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
16 # Lesser General Public License for more details. #
18 ###############################################################################
35 # Load our location module
37 from location
.i18n
import _
39 DATABASE_FILENAME
= "location.db.xz"
41 "https://location.ipfire.org/databases/",
45 log
= logging
.getLogger("location.downloader")
48 class Downloader(object):
49 def __init__(self
, version
, mirrors
):
50 self
.version
= version
51 self
.mirrors
= list(mirrors
)
54 random
.shuffle(self
.mirrors
)
56 # Get proxies from environment
57 self
.proxies
= self
._get
_proxies
()
59 def _get_proxies(self
):
62 for protocol
in ("https", "http"):
63 proxy
= os
.environ
.get("%s_proxy" % protocol
, None)
66 proxies
[protocol
] = proxy
70 def _make_request(self
, url
, baseurl
=None, headers
={}):
72 url
= urllib
.parse
.urljoin(baseurl
, url
)
74 req
= urllib
.request
.Request(url
, method
="GET")
78 "User-Agent" : "location-downloader/@VERSION@",
82 for header
in headers
:
83 req
.add_header(header
, headers
[header
])
86 for protocol
in self
.proxies
:
87 req
.set_proxy(self
.proxies
[protocol
], protocol
)
91 def _send_request(self
, req
, **kwargs
):
93 log
.debug("HTTP %s Request to %s" % (req
.method
, req
.host
))
94 log
.debug(" URL: %s" % req
.full_url
)
95 log
.debug(" Headers:")
96 for k
, v
in req
.header_items():
97 log
.debug(" %s: %s" % (k
, v
))
100 res
= urllib
.request
.urlopen(req
, **kwargs
)
102 except urllib
.error
.HTTPError
as e
:
103 # Log response headers
104 log
.debug("HTTP Response: %s" % e
.code
)
105 log
.debug(" Headers:")
106 for header
in e
.headers
:
107 log
.debug(" %s: %s" % (header
, e
.headers
[header
]))
109 # Raise all other errors
112 # Log response headers
113 log
.debug("HTTP Response: %s" % res
.code
)
114 log
.debug(" Headers:")
115 for k
, v
in res
.getheaders():
116 log
.debug(" %s: %s" % (k
, v
))
120 def download(self
, url
, public_key
, timestamp
=None, tmpdir
=None, **kwargs
):
124 headers
["If-Modified-Since"] = timestamp
.strftime(
125 "%a, %d %b %Y %H:%M:%S GMT",
128 t
= tempfile
.NamedTemporaryFile(dir=tmpdir
, delete
=False)
131 for mirror
in self
.mirrors
:
132 # Prepare HTTP request
133 req
= self
._make
_request
(url
, baseurl
=mirror
, headers
=headers
)
136 with self
._send
_request
(req
) as res
:
137 decompressor
= lzma
.LZMADecompressor()
146 buf
= decompressor
.decompress(buf
)
150 # Write all data to disk
153 # Catch decompression errors
154 except lzma
.LZMAError
as e
:
155 log
.warning("Could not decompress downloaded file: %s" % e
)
158 except urllib
.error
.HTTPError
as e
:
159 # The file on the server was too old
161 log
.warning("%s is serving an outdated database. Trying next mirror..." % mirror
)
163 # Log any other HTTP errors
165 log
.warning("%s reported: %s" % (mirror
, e
))
167 # Throw away any downloaded content and try again
171 # Check if the downloaded database is recent
172 if not self
._check
_database
(t
, public_key
, timestamp
):
173 log
.warning("Downloaded database is outdated. Trying next mirror...")
175 # Throw away the data and try again
179 # Make the file readable for everyone
180 os
.chmod(t
.name
, stat
.S_IRUSR|stat
.S_IRGRP|stat
.S_IROTH
)
182 # Return temporary file
185 raise FileNotFoundError(url
)
187 def _check_database(self
, f
, public_key
, timestamp
=None):
189 Checks the downloaded database if it can be opened,
190 verified and if it is recent enough
192 log
.debug("Opening downloaded database at %s" % f
.name
)
194 db
= location
.Database(f
.name
)
196 # Database is not recent
197 if timestamp
and db
.created_at
< timestamp
.timestamp():
200 log
.info("Downloaded new database from %s" % (time
.strftime(
201 "%a, %d %b %Y %H:%M:%S GMT", time
.gmtime(db
.created_at
),
204 # Verify the database
205 with
open(public_key
, "r") as f
:
207 log
.error("Could not verify database")
215 # Which version are we downloading?
216 self
.version
= location
.DATABASE_VERSION_LATEST
218 self
.downloader
= Downloader(version
=self
.version
, mirrors
=MIRRORS
)
221 parser
= argparse
.ArgumentParser(
222 description
=_("Location Downloader Command Line Interface"),
224 subparsers
= parser
.add_subparsers()
226 # Global configuration flags
227 parser
.add_argument("--debug", action
="store_true",
228 help=_("Enable debug output"))
229 parser
.add_argument("--quiet", action
="store_true",
230 help=_("Enable quiet mode"))
233 parser
.add_argument("--version", action
="version",
234 version
="%(prog)s @VERSION@")
237 parser
.add_argument("--database", "-d",
238 default
="@databasedir@/database.db", help=_("Path to database"),
242 parser
.add_argument("--public-key", "-k",
243 default
="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
247 update
= subparsers
.add_parser("update", help=_("Update database"))
248 update
.set_defaults(func
=self
.handle_update
)
251 verify
= subparsers
.add_parser("verify",
252 help=_("Verify the downloaded database"))
253 verify
.set_defaults(func
=self
.handle_verify
)
255 args
= parser
.parse_args()
259 location
.logger
.set_level(logging
.DEBUG
)
261 location
.logger
.set_level(logging
.WARNING
)
263 # Print usage if no action was given
264 if not "func" in args
:
271 # Parse command line arguments
272 args
= self
.parse_cli()
275 ret
= args
.func(args
)
277 # Return with exit code
281 # Otherwise just exit
284 def handle_update(self
, ns
):
285 # Fetch the timestamp we need from DNS
286 t
= location
.discover_latest_version(self
.version
)
288 # Parse timestamp into datetime format
289 timestamp
= datetime
.datetime
.fromtimestamp(t
) if t
else None
293 db
= location
.Database(ns
.database
)
295 # Check if we are already on the latest version
296 if timestamp
and db
.created_at
>= timestamp
.timestamp():
297 log
.info("Already on the latest version")
300 except FileNotFoundError
as e
:
303 # Download the database into the correct directory
304 tmpdir
= os
.path
.dirname(ns
.database
)
306 # Try downloading a new database
308 t
= self
.downloader
.download("%s/%s" % (self
.version
, DATABASE_FILENAME
),
309 public_key
=ns
.public_key
, timestamp
=timestamp
, tmpdir
=tmpdir
)
311 # If no file could be downloaded, log a message
312 except FileNotFoundError
as e
:
313 log
.error("Could not download a new database")
316 # If we have not received a new file, there is nothing to do
320 # Move temporary file to destination
321 shutil
.move(t
.name
, ns
.database
)
325 def handle_verify(self
, ns
):
327 db
= location
.Database(ns
.database
)
328 except FileNotFoundError
as e
:
329 log
.error("%s: %s" % (ns
.database
, e
))
332 # Verify the database
333 with
open(ns
.public_key
, "r") as f
:
335 log
.error("Could not verify database")
339 log
.debug("Database successfully verified")
344 # Run the command line interface