Example #1
0
 def check_for_write_access(self, request_data):
     if not request_data.username:
         if request_data.is_trusted_ip:
             return
         raise HTTPForbidden('Anonymous users are not allowed to make changes')
     if self.user_manager.is_readonly(request_data.username):
         raise HTTPForbidden('The user {} does not have permission to make changes'.format(request_data.username))
Example #2
0
 def get_library(self, request_data, library_id=None):
     if not request_data.username:
         return self.library_broker.get(library_id)
     lf = partial(self.user_manager.allowed_library_names, request_data.username)
     allowed_libraries = self.library_broker.allowed_libraries(lf)
     if not allowed_libraries:
         raise HTTPForbidden('The user {} is not allowed to access any libraries on this server'.format(request_data.username))
     library_id = library_id or next(iter(allowed_libraries))
     if library_id in allowed_libraries:
         return self.library_broker.get(library_id)
     raise HTTPForbidden('The user {} is not allowed to access the library {}'.format(request_data.username, library_id))
Example #3
0
def cdb_add_book(ctx, rd, job_id, add_duplicates, filename, library_id):
    '''
    Add a file as a new book. The file contents must be in the body of the request.

    The response will also have the title/authors/languages read from the
    metadata of the file/filename. It will contain a `book_id` field specifying the id of the newly added book,
    or if add_duplicates is not specified and a duplicate was found, no book_id will be present. It will also
    return the value of `job_id` as the `id` field and `filename` as the `filename` field.
    '''
    db = get_db(ctx, rd, library_id)
    if ctx.restriction_for(rd, db):
        raise HTTPForbidden('Cannot use the add book interface with a user who has per library restrictions')
    if not filename:
        raise HTTPBadRequest('An empty filename is not allowed')
    sfilename = sanitize_file_name_unicode(filename)
    fmt = os.path.splitext(sfilename)[1]
    fmt = fmt[1:] if fmt else None
    if not fmt:
        raise HTTPBadRequest('An filename with no extension is not allowed')
    if isinstance(rd.request_body_file, BytesIO):
        raise HTTPBadRequest('A request body containing the file data must be specified')
    add_duplicates = add_duplicates in ('y', '1')
    path = os.path.join(rd.tdir, sfilename)
    rd.request_body_file.name = path
    rd.request_body_file.seek(0)
    mi = get_metadata(rd.request_body_file, stream_type=fmt, use_libprs_metadata=True)
    rd.request_body_file.seek(0)
    ids, duplicates = db.add_books([(mi, {fmt: rd.request_body_file})], add_duplicates=add_duplicates)
    ans = {'title': mi.title, 'authors': mi.authors, 'languages': mi.languages, 'filename': filename, 'id': job_id}
    if ids:
        ans['book_id'] = ids[0]
        books_added(ids)
    return ans
Example #4
0
def cdb_run(ctx, rd, which, version):
    try:
        m = module_for_cmd(which)
    except ImportError:
        raise HTTPNotFound('No module named: {}'.format(which))
    if not getattr(m, 'readonly', False):
        ctx.check_for_write_access(rd)
    if getattr(m, 'version', 0) != int(version):
        raise HTTPNotFound(('The module {} is not available in version: {}.'
                           'Make sure the version of calibre used for the'
                            ' server and calibredb match').format(which, version))
    db = get_library_data(ctx, rd, strict_library_id=True)[0]
    if ctx.restriction_for(rd, db):
        raise HTTPForbidden('Cannot use the command-line db interface with a user who has per library restrictions')
    raw = rd.read()
    ct = rd.inheaders.get('Content-Type', all=True)
    try:
        if MSGPACK_MIME in ct:
            args = msgpack_loads(raw)
        elif 'application/json' in ct:
            args = json_loads(raw)
        else:
            raise HTTPBadRequest('Only JSON or msgpack requests are supported')
    except Exception:
        raise HTTPBadRequest('args are not valid encoded data')
    if getattr(m, 'needs_srv_ctx', False):
        args = [ctx] + list(args)
    try:
        result = m.implementation(db, partial(ctx.notify_changes, db.backend.library_path), *args)
    except Exception as err:
        import traceback
        return {'err': as_unicode(err), 'tb': traceback.format_exc()}
    return {'result': result}
Example #5
0
def cdb_set_fields(ctx, rd, book_id, library_id):
    db = get_db(ctx, rd, library_id)
    if ctx.restriction_for(rd, db):
        raise HTTPForbidden('Cannot use the set fields interface with a user who has per library restrictions')
    data = load_payload_data(rd)
    try:
        changes, loaded_book_ids = data['changes'], frozenset(map(int, data.get('loaded_book_ids', ())))
        all_dirtied = bool(data.get('all_dirtied'))
        if not isinstance(changes, dict):
            raise TypeError('changes must be a dict')
    except Exception:
        raise HTTPBadRequest(
        '''Data must be of the form {'changes': {'title': 'New Title', ...}, 'loaded_book_ids':[book_id1, book_id2, ...]'}''')
    dirtied = set()
    cdata = changes.pop('cover', False)
    if cdata is not False:
        if cdata is not None:
            try:
                cdata = standard_b64decode(cdata.split(',', 1)[-1].encode('ascii'))
            except Exception:
                raise HTTPBadRequest('Cover data is not valid base64 encoded data')
            try:
                fmt = what(None, cdata)
            except Exception:
                fmt = None
            if fmt not in ('jpeg', 'png'):
                raise HTTPBadRequest('Cover data must be either JPEG or PNG')
        dirtied |= db.set_cover({book_id: cdata})

    for field, value in iteritems(changes):
        dirtied |= db.set_field(field, {book_id: value})
    ctx.notify_changes(db.backend.library_path, metadata(dirtied))
    all_ids = dirtied if all_dirtied else (dirtied & loaded_book_ids)
    all_ids |= {book_id}
    return {bid: book_as_json(db, bid) for bid in all_ids}
Example #6
0
def console_print(ctx, rd):
    if not getattr(rd.opts, 'allow_console_print', False):
        raise HTTPForbidden('console printing is not allowed')
    with print_lock:
        print(rd.remote_addr, end=' ')
        shutil.copyfileobj(rd.request_body_file, sys.stdout)
    return ''
Example #7
0
    def do_http_auth(self, data, endpoint):
        ban_key = data.remote_addr, data.forwarded_for
        if self.ban_list.is_banned(ban_key):
            raise HTTPForbidden(
                'Too many login attempts',
                log='Too many login attempts from: %s' %
                (ban_key if data.forwarded_for else data.remote_addr))
        auth = data.inheaders.get('Authorization')
        nonce_is_stale = False
        log_msg = None
        data.username = None

        if auth:
            scheme, rest = auth.partition(' ')[::2]
            scheme = scheme.lower()
            if scheme == 'digest':
                da = DigestAuth(rest.strip())
                if validate_nonce(self.key_order, da.nonce, self.realm,
                                  self.secret):
                    pw = self.user_credentials.get(da.username)
                    if pw and da.validate_request(pw, data, self.log):
                        nonce_is_stale = is_nonce_stale(
                            da.nonce, self.max_age_seconds)
                        if not nonce_is_stale:
                            data.username = da.username
                            return
                log_msg = 'Failed login attempt from: %s' % data.remote_addr
                self.ban_list.failed(ban_key)
            elif self.prefer_basic_auth and scheme == 'basic':
                try:
                    un, pw = base64_decode(rest.strip()).partition(':')[::2]
                except ValueError:
                    raise HTTPSimpleResponse(
                        http_client.BAD_REQUEST,
                        'The username or password contained non-UTF8 encoded characters'
                    )
                if not un or not pw:
                    raise HTTPSimpleResponse(
                        http_client.BAD_REQUEST,
                        'The username or password was empty')
                if self.check(un, pw):
                    data.username = un
                    return
                log_msg = 'Failed login attempt from: %s' % data.remote_addr
                self.ban_list.failed(ban_key)
            else:
                raise HTTPSimpleResponse(http_client.BAD_REQUEST,
                                         'Unsupported authentication method')

        if self.prefer_basic_auth:
            raise HTTPAuthRequired('Basic realm="%s"' % self.realm,
                                   log=log_msg)

        s = 'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"' % (
            self.realm,
            synthesize_nonce(self.key_order, self.realm, self.secret))
        if nonce_is_stale:
            s += ', stale="true"'
        raise HTTPAuthRequired(s, log=log_msg)
Example #8
0
def cdb_set_cover(ctx, rd, book_id, library_id):
    db = get_db(ctx, rd, library_id)
    if ctx.restriction_for(rd, db):
        raise HTTPForbidden('Cannot use the add book interface with a user who has per library restrictions')
    rd.request_body_file.seek(0)
    dirtied = db.set_cover({book_id: rd.request_body_file})
    ctx.notify_changes(db.backend.library_path, metadata(dirtied))
    return tuple(dirtied)
Example #9
0
 def library_info(self, request_data):
     if not request_data.username:
         return self.library_broker.library_map, self.library_broker.default_library
     lf = partial(self.user_manager.allowed_library_names, request_data.username)
     allowed_libraries = self.library_broker.allowed_libraries(lf)
     if not allowed_libraries:
         raise HTTPForbidden('The user {} is not allowed to access any libraries on this server'.format(request_data.username))
     return dict(allowed_libraries), next(iter(allowed_libraries))
Example #10
0
def cdb_copy_to_library(ctx, rd, target_library_id, library_id):
    db_src = get_db(ctx, rd, library_id)
    db_dest = get_db(ctx, rd, target_library_id)
    if ctx.restriction_for(rd, db_src) or ctx.restriction_for(rd, db_dest):
        raise HTTPForbidden(
            'Cannot use the copy to library interface with a user who has per library restrictions'
        )
    data = load_payload_data(rd)
    try:
        book_ids = {int(x) for x in data['book_ids']}
        move_books = bool(data.get('move', False))
        preserve_date = bool(data.get('preserve_date', True))
        duplicate_action = data.get('duplicate_action') or 'add'
        automerge_action = data.get('automerge_action') or 'overwrite'
    except Exception:
        raise HTTPBadRequest(
            'Invalid encoded data, must be of the form: {book_ids: [id1, id2, ..]}'
        )
    if duplicate_action not in ('add', 'add_formats_to_existing', 'ignore'):
        raise HTTPBadRequest(
            'duplicate_action must be one of: add, add_formats_to_existing, ignore'
        )
    if automerge_action not in ('overwrite', 'ignore', 'new record'):
        raise HTTPBadRequest(
            'automerge_action must be one of: overwrite, ignore, new record')
    response = {}
    identical_books_data = None
    if duplicate_action != 'add':
        identical_books_data = db_dest.data_for_find_identical_books()
    to_remove = set()
    from calibre.db.copy_to_library import copy_one_book
    for book_id in book_ids:
        try:
            rdata = copy_one_book(book_id,
                                  db_src,
                                  db_dest,
                                  duplicate_action=duplicate_action,
                                  automerge_action=automerge_action,
                                  preserve_uuid=move_books,
                                  preserve_date=preserve_date,
                                  identical_books_data=identical_books_data)
            if move_books:
                to_remove.add(book_id)
            response[book_id] = {'ok': True, 'payload': rdata}
        except Exception:
            import traceback
            response[book_id] = {
                'ok': False,
                'payload': traceback.format_exc()
            }

    if to_remove:
        db_src.remove_books(to_remove, permanent=True)

    return response
Example #11
0
def cdb_delete_book(ctx, rd, book_ids, library_id):
    db = get_db(ctx, rd, library_id)
    if ctx.restriction_for(rd, db):
        raise HTTPForbidden('Cannot use the delete book interface with a user who has per library restrictions')
    try:
        ids = {int(x) for x in book_ids.split(',')}
    except Exception:
        raise HTTPBadRequest('invalid book_ids: {}'.format(book_ids))
    db.remove_books(ids)
    books_deleted(ids)
    return {}
Example #12
0
def cdb_set_fields(ctx, rd, book_id, library_id):
    db = get_db(ctx, rd, library_id)
    if ctx.restriction_for(rd, db):
        raise HTTPForbidden(
            'Cannot use the set fields interface with a user who has per library restrictions'
        )
    raw = rd.read()
    ct = rd.inheaders.get('Content-Type', all=True)
    ct = {x.lower().partition(';')[0] for x in ct}
    try:
        if MSGPACK_MIME in ct:
            data = msgpack_loads(raw)
        elif 'application/json' in ct:
            data = json_loads(raw)
        else:
            raise HTTPBadRequest('Only JSON or msgpack requests are supported')
    except Exception:
        raise HTTPBadRequest('Invalid encoded data')
    try:
        changes, loaded_book_ids = data['changes'], frozenset(
            map(int, data.get('loaded_book_ids', ())))
        all_dirtied = bool(data.get('all_dirtied'))
        if not isinstance(changes, dict):
            raise TypeError('changes must be a dict')
    except Exception:
        raise HTTPBadRequest(
            '''Data must be of the form {'changes': {'title': 'New Title', ...}, 'loaded_book_ids':[book_id1, book_id2, ...]'}'''
        )
    dirtied = set()
    cdata = changes.pop('cover', False)
    if cdata is not False:
        if cdata is not None:
            try:
                cdata = standard_b64decode(
                    cdata.split(',', 1)[-1].encode('ascii'))
            except Exception:
                raise HTTPBadRequest(
                    'Cover data is not valid base64 encoded data')
            try:
                fmt = what(None, cdata)
            except Exception:
                fmt = None
            if fmt not in ('jpeg', 'png'):
                raise HTTPBadRequest('Cover data must be either JPEG or PNG')
        dirtied |= db.set_cover({book_id: cdata})

    for field, value in changes.iteritems():
        dirtied |= db.set_field(field, {book_id: value})
    ctx.notify_changes(db.backend.library_path, metadata(dirtied))
    all_ids = dirtied if all_dirtied else (dirtied & loaded_book_ids)
    all_ids |= {book_id}
    return {bid: book_as_json(db, book_id) for bid in all_ids}
def change_pw(ctx, rd):
    user = rd.username or None
    if user is None:
        raise HTTPForbidden(
            'Anonymous users are not allowed to change passwords')
    try:
        pw = json.loads(rd.request_body_file.read())
        oldpw, newpw = pw['oldpw'], pw['newpw']
    except Exception:
        raise HTTPBadRequest('No decodable password found')
    if oldpw != ctx.user_manager.get(user):
        raise HTTPBadRequest(_('Existing password is incorrect'))
    err = validate_password(newpw)
    if err:
        raise HTTPBadRequest(err)
    try:
        ctx.user_manager.change_password(user, newpw)
    except Exception as err:
        raise HTTPBadRequest(as_unicode(err))
    ctx.log.warn('Changed password for user', user)
    return 'password for {} changed'.format(user)
Example #14
0
def cdb_set_fields(ctx, rd, book_id, library_id):
    db = get_db(ctx, rd, library_id)
    if ctx.restriction_for(rd, db):
        raise HTTPForbidden('Cannot use the set fields interface with a user who has per library restrictions')
    raw = rd.read()
    ct = rd.inheaders.get('Content-Type', all=True)
    ct = {x.lower().partition(';')[0] for x in ct}
    try:
        if MSGPACK_MIME in ct:
            data = msgpack_loads(raw)
        elif 'application/json' in ct:
            data = json_loads(raw)
        else:
            raise HTTPBadRequest('Only JSON or msgpack requests are supported')
        changes, loaded_book_ids = data['changes'], frozenset(map(int, data['loaded_book_ids']))
    except Exception:
        raise HTTPBadRequest('Invalid encoded data')
    dirtied = set()
    for field, value in changes.iteritems():
        dirtied |= db.set_field(field, {book_id: value})
    metadata(dirtied)
    return {bid: book_as_json(db, book_id) for bid in (dirtied & loaded_book_ids) | {book_id}}