]>
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 ###############################################################################
24 import logging
.handlers
36 # Load our location module
39 DATABASE_FILENAME
= "test.db.xz"
41 "https://location.ipfire.org/databases/",
42 "https://people.ipfire.org/~ms/location/",
45 def setup_logging(level
=logging
.INFO
):
46 l
= logging
.getLogger("location-downloader")
50 h
= logging
.StreamHandler()
51 h
.setLevel(logging
.DEBUG
)
55 h
= logging
.handlers
.SysLogHandler(address
="/dev/log",
56 facility
=logging
.handlers
.SysLogHandler
.LOG_DAEMON
)
57 h
.setLevel(logging
.INFO
)
60 # Format syslog messages
61 formatter
= logging
.Formatter("location-downloader[%(process)d]: %(message)s")
62 h
.setFormatter(formatter
)
70 def _(singular
, plural
=None, n
=None):
72 return gettext
.dngettext("libloc", singular
, plural
, n
)
74 return gettext
.dgettext("libloc", singular
)
77 class Downloader(object):
78 def __init__(self
, mirrors
):
79 self
.mirrors
= list(mirrors
)
82 random
.shuffle(self
.mirrors
)
84 # Get proxies from environment
85 self
.proxies
= self
._get
_proxies
()
87 def _get_proxies(self
):
90 for protocol
in ("https", "http"):
91 proxy
= os
.environ
.get("%s_proxy" % protocol
, None)
94 proxies
[protocol
] = proxy
98 def _make_request(self
, url
, baseurl
=None, headers
={}):
100 url
= urllib
.parse
.urljoin(baseurl
, url
)
102 req
= urllib
.request
.Request(url
, method
="GET")
106 "User-Agent" : "location-downloader/%s" % location
.__version
__,
110 for header
in headers
:
111 req
.add_header(header
, headers
[header
])
114 for protocol
in self
.proxies
:
115 req
.set_proxy(self
.proxies
[protocol
], protocol
)
119 def _send_request(self
, req
, **kwargs
):
120 # Log request headers
121 log
.debug("HTTP %s Request to %s" % (req
.method
, req
.host
))
122 log
.debug(" URL: %s" % req
.full_url
)
123 log
.debug(" Headers:")
124 for k
, v
in req
.header_items():
125 log
.debug(" %s: %s" % (k
, v
))
128 res
= urllib
.request
.urlopen(req
, **kwargs
)
130 except urllib
.error
.HTTPError
as e
:
131 # Log response headers
132 log
.debug("HTTP Response: %s" % e
.code
)
133 log
.debug(" Headers:")
134 for header
in e
.headers
:
135 log
.debug(" %s: %s" % (header
, e
.headers
[header
]))
137 # Raise all other errors
140 # Log response headers
141 log
.debug("HTTP Response: %s" % res
.code
)
142 log
.debug(" Headers:")
143 for k
, v
in res
.getheaders():
144 log
.debug(" %s: %s" % (k
, v
))
148 def download(self
, url
, public_key
, timestamp
=None, **kwargs
):
152 headers
["If-Modified-Since"] = timestamp
.strftime(
153 "%a, %d %b %Y %H:%M:%S GMT",
156 t
= tempfile
.NamedTemporaryFile(delete
=False)
159 for mirror
in self
.mirrors
:
160 # Prepare HTTP request
161 req
= self
._make
_request
(url
, baseurl
=mirror
, headers
=headers
)
164 with self
._send
_request
(req
) as res
:
165 decompressor
= lzma
.LZMADecompressor()
174 buf
= decompressor
.decompress(buf
)
178 # Write all data to disk
181 # Catch decompression errors
182 except lzma
.LZMAError
as e
:
183 log
.warning("Could not decompress downloaded file: %s" % e
)
186 except urllib
.error
.HTTPError
as e
:
187 # The file on the server was too old
189 log
.warning("%s is serving an outdated database. Trying next mirror..." % mirror
)
191 # Log any other HTTP errors
193 log
.warning("%s reported: %s" % (mirror
, e
))
195 # Throw away any downloaded content and try again
199 # Check if the downloaded database is recent
200 if not self
._check
_database
(t
, public_key
, timestamp
):
201 log
.warning("Downloaded database is outdated. Trying next mirror...")
203 # Throw away the data and try again
207 # Return temporary file
210 raise FileNotFoundError(url
)
212 def _check_database(self
, f
, public_key
, timestamp
=None):
214 Checks the downloaded database if it can be opened,
215 verified and if it is recent enough
217 log
.debug("Opening downloaded database at %s" % f
.name
)
219 db
= location
.Database(f
.name
)
221 # Database is not recent
222 if timestamp
and db
.created_at
< timestamp
.timestamp():
225 log
.info("Downloaded new database from %s" % (time
.strftime(
226 "%a, %d %b %Y %H:%M:%S GMT", time
.gmtime(db
.created_at
),
229 # Verify the database
230 with
open(public_key
, "r") as f
:
232 log
.error("Could not verify database")
240 self
.downloader
= Downloader(mirrors
=MIRRORS
)
243 parser
= argparse
.ArgumentParser(
244 description
=_("Location Downloader Command Line Interface"),
246 subparsers
= parser
.add_subparsers()
248 # Global configuration flags
249 parser
.add_argument("--debug", action
="store_true",
250 help=_("Enable debug output"))
253 parser
.add_argument("--version", action
="version",
254 version
="%%(prog)s %s" % location
.__version
__)
257 parser
.add_argument("--database", "-d",
258 default
="@databasedir@/database.db", help=_("Path to database"),
262 parser
.add_argument("--public-key", "-k",
263 default
="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
267 update
= subparsers
.add_parser("update", help=_("Update database"))
268 update
.set_defaults(func
=self
.handle_update
)
270 args
= parser
.parse_args()
272 # Enable debug logging
274 log
.setLevel(logging
.DEBUG
)
276 # Print usage if no action was given
277 if not "func" in args
:
284 # Parse command line arguments
285 args
= self
.parse_cli()
288 ret
= args
.func(args
)
290 # Return with exit code
294 # Otherwise just exit
297 def handle_update(self
, ns
):
298 # Fetch the version we need from DNS
299 t
= location
.discover_latest_version()
301 # Parse timestamp into datetime format
303 timestamp
= datetime
.datetime
.fromtimestamp(t
)
309 db
= location
.Database(ns
.database
)
311 # Check if we are already on the latest version
312 if db
.created_at
>= timestamp
.timestamp():
313 log
.info("Already on the latest version")
316 except FileNotFoundError
as e
:
319 # Try downloading a new database
321 t
= self
.downloader
.download(DATABASE_FILENAME
,
322 public_key
=ns
.public_key
, timestamp
=timestamp
)
324 # If no file could be downloaded, log a message
325 except FileNotFoundError
as e
:
326 log
.error("Could not download a new database")
329 # If we have not received a new file, there is nothing to do
333 # Write temporary file to destination
334 shutil
.copyfile(t
.name
, ns
.database
)
336 # Remove temporary file
341 # Run the command line interface