Ejemplo n.º 1
0
def clearcache(maxcache=MAXCACHE):
    '''
    delete entries if CACHED size is greater than MAXCACHE

    >>> CACHED.update({':2': 'x' * 1024, ':1': 'y' * 64, ':3': 'z' * 2048})
    >>> logging.debug('sum of lengths: %d', sum(map(len,
    ...  [v for k, v in CACHED.items() if k.startswith(':')])))
    >>> logging.info('doctest CACHED.keys(): %s', list(CACHED.keys()))
    >>> clearcache(100)
    >>> {k: v for k, v in CACHED.items() if k.startswith(':')}
    {':1': 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'}
    >>> len(CACHED[':1'])
    64
    '''
    logging.debug('CACHED.keys(): %s', list(CACHED.keys()))
    while sum(map(len, [v for k, v in CACHED.items() if k.startswith(':')
                        ])) > maxcache:
        next_biggest = max(
            {k: v
             for k, v in CACHED.items() if k.startswith(':')},
            key=lambda k: len(CACHED[k]))
        logging.debug('next_biggest: %s', next_biggest)
        logging.warning('deleting CACHED[%s] of length %d', next_biggest,
                        len(CACHED[next_biggest]))
        del CACHED[next_biggest]
Ejemplo n.º 2
0
def register(username=None, email=None, gpgkey=None):
    '''
    register kybyz account
    '''
    if gpgkey is None:
        gpgkey = verify_key(email)
    current = registration()  # see what we already have, if anything
    if username is None or email is None:
        logging.error('Usage: %s %s USERNAME EMAIL_ADDRESS', COMMAND, ARGS[0])
        raise ValueError('Must specify desired username and email address')
    if any(current):
        if (username, email, gpgkey) != current:
            raise ValueError('Previously registered as %s %s %s' % current)
        logging.warning('Already registered as %s %s %s', *current)
    else:
        os.makedirs(os.path.join(CACHE, gpgkey))
        os.symlink(os.path.join(CACHE, gpgkey), os.path.join(CACHE, email))
        os.symlink(os.path.join(CACHE, email), os.path.join(CACHE, username))
        os.symlink(os.path.join(CACHE, username), KYBYZ_HOME)
        logging.info('Now registered as %s %s %s', username, email, gpgkey)
        if CACHED.get('ircbot', None):
            CACHED['ircbot'].nick(username)
            CACHED['ircbot'].leave()  # rejoin to freshen CACHED['irc_id']
            CACHED['ircbot'].join()
        else:
            logging.info('registering outside of running application')
Ejemplo n.º 3
0
def test(nickname=None, realname=None):
    '''
    run a bot from the command line, for testing
    '''
    ircbot = type('IRCBot', (), {'terminate': False})()
    try:
        ircbot = IRCBot(nickname=nickname, realname=realname)
        time.sleep(TIMEOUT)
    except KeyboardInterrupt:
        logging.warning('Telling monitor to terminate')
        ircbot.terminate = True
Ejemplo n.º 4
0
 def to_json(self, for_hashing=False):
     '''
     output contents as JSON
     '''
     if for_hashing:
         dictionary = dict(
             (value.hashvalue(self)
              for value in self.versions[self.version].values()))
         del dictionary[None]  # clears out last of values not to be hashed
     else:
         dictionary = self.__dict__
         logging.warning('dictionary: %s', dictionary)
     return canonicalize(dictionary)
Ejemplo n.º 5
0
def commandloop():
    '''
    simple repl (read-evaluate-process-loop) for command-line testing
    '''
    time.sleep(10)  # give page a chance to load before starting repl
    args = []
    logging.info('Ready to accept commands; `quit` to terminate input loop')
    while args[0:1] != ['quit']:
        try:
            print(process(args))
            args = shlex.split(input('kbz> '))
        except EXPECTED_ERRORS:
            logging.exception('command failed, please try again')
            args[:] = []
        except EOFError:
            break
    logging.warning('input loop terminated')
Ejemplo n.º 6
0
def post(post_type, *args, returned='hashed', **kwargs):
    '''
    make a new post from the command line or from another subroutine
    '''
    if len(args) == 1 and JSON.match(args[0]):
        try:
            kwargs.update(json.loads(args[0]))
        except json.decoder.JSONDecodeError:
            logging.error('Post not valid JSON format: %s' % args[0])
    else:
        logging.debug('args %s not valid JSON, using as key-value pairs', args)
        for arg in args:
            logging.debug('parsing %s', arg)
            kwargs.update(dict((arg.split('=', 1), )))
    # override post_type if specified
    if post_type:
        kwargs.update({'type': post_type})
    try:
        newpost = BasePost(None, **kwargs)
        jsonified = newpost.to_json()
        post_type = newpost.type
        hashed = kbhash(jsonified)
        cached = cache('.'.join((hashed, post_type)), jsonified)
        jsonified = newpost.to_json(for_hashing=True)
        hashed = kbhash(jsonified)
        hashcached = cache('.'.join((hashed, post_type)), jsonified)
        unadorned = os.path.splitext(hashcached)[0]
        try:
            os.symlink(cached, unadorned)
        except FileExistsError:
            existing = os.readlink(unadorned)
            if existing != cached:
                logging.warning('updating post %s to %s', unadorned, cached)
                os.unlink(unadorned)
                os.symlink(cached, unadorned)
            else:
                logging.debug('%s already symlinked to %s', unadorned, cached)
        return hashed if returned == 'hashed' else newpost
    except AttributeError:
        logging.exception('Post failed: attribute error')
        return None
    except TypeError:
        logging.exception('Post failed with kwargs: %s', kwargs)
        return None
Ejemplo n.º 7
0
def decrypt(message):
    '''
    decrypt a message sent to me, and verify sender email
    '''
    gpg = GPG()
    verified = decoded = b''
    logging.debug('decoding %s...', message[:64])
    try:
        decoded = b58decode(message)
        logging.debug('decrypting %r...', decoded[:64])
        decrypted = gpg.decrypt(decoded)
        # pylint: disable=no-member
        verified = 'trust level %s' % decrypted.trust_text
    except ValueError:
        logging.warning('%r... not base58 encoded', message[:32])
        decrypted = type('', (), {'data': message})
        verified = 'unencoded'
    except subprocess.CalledProcessError as problem:
        logging.exception(problem)
        decrypted = type('', (), {'data': b''})
    return decrypted.data, verified
Ejemplo n.º 8
0
def send(recipient, email, *words):
    '''
    encrypt, sign, and send a private message to recipient

    `recipient` is the 'nick' (nickname) of the user to whom you wish to send
    the message. `email` is not necessarily an email address, but is used to
    find the GPG key of the recipient.

    use `-` instead of email to send plain text
    '''
    if len(words) > 1 or isinstance(words[0], str):
        text = ' '.join(words).encode()
    else:
        text = words[0]  # as when called by `publish`
    logging.debug('words: %s', words)
    encoded = None
    if email != '-':
        gpg = GPG()
        logging.debug('message before encrypting: %s', text)
        encrypted = gpg.encrypt(
            text,  # pylint: disable=no-member
            [email],
            sign=True,
            armor=False)
        logging.debug('encrypted: %r...', encrypted.data[:64])
        encoded = b58encode(encrypted.data).decode()
        logging.debug('encoded: %s', encoded)
    if text and not encoded:
        if email == '-' or os.getenv('KB_SEND_PLAINTEXT_OK'):
            logging.warning('encryption %s, sending plaintext',
                            'bypassed' if email == '-' else 'failed')
            encoded = text.decode()
        else:
            logging.warning('encryption failed, run with '
                            'KB_SEND_PLAINTEXT_OK=1 to send anyway')
            logging.warning('setting message to "(encryption failed)"')
            encoded = '(encryption failed)'
    CACHED['ircbot'].privmsg(recipient, encoded)
Ejemplo n.º 9
0
def serve(env=None, start_response=None):
    '''
    handle web requests
    '''
    # pylint: disable=too-many-locals, too-many-statements
    fields = cgi.FieldStorage(fp=env.get('wsgi.input'), environ=env)
    args = {k: fields[k].value for k in fields}
    logging.debug('args: %s', args)
    #sections = ['posts', 'messages']
    page = b'(Something went wrong)'
    env = env or {}
    requested = env.get('REQUEST_URI', None).lstrip('/')
    logging.debug('requested: "%s"', requested)
    status = '200 OK'
    headers = [('Content-type', 'text/html')]
    template = read('timeline.html').decode()
    messages = ''.join(['<div>%s</div>' % message for message in
                        reversed(MESSAGE_QUEUE)])
    messages_hash = md5(messages.encode()).hexdigest()
    messages = MESSAGES.format(
        messages=messages,
        messages_hash=messages_hash,
        javascript=CACHED['javascript'])
    posts = ''.join(['<div>%s</div>' % post for post in loadposts()])
    posts_hash = md5(posts.encode()).hexdigest()
    posts = POSTS.format(posts=posts, posts_hash=posts_hash)
    navigation = NAVIGATION.format(navigation=''.join(['<h3>Navigation</h3>']))

    # make helper functions for dispatcher
    def update():
        '''
        process xhr request for update to posts or messages
        '''
        name, hashed = args.get('name', None), args.get('hash', None)
        update_status = status  # default from outer variable
        if name in ('messages', 'posts'):
            # pylint: disable=eval-used
            # check outer variables
            # must be done before eval or it will fail
            logging.debug('messages: ...%s', messages[-128:])
            logging.debug('messages_hash: %s', messages_hash)
            logging.debug('posts: %s...', posts[:128])
            logging.debug('posts_hash: %s', posts_hash)
            if hashed and hashed != eval(name + '_hash'):
                update_page = eval(name).encode()
            elif hashed:
                logging.debug('%s unchanged', args['name'])
                update_page = b''
                update_status = '304 Not Modified'
            else:
                logging.error('no hash passed to /update/')
                update_page = b''
                update_status = '406 Not Acceptable'
        else:
            update_page = (
                '<div>no updates for %s</div>' % args['name']
            ).encode()
            update_status = '404 Not Found'
        return update_status, update_page

    if requested is not None and start_response:
        if requested == '':
            page = template.format(
                posts=posts,
                messages=messages,
                navigation=navigation,
                posts_hash=posts_hash,
                messages_hash=messages_hash,
            ).encode()
        elif os.path.exists(requested):
            page = read(requested)
            headers = [('Content-type', guess_mimetype(requested, page))]
        elif requested.startswith('update/'):
            # assume called by javascript, and thus that it's working
            CACHED['javascript'] = 'INFO:found compatible javascript engine'
            status, page = update()
        elif requested.startswith('ipfs/'):
            logging.debug('fetching uncached ipfs URL %s', requested)
            try:
                with urlopen('https://ipfs.io/' + requested) as request:
                    page = request.read()
                    headers = [
                        ('Content-type', guess_mimetype(requested, page))
                    ]
                cache(requested, page)
            except HTTPError as failed:
                headers = failed.headers
                status = ' '.join([str(failed.code), failed.msg])
                page = b'<div>%s</div>' % status
        else:
            logging.warning('%s not found', requested)
            status = '404 Not Found'
            page = b'<div>not yet implemented</div>'
        # NOTE: page must be a bytestring at this point!
        logging.debug('starting response with status %s and page %s...',
                      status, page[:128])
        start_response(status, headers)
        return [page]
    logging.warning('serve: failing with env=%s and start_response=%s',
                    env, start_response)
    return [b'']
Ejemplo n.º 10
0
    def monitor(self):
        '''
        wait for input. send a PONG for every PING

        intended to run in a daemon thread

        set ircbot.terminate to True in order to shut it down
        '''
        logging.debug('ircbot monitoring incoming traffic')
        tries = 0
        while tries < 10:
            try:
                received = self.stream.readline().rstrip()
                tries = 0
            except ConnectionResetError:
                tries += 1
                self.connect(self.server, self.port, self.nickname,
                             self.realname)
                continue
            logging.info('received: %r, length: %d', received, len(received))
            end_message = len(received) < 510
            # make sure all words[n] references are accounted for
            words = received.split() + ['', '', '']
            nickname, matched = check_username(words[0])
            if words[0] == 'PING':
                pong = received.replace('I', 'O', 1).rstrip() + CRLF
                logging.info('sending: %r', pong)
                self.client.send(pong.encode())
            elif words[1] == 'JOIN' and matched:
                CACHED['irc_id'] = words[0]
                logging.info("CACHED['irc_id'] = %s", CACHED['irc_id'])
            elif words[1] == 'PRIVMSG':
                sender = nickname
                privacy = 'public' if words[2] == CHANNEL else 'private'
                logging.info('%s message received from %s:', privacy, sender)
                # chop preceding ':' from ':this is a private message'
                CACHED[sender] += ' '.join(words[3:])[1:].rstrip()
                # try decoding what we have so far
                logging.debug('attempting to decode %s', CACHED[sender])
                text, trustlevel = decrypt(CACHED[sender].encode())
                logging.debug('text: %s, trustlevel: %s', text, trustlevel)
                if text or end_message:
                    text = text or CACHED[sender][:256].encode()
                    logging.info(
                        '%s %s message from %s: %s', trustlevel, privacy,
                        sender,
                        text.decode().replace('<',
                                              '&lt;').replace('>', '&gt;'),
                        **TO_PAGE)
                    if JSON.match(CACHED[sender]):
                        POSTS_QUEUE.append(CACHED[sender])
                        logging.debug('appended %r to POSTS_QUEUE',
                                      CACHED[sender])
                    else:
                        logging.debug('Not JSON: %s', CACHED[sender])
                    CACHED[sender] = ''
                elif len(CACHED[sender]) > MAXSIZE:
                    logging.info(
                        'clearing overflow CACHED[%s]: %r..., length %d',
                        sender, CACHED[sender][:256], len(CACHED[sender]))
                    CACHED[sender] = ''
                else:
                    logging.debug('CACHED[%s] now %r', sender, CACHED[sender])
            clearcache()
        logging.warning('ircbot terminated from launching thread')
Ejemplo n.º 11
0
#!/usr/bin/python3
'''
made for developing without functional python-gnupg package

this is probably not needed. I didn't realize that `gnupg` and `python-gnupg`
were two separate PyPI (pip) packages until Ildar told me -- jc
'''
import subprocess, re  # pylint: disable=multiple-imports
from kbcommon import logging

logging.warning('Using primitive GPG functionality')

def run_process(command, **kwargs):
    '''
    implementation of subprocess.run for older Python3

    https://pymotw.com/3/subprocess/
    '''
    text_input = kwargs.get('input', None)
    capture_output = kwargs.get('capture_output', False)
    logging.debug('capture_output %s ignored', capture_output)
    timeout = kwargs.get('timeout', None)
    check = kwargs.get('check', None)
    if timeout:
        raise NotImplementedError('"timeout" not supported')
    # pylint: disable=bad-option-value, consider-using-with
    process = subprocess.Popen(
        command,
        stdin=kwargs.get('stdin', subprocess.PIPE),
        stdout=kwargs.get('stdout', subprocess.PIPE),
        stderr=kwargs.get('stderr', subprocess.PIPE),