diff options
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | omaha_server/omaha/core.py | 8 | ||||
-rw-r--r-- | omaha_server/omaha/request.xsd | 13 | ||||
-rw-r--r-- | omaha_server/omaha/response.xsd | 2 | ||||
-rw-r--r-- | omaha_server/omaha/tests/fixtures.py | 10 | ||||
-rw-r--r-- | omaha_server/omaha/utils.py | 13 | ||||
-rw-r--r-- | omaha_server/omaha_server/middlewares.py | 73 | ||||
-rw-r--r-- | omaha_server/omaha_server/settings.py | 9 | ||||
-rw-r--r-- | requirements/base.txt | 1 |
9 files changed, 126 insertions, 10 deletions
@@ -227,6 +227,7 @@ app: | OMAHA_URL_PREFIX | no trailing slash! | | | SENTRY_STACKTRACE_API_KEY | Auth API token | | | OMAHA_ONLY_HTTPS | HTTPS-only | False | +| CUP_REQUEST_VALIDATION | | False | - [uWSGI Options](http://uwsgi-docs.readthedocs.org/en/latest/Options.html) & [Environment variables](http://uwsgi-docs.readthedocs.org/en/latest/Configuration.html#environment-variables) - [Sentry](https://github.com/getsentry/sentry) @@ -261,6 +262,12 @@ $ ebs-deploy deploy -e omaha-server-dev 3. Finally, in the case if you want to redirect all HTTP traffic to HTTPS, you can add `OMAHA_ONLY_HTTPS: true` to environment variables in the *environment* section. *Warning:* Please, don't activate the redirection of HTTP to HTTPS if you don't enable HTTPS. It will lead to that an Omaha server won't be accessible. +#### Enable Client Update Protocol v2 + +1. Use [Omaha eckeytool](https://github.com/google/omaha/tree/master/omaha/tools/eckeytool) to generate private.pem key and cup_ecdsa_pubkey.{KEYID}.h files. +2. Add cup_ecdsa_pubkey.{KEYID}.h to Omaha source directory /path/to/omaha/omaha/net/, set CupEcdsaRequestImpl::kCupProductionPublicKey in /path/to/omaha/omaha/net/cup_ecdsa_request.cc to new key and build Omaha client. +3. Add private.pem keyid and path to omaha CUP_PEM_KEYS dictionary. + ## Links - Presentation: [omaha-server High Fidelity, High Velocity Deployments in the Cloud](http://slides.com/andreylisin/omaha-server/#/) diff --git a/omaha_server/omaha/core.py b/omaha_server/omaha/core.py index d5a6870..f252501 100644 --- a/omaha_server/omaha/core.py +++ b/omaha_server/omaha/core.py @@ -24,7 +24,7 @@ from datetime import datetime from lxml.builder import E -from omaha.utils import get_sec_since_midnight +from omaha.utils import get_sec_since_midnight, get_days_since_20070101 __all__ = [ @@ -35,10 +35,12 @@ __all__ = [ def Response(apps_list, protocol='3.0', date=None, server='prod'): - elapsed_seconds = get_sec_since_midnight(date or datetime.utcnow()) + date = date or datetime.utcnow() + elapsed_seconds = get_sec_since_midnight(date) + elapsed_days = get_days_since_20070101(date) resp = E.response( dict(protocol=protocol, server=server), - E.daystart(elapsed_seconds=str(elapsed_seconds)), + E.daystart(elapsed_seconds=str(elapsed_seconds), elapsed_days=str(elapsed_days)), ) list(map(resp.append, apps_list)) return resp diff --git a/omaha_server/omaha/request.xsd b/omaha_server/omaha/request.xsd index e9cdeed..5d999fb 100644 --- a/omaha_server/omaha/request.xsd +++ b/omaha_server/omaha/request.xsd @@ -50,6 +50,9 @@ <xs:attribute name='testsource' type='xs:NCName' use="optional"/> <xs:attribute name='dedup' type='xs:NCName' use="optional"/> <xs:attribute name='updaterchannel' type='xs:NCName' use="optional"/> + <xs:attribute name='shell_version' type='xs:string' use="optional"/> + <xs:attribute name='periodoverridesec' type='xs:integer' use="optional"/> + <xs:attribute name='dlpref' type='xs:string' use="optional"/> </xs:complexType> </xs:element> <xs:element name="hw"> @@ -90,7 +93,11 @@ <xs:attribute name='experiments' use='optional' type="xs:string"/> <xs:attribute name='iid' use='optional' type="GuidType"/> <xs:attribute name='installage' use='optional' type='xs:integer'/> + <xs:attribute name='installdate' use='optional' type='xs:integer'/> <xs:attribute name='tag' use='optional' type='xs:string'/> + <xs:attribute name='cohort' use='optional' type='xs:string'/> + <xs:attribute name='cohorthint' use='optional' type='xs:string'/> + <xs:attribute name='cohortname' use='optional' type='xs:string'/> </xs:complexType> </xs:element> <xs:element name='updatecheck'> @@ -108,6 +115,7 @@ <xs:attribute name="extracode1" use="optional" type="xs:integer"/> <xs:attribute name="download_time_ms" use="optional" type="xs:integer"/> <xs:attribute name="downloaded" use="optional" type="xs:integer"/> + <xs:attribute name="downloader" use="optional" type="xs:string"/> <xs:attribute name="total" use="optional" type="xs:integer"/> <xs:attribute name="update_check_time_ms" use="optional" type="xs:integer"/> <xs:attribute name="install_time_ms" use="optional" type="xs:integer"/> @@ -117,6 +125,8 @@ <xs:attribute name="time_since_download_start_ms" use="optional" type="xs:integer"/> <xs:attribute name="nextversion" use="optional" type="xs:integer"/> <xs:attribute name="previousversion" use="optional" type="xs:integer"/> + <xs:attribute name="url" use="optional" type="xs:string"/> + <xs:attribute name="is_bundled" use="optional" type="xs:integer"/> </xs:complexType> </xs:element> <xs:element name='ping'> @@ -124,6 +134,9 @@ <xs:attribute name='active' use='optional' type='xs:integer' default="0"/> <xs:attribute name='r' use='optional' type='xs:integer' default="0"/> <xs:attribute name='a' use='optional' type='xs:integer' default="0"/> + <xs:attribute name='rd' use='optional' type='xs:integer' default="-2"/> + <xs:attribute name='ad' use='optional' type='xs:integer' default="-2"/> + <xs:attribute name='ping_freshness' use='optional' type='xs:string' default="0"/> </xs:complexType> </xs:element> <xs:element name="data" type="DataType"/> diff --git a/omaha_server/omaha/response.xsd b/omaha_server/omaha/response.xsd index a81cbfe..72ece40 100644 --- a/omaha_server/omaha/response.xsd +++ b/omaha_server/omaha/response.xsd @@ -18,7 +18,7 @@ <xs:element name="daystart"> <xs:complexType> <xs:attribute name="elapsed_seconds" use="required" type="xs:positiveInteger"/> - <xs:attribute name="elapsed_days" use="optional" type="xs:nonPositiveInteger" default="0"/> + <xs:attribute name="elapsed_days" use="required" type="xs:positiveInteger"/> </xs:complexType> </xs:element> <xs:element name='app'> diff --git a/omaha_server/omaha/tests/fixtures.py b/omaha_server/omaha/tests/fixtures.py index 4be086e..4efdfe5 100644 --- a/omaha_server/omaha/tests/fixtures.py +++ b/omaha_server/omaha/tests/fixtures.py @@ -97,7 +97,7 @@ request_data = b"""<?xml version="1.0" encoding="UTF-8"?> response_update_check_negative = b"""<?xml version="1.0" encoding="UTF-8"?> <response protocol="3.0" server="prod"> - <daystart elapsed_seconds="56508"/> + <daystart elapsed_seconds="56508" elapsed_days="2557"/> <app appid="{430FD4D0-B729-4F61-AA34-91526481799D}" status="ok"> <updatecheck status="noupdate"/> <ping status="ok"/> @@ -110,7 +110,7 @@ response_update_check_negative = b"""<?xml version="1.0" encoding="UTF-8"?> response_update_check_positive = b"""<?xml version="1.0" encoding="UTF-8"?> <response protocol="3.0" server="prod"> - <daystart elapsed_seconds="56508"/> + <daystart elapsed_seconds="56508" elapsed_days="2557"/> <app appid="{430FD4D0-B729-4F61-AA34-91526481799D}" status="ok"> <updatecheck status="noupdate"/> <ping status="ok"/> @@ -136,7 +136,7 @@ response_update_check_positive = b"""<?xml version="1.0" encoding="UTF-8"?> response_event = b"""<?xml version="1.0" encoding="UTF-8"?> <response protocol="3.0" server="prod"> - <daystart elapsed_seconds="56754"/> + <daystart elapsed_seconds="56754" elapsed_days="2557"/> <app appid="{D0AB2EBC-931B-4013-9FEB-C9C4C2225C8C}" status="ok"> <event status="ok"/> <event status="ok"/> @@ -146,7 +146,7 @@ response_event = b"""<?xml version="1.0" encoding="UTF-8"?> response_data_doc = b"""<?xml version="1.0" encoding="UTF-8"?> <response protocol="3.0" server="prod"> - <daystart elapsed_seconds="56754"/> + <daystart elapsed_seconds="56754" elapsed_days="2557"/> <app appid="{D0AB2EBC-931B-4013-9FEB-C9C4C2225C8C}" status="ok"> <data index="verboselogging" name="install" status="ok"> app-specific values here @@ -157,7 +157,7 @@ response_data_doc = b"""<?xml version="1.0" encoding="UTF-8"?> response_data = b"""<?xml version="1.0" encoding="UTF-8"?> <response protocol="3.0" server="prod"> - <daystart elapsed_seconds="56754"/> + <daystart elapsed_seconds="56754" elapsed_days="2557"/> <app status="ok" appid="{430FD4D0-B729-4F61-AA34-91526481799D}"> <data status="ok" index="verboselogging" name="install">app-specific values here</data> <data status="ok" name="untrusted"/> diff --git a/omaha_server/omaha/utils.py b/omaha_server/omaha/utils.py index 5be380d..bb164cf 100644 --- a/omaha_server/omaha/utils.py +++ b/omaha_server/omaha/utils.py @@ -47,6 +47,19 @@ def get_sec_since_midnight(date): return delta.seconds +def get_days_since_20070101(date): + """ + Return days since 2007-01-01 + + >>> from datetime import datetime + >>> get_days_since_20070101(datetime(year=2016, month=3, day=4)) + 3350 + """ + date_20070101 = datetime.datetime(year=2007, month=1, day=1, tzinfo=date.tzinfo) + delta = date - date_20070101 + return delta.days + + def get_id(uuid): """ >>> get_id('{8C65E04C-0383-4AE2-893F-4EC7C58F70DC}') diff --git a/omaha_server/omaha_server/middlewares.py b/omaha_server/omaha_server/middlewares.py index 3cda3f2..d51d08a 100644 --- a/omaha_server/omaha_server/middlewares.py +++ b/omaha_server/omaha_server/middlewares.py @@ -1,6 +1,19 @@ +import logging +from hashlib import sha256 + +from django.conf import settings +from django.http import HttpResponse from django.utils import timezone import pytz +from ecdsa import SigningKey +from ecdsa.util import sigencode_der + +logger = logging.getLogger(__name__) + + +class CUP2Exception(Exception): + pass class TimezoneMiddleware(object): @@ -9,4 +22,62 @@ class TimezoneMiddleware(object): if tzname: timezone.activate(pytz.timezone(tzname)) else: - timezone.deactivate()
\ No newline at end of file + timezone.deactivate() + + +class CUP2Middleware(object): + """Support CUP2 protocol of Omaha Client. + """ + + def __init__(self): + self.sk = {} + # Loading signature keys to memory + for keyid, private_key in settings.CUP_PEM_KEYS.iteritems(): + self.sk[keyid] = SigningKey.from_pem(open(private_key).read()) + + def process_request(self, request): + if getattr(settings, 'CUP_REQUEST_VALIDATION', False) and self.is_cup2_request(request): + try: + self.validate_cup2_request(request) + except Exception as e: + logger.error('%s: %s\nrequest:\n%s\n\n%s\n' % (e.__class__.__name__, e.message, + request.META, request.body)) + msg = b'<?xml version="1.0" encoding="utf-8"?><data><message>Bad Request</message></data>' + return HttpResponse(msg, status=400, content_type="text/html; charset=utf-8") + + def process_response(self, request, response): + if self.is_cup2_request(request) and response.status_code // 100 == 2: + self.sign_cup2_response(request, response) + + return response + + @staticmethod + def is_cup2_request(request): + """Detects CUP2 request by passed cup2key parameter. + """ + return request.GET.get('cup2key') is not None + + def validate_cup2_request(self, request): + cup2key = request.GET.get('cup2key') + cup2hreq = request.GET.get('cup2hreq') + + keyid, k = cup2key.split(':') + if keyid not in self.sk.keys(): + raise CUP2Exception('There is no key with id %s' % keyid) + + request_hash = sha256(request.body).hexdigest() + if cup2hreq and request_hash != cup2hreq: + raise CUP2Exception('Bad request hash\n"%s" != "%s"' % (request_hash, cup2hreq)) + + def sign_cup2_response(self, request, response): + cup2key = request.GET.get('cup2key') + + request_hash = sha256(request.body).digest() + response_hash = sha256(response.content).digest() + + keyid, k = cup2key.split(':') + # hash( hash(request) | hash(response) | cup2key ) + message = sha256(request_hash + response_hash + cup2key.encode()).digest() + signature = self.sk[keyid].sign(message, hashfunc=sha256, sigencode=sigencode_der, k=int(k)) + + response['ETag'] = '%s:%s' % (signature.encode('hex'), request_hash.encode('hex')) diff --git a/omaha_server/omaha_server/settings.py b/omaha_server/omaha_server/settings.py index ed6d9f1..2186e5d 100644 --- a/omaha_server/omaha_server/settings.py +++ b/omaha_server/omaha_server/settings.py @@ -135,6 +135,7 @@ if IS_PRIVATE: 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'omaha_server.middlewares.TimezoneMiddleware', + 'omaha_server.middlewares.CUP2Middleware', ) + MIDDLEWARE_CLASSES ROOT_URLCONF = 'omaha_server.urls' @@ -308,3 +309,11 @@ REST_FRAMEWORK = { # django_select2 AUTO_RENDER_SELECT2_STATICS = False + +# Client Update Protocol + +CUP_REQUEST_VALIDATION = os.environ.get('CUP_REQUEST_VALIDATION', False) + +CUP_PEM_KEYS = { + # 'keyid': 'private_key_path', +} diff --git a/requirements/base.txt b/requirements/base.txt index a99974c..795a939 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -31,6 +31,7 @@ django-bootstrap3==7.0.1 protobuf==3.0.0a3 protobuf-to-dict==0.1.0 django-dynamic-preferences==0.8.1 +ecdsa==0.13 # Only dev #django-httplog==0.2.3 |