From 8d424f19ec71bd0e6a3fef7d592fca2b9e547408 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 2 May 2017 12:05:48 -0300 Subject: [PATCH] Support ETag when downloading repository index --- fdroidserver/index.py | 19 +++++----- fdroidserver/net.py | 31 ++++++++++++++++ tests/index.TestCase | 47 ++++++++++++++++++++++--- tests/signindex/guardianproject-v1.jar | Bin 0 -> 19941 bytes 4 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 tests/signindex/guardianproject-v1.jar diff --git a/fdroidserver/index.py b/fdroidserver/index.py index 0035c3ed..643210b2 100644 --- a/fdroidserver/index.py +++ b/fdroidserver/index.py @@ -35,9 +35,7 @@ from binascii import hexlify, unhexlify from datetime import datetime from xml.dom.minidom import Document -import requests - -from fdroidserver import metadata, signindex, common +from fdroidserver import metadata, signindex, common, net from fdroidserver.common import FDroidPopen, FDroidPopenBytes from fdroidserver.metadata import MetaDataException @@ -557,14 +555,16 @@ class VerificationException(Exception): pass -def download_repo_index(url_str, verify_fingerprint=True): +def download_repo_index(url_str, etag=None, verify_fingerprint=True): """ Downloads the repository index from the given :param url_str and verifies the repository's fingerprint if :param verify_fingerprint is not False. :raises: VerificationException() if the repository could not be verified - :return: The index in JSON format. + :return: A tuple consisting of: + - The index in JSON format or None if the index did not change + - The new eTag as returned by the HTTP request """ url = urllib.parse.urlsplit(url_str) @@ -576,11 +576,14 @@ def download_repo_index(url_str, verify_fingerprint=True): fingerprint = query['fingerprint'][0] url = urllib.parse.SplitResult(url.scheme, url.netloc, url.path + '/index-v1.jar', '', '') - r = requests.get(url.geturl()) + download, new_etag = net.http_get(url.geturl(), etag) + + if download is None: + return None, new_etag with tempfile.NamedTemporaryFile() as fp: # write and open JAR file - fp.write(r.content) + fp.write(download) jar = zipfile.ZipFile(fp) # verify that the JAR signature is valid @@ -601,7 +604,7 @@ def download_repo_index(url_str, verify_fingerprint=True): # turn the apps into App objects index["apps"] = [metadata.App(app) for app in index["apps"]] - return index + return index, new_etag def verify_jar_signature(file): diff --git a/fdroidserver/net.py b/fdroidserver/net.py index f7932440..7e8821ea 100644 --- a/fdroidserver/net.py +++ b/fdroidserver/net.py @@ -34,3 +34,34 @@ def download_file(url, local_filename=None, dldir='tmp'): f.write(chunk) f.flush() return local_filename + + +def http_get(url, etag=None): + """ + Downloads the content from the given URL by making a GET request. + + If an ETag is given, it will do a HEAD request first, to see if the content changed. + + :param url: The URL to download from. + :param etag: The last ETag to be used for the request (optional). + :return: A tuple consisting of: + - The raw content that was downloaded or None if it did not change + - The new eTag as returned by the HTTP request + """ + headers = {'User-Agent': 'F-Droid'} + # TODO disable TLS Session IDs and TLS Session Tickets + # (plain text cookie visible to anyone who can see the network traffic) + if etag: + r = requests.head(url, headers=headers) + r.raise_for_status() + if 'ETag' in r.headers and etag == r.headers['ETag']: + return None, etag + + r = requests.get(url, headers=headers) + r.raise_for_status() + + new_etag = None + if 'ETag' in r.headers: + new_etag = r.headers['ETag'] + + return r.content, new_etag diff --git a/tests/index.TestCase b/tests/index.TestCase index 780da775..2798e781 100755 --- a/tests/index.TestCase +++ b/tests/index.TestCase @@ -6,6 +6,9 @@ import os import sys import unittest import zipfile +from unittest.mock import patch + +import requests localmodule = os.path.realpath( os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')) @@ -18,6 +21,9 @@ import fdroidserver.index import fdroidserver.signindex +GP_FINGERPRINT = 'B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135' + + class IndexTest(unittest.TestCase): def setUp(self): @@ -55,9 +61,7 @@ class IndexTest(unittest.TestCase): '818E469465F96B704E27BE2FEE4C63AB' + '9F83DDF30E7A34C7371A4728D83B0BC1') if f == 'guardianproject.jar': - self.assertTrue(fingerprint == - 'B7C2EEFD8DAC7806AF67DFCD92EB1812' + - '6BC08312A7F2D6F3862E46013C7A6135') + self.assertTrue(fingerprint == GP_FINGERPRINT) def test_get_public_key_from_jar_fails(self): basedir = os.path.dirname(__file__) @@ -72,10 +76,43 @@ class IndexTest(unittest.TestCase): fdroidserver.index.download_repo_index("http://example.org") def test_download_repo_index_no_jar(self): - with self.assertRaises(zipfile.BadZipFile): + with self.assertRaises(requests.exceptions.HTTPError): fdroidserver.index.download_repo_index("http://example.org?fingerprint=nope") - # TODO test_download_repo_index with an actual repository + @patch('requests.head') + def test_download_repo_index_same_etag(self, head): + url = 'http://example.org?fingerprint=test' + etag = '"4de5-54d840ce95cb9"' + + head.return_value.headers = {'ETag': etag} + index, new_etag = fdroidserver.index.download_repo_index(url, etag=etag) + + self.assertIsNone(index) + self.assertEqual(etag, new_etag) + + @patch('requests.get') + @patch('requests.head') + def test_download_repo_index_new_etag(self, head, get): + url = 'http://example.org?fingerprint=' + GP_FINGERPRINT + etag = '"4de5-54d840ce95cb9"' + + # fake HTTP answers + head.return_value.headers = {'ETag': 'new_etag'} + get.return_value.headers = {'ETag': 'new_etag'} + get.return_value.status_code = 200 + testfile = os.path.join(os.path.dirname(__file__), 'signindex', 'guardianproject-v1.jar') + with open(testfile, 'rb') as file: + get.return_value.content = file.read() + + index, new_etag = fdroidserver.index.download_repo_index(url, etag=etag) + + # assert that the index was retrieved properly + self.assertEqual('Guardian Project Official Releases', index['repo']['name']) + self.assertEqual(GP_FINGERPRINT, index['repo']['fingerprint']) + self.assertTrue(len(index['repo']['pubkey']) > 500) + self.assertEqual(10, len(index['apps'])) + self.assertEqual(10, len(index['packages'])) + self.assertEqual('new_etag', new_etag) if __name__ == "__main__": diff --git a/tests/signindex/guardianproject-v1.jar b/tests/signindex/guardianproject-v1.jar new file mode 100644 index 0000000000000000000000000000000000000000..59edc87caa06d4f18c458f301a5e776d654e8cc6 GIT binary patch literal 19941 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