Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugar_network/client
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2013-10-21 08:06:00 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2013-10-21 08:06:00 (GMT)
commit8223b2b538540aa227a30cc18434571455af5bc3 (patch)
tree1c5c275ff8c6471687335d50aee6666dd2f67412 /sugar_network/client
parent58471a0a713dd3ef77276a3d0df23c44a5ddeb0d (diff)
Switch to RSA auth keys; keep private key per user, not per sugar profile
Diffstat (limited to 'sugar_network/client')
-rw-r--r--sugar_network/client/__init__.py169
-rw-r--r--sugar_network/client/routes.py81
-rw-r--r--sugar_network/client/solver.py10
3 files changed, 115 insertions, 145 deletions
diff --git a/sugar_network/client/__init__.py b/sugar_network/client/__init__.py
index 6a620f9..d8eefcb 100644
--- a/sugar_network/client/__init__.py
+++ b/sugar_network/client/__init__.py
@@ -15,42 +15,10 @@
import os
import logging
+from base64 import b64encode
from os.path import join, expanduser, exists
-from sugar_network import toolkit
-from sugar_network.toolkit import Option
-
-
-SUGAR_API_COMPATIBILITY = {
- '0.94': frozenset(['0.86', '0.88', '0.90', '0.92', '0.94']),
- }
-
-_NICKNAME_GCONF = '/desktop/sugar/user/nick'
-_COLOR_GCONF = '/desktop/sugar/user/color'
-_XO_SERIAL_PATH = ['/ofw/mfg-data/SN', '/proc/device-tree/mfg-data/SN']
-_XO_UUID_PATH = ['/ofw/mfg-data/U#', '/proc/device-tree/mfg-data/U#']
-
-_logger = logging.getLogger('client')
-_sugar_uid = None
-
-
-def profile_path(*args):
- """Path within sugar profile directory.
-
- Missed directories will be created.
-
- :param args:
- path parts that will be added to the resulting path
- :returns:
- full path with directory part existed
-
- """
- if os.geteuid():
- root_dir = join(os.environ['HOME'], '.sugar',
- os.environ.get('SUGAR_PROFILE', 'default'))
- else:
- root_dir = '/var/sugar-network'
- return join(root_dir, *args)
+from sugar_network.toolkit import http, Option
api_url = Option(
@@ -68,7 +36,7 @@ no_check_certificate = Option(
local_root = Option(
'path to the directory to keep all local data',
- default=profile_path('network'), name='local_root')
+ default=lambda: profile_path('network'), name='local_root')
server_mode = Option(
'start server to share local documents',
@@ -104,23 +72,6 @@ discover_server = Option(
default=False, type_cast=Option.bool_cast,
action='store_true', name='discover_server')
-no_dbus = Option(
- 'disable any DBus usage',
- default=False, type_cast=Option.bool_cast,
- action='store_true', name='no-dbus')
-
-anonymous = Option(
- 'use anonymous user to access to Sugar Network server; '
- 'only read-only operations are available in this mode',
- default=False, type_cast=Option.bool_cast, action='store_true',
- name='anonymous')
-
-accept_language = Option(
- 'comma separated list to specify HTTP Accept-Language '
- 'header field value manually',
- default=[], type_cast=Option.list_cast, type_repr=Option.list_repr,
- name='accept-language', short_option='-l')
-
cache_limit = Option(
'the minimal disk free space, in bytes, to preserve while recycling '
'disk cache; the final limit will be a minimal between --cache-limit '
@@ -142,6 +93,44 @@ cache_timeout = Option(
'check disk cache for recycling in specified delay in seconds',
default=3600, type_cast=int, name='cache-timeout')
+login = Option(
+ 'Sugar Labs account to connect to Sugar Network API server; '
+ 'should be set only if either password is provided or public key '
+ 'for Sugar Labs account was uploaded to the Sugar Network',
+ name='login', short_option='-l')
+
+password = Option(
+ 'Sugar Labs account password to connect to Sugar Network API server '
+ 'using Basic authentication; if omitted, keys based authentication '
+ 'will be used',
+ name='password', short_option='-p')
+
+keyfile = Option(
+ 'path to RSA private key to connect to Sugar Network API server',
+ name='keyfile', short_option='-k', default='~/.ssh/sugar-network')
+
+
+_logger = logging.getLogger('client')
+
+
+def profile_path(*args):
+ """Path within sugar profile directory.
+
+ Missed directories will be created.
+
+ :param args:
+ path parts that will be added to the resulting path
+ :returns:
+ full path with directory part existed
+
+ """
+ if os.geteuid():
+ root_dir = join(os.environ['HOME'], '.sugar',
+ os.environ.get('SUGAR_PROFILE', 'default'))
+ else:
+ root_dir = '/var/sugar-network'
+ return join(root_dir, *args)
+
def path(*args):
"""Calculate a path from the root.
@@ -162,36 +151,6 @@ def path(*args):
return str(result)
-def Connection(url=None):
- from sugar_network.toolkit import http
- if url is None:
- url = api_url.value
- creds = None
- if not anonymous.value:
- if exists(key_path()):
- creds = (sugar_uid(), key_path(), sugar_profile)
- else:
- _logger.warning('Sugar session was never started (no DSA key),'
- 'fallback to anonymous mode')
- return http.Connection(url, creds=creds)
-
-
-def IPCConnection():
- from sugar_network.toolkit import http
-
- return http.Connection(
- api_url='http://127.0.0.1:%s' % ipc_port.value,
- creds=None,
- # No need in proxy for localhost
- trust_env=False,
- # The 1st ipc->client->node request might fail if connection
- # to the node is lost, so, initiate the 2nd request from ipc
- # to retrive data from client in offline mode without propagating
- # errors from ipc
- max_retries=1,
- )
-
-
def logger_level():
"""Current Sugar logger level as --debug value."""
_LEVELS = {
@@ -205,30 +164,6 @@ def logger_level():
return _LEVELS.get(level, 0)
-def key_path():
- return profile_path('owner.key')
-
-
-def sugar_uid():
- global _sugar_uid
- if _sugar_uid is None:
- import hashlib
- pubkey = toolkit.pubkey(key_path()).split()[1]
- _sugar_uid = str(hashlib.sha1(pubkey).hexdigest())
- return _sugar_uid
-
-
-def sugar_profile():
- import gconf
- conf = gconf.client_get_default()
- return {'name': conf.get_string(_NICKNAME_GCONF) or '',
- 'color': conf.get_string(_COLOR_GCONF) or '#000000,#000000',
- 'machine_sn': _read_XO_value(_XO_SERIAL_PATH) or '',
- 'machine_uuid': _read_XO_value(_XO_UUID_PATH) or '',
- 'pubkey': toolkit.pubkey(key_path()),
- }
-
-
def stability(context):
value = Option.get('stabilities', context) or \
Option.get('stabilities', 'default') or \
@@ -236,8 +171,20 @@ def stability(context):
return value.split()
-def _read_XO_value(paths):
- for value_path in paths:
- if exists(value_path):
- with file(value_path) as f:
- return f.read().rstrip('\x00\n')
+def Connection(url=None, **args):
+ if url is None:
+ url = api_url.value
+ return http.Connection(url, verify=not no_check_certificate.value, **args)
+
+
+def IPCConnection():
+ return http.Connection(
+ api_url='http://127.0.0.1:%s' % ipc_port.value,
+ # Online ipc->client->node request might fail if node connection
+ # is lost in client process, so, re-send ipc request immediately
+ # to retrive data from client in offline mode without propagating
+ # errors on ipc side
+ max_retries=1,
+ # No need in proxy settings to connect to localhost
+ trust_env=False,
+ )
diff --git a/sugar_network/client/routes.py b/sugar_network/client/routes.py
index 9960b9d..1feedec 100644
--- a/sugar_network/client/routes.py
+++ b/sugar_network/client/routes.py
@@ -16,6 +16,7 @@
import os
import logging
import httplib
+from base64 import b64encode
from zipfile import ZipFile, ZIP_DEFLATED
from os.path import join, basename
@@ -44,8 +45,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
def __init__(self, home_volume, api_url=None, no_subscription=False):
model.FrontRoutes.__init__(self)
implementations.Routes.__init__(self, home_volume)
- if not client.no_dbus.value:
- journal.Routes.__init__(self)
+ journal.Routes.__init__(self)
self._local = _LocalRoutes(home_volume)
self._inline = coroutine.Event()
@@ -56,6 +56,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
self._no_subscription = no_subscription
self._server_mode = not api_url
self._api_url = api_url
+ self._auth = _Auth()
if not client.delayed_start.value:
self.connect()
@@ -63,6 +64,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
def connect(self):
self._got_offline(force=True)
if self._server_mode:
+ enforce(not client.login.value)
mountpoints.connect(_SN_DIRNAME,
self._found_mount, self._lost_mount)
else:
@@ -113,24 +115,20 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
# no way to process specified request on the node
raise http.ServiceUnavailable()
- @route('GET', cmd='status',
- mime_type='application/json')
- def status(self):
- result = {'route': 'proxy' if self._inline.is_set() else 'offline'}
- if self._inline.is_set():
- result['node'] = self._node.api_url
- return result
-
@route('GET', cmd='inline',
mime_type='application/json')
def inline(self):
return self._inline.is_set()
+ @route('GET', cmd='whoami', mime_type='application/json')
def whoami(self, request, response):
if self._inline.is_set():
- return self.fallback(request, response)
+ result = self.fallback(request, response)
+ result['route'] = 'proxy'
else:
- return {'roles': [], 'guid': client.sugar_uid()}
+ result = {'roles': [], 'route': 'offline'}
+ result['guid'] = self._auth.login
+ return result
@route('GET', [None],
arguments={
@@ -205,6 +203,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
if client.layers.value and \
request.resource in ('context', 'implementation'):
request.add('layer', *client.layers.value)
+ request.principal = self._auth.login
try:
reply = self._node.call(request, response)
if hasattr(reply, 'read'):
@@ -228,11 +227,11 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
if not force and not self._inline.is_set():
return
if self._node is not None:
- _logger.debug('Got offline on %r', self._node)
self._node.close()
if self._inline.is_set():
+ _logger.debug('Got offline on %r', self._node)
self.broadcast({'event': 'inline', 'state': 'offline'})
- self._inline.clear()
+ self._inline.clear()
self._local.volume.broadcast = self.broadcast
def _restart_online(self):
@@ -266,9 +265,10 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
def handshake(url):
_logger.debug('Connecting to %r node', url)
- self._node = client.Connection(url)
- info = self._node.get(cmd='info')
- impl_info = info['resources'].get('implementation')
+ self._node = client.Connection(url, auth=self._auth)
+ status = self._node.get(cmd='status')
+ self._auth.allow_basic_auth = (status.get('level') == 'master')
+ impl_info = status['resources'].get('implementation')
if impl_info:
self.invalidate_solutions(impl_info['mtime'])
if self._inline.is_set():
@@ -317,8 +317,13 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes):
node.data_root.value = db_path
node.stats_root.value = join(root, _SN_DIRNAME, 'stats')
node.files_root.value = join(root, _SN_DIRNAME, 'files')
-
volume = db.Volume(db_path, model.RESOURCES)
+
+ if not volume['user'].exists(self._auth.login):
+ profile = self._auth.profile()
+ profile['guid'] = self._auth.login
+ volume['user'].create(profile)
+
self._node = _NodeRoutes(join(db_path, 'node'), volume,
self.broadcast)
self._jobs.spawn(volume.populate)
@@ -360,7 +365,7 @@ class CachedClientRoutes(ClientRoutes):
def push(request, seq):
try:
- self._node.call(request)
+ self.fallback(request)
except Exception:
_logger.exception('Cannot push %r, will postpone', request)
skiped_seq.include(seq)
@@ -456,21 +461,9 @@ class _NodeRoutes(SlaveRoutes, Router):
self._mounts = toolkit.Pool()
self._jobs = coroutine.Pool()
- users = volume['user']
- if not users.exists(client.sugar_uid()):
- profile = client.sugar_profile()
- profile['guid'] = client.sugar_uid()
- users.create(profile)
-
mountpoints.connect(_SYNC_DIRNAME,
self.__found_mountcb, self.__lost_mount_cb)
- def preroute(self, op, request):
- request.principal = client.sugar_uid()
-
- def whoami(self, request, response):
- return {'roles': [], 'guid': client.sugar_uid()}
-
def broadcast(self, event=None, request=None):
SlaveRoutes.broadcast(self, event, request)
self._localcast(event)
@@ -537,3 +530,29 @@ class _ResponseStream(object):
except (http.ConnectionError, httplib.IncompleteRead):
self._on_fail_cb()
raise
+
+
+class _Auth(http.SugarAuth):
+
+ def __init__(self):
+ http.SugarAuth.__init__(self, client.keyfile.value)
+ if client.login.value:
+ self.login = client.login.value
+ self.allow_basic_auth = False
+
+ def profile(self):
+ if self.allow_basic_auth and \
+ client.login.value and client.password.value:
+ return None
+ import gconf
+ conf = gconf.client_get_default()
+ self._profile['name'] = conf.get_string('/desktop/sugar/user/nick')
+ self._profile['color'] = conf.get_string('/desktop/sugar/user/color')
+ return http.SugarAuth.profile(self)
+
+ def __call__(self, nonce):
+ if not self.allow_basic_auth or \
+ not client.login.value or not client.password.value:
+ return http.SugarAuth.__call__(self, nonce)
+ auth = b64encode('%s:%s' % (client.login.value, client.password.value))
+ return 'Basic %s' % auth
diff --git a/sugar_network/client/solver.py b/sugar_network/client/solver.py
index 4d4e341..9f29e1d 100644
--- a/sugar_network/client/solver.py
+++ b/sugar_network/client/solver.py
@@ -19,7 +19,7 @@ import sys
import logging
from os.path import isabs, join, dirname
-from sugar_network.client import packagekit, SUGAR_API_COMPATIBILITY
+from sugar_network.client import packagekit
from sugar_network.toolkit.spec import parse_version
from sugar_network.toolkit import http, lsb_release
@@ -33,6 +33,10 @@ from zeroinstall.injector.arch import machine_ranks
from zeroinstall.injector.distro import try_cleanup_distro_version
+_SUGAR_API_COMPATIBILITY = {
+ '0.94': frozenset(['0.86', '0.88', '0.90', '0.92', '0.94']),
+ }
+
model.Interface.__init__ = lambda *args: _interface_init(*args)
reader.check_readable = lambda *args, **kwargs: True
reader.update_from_cache = lambda *args, **kwargs: None
@@ -102,7 +106,7 @@ def solve(call, context, stability):
if feed.to_resolve:
continue
if status is None:
- status = call(method='GET', cmd='status')
+ status = call(method='GET', cmd='whoami')
if status['route'] == 'offline':
raise http.ServiceUnavailable(str(error))
else:
@@ -182,7 +186,7 @@ def _load_feed(context):
except ImportError:
# XXX sweets-sugar binding might be not sourced
host_version = '0.94'
- for version in SUGAR_API_COMPATIBILITY.get(host_version) or []:
+ for version in _SUGAR_API_COMPATIBILITY.get(host_version) or []:
feed.implement_sugar(version)
feed.name = context
return feed