censored resolve responses return appropriate error

This commit is contained in:
Lex Berezhny 2020-02-01 12:53:39 -05:00
parent b7eec0586c
commit 15abf49211
5 changed files with 52 additions and 15 deletions

View file

@ -4,7 +4,13 @@ from typing import List
from binascii import hexlify from binascii import hexlify
from itertools import chain from itertools import chain
from lbry.error import ResolveCensoredError
from lbry.schema.types.v2.result_pb2 import Outputs as OutputsMessage from lbry.schema.types.v2.result_pb2 import Outputs as OutputsMessage
from lbry.schema.types.v2.result_pb2 import Error as ErrorMessage
INVALID = ErrorMessage.Code.Name(ErrorMessage.INVALID)
NOT_FOUND = ErrorMessage.Code.Name(ErrorMessage.NOT_FOUND)
BLOCKED = ErrorMessage.Code.Name(ErrorMessage.BLOCKED)
class Censor: class Censor:
@ -73,7 +79,12 @@ class Outputs:
def message_to_txo(self, txo_message, tx_map): def message_to_txo(self, txo_message, tx_map):
if txo_message.WhichOneof('meta') == 'error': if txo_message.WhichOneof('meta') == 'error':
return None return {
'error': {
'name': txo_message.error.Code.Name(txo_message.error.code).lower(),
'text': txo_message.error.text,
}
}
txo = tx_map[txo_message.tx_hash].outputs[txo_message.nout] txo = tx_map[txo_message.tx_hash].outputs[txo_message.nout]
if txo_message.WhichOneof('meta') == 'claim': if txo_message.WhichOneof('meta') == 'claim':
claim = txo_message.claim claim = txo_message.claim
@ -146,9 +157,11 @@ class Outputs:
if isinstance(txo, Exception): if isinstance(txo, Exception):
txo_message.error.text = txo.args[0] txo_message.error.text = txo.args[0]
if isinstance(txo, ValueError): if isinstance(txo, ValueError):
txo_message.error.code = txo_message.error.INVALID txo_message.error.code = ErrorMessage.INVALID
elif isinstance(txo, LookupError): elif isinstance(txo, LookupError):
txo_message.error.code = txo_message.error.NOT_FOUND txo_message.error.code = ErrorMessage.NOT_FOUND
elif isinstance(txo, ResolveCensoredError):
txo_message.error.code = ErrorMessage.BLOCKED
return return
txo_message.tx_hash = txo['txo_hash'][:32] txo_message.tx_hash = txo['txo_hash'][:32]
txo_message.nout, = struct.unpack('<I', txo['txo_hash'][32:]) txo_message.nout, = struct.unpack('<I', txo['txo_hash'][32:])

View file

@ -12,7 +12,7 @@ from binascii import hexlify, unhexlify
from typing import Dict, Tuple, Type, Iterable, List, Optional, DefaultDict from typing import Dict, Tuple, Type, Iterable, List, Optional, DefaultDict
import pylru import pylru
from lbry.schema.result import Outputs from lbry.schema.result import Outputs, INVALID, NOT_FOUND
from lbry.schema.url import URL from lbry.schema.url import URL
from lbry.crypto.hash import hash160, double_sha256, sha256 from lbry.crypto.hash import hash160, double_sha256, sha256
from lbry.crypto.base58 import Base58 from lbry.crypto.base58 import Base58
@ -661,13 +661,13 @@ class Ledger(metaclass=LedgerRegistry):
assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received." assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received."
result = {} result = {}
for url, txo in zip(urls, txos): for url, txo in zip(urls, txos):
if txo and URL.parse(url).has_stream_in_channel:
if not txo.channel or not txo.is_signed_by(txo.channel, self):
txo = None
if txo: if txo:
result[url] = txo if isinstance(txo, Output) and URL.parse(url).has_stream_in_channel:
if not txo.channel or not txo.is_signed_by(txo.channel, self):
txo = {'error': {'name': INVALID, 'text': f'{url} has invalid channel signature'}}
else: else:
result[url] = {'error': f'{url} did not resolve to a claim'} txo = {'error': {'name': NOT_FOUND, 'text': f'{url} did not resolve to a claim'}}
result[url] = txo
return result return result
async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], dict, int, int]: async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], dict, int, int]:

View file

@ -4,14 +4,14 @@ import apsw
import logging import logging
from operator import itemgetter from operator import itemgetter
from typing import Tuple, List, Dict, Union, Type, Optional from typing import Tuple, List, Dict, Union, Type, Optional
from binascii import unhexlify from binascii import unhexlify, hexlify
from decimal import Decimal from decimal import Decimal
from contextvars import ContextVar from contextvars import ContextVar
from functools import wraps from functools import wraps
from dataclasses import dataclass from dataclasses import dataclass
from lbry.wallet.database import query, interpolate from lbry.wallet.database import query, interpolate
from lbry.error import ResolveCensoredError
from lbry.schema.url import URL, normalize_name from lbry.schema.url import URL, normalize_name
from lbry.schema.tags import clean_tags from lbry.schema.tags import clean_tags
from lbry.schema.result import Outputs, Censor from lbry.schema.result import Outputs, Censor
@ -451,6 +451,8 @@ def resolve_url(raw_url):
matches = search_claims(censor, **query, limit=1) matches = search_claims(censor, **query, limit=1)
if matches: if matches:
channel = matches[0] channel = matches[0]
elif censor.censored:
return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode())
else: else:
return LookupError(f'Could not find channel in "{raw_url}".') return LookupError(f'Could not find channel in "{raw_url}".')
@ -469,6 +471,8 @@ def resolve_url(raw_url):
matches = search_claims(censor, **query, limit=1) matches = search_claims(censor, **query, limit=1)
if matches: if matches:
return matches[0] return matches[0]
elif censor.censored:
return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode())
else: else:
return LookupError(f'Could not find stream in "{raw_url}".') return LookupError(f'Could not find stream in "{raw_url}".')

View file

@ -763,29 +763,48 @@ class StreamCommands(ClaimTestCase):
bad_content_id = self.get_claim_id( bad_content_id = self.get_claim_id(
await self.stream_create('bad_content', '1.1', channel_name='@some_channel', tags=['bad']) await self.stream_create('bad_content', '1.1', channel_name='@some_channel', tags=['bad'])
) )
blocking_channel_id = self.get_claim_id( filtering_channel_id = self.get_claim_id(
await self.channel_create('@filtering', '1.0') await self.channel_create('@filtering', '1.0')
) )
self.conductor.spv_node.server.db.sql.filtering_channel_hashes.add( self.conductor.spv_node.server.db.sql.filtering_channel_hashes.add(
unhexlify(blocking_channel_id)[::-1] unhexlify(filtering_channel_id)[::-1]
) )
await self.stream_repost(bad_content_id, 'filter1', '1.1', channel_name='@filtering') await self.stream_repost(bad_content_id, 'filter1', '1.1', channel_name='@filtering')
# search for blocked content directly # search for blocked content directly
result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content')) result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content'))
self.assertEqual([], result['items']) self.assertEqual([], result['items'])
self.assertEqual({"channels": {blocking_channel_id: 1}, "total": 1}, result['blocked']) self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked'])
# search channel containing blocked content # search channel containing blocked content
result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel')) result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel'))
self.assertEqual(1, len(result['items'])) self.assertEqual(1, len(result['items']))
self.assertEqual({"channels": {blocking_channel_id: 1}, "total": 1}, result['blocked']) self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked'])
# content was filtered by not_tag before censoring # content was filtered by not_tag before censoring
result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', not_tags=["good", "bad"])) result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', not_tags=["good", "bad"]))
self.assertEqual(0, len(result['items'])) self.assertEqual(0, len(result['items']))
self.assertEqual({"channels": {}, "total": 0}, result['blocked']) self.assertEqual({"channels": {}, "total": 0}, result['blocked'])
blocking_channel_id = self.get_claim_id(
await self.channel_create('@blocking', '1.0')
)
self.conductor.spv_node.server.db.sql.blocking_channel_hashes.add(
unhexlify(blocking_channel_id)[::-1]
)
# filtered content can still be resolved
result = await self.out(self.daemon.jsonrpc_resolve('lbry://@some_channel/bad_content'))
self.assertEqual(bad_content_id, result['lbry://@some_channel/bad_content']['claim_id'])
await self.stream_repost(bad_content_id, 'block1', '1.1', channel_name='@blocking')
# blocked content is not resolveable
result = await self.out(self.daemon.jsonrpc_resolve('lbry://@some_channel/bad_content'))
error = result['lbry://@some_channel/bad_content']['error']
self.assertTrue(error['name'], 'blocked')
self.assertTrue(error['text'].startswith("Resolve of 'lbry://@some_channel/bad_content' was censored"))
async def test_publish_updates_file_list(self): async def test_publish_updates_file_list(self):
tx = await self.stream_create(title='created') tx = await self.stream_create(title='created')
txo = tx['outputs'][0] txo = tx['outputs'][0]

View file

@ -15,6 +15,7 @@ class BaseResolveTestCase(CommandTestCase):
other = (await self.resolve(name))[name] other = (await self.resolve(name))[name]
if claim_id is None: if claim_id is None:
self.assertIn('error', other) self.assertIn('error', other)
self.assertEqual(other['error']['name'], 'not_found')
else: else:
self.assertEqual(claim_id, other['claim_id']) self.assertEqual(claim_id, other['claim_id'])