Skip to content

Commit 6d00231

Browse files
authored
fix(rtdb): Support parsing non-US RTDB instance URLs (#517)
* fix(rtdb): Support parsing non-US RTDB instance URLs * fix: Deferred credential loading until emulator URL is determined
1 parent eefc31b commit 6d00231

File tree

2 files changed

+81
-72
lines changed

2 files changed

+81
-72
lines changed

firebase_admin/db.py

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -768,10 +768,10 @@ def __init__(self, app):
768768
self._credential = app.credential
769769
db_url = app.options.get('databaseURL')
770770
if db_url:
771-
_DatabaseService._parse_db_url(db_url) # Just for validation.
772771
self._db_url = db_url
773772
else:
774773
self._db_url = None
774+
775775
auth_override = _DatabaseService._get_auth_override(app)
776776
if auth_override not in (self._DEFAULT_AUTH_OVERRIDE, {}):
777777
self._auth_override = json.dumps(auth_override, separators=(',', ':'))
@@ -795,15 +795,29 @@ def get_client(self, db_url=None):
795795
if db_url is None:
796796
db_url = self._db_url
797797

798-
base_url, namespace = _DatabaseService._parse_db_url(db_url, self._emulator_host)
799-
if base_url == 'https://{0}.firebaseio.com'.format(namespace):
800-
# Production base_url. No need to specify namespace in query params.
801-
params = {}
802-
credential = self._credential.get_credential()
803-
else:
804-
# Emulator base_url. Use fake credentials and specify ?ns=foo in query params.
798+
if not db_url or not isinstance(db_url, str):
799+
raise ValueError(
800+
'Invalid database URL: "{0}". Database URL must be a non-empty '
801+
'URL string.'.format(db_url))
802+
803+
parsed_url = parse.urlparse(db_url)
804+
if not parsed_url.netloc:
805+
raise ValueError(
806+
'Invalid database URL: "{0}". Database URL must be a wellformed '
807+
'URL string.'.format(db_url))
808+
809+
emulator_config = self._get_emulator_config(parsed_url)
810+
if emulator_config:
805811
credential = _EmulatorAdminCredentials()
806-
params = {'ns': namespace}
812+
base_url = emulator_config.base_url
813+
params = {'ns': emulator_config.namespace}
814+
else:
815+
# Defer credential lookup until we are certain it's going to be prod connection.
816+
credential = self._credential.get_credential()
817+
base_url = 'https://{0}'.format(parsed_url.netloc)
818+
params = {}
819+
820+
807821
if self._auth_override:
808822
params['auth_variable_override'] = self._auth_override
809823

@@ -813,47 +827,20 @@ def get_client(self, db_url=None):
813827
self._clients[client_cache_key] = client
814828
return self._clients[client_cache_key]
815829

816-
@classmethod
817-
def _parse_db_url(cls, url, emulator_host=None):
818-
"""Parses (base_url, namespace) from a database URL.
819-
820-
The input can be either a production URL (https://foo-bar.firebaseio.com/)
821-
or an Emulator URL (http://localhost:8080/?ns=foo-bar). In case of Emulator
822-
URL, the namespace is extracted from the query param ns. The resulting
823-
base_url never includes query params.
824-
825-
If url is a production URL and emulator_host is specified, the result
826-
base URL will use emulator_host instead. emulator_host is ignored
827-
if url is already an emulator URL.
828-
"""
829-
if not url or not isinstance(url, str):
830-
raise ValueError(
831-
'Invalid database URL: "{0}". Database URL must be a non-empty '
832-
'URL string.'.format(url))
833-
parsed_url = parse.urlparse(url)
834-
if parsed_url.netloc.endswith('.firebaseio.com'):
835-
return cls._parse_production_url(parsed_url, emulator_host)
836-
837-
return cls._parse_emulator_url(parsed_url)
838-
839-
@classmethod
840-
def _parse_production_url(cls, parsed_url, emulator_host):
841-
"""Parses production URL like https://foo-bar.firebaseio.com/"""
830+
def _get_emulator_config(self, parsed_url):
831+
"""Checks whether the SDK should connect to the RTDB emulator."""
832+
EmulatorConfig = collections.namedtuple('EmulatorConfig', ['base_url', 'namespace'])
842833
if parsed_url.scheme != 'https':
843-
raise ValueError(
844-
'Invalid database URL scheme: "{0}". Database URL must be an HTTPS URL.'.format(
845-
parsed_url.scheme))
846-
namespace = parsed_url.netloc.split('.')[0]
847-
if not namespace:
848-
raise ValueError(
849-
'Invalid database URL: "{0}". Database URL must be a valid URL to a '
850-
'Firebase Realtime Database instance.'.format(parsed_url.geturl()))
834+
# Emulator mode enabled by passing http URL via AppOptions
835+
base_url, namespace = _DatabaseService._parse_emulator_url(parsed_url)
836+
return EmulatorConfig(base_url, namespace)
837+
if self._emulator_host:
838+
# Emulator mode enabled via environment variable
839+
base_url = 'http://{0}'.format(self._emulator_host)
840+
namespace = parsed_url.netloc.split('.')[0]
841+
return EmulatorConfig(base_url, namespace)
851842

852-
if emulator_host:
853-
base_url = 'http://{0}'.format(emulator_host)
854-
else:
855-
base_url = 'https://{0}'.format(parsed_url.netloc)
856-
return base_url, namespace
843+
return None
857844

858845
@classmethod
859846
def _parse_emulator_url(cls, parsed_url):

tests/test_db.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Tests for firebase_admin.db."""
1616
import collections
1717
import json
18+
import os
1819
import sys
1920
import time
2021

@@ -28,6 +29,9 @@
2829
from tests import testutils
2930

3031

32+
_EMULATOR_HOST_ENV_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST'
33+
34+
3135
class MockAdapter(testutils.MockAdapter):
3236
"""A mock HTTP adapter that mimics RTDB server behavior."""
3337

@@ -702,52 +706,70 @@ def test_no_db_url(self):
702706
'url,emulator_host,expected_base_url,expected_namespace',
703707
[
704708
# Production URLs with no override:
705-
('https://test.firebaseio.com', None, 'https://test.firebaseio.com', 'test'),
706-
('https://test.firebaseio.com/', None, 'https://test.firebaseio.com', 'test'),
709+
('https://test.firebaseio.com', None, 'https://test.firebaseio.com', None),
710+
('https://test.firebaseio.com/', None, 'https://test.firebaseio.com', None),
707711
708712
# Production URLs with emulator_host override:
709713
('https://test.firebaseio.com', 'localhost:9000', 'http://localhost:9000', 'test'),
710714
('https://test.firebaseio.com/', 'localhost:9000', 'http://localhost:9000', 'test'),
711715
712-
# Emulator URLs with no override.
716+
# Emulator URL with no override.
713717
('http://localhost:8000/?ns=test', None, 'http://localhost:8000', 'test'),
718+
714719
# emulator_host is ignored when the original URL is already emulator.
715720
('http://localhost:8000/?ns=test', 'localhost:9999', 'http://localhost:8000', 'test'),
716721
]
717722
)
718723
def test_parse_db_url(self, url, emulator_host, expected_base_url, expected_namespace):
719-
base_url, namespace = db._DatabaseService._parse_db_url(url, emulator_host)
720-
assert base_url == expected_base_url
721-
assert namespace == expected_namespace
722-
723-
@pytest.mark.parametrize('url,emulator_host', [
724-
('', None),
725-
(None, None),
726-
(42, None),
727-
('test.firebaseio.com', None), # Not a URL.
728-
('http://test.firebaseio.com', None), # Use of non-HTTPs in production URLs.
729-
('ftp://test.firebaseio.com', None), # Use of non-HTTPs in production URLs.
730-
('https://example.com', None), # Invalid RTDB URL.
731-
('http://localhost:9000/', None), # No ns specified.
732-
('http://localhost:9000/?ns=', None), # No ns specified.
733-
('http://localhost:9000/?ns=test1&ns=test2', None), # Two ns parameters specified.
734-
('ftp://localhost:9000/?ns=test', None), # Neither HTTP or HTTPS.
724+
if emulator_host:
725+
os.environ[_EMULATOR_HOST_ENV_VAR] = emulator_host
726+
727+
try:
728+
firebase_admin.initialize_app(testutils.MockCredential(), {'databaseURL' : url})
729+
ref = db.reference()
730+
assert ref._client._base_url == expected_base_url
731+
assert ref._client.params.get('ns') == expected_namespace
732+
if expected_base_url.startswith('http://localhost'):
733+
assert isinstance(ref._client.credential, db._EmulatorAdminCredentials)
734+
else:
735+
assert isinstance(ref._client.credential, testutils.MockGoogleCredential)
736+
finally:
737+
if _EMULATOR_HOST_ENV_VAR in os.environ:
738+
del os.environ[_EMULATOR_HOST_ENV_VAR]
739+
740+
@pytest.mark.parametrize('url', [
741+
'',
742+
None,
743+
42,
744+
'test.firebaseio.com', # Not a URL.
745+
'http://test.firebaseio.com', # Use of non-HTTPs in production URLs.
746+
'ftp://test.firebaseio.com', # Use of non-HTTPs in production URLs.
747+
'http://localhost:9000/', # No ns specified.
748+
'http://localhost:9000/?ns=', # No ns specified.
749+
'http://localhost:9000/?ns=test1&ns=test2', # Two ns parameters specified.
750+
'ftp://localhost:9000/?ns=test', # Neither HTTP or HTTPS.
735751
])
736-
def test_parse_db_url_errors(self, url, emulator_host):
752+
def test_parse_db_url_errors(self, url):
753+
firebase_admin.initialize_app(testutils.MockCredential(), {'databaseURL' : url})
737754
with pytest.raises(ValueError):
738-
db._DatabaseService._parse_db_url(url, emulator_host)
755+
db.reference()
739756

740757
@pytest.mark.parametrize('url', [
741-
'https://test.firebaseio.com', 'https://test.firebaseio.com/'
758+
'https://test.firebaseio.com', 'https://test.firebaseio.com/',
759+
'https://test.eu-west1.firebasdatabase.app', 'https://test.eu-west1.firebasdatabase.app/'
742760
])
743761
def test_valid_db_url(self, url):
744762
firebase_admin.initialize_app(testutils.MockCredential(), {'databaseURL' : url})
745763
ref = db.reference()
746-
assert ref._client.base_url == 'https://test.firebaseio.com'
764+
expected_url = url
765+
if url.endswith('/'):
766+
expected_url = url[:-1]
767+
assert ref._client.base_url == expected_url
747768
assert 'auth_variable_override' not in ref._client.params
769+
assert 'ns' not in ref._client.params
748770

749771
@pytest.mark.parametrize('url', [
750-
None, '', 'foo', 'http://test.firebaseio.com', 'https://google.com',
772+
None, '', 'foo', 'http://test.firebaseio.com', 'http://test.firebasedatabase.app',
751773
True, False, 1, 0, dict(), list(), tuple(), _Object()
752774
])
753775
def test_invalid_db_url(self, url):

0 commit comments

Comments
 (0)