diff --git a/api/resolver.py b/api/resolver.py index ba3865225..86475bf98 100644 --- a/api/resolver.py +++ b/api/resolver.py @@ -27,25 +27,19 @@ import re import json import collections import logging -import xmlrpclib from flask import Flask, make_response, jsonify, abort, request from flask import Blueprint from flask_crossdomain import crossdomain from time import time -from basicrpc import Proxy from blockstack_proofs import profile_to_proofs, profile_v3_to_proofs -from blockstack_profiles import resolve_zone_file_to_profile -from blockstack_profiles import get_token_file_url_from_zone_file -from blockstack_profiles import get_profile_from_tokens -#from blockstack_profiles import is_profile_in_legacy_format -from blockstack_zones import parse_zone_file -from blockstack_client.proxy import get_name_blockchain_record import blockstack_client.profile +from blockstack_client.schemas import OP_NAME_PATTERN, OP_NAMESPACE_PATTERN + from api.utils import cache_control, get_mc_client from .config import DEBUG @@ -72,39 +66,6 @@ else: mc = get_mc_client() -def validName(name): - """ Return True if valid name - """ - - # current regrex doesn't account for .namespace - regrex = re.compile('^[a-z0-9_]{1,60}$') - - if regrex.match(name): - return True - else: - return False - - -def fetch_from_dht(profile_hash): - """ Given a @profile_hash fetch full profile JSON - """ - - dht_client = Proxy(DHT_MIRROR_IP, DHT_MIRROR_PORT) - - try: - dht_resp = dht_client.get(profile_hash) - except: - #abort(500, "Connection to DHT timed out") - return {"error": "Data not saved in DHT yet."} - - dht_resp = dht_resp[0] - - if dht_resp is None: - return {"error": "Data not saved in DHT yet."} - - return dht_resp['value'] - - def fetch_proofs(profile, username, profile_ver=2, refresh=False): """ Get proofs for a profile and: a) check cached entries @@ -134,7 +95,6 @@ def fetch_proofs(profile, username, profile_ver=2, refresh=False): return proofs - def is_profile_in_legacy_format(profile): """ Is a given profile JSON object in legacy format? @@ -170,126 +130,48 @@ def is_profile_in_legacy_format(profile): return is_in_legacy_format - -def parse_uri_from_zone_file(zone_file): - - token_file_url = None - zone_file = dict(parse_zone_file(zone_file)) - - if isinstance(zone_file["uri"], list) and len(zone_file["uri"]) > 0: - - index = 0 - while(index < len(zone_file["uri"])): - - record = zone_file["uri"][index] - - if 'name' in record and record['name'] == '_http._tcp': - first_uri_record = zone_file["uri"][index] - token_file_url = first_uri_record["target"] - break - - index += 1 - - return token_file_url - - -def resolve_zone_file_from_rpc(zone_file, owner_address): - - rpc_uri = parse_uri_from_zone_file(zone_file) - - try: - uri, fqu = rpc_uri.rsplit('#') - except: - return None - - try: - s = xmlrpclib.ServerProxy(uri, allow_none=True) - data = s.get_profile(fqu) - except Exception as e: - print e - - data = json.loads(data) - profile = json.loads(data['profile']) - pubkey = profile[0]['parentPublicKey'] - - try: - profile = get_profile_from_tokens(profile, pubkey) - except Exception as e: - print e - - return profile - - -def resolve_zone_file_to_profile(zone_file, address_or_public_key): - - profile = None - - if is_profile_in_legacy_format(zone_file): - return zone_file - - try: - token_file_url = get_token_file_url_from_zone_file(zone_file) - - r = requests.get(token_file_url) - - profile_token_records = json.loads(r.text) - - profile = get_profile_from_tokens(profile_token_records, address_or_public_key) - except Exception as e: - - profile = resolve_zone_file_from_rpc(zone_file, address_or_public_key) - - print profile - return profile, None - - -def format_profile(profile, username, zone_file, refresh=False): +def format_profile(profile, fqa, zone_file, refresh=False): """ Process profile data and 1) Insert verifications 2) Check if profile data is valid JSON """ - data = {} - - if 'error' in profile: - data['profile'] = {} - data['error'] = profile['error'] - data['verifications'] = [] - data['zone_file'] = zone_file + data = {'profile' : profile, + 'zone_file' : zone_file} + try: + username, ns = fqa.split(".") + except: + data = {'error' : "Failed to split fqa into name and namespace."} + return data + if ns != 'id': + data['verifications'] = ["No verifications for non-id namespaces."] return data - if profile is None: - data['profile'] = {} - - if error is not None: - data['error'] = error - else: - data['error'] = "Malformed profile data." - data['verifications'] = [] + profile_in_legacy_format = is_profile_in_legacy_format(profile) + if not profile_in_legacy_format: + data['verifications'] = fetch_proofs(data['profile'], username, + profile_ver=3, refresh=refresh) else: - - profile_in_legacy_format = is_profile_in_legacy_format(profile) - - if not profile_in_legacy_format: - data['profile'] = profile - data['verifications'] = fetch_proofs(data['profile'], username, - profile_ver=3, refresh=refresh) - else: - if type(profile) is not dict: - data['profile'] = json.loads(profile) - else: - data['profile'] = profile - data['verifications'] = fetch_proofs(data['profile'], username, - refresh=refresh) - - data['zone_file'] = zone_file + if type(profile) is not dict: + data['profile'] = json.loads(profile) + data['verifications'] = fetch_proofs(data['profile'], username, + refresh=refresh) return data +NAME_PATTERN = re.compile(OP_NAME_PATTERN) +NS_PATTERN = re.compile(OP_NAMESPACE_PATTERN) +def is_valid_fqa(fqa): + try: + username, ns = fqa.split(".") + except: + return False + return ((NAME_PATTERN.match(username) is not None) and + (NS_PATTERN.match(ns) is not None)) -def get_profile(username, refresh=False, namespace=DEFAULT_NAMESPACE): +def get_profile(fqa, refresh=False): """ Given a fully-qualified username (username.namespace) get the data associated with that fqu. Return cached entries, if possible. @@ -298,22 +180,23 @@ def get_profile(username, refresh=False, namespace=DEFAULT_NAMESPACE): global MEMCACHED_ENABLED global mc - username = username.lower() + fqa = fqa.lower() + if not is_valid_fqa(fqa): + return {'error' : 'Malformed name {}'.format(fqa)} if MEMCACHED_ENABLED and not refresh: - log.debug("Memcache get DHT: %s" % username) - dht_cache_reply = mc.get("dht_" + str(username)) + log.debug("Memcache get DHT: %s" % fqa) + dht_cache_reply = mc.get("dht_" + str(fqa)) else: - log.debug("Memcache disabled: %s" % username) dht_cache_reply = None if dht_cache_reply is None: try: profile, zonefile = blockstack_client.profile.get_profile( - "{}.{}".format(username, namespace), - use_legacy = True) + fqa, use_legacy = True) except: - abort(500, "Connection to blockstack-server %s:%s timed out" % (BLOCKSTACKD_IP, BLOCKSTACKD_PORT)) + abort(500, "Connection to blockstack-server %s:%s timed out" % + (BLOCKSTACKD_IP, BLOCKSTACKD_PORT)) if profile is None or 'error' in zonefile: log.error("{}".format(zonefile)) @@ -322,13 +205,13 @@ def get_profile(username, refresh=False, namespace=DEFAULT_NAMESPACE): prof_data = {'response' : profile} if MEMCACHED_ENABLED or refresh: - log.debug("Memcache set DHT: %s" % username) - mc.set("dht_" + str(username), json.dumps(data), + log.debug("Memcache set DHT: %s" % fqa) + mc.set("dht_" + str(fqa), json.dumps(data), int(time() + MEMCACHED_TIMEOUT)) else: prof_data = json.loads(dht_cache_reply) - data = format_profile(prof_data['response'], username, zonefile) + data = format_profile(prof_data['response'], fqa, zonefile) return data @@ -337,15 +220,10 @@ def get_all_users(): """ Return all users in the .id namespace """ - try: - fout = open(NAMES_FILE, 'r') - data = fout.read() - data = json.loads(data) - fout.close() - except: - data = {} - - return data + # aaron: hardcode a non-response for the time being -- + # the previous code was trying to load a non-existent file + # anyways. + return {} # aaron note: do we need to support multiple users in a query? # this seems like a potential avenue for abuse. @@ -356,7 +234,6 @@ def get_all_users(): def get_users(usernames): """ Fetch data from username in .id namespace """ - reply = {} refresh = False @@ -367,39 +244,28 @@ def get_users(usernames): if usernames is None: reply['error'] = "No usernames given" - return jsonify(reply) + return jsonify(reply), 404 if ',' not in usernames: - - username = usernames - - info = get_profile(username, refresh=refresh) - - if 'error' in info: - reply[username] = info - return jsonify(reply), 502 - else: - reply[username] = info - - return jsonify(reply), 200 - - try: - usernames = usernames.rsplit(',') - except: - reply['error'] = "Invalid input format" - return jsonify(reply) + usernames = [usernames] + else: + try: + usernames = usernames.rsplit(',') + except: + reply['error'] = "Invalid input format" + return jsonify(reply), 401 for username in usernames: + if "." not in username: + username = "{}.{}".format(username, 'id') + profile = get_profile(username, refresh=refresh) - try: - profile = get_profile(username, refresh=refresh) - - if 'error' in profile: - pass - else: + if 'error' in profile: + if len(usernames) == 1: reply[username] = profile - except: - pass + return jsonify(reply), 502 + else: + reply[username] = profile return jsonify(reply), 200