This reverts us *all* the way back to 523cf405d7 -- this removes all changes to support the token file from this branch.

Revert "added example request and response for PUT /v1/wallet/keys/owner to api docs"

This reverts commit d52ee4b31e.

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 commit 87e3e7ab0d.

Revert "adding dropbox as a default storage driver to load, and switched default 'required' drivers to 'disk,dropbox'"

This reverts commit 9471b0a20a.

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 commit 32efc99d62.

Revert "bugfix for the address reencoding in get_zonefile -- checks to see if the address is an address before trying to reencode"

This reverts commit 1488013b93.

Revert "Merge branch 'rc-0.14.3' of github.com:blockstack/blockstack-core into rc-0.14.3"

This reverts commit f75ab67960, reversing
changes made to fe863bcd3c.

Revert "don't create the metadata dir"

This reverts commit fe863bcd3c.

Revert "make all metadata directories inside the critical section"

This reverts commit e66236abd2.

Revert "don't cast 'None' to string by accident"

This reverts commit c6250d5349.

Revert "force string"

This reverts commit e72d43d0be.

Revert "add unbound proxy variable"

This reverts commit 7f1f7e9731.

Revert "return raw zonefile"

This reverts commit 51e858428d.

Revert "force string"

This reverts commit 1ce371644f.

Revert "force string"

This reverts commit 5353cb1015.

Revert "require virtualchain rc-0.14.3 and jsontokens-py 0.0.4"

This reverts commit 346f042db7.

Revert "Merge branch 'rc-0.14.3' of https://github.com/blockstack/blockstack-core into rc-0.14.3"

This reverts commit 1fa1de3e54, reversing
changes made to 523cf405d7.
This commit is contained in:
Aaron Blankstein
2017-07-10 14:49:16 -04:00
parent d52ee4b31e
commit 93d07d68b3
51 changed files with 1476 additions and 3363 deletions

View File

@@ -32,88 +32,321 @@ import virtualchain
from virtualchain.lib.ecdsalib import *
import keylib
from proxy import *
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 .logger import get_logger
from .constants import USER_ZONEFILE_TTL, CONFIG_PATH, BLOCKSTACK_TEST, BLOCKSTACK_DEBUG
from token_file import token_file_parse, token_file_update_profile, token_file_get, token_file_put
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
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 get_profile(name, **kw):
def set_profile_timestamp(profile, now=None):
"""
Legacy compatibility method for get_token_file().
Wraps get_token_file, and if a token file is successfully resolved, returns
{'status': True,
'profile': profile,
'zonefile': zonefile,
'token_file': token_file,
'raw_zonefile': unparsed zone file
'token_file': actual token file (if present)
'legacy': whether or not the profile was legacy}
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
"""
res = token_file_get(name, **kw)
if 'error' in res:
return res
token_file = res['token_file']
zonefile = res['zonefile']
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)
profile = None
legacy = False
if res.get('legacy_profile') is not None:
profile = res['legacy_profile']
legacy = True
else:
profile = res['profile']
raw_zonefile = res.get('raw_zonefile')
name_record = res.get('name_record')
token_file = res.get('token_file')
# 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': profile,
'zonefile': zonefile,
'raw_zonefile': raw_zonefile,
'name_record': name_record,
'token_file': token_file,
'legacy': legacy
'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 profile.
Works for raw profiles, and for profiles within token files.
Only works if the profile is a Persona.
Return {'profile': ..., 'person': ...} on success
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)
res = get_profile(name, proxy=proxy, use_legacy_zonefile=True)
if 'error' in res:
return {'error': 'Failed to load profile: {}'.format(res['error'])}
return {'error': 'Failed to load zonefile: {}'.format(zonefile['error'])}
if res['legacy']:
return {'error': 'Failed to load profile: legacy format'}
token_file = res.pop('token_file')
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)
@@ -121,28 +354,29 @@ def _get_person_profile(name, proxy=None):
log.exception(e)
return {'error': 'Failed to parse profile data into a Person record'}
return {'profile': profile, 'person': person, 'token_file': token_file}
return {'profile': profile, 'zonefile': zonefile, 'person': person}
def _save_person_profile(name, cur_token_file, profile, signing_private_key, blockchain_id=None, proxy=None, config_path=CONFIG_PATH):
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 = conf.get(
'storage_drivers_required_write',
BLOCKSTACK_REQUIRED_STORAGE_DRIVERS_WRITE
)
required_storage_drivers = required_storage_drivers.split()
res = token_file_update_profile(cur_token_file, profile, signing_private_key)
if 'error' in res:
return res
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 )
new_token_file = res['token_file']
res = token_file_put(name, new_token_file, signing_private_key, proxy=proxy, required_drivers=required_storage_drivers, config_path=config_path)
return res
@@ -159,16 +393,16 @@ def profile_list_accounts(name, proxy=None):
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')
person_accounts = []
accounts = []
if hasattr(person, 'account'):
person_accounts = person.account
accounts = []
for acct in person_accounts:
accounts = person.account
for acct in accounts:
try:
jsonschema.validate(acct, PROFILE_ACCOUNT_SCHEMA)
accounts.append(acct)
@@ -185,7 +419,7 @@ def profile_get_account(blockchain_id, service, identifier, config_path=CONFIG_P
Return {'error': ...} on error
"""
account_info = profile_list_accounts(blockchain_id, proxy=proxy )
account_info = _list_accounts(blockchain_id, proxy=proxy )
if 'error' in account_info:
return account_info
@@ -259,7 +493,7 @@ def profile_patch_account(cur_profile, service, identifier, content_url, extra_d
return profile
def profile_put_account(blockchain_id, service, identifier, content_url, extra_data, signing_private_key, config_path=CONFIG_PATH, proxy=None):
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
@@ -272,23 +506,20 @@ def profile_put_account(blockchain_id, service, identifier, content_url, extra_d
person_info = _get_person_profile(blockchain_id, proxy=proxy)
if 'error' in person_info:
return person_info
token_file = person_info['token_file']
if token_file is None:
return {'error': 'Name points to raw, legacy profile. Please use the `migrate` command to migrate to the latest profile data format'}
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, token_file, profile, signing_private_key, blockchain_id=blockchain_id, proxy=proxy, config_path=config_path)
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, signing_private_key, config_path=CONFIG_PATH, proxy=None):
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
@@ -299,14 +530,11 @@ def profile_delete_account(blockchain_id, service, identifier, signing_private_k
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'}
token_file = person_info['token_file']
if token_file is None:
return {'error': 'Name points to raw, legacy profile. Please use the `migrate` command to migrate to the latest profile data format'}
found = False
for i in xrange(0, len(profile['account'])):
@@ -325,10 +553,38 @@ def profile_delete_account(blockchain_id, service, identifier, signing_private_k
if not found:
return {'error': 'No such account'}
result = _save_person_profile(blockchain_id, token_file, profile, signing_private_key, blockchain_id=blockchain_id, proxy=proxy, config_path=config_path)
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")