Files
stacks-puppet-node/blockstack_client/app.py
Aaron Blankstein 93d07d68b3 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.
2017-07-10 14:59:23 -04:00

443 lines
17 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
"""
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 jsonschema
from jsonschema.exceptions import ValidationError
import time
from keylib import *
import virtualchain
from virtualchain.lib.ecdsalib import *
import jsontokens
import storage
import data
import user as user_db
from .proxy import *
from config import get_config
from .constants import CONFIG_PATH, BLOCKSTACK_TEST, LENGTH_MAX_NAME, DEFAULT_API_PORT, DEFAULT_API_HOST
from .schemas import *
from .storage import classify_storage_drivers
def app_make_session( blockchain_id, app_private_key, app_domain, methods, app_public_keys, requester_device_id, master_data_privkey, session_lifetime=None, config_path=CONFIG_PATH ):
"""
Make a session JWT for this application.
Verify with user private key
Sign with master private key
Return {'session': session jwt, 'session_token': session token} on success
Return {'error': ...} on error
"""
conf = get_config(path=config_path)
assert conf
if session_lifetime is None:
session_lifetime = conf.get('default_session_lifetime', 1e80)
app_public_key = get_pubkey_hex(app_private_key)
app_user_id = data.datastore_get_id(app_public_key)
api_endpoint_host = conf.get('api_endpoint_host', DEFAULT_API_HOST)
api_endpoint_port = conf.get('api_endpoint_port', DEFAULT_API_PORT)
api_endpoint = '{}:{}'.format(api_endpoint_host, api_endpoint_port)
ses = {
'version': 1,
'blockchain_id': blockchain_id,
'app_domain': app_domain,
'methods': methods,
'app_public_keys': app_public_keys,
'app_user_id': app_user_id,
'api_endpoint': api_endpoint,
'device_id': requester_device_id,
'storage': {
'classes': classify_storage_drivers(),
'preferences': {}
},
'timestamp': int(time.time()),
'expires': int(time.time() + session_lifetime),
}
jsonschema.validate(ses, APP_SESSION_SCHEMA)
signer = jsontokens.TokenSigner()
session_token = signer.sign( ses, master_data_privkey )
session = jsontokens.decode_token(session_token)
return {'session': session, 'session_token': session_token}
def app_verify_session( app_session_token, data_pubkey_hex, config_path=CONFIG_PATH ):
"""
Verify and decode a JWT app session token.
The session is valid if the signature matches and the token is not expired.
Return the decoded session token payload on success
Return None on error
"""
pubkey = str(data_pubkey_hex)
verifier = jsontokens.TokenVerifier()
valid = False
try:
valid = verifier.verify( app_session_token, pubkey )
if not valid:
log.debug("Failed to verify with {}".format(pubkey))
return None
except:
log.debug("Not a valid token")
return None
session = None
session_jwt = None
try:
session_jwt = jsontokens.decode_token(app_session_token)
session = session_jwt['payload']
except:
log.debug("Failed to decode token")
return None
# must match session structure
try:
jsonschema.validate(session, APP_SESSION_SCHEMA)
except ValidationError as ve:
if BLOCKSTACK_TEST:
log.exception(ve)
return None
# session must not be expired
if session['expires'] < time.time():
log.debug("Token is expired")
return None
return session
def app_get_datastore_pubkey( session ):
"""
Given a session, identify and return the datastore public key
Return None on invalid session
"""
device_id = session['device_id']
for apk in session['app_public_keys']:
if apk['device_id'] == device_id:
return apk['public_key']
return None
def app_publish( dev_blockchain_id, app_domain, app_method_list, app_index_uris, app_index_file, app_driver_hints=[], data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ):
"""
Instantiate an application.
* replicate the (opaque) app index file to "index.html" to each URL in app_uris
* replicate the list of URIs and the list of methods to ".blockstack" via each of the client's storage drivers.
This succeeds even if the app already exists (in which case,
it will be overwritten). This method is idempotent, so it
can be retried on failure.
data_privkey should be the publisher's private key (i.e. their data key)
name should be the blockchain ID that points to data_pubkey
Return {'status': True, 'config_fq_data_id': config's fully-qualified data ID, 'index_fq_data_id': index file's fully-qualified data ID} on success
Return {'error': ...} on error
"""
if data_privkey is None:
assert wallet_keys, 'Missing both data private key and wallet keys'
data_privkey = wallet_keys.get('data_privkey')
assert data_privkey, "Wallet does not have a data private key"
proxy = get_default_proxy() if proxy is None else proxy
# replicate configuration data (method list and app URIs)
app_cfg = {
'blockchain_id': dev_blockchain_id,
'app_domain': app_domain,
'index_uris': app_index_uris,
'api_methods': app_method_list,
'driver_hints': app_driver_hints,
}
jsonschema.validate(app_cfg, APP_CONFIG_SCHEMA)
data_pubkey = get_pubkey_hex(data_privkey)
config_data_id = storage.make_fq_data_id(app_domain, '.blockstack')
app_cfg_blob = data.make_mutable_data_info(config_data_id, app_cfg, is_fq_data_id=True)
app_cfg_str = data.data_blob_serialize(app_cfg_blob)
app_cfg_sig = data.data_blob_sign( app_cfg_str, data_privkey )
res = data.put_mutable(config_data_id, app_cfg_str, data_pubkey, app_cfg_sig, app_cfg_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path)
if 'error' in res:
log.error('Failed to replicate application configuration {}: {}'.format(config_data_id, res['error']))
return {'error': 'Failed to replicate application config'}
# what drivers to use for the index file?
urls = user_db.urls_from_uris(app_index_uris)
driver_names = []
for url in urls:
drivers = storage.get_drivers_for_url(url)
driver_names += [d.__name__ for d in drivers]
driver_names = list(set(driver_names))
index_data_id = storage.make_fq_data_id(app_domain, 'index.html')
# replicate app index file (at least one must succeed)
# NOTE: the publisher is free to use alternative URIs that are not supported; they'll just be ignored.
app_index_blob = data.make_mutable_data_info(index_data_id, app_index_file, is_fq_data_id=True)
app_index_blob_str = data.data_blob_serialize(app_index_blob)
app_index_sig = data.data_blob_sign(app_index_blob_str, data_privkey)
res = data.put_mutable( index_data_id, app_index_blob_str, data_pubkey, app_index_sig, app_index_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path, storage_drivers=app_driver_hints )
if 'error' in res:
log.error("Failed to replicate application index file to {}: {}".format(",".join(urls), res['error']))
return {'error': 'Failed to replicate index file'}
return {'status': True, 'config_fq_data_id': config_data_id, 'index_fq_data_id': index_data_id}
def app_get_config( blockchain_id, app_domain, data_pubkey=None, proxy=None, config_path=CONFIG_PATH ):
"""
Get application configuration bundle.
data_pubkey should be the publisher's public key.
Return {'status': True, 'config': config} on success
Return {'error': ...} on error
"""
proxy = get_default_proxy() if proxy is None else proxy
# go get config
res = data.get_mutable( ".blockstack", [app_domain], data_pubkey=data_pubkey, proxy=proxy, config_path=config_path, blockchain_id=blockchain_id )
if 'error' in res:
log.error("Failed to get application config file {}: {}".format(config_data_id, res['error']))
return res
app_cfg = None
try:
app_cfg = res['data']
jsonschema.validate(app_cfg, APP_CONFIG_SCHEMA)
except ValidationError as ve:
if BLOCKSTACK_TEST:
log.exception(ve)
log.error("Invalid application config file {}".format(config_data_id))
return {'error': 'Invalid application config'}
return {'status': True, 'config': app_cfg}
def app_get_resource( blockchain_id, app_domain, res_name, app_config=None, data_pubkey=None, proxy=None, config_path=CONFIG_PATH ):
"""
Get a named application resource from mutable storage
data_pubkey should be the publisher's public key
If app_config is not None, then the driver hints will be honored.
Return {'status': True, 'res': resource} on success
Return {'error': ...} on error
"""
proxy = get_default_proxy() if proxy is None else proxy
urls = None
if app_config is not None:
# use driver hints
driver_hints = app_config['driver_hints']
urls = storage.get_driver_urls( res_data_id, storage.get_storage_handlers() )
res = data.get_mutable( res_name, [app_domain], data_pubkey=data_pubkey, proxy=proxy, config_path=config_path, urls=urls, blockchain_id=blockchain_id )
if 'error' in res:
log.error("Failed to get resource {}: {}".format(res_name, res['error']))
return {'error': 'Failed to load resource'}
return {'status': True, 'res': res['data']}
def app_put_resource( blockchain_id, app_domain, res_name, res_data, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ):
"""
Store data to a named application resource in mutable storage.
data_privkey should be the publisher's private key
name should be a blockchain ID that points to the public key
if app_config is not None, then the driver hints will be honored.
Return {'status': True, 'version': ...} on success
Return {'error': ...} on error
"""
assert isinstance(res_data, (str, unicode)), "Resource must be a string"
try:
json.dumps(res_data)
except:
raise AssertionError("Resource must be a JSON-serializable string")
if data_privkey is None:
assert wallet_keys, 'Missing both data private key and wallet keys'
data_privkey = wallet_keys.get('data_privkey')
assert data_privkey, "Wallet does not have a data private key"
proxy = get_default_proxy() if proxy is None else proxy
res_data_id = storage.make_fq_data_id(app_domain, res_name)
data_pubkey = get_pubkey_hex(data_privkey)
driver_hints = None
if app_config is not None:
# use driver hints
driver_hints = app_config['driver_hints']
res_blob = data.make_mutable_data_info(res_data_id, res_data, is_fq_data_id=True)
res_blob_str = data.data_blob_serialize(res_blob)
res_sig = data.data_blob_sign(res_blob_str, data_privkey)
res = data.put_mutable(res_data_id, res_blob_str, data_pubkey, res_sig, res_blob['version'], blockchain_id=blockchain_id, config_path=config_path, storage_drivers=driver_hints)
if 'error' in res:
log.error("Failed to store resource {}: {}".format(res_data_id, res['error']))
return {'error': 'Failed to store resource'}
return {'status': True, 'version': res_blob['version']}
def app_delete_resource( blockchain_id, app_domain, res_name, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ):
"""
Remove data from a named application resource in mutable storage.
data_privkey should be the publisher's private key
name should be a blockchain ID that points to the public key
if app_config is not None, then the driver hints will be honored.
Return {'status': True, 'version': ...} on success
Return {'error': ...} on error
"""
if data_privkey is None:
assert wallet_keys, "No data private key or wallet given"
data_privkey = wallet_keys.get('data_privkey', None)
assert data_privkey, "Wallet does not contain a data private key"
data_pubkey = get_pubkey_hex(data_privkey)
proxy = get_default_proxy() if proxy is None else proxy
res_data_id = storage.make_fq_data_id(app_domain, res_name)
driver_hints = None
if app_config is not None:
# use driver hints
driver_hints = app_config['driver_hints']
tombstone = storage.make_data_tombstone(res_data_id)
signed_tombstone = storage.sign_data_tombstone(res_data_id, data_privkey)
res = data.delete_mutable(res_data_id, [signed_tombstone], proxy=proxy, storage_drivers=driver_hints, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path)
if 'error' in res:
log.error("Failed to delete resource {}: {}".format(res_data_id, res['error']))
return {'error': 'Failed to delete resource'}
return {'status': True}
def app_unpublish( blockchain_id, app_domain, force=False, data_privkey=None, app_config=None, wallet_keys=None, proxy=None, config_path=CONFIG_PATH ):
"""
Unpublish an application
Deletes its config and index.
Does NOT delete its resources.
Does NOT delete user data.
if force is True, then we will try to delete the app state even if we can't load the app config
WARNING: force can be dangerous, since it can delete data via drivers that were never meant for this app. Use with caution!
Return {'status': True, 'app_config': ..., 'retry': ...} on success. If retry is True, then retry this method with the given app_config
Return {'error': ...} on error
"""
proxy = get_default_proxy() if proxy is None else proxy
# find out where to delete from
data_pubkey = None
if data_privkey is not None:
data_pubkey = get_pubkey_hex(str(data_privkey))
if app_config is None:
app_config = app_get_config(blockchain_id, app_domain, data_pubkey=data_pubkey, proxy=proxy, config_path=CONFIG_PATH )
if 'error' in app_config:
if not force:
log.error("Failed to load app config for {}'s {}".format(blockchain_id, app_domain))
return {'error': 'Failed to load app config'}
else:
# keep going
app_config = None
log.warning("Failed to load app config, but proceeding at caller request")
config_data_id = storage.make_fq_data_id(app_domain, '.blockstack')
index_data_id = storage.make_fq_data_id(app_domain, 'index.html')
storage_drivers = None
if app_config is not None:
# only use the ones we have to
urls = user_db.urls_from_uris(app_config['index_uris'])
driver_names = []
for url in urls:
drivers = storage.get_drivers_for_url(url)
driver_names += [d.__name__ for d in drivers]
storage_drivers = list(set(driver_names))
ret = {}
# delete the index
index_tombstone = storage.make_data_tombstone(index_data_id)
signed_index_tombstone = storage.sign_data_tombstone(index_data_id, data_privkey)
res = data.delete_mutable(index_data_id, [signed_index_tombstone], proxy=proxy, storage_drivers=storage_drivers, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path)
if 'error' in res:
log.warning("Failed to delete index file {}".format(index_data_id))
ret['app_config'] = app_config
ret['retry'] = True
# delete the config
config_tombstone = storage.make_data_tombstone(config_data_id)
signed_config_tombstone = storage.sign_data_tombstone(config_data_id, data_privkey)
res = data.delete_mutable(config_data_id, [signed_config_tombstone], proxy=proxy, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path)
if 'error' in res:
log.warning("Failed to delete config file {}".format(config_data_id))
if not ret.has_key('app_config'):
ret['app_config'] = app_config
ret['retry'] = True
ret['status'] = True
return ret