mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-10 16:56:34 +08:00
Revert "added example request and response for PUT /v1/wallet/keys/owner to api docs" This reverts commitd52ee4b31e. Revert "cutting down on the verbosity of logging outputs -- registrar now only prints 1 line on wakeups. storage drivers are concatenated into 1 line" This reverts commit87e3e7ab0d. Revert "adding dropbox as a default storage driver to load, and switched default 'required' drivers to 'disk,dropbox'" This reverts commit9471b0a20a. Revert "adding test case for issue 483, which *also* required fixing the app session schema to handle empty string methods a little bit more gracefully" This reverts commit32efc99d62. Revert "bugfix for the address reencoding in get_zonefile -- checks to see if the address is an address before trying to reencode" This reverts commit1488013b93. Revert "Merge branch 'rc-0.14.3' of github.com:blockstack/blockstack-core into rc-0.14.3" This reverts commitf75ab67960, reversing changes made tofe863bcd3c. Revert "don't create the metadata dir" This reverts commitfe863bcd3c. Revert "make all metadata directories inside the critical section" This reverts commite66236abd2. Revert "don't cast 'None' to string by accident" This reverts commitc6250d5349. Revert "force string" This reverts commite72d43d0be. Revert "add unbound proxy variable" This reverts commit7f1f7e9731. Revert "return raw zonefile" This reverts commit51e858428d. Revert "force string" This reverts commit1ce371644f. Revert "force string" This reverts commit5353cb1015. Revert "require virtualchain rc-0.14.3 and jsontokens-py 0.0.4" This reverts commit346f042db7. Revert "Merge branch 'rc-0.14.3' of https://github.com/blockstack/blockstack-core into rc-0.14.3" This reverts commit1fa1de3e54, reversing changes made to523cf405d7.
591 lines
19 KiB
Python
591 lines
19 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Blockstack-client
|
|
~~~~~
|
|
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
|
|
copyright: (c) 2016 by Blockstack.org
|
|
|
|
This file is part of Blockstack-client.
|
|
|
|
Blockstack-client is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Blockstack-client is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
You should have received a copy of the GNU General Public License
|
|
along with Blockstack-client. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
import copy
|
|
import blockstack_profiles
|
|
import httplib
|
|
import virtualchain
|
|
import jsonschema
|
|
import virtualchain
|
|
from virtualchain.lib.ecdsalib import *
|
|
import keylib
|
|
|
|
from .proxy import *
|
|
from blockstack_client import storage
|
|
from blockstack_client import user as user_db
|
|
|
|
from .logger import get_logger
|
|
from .constants import USER_ZONEFILE_TTL, CONFIG_PATH, BLOCKSTACK_TEST, BLOCKSTACK_DEBUG
|
|
|
|
from .zonefile import get_name_zonefile
|
|
from .keys import get_data_privkey_info
|
|
from .schemas import *
|
|
from .config import get_config
|
|
from .constants import BLOCKSTACK_REQUIRED_STORAGE_DRIVERS_WRITE
|
|
|
|
log = get_logger()
|
|
|
|
|
|
def set_profile_timestamp(profile, now=None):
|
|
"""
|
|
Set the profile's timestamp to now
|
|
"""
|
|
now = time.time() if now is None else now
|
|
profile['timestamp'] = now
|
|
|
|
return profile
|
|
|
|
|
|
def get_profile_timestamp(profile):
|
|
"""
|
|
Get profile timestamp
|
|
"""
|
|
return profile['timestamp']
|
|
|
|
|
|
def load_legacy_user_profile(name, expected_hash):
|
|
"""
|
|
Load a legacy user profile, and convert it into
|
|
the new zonefile-esque profile format that can
|
|
be serialized into a JWT.
|
|
|
|
Verify that the profile hashses to the above expected hash
|
|
"""
|
|
|
|
# fetch...
|
|
storage_host = 'onename.com'
|
|
assert name.endswith('.id')
|
|
|
|
name_without_namespace = '.'.join(name.split('.')[:-1])
|
|
storage_path = '/{}.json'.format(name_without_namespace)
|
|
|
|
try:
|
|
req = httplib.HTTPConnection(storage_host)
|
|
resp = req.request('GET', storage_path)
|
|
data = resp.read()
|
|
except Exception as e:
|
|
log.error('Failed to fetch http://{}/{}: {}'.format(storage_host, storage_path, e))
|
|
return None
|
|
|
|
try:
|
|
data_json = json.loads(data)
|
|
except Exception as e:
|
|
log.error('Unparseable profile data')
|
|
return None
|
|
|
|
data_hash = storage.get_blockchain_compat_hash(data_json)
|
|
if expected_hash != data_hash:
|
|
log.error('Hash mismatch: expected {}, got {}'.format(expected_hash, data_hash))
|
|
return None
|
|
|
|
assert blockstack_profiles.is_profile_in_legacy_format(data_json)
|
|
new_profile = blockstack_profiles.get_person_from_legacy_format(data_json)
|
|
return new_profile
|
|
|
|
|
|
|
|
def put_profile(name, new_profile, blockchain_id=None, user_data_privkey=None, user_zonefile=None,
|
|
proxy=None, wallet_keys=None, required_drivers=None, config_path=CONFIG_PATH):
|
|
"""
|
|
Set the new profile data. CLIENTS SHOULD NOT CALL THIS METHOD DIRECTLY.
|
|
|
|
if user_data_privkey is given, then wallet_keys does not need to be given.
|
|
|
|
Return {'status: True} on success
|
|
Return {'error': ...} on failure.
|
|
"""
|
|
|
|
ret = {}
|
|
|
|
proxy = get_default_proxy() if proxy is None else proxy
|
|
config = proxy.conf
|
|
|
|
# deduce storage drivers
|
|
required_storage_drivers = None
|
|
if required_drivers is not None:
|
|
required_storage_drivers = required_drivers
|
|
else:
|
|
required_storage_drivers = config.get('storage_drivers_required_write', None)
|
|
if required_storage_drivers is not None:
|
|
required_storage_drivers = required_storage_drivers.split(',')
|
|
else:
|
|
required_storage_drivers = config.get('storage_drivers', '').split(',')
|
|
|
|
# deduce private key
|
|
if user_data_privkey is None:
|
|
user_data_privkey = get_data_privkey_info(user_zonefile, wallet_keys=wallet_keys, config_path=config_path)
|
|
if json_is_error(user_data_privkey):
|
|
log.error("Failed to get data private key: {}".format(user_data_privkey['error']))
|
|
return {'error': 'No data key defined'}
|
|
|
|
profile_payload = copy.deepcopy(new_profile)
|
|
profile_payload = set_profile_timestamp(profile_payload)
|
|
|
|
if BLOCKSTACK_DEBUG:
|
|
# NOTE: don't calculate this string unless we're actually debugging...
|
|
log.debug('Save updated profile for "{}" to {} at {} by {}'.format(
|
|
name, ','.join(required_storage_drivers), get_profile_timestamp(profile_payload), get_pubkey_hex(user_data_privkey))
|
|
)
|
|
|
|
rc = storage.put_mutable_data(
|
|
name, profile_payload, data_privkey=user_data_privkey,
|
|
required=required_storage_drivers,
|
|
profile=True, blockchain_id=blockchain_id
|
|
)
|
|
|
|
if rc:
|
|
ret['status'] = True
|
|
else:
|
|
ret['error'] = 'Failed to update profile'
|
|
|
|
return ret
|
|
|
|
|
|
def delete_profile(blockchain_id, user_data_privkey=None, user_zonefile=None,
|
|
proxy=None, wallet_keys=None):
|
|
"""
|
|
Delete profile data. CLIENTS SHOULD NOT CALL THIS DIRECTLY
|
|
Return {'status: True} on success
|
|
Return {'error': ...} on failure.
|
|
"""
|
|
|
|
ret = {}
|
|
|
|
proxy = get_default_proxy() if proxy is None else proxy
|
|
config = proxy.conf
|
|
|
|
# deduce private key
|
|
if user_data_privkey is None:
|
|
user_data_privkey = get_data_privkey_info(user_zonefile, wallet_keys=wallet_keys, config_path=proxy.conf['path'])
|
|
if json_is_error(user_data_privkey):
|
|
log.error("Failed to get data private key: {}".format(user_data_privkey['error']))
|
|
return {'error': 'No data key defined'}
|
|
|
|
rc = storage.delete_mutable_data(blockchain_id, user_data_privkey)
|
|
if rc:
|
|
ret['status'] = True
|
|
else:
|
|
ret['error'] = 'Failed to update profile'
|
|
|
|
return ret
|
|
|
|
|
|
def get_profile(name, zonefile_storage_drivers=None, profile_storage_drivers=None,
|
|
proxy=None, user_zonefile=None, name_record=None,
|
|
include_name_record=False, include_raw_zonefile=False, use_zonefile_urls=True,
|
|
use_legacy=False, use_legacy_zonefile=True, decode_profile=True):
|
|
"""
|
|
Given a name, look up an associated profile.
|
|
Do so by first looking up the zonefile the name points to,
|
|
and then loading the profile from that zonefile's public key.
|
|
|
|
Notes on backwards compatibility (activated if use_legacy=True and use_legacy_zonefile=True):
|
|
|
|
* (use_legacy=True) If the user's zonefile is really a legacy profile from Onename, then
|
|
the profile returned will be the converted legacy profile. The returned zonefile will still
|
|
be a legacy profile, however.
|
|
The caller can check this and perform the conversion automatically.
|
|
|
|
* (use_legacy_zonefile=True) If the name points to a current zonefile that does not have a
|
|
data public key, then the owner address of the name will be used to verify
|
|
the profile's authenticity.
|
|
|
|
Returns {'status': True, 'profile': profile, 'zonefile': zonefile} on success.
|
|
* If include_name_record is True, then include 'name_record': name_record with the user's blockchain information
|
|
* If include_raw_zonefile is True, then include 'raw_zonefile': raw_zonefile with unparsed zone file
|
|
|
|
Returns {'error': ...} on error
|
|
"""
|
|
|
|
proxy = get_default_proxy() if proxy is None else proxy
|
|
|
|
raw_zonefile = None
|
|
if user_zonefile is None:
|
|
user_zonefile = get_name_zonefile(
|
|
name, proxy=proxy,
|
|
name_record=name_record, include_name_record=True,
|
|
storage_drivers=zonefile_storage_drivers,
|
|
include_raw_zonefile=include_raw_zonefile,
|
|
allow_legacy=True
|
|
)
|
|
|
|
if 'error' in user_zonefile:
|
|
return user_zonefile
|
|
|
|
raw_zonefile = None
|
|
if include_raw_zonefile:
|
|
raw_zonefile = user_zonefile.pop('raw_zonefile')
|
|
|
|
user_zonefile = user_zonefile['zonefile']
|
|
|
|
# is this really a legacy profile?
|
|
if blockstack_profiles.is_profile_in_legacy_format(user_zonefile):
|
|
if not use_legacy:
|
|
return {'error': 'Profile is in legacy format'}
|
|
|
|
# convert it
|
|
log.debug('Converting legacy profile to modern profile')
|
|
user_profile = blockstack_profiles.get_person_from_legacy_format(user_zonefile)
|
|
|
|
elif not user_db.is_user_zonefile(user_zonefile):
|
|
if not use_legacy:
|
|
return {'error': 'Name zonefile is non-standard'}
|
|
|
|
# not a legacy profile, but a custom profile
|
|
log.debug('Using custom legacy profile')
|
|
user_profile = copy.deepcopy(user_zonefile)
|
|
|
|
else:
|
|
# get user's data public key
|
|
data_address, owner_address = None, None
|
|
|
|
try:
|
|
user_data_pubkey = user_db.user_zonefile_data_pubkey(user_zonefile)
|
|
if user_data_pubkey is not None:
|
|
user_data_pubkey = str(user_data_pubkey)
|
|
data_address = keylib.ECPublicKey(user_data_pubkey).address()
|
|
|
|
except ValueError:
|
|
# multiple keys defined; we don't know which one to use
|
|
user_data_pubkey = None
|
|
|
|
if not use_legacy_zonefile and user_data_pubkey is None:
|
|
# legacy zonefile without a data public key
|
|
return {'error': 'Name zonefile is missing a public key'}
|
|
|
|
# find owner address
|
|
if name_record is None:
|
|
name_record = get_name_blockchain_record(name, proxy=proxy)
|
|
if name_record is None or 'error' in name_record:
|
|
log.error('Failed to look up name record for "{}"'.format(name))
|
|
return {'error': 'Failed to look up name record'}
|
|
|
|
assert 'address' in name_record.keys(), json.dumps(name_record, indent=4, sort_keys=True)
|
|
owner_address = name_record['address']
|
|
|
|
# get user's data public key from the zonefile
|
|
urls = None
|
|
if use_zonefile_urls and user_zonefile is not None:
|
|
urls = user_db.user_zonefile_urls(user_zonefile)
|
|
|
|
user_profile = storage.get_mutable_data(
|
|
name, user_data_pubkey, blockchain_id=name,
|
|
data_address=data_address, owner_address=owner_address,
|
|
urls=urls, drivers=profile_storage_drivers, decode=decode_profile,
|
|
)
|
|
|
|
if user_profile is None or json_is_error(user_profile):
|
|
if user_profile is None:
|
|
log.error('no user profile for {}'.format(name))
|
|
else:
|
|
log.error('failed to load profile for {}: {}'.format(name, user_profile['error']))
|
|
|
|
return {'error': 'Failed to load user profile'}
|
|
|
|
# finally, if the caller asked for the name record, and we didn't get a chance to look it up,
|
|
# then go get it.
|
|
ret = {
|
|
'status': True,
|
|
'profile': user_profile,
|
|
'zonefile': user_zonefile
|
|
}
|
|
|
|
if include_name_record:
|
|
if name_record is None:
|
|
name_record = get_name_blockchain_record(name, proxy=proxy)
|
|
|
|
if name_record is None or 'error' in name_record:
|
|
log.error('Failed to look up name record for "{}"'.format(name))
|
|
return {'error': 'Failed to look up name record'}
|
|
|
|
ret['name_record'] = name_record
|
|
|
|
if include_raw_zonefile:
|
|
if raw_zonefile is not None:
|
|
ret['raw_zonefile'] = raw_zonefile
|
|
|
|
return ret
|
|
|
|
|
|
def _get_person_profile(name, proxy=None):
|
|
"""
|
|
Get the person's zonefile and profile.
|
|
Handle legacy zonefiles, but not legacy profiles.
|
|
Return {'profile': ..., 'zonefile': ..., 'person': ...} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
res = get_profile(name, proxy=proxy, use_legacy_zonefile=True)
|
|
if 'error' in res:
|
|
return {'error': 'Failed to load zonefile: {}'.format(zonefile['error'])}
|
|
|
|
profile = res.pop('profile')
|
|
zonefile = res.pop('zonefile')
|
|
|
|
if blockstack_profiles.is_profile_in_legacy_format(profile):
|
|
return {'error': 'Legacy profile'}
|
|
|
|
person = None
|
|
try:
|
|
person = blockstack_profiles.Person(profile)
|
|
except Exception as e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to parse profile data into a Person record'}
|
|
|
|
return {'profile': profile, 'zonefile': zonefile, 'person': person}
|
|
|
|
|
|
def _save_person_profile(name, zonefile, profile, wallet_keys, user_data_privkey=None, blockchain_id=None, proxy=None, config_path=CONFIG_PATH):
|
|
"""
|
|
Save a person's profile, given information fetched with _get_person_profile.
|
|
Return {'status': True} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
conf = get_config(config_path)
|
|
assert conf
|
|
|
|
required_storage_drivers = conf.get(
|
|
'storage_drivers_required_write',
|
|
BLOCKSTACK_REQUIRED_STORAGE_DRIVERS_WRITE
|
|
)
|
|
required_storage_drivers = required_storage_drivers.split()
|
|
|
|
res = put_profile(name, profile, user_zonefile=zonefile,
|
|
wallet_keys=wallet_keys, user_data_privkey=user_data_privkey, proxy=proxy,
|
|
required_drivers=required_storage_drivers, blockchain_id=name,
|
|
config_path=config_path )
|
|
|
|
return res
|
|
|
|
|
|
def profile_list_accounts(name, proxy=None):
|
|
"""
|
|
Get the list of accounts in a name's Person-formatted profile.
|
|
Return {'accounts': ...} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
name_info = _get_person_profile(name, proxy=proxy)
|
|
if 'error' in name_info:
|
|
return name_info
|
|
|
|
profile = name_info.pop('profile')
|
|
zonefile = name_info.pop('zonefile')
|
|
person = name_info.pop('person')
|
|
|
|
accounts = []
|
|
if hasattr(person, 'account'):
|
|
accounts = person.account
|
|
|
|
for acct in accounts:
|
|
try:
|
|
jsonschema.validate(acct, PROFILE_ACCOUNT_SCHEMA)
|
|
accounts.append(acct)
|
|
except jsonschema.ValidationError:
|
|
continue
|
|
|
|
return {'accounts': accounts}
|
|
|
|
|
|
def profile_get_account(blockchain_id, service, identifier, config_path=CONFIG_PATH, proxy=None):
|
|
"""
|
|
Get an account. The first hit is returned.
|
|
Return {'status': True, 'account': ...} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
account_info = _list_accounts(blockchain_id, proxy=proxy )
|
|
if 'error' in account_info:
|
|
return account_info
|
|
|
|
accounts = account_info['accounts']
|
|
for account in accounts:
|
|
if account['service'] == service and account['identifier'] == identifier:
|
|
return {'status': True, 'account': account}
|
|
|
|
return {'error': 'No such account', 'errno': errno.ENOENT}
|
|
|
|
|
|
def profile_find_accounts(cur_profile, service, identifer):
|
|
"""
|
|
Given an profile, find accounts that match the service and identifier
|
|
Returns a list of accounts on success
|
|
"""
|
|
accounts = []
|
|
for acct in cur_profile.get('account', []):
|
|
try:
|
|
jsonschema.validate(acct, PROFILE_ACCOUNT_SCHEMA)
|
|
if acct['service'] == service and acct['identifier'] == identifier:
|
|
accounts.append(acct)
|
|
|
|
except jsonschema.ValidationError:
|
|
continue
|
|
|
|
return accounts
|
|
|
|
|
|
def profile_patch_account(cur_profile, service, identifier, content_url, extra_data):
|
|
"""
|
|
Patch a given profile to add an account
|
|
Return the new profile
|
|
"""
|
|
profile = copy.deepcopy(cur_profile)
|
|
|
|
# make data
|
|
new_account = {
|
|
'service': service,
|
|
'identifier': identifier,
|
|
}
|
|
|
|
if content_url:
|
|
new_account['contentUrl'] = content_url
|
|
|
|
if extra_data:
|
|
new_account.update(extra_data)
|
|
|
|
if not profile.has_key('account'):
|
|
profile['account'] = []
|
|
|
|
# overwrite existing, if given
|
|
replaced = False
|
|
for i in xrange(0, len(profile['account'])):
|
|
|
|
account = profile['account'][i]
|
|
|
|
try:
|
|
jsonschema.validate(account, PROFILE_ACCOUNT_SCHEMA)
|
|
except jsonschema.ValidationError:
|
|
continue
|
|
|
|
if account['service'] == service and account['identifier'] == identifier:
|
|
profile['account'][i] = new_account
|
|
replaced = True
|
|
break
|
|
|
|
if not replaced:
|
|
profile['account'].append(new_account)
|
|
|
|
return profile
|
|
|
|
|
|
def profile_put_account(blockchain_id, service, identifier, content_url, extra_data, wallet_keys, user_data_privkey=None, config_path=CONFIG_PATH, proxy=None):
|
|
"""
|
|
Save a new account to a profile.
|
|
Return {'status': True, 'replaced': True/False} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
person_info = _get_person_profile(blockchain_id, proxy=proxy)
|
|
if 'error' in person_info:
|
|
return person_info
|
|
|
|
zonefile = person_info.pop('zonefile')
|
|
profile = person_info.pop('profile')
|
|
profile = profile_patch_account(profile, service, identifier, content_url, extra_data)
|
|
|
|
# save
|
|
result = _save_person_profile(blockchain_id, zonefile, profile, wallet_keys, user_data_privkey=user_data_privkey, blockchain_id=blockchain_id, proxy=proxy, config_path=config_path)
|
|
if 'error' in result:
|
|
return result
|
|
|
|
return {'status': True}
|
|
|
|
|
|
def profile_delete_account(blockchain_id, service, identifier, wallet_keys, user_data_privkey=None, config_path=CONFIG_PATH, proxy=None):
|
|
"""
|
|
Delete an account, given the blockchain ID, service, and identifier
|
|
Return {'status': True} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
person_info = _get_person_profile(blockchain_id, proxy=proxy)
|
|
if 'error' in person_info:
|
|
return person_info
|
|
|
|
zonefile = person_info['zonefile']
|
|
profile = person_info['profile']
|
|
if not profile.has_key('account'):
|
|
# nothing to do
|
|
return {'error': 'No such account'}
|
|
|
|
found = False
|
|
for i in xrange(0, len(profile['account'])):
|
|
account = profile['account'][i]
|
|
|
|
try:
|
|
jsonschema.validate(account, PROFILE_ACCOUNT_SCHEMA)
|
|
except jsonschema.ValidationError:
|
|
continue
|
|
|
|
if account['service'] == service and account['identifier'] == identifier:
|
|
profile['account'].pop(i)
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
return {'error': 'No such account'}
|
|
|
|
result = _save_person_profile(blockchain_id, zonefile, profile, wallet_keys, user_data_privkey=user_data_privkey, blockchain_id=blockchain_id, proxy=proxy, config_path=config_path)
|
|
if 'error' in result:
|
|
return result
|
|
|
|
return {'status': True}
|
|
|
|
|
|
def profile_list_device_ids( blockchain_id, proxy=None ):
|
|
"""
|
|
Given a blockchain ID, identify the set of device IDs for it.
|
|
|
|
Returns {'status': True, 'device_ids': ...} on success
|
|
Returns {'error': ...} on error
|
|
"""
|
|
raise NotImplemented("Token file logic is not implemented yet")
|
|
|
|
|
|
def profile_add_device_id( blockchain_id, device_id, wallet_keys, config_path=CONFIG_PATH, proxy=None):
|
|
"""
|
|
Add a device ID to a profile
|
|
Return {'status': True} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
raise NotImplemented("Token file logic is not implemented yet")
|
|
|
|
|
|
def profile_remove_device_id( blockchain_id, device_id, wallet_keys, config_path=CONFIG_PATH, proxy=None):
|
|
"""
|
|
Remove a device ID from a profile
|
|
Return {'status': True} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
raise NotImplemented("Token file logic is not implemented yet")
|
|
|
|
|