Example #1
0
def route_hwinfo():
    """ Upload a hwinfo binary file to the LVFS service without authentication """

    # not correct parameters
    if not 'type' in request.form:
        return _json_error('no type')
    if not 'machine_id' in request.form:
        return _json_error('no machine_id')
    if not 'file' in request.files:
        return _json_error('no file')
    if len(request.form['machine_id']) != 32:
        return _json_error('machine_id %s not valid' % request.form['machine_id'])
    try:
        int(request.form['machine_id'], 16)
    except ValueError as e:
        return _json_error(str(e))

    # check type against defined list
    settings = _get_settings()
    if request.form['type'] not in settings['hwinfo_kinds'].split(','):
        return _json_error('type not valid')

    # read in entire file
    fileitem = request.files['file']
    if not fileitem:
        return _json_error('no file object')
    filebuf = fileitem.read()
    if len(filebuf) > 0x40000:
        return _json_error('file is too large')

    # dump to a file
    hwinfo_dir = os.path.join(app.config['HWINFO_DIR'], request.form['type'])
    if not os.path.exists(hwinfo_dir):
        os.mkdir(hwinfo_dir)
    fn = os.path.join(hwinfo_dir, '%s' % request.form['machine_id'])
    if os.path.exists(fn):
        return _json_error('already reported from this machine-id')
    with open(fn, 'wb') as f:
        f.write(filebuf)
    return _json_success()
Example #2
0
def route_import():

    # security check
    if not g.user.check_acl('@partner'):
        return _json_error('Permission denied: Unable to import data')

    # manual upload
    if 'file' in request.files:
        request_data = request.files['file'].read()
    else:
        request_data = request.data

    # parse JSON data, which can be optionally compressed
    try:
        payload = gzip.decompress(request_data)
    except OSError as e:
        payload = request_data.decode('utf8')
    try:
        obj = json.loads(payload)
    except ValueError as e:
        return _json_error('No JSON object could be decoded: ' + str(e))

    # get available protocols
    protocols = {}
    for protocol in db.session.query(Protocol):
        protocols[protocol.value] = protocol

    # get available components
    mds_by_id = {}
    mds_by_tag = {}
    mds_by_ver = {}
    for md in db.session.query(Component).\
                            join(Firmware).\
                            join(Remote).\
                            filter(Remote.name != 'private',
                                   Remote.name != 'deleted'):
        mds_by_id[md.component_id] = md
        for version in _get_plausible_versions_for_md(md):
            mds_by_ver['{}/{}'.format(md.appstream_id, version)] = md
        if md.release_tag:
            mds_by_tag['{}/{}'.format(md.appstream_id, md.release_tag.casefold())] = md

    # get available vendors
    vendors = {}
    for vendor in db.session.query(Vendor).filter(Vendor.visible):
        vendors[vendor.vendor_id] = vendor

    # delete all old mdrefs from this vendor, and force a commit
    for mdref in db.session.query(ComponentRef).\
                    filter(ComponentRef.vendor_id_partner == g.user.vendor_id):
        db.session.delete(mdref)
    db.session.commit()

    # parse blob
    if 'metadata' not in obj:
        return _json_error('metadata object missing')
    metadata = obj['metadata']
    if metadata['version'] != 0:
        return _json_error('metadata schema version unsupported: ' + str(e))
    if 'devices' not in obj:
        return _json_error('devices object missing')
    for obj_dev in obj['devices']:
        try:
            # may be missing for devices not (yet) in LVFS
            appstream_id = obj_dev.get('appstream_id', None)

            # has to match something specified on the LVFS if specified
            if 'protocol' in obj_dev:
                protocol = protocols.get(obj_dev['protocol'], None)
            else:
                protocol = None

            # squash this together as one string as it's only used for analysis
            name = '/'.join(obj_dev.get('names', []))

            # parse each version for the model
            for version in obj_dev['versions']:
                obj_ver = obj_dev['versions'][version]

                # the component_id is optional, but allows the LVFS to link FW
                md = None
                if 'component_id' in obj_ver:
                    md = mds_by_id.get(obj_ver['component_id'], None)

                # release_tag uniquely identifies the firmware download
                release_tag = obj_ver.get('release_tag', None)
                if release_tag and release_tag.find('_') != -1:
                    # this will be fixed by the importer...
                    release_tag = None

                # date has to be in ISO8601 format
                date = None
                if 'date' in obj_ver:
                    try:
                        date = dateutil.parser.parse(obj_ver['date'])
                    except dateutil.parser.ParserError as _:
                        pass

                # prefer the changelog url
                url = obj_ver.get('changelog_url', None)
                if not url:
                    url = obj_ver.get('file_url', None)

                # try getting the md using the appstream-id and the version/tag
                if appstream_id:
                    if not md:
                        md = mds_by_ver.get('{}/{}'.format(appstream_id, version), None)
                    if not md and release_tag:
                        md = mds_by_tag.get('{}/{}'.format(appstream_id, release_tag.casefold()), None)

                # prefer the vendor from the component but fallback to the vendor ID
                vendor = None
                if md:
                    vendor = md.fw.vendor
                elif 'vendor_id' in obj_dev:
                    vendor = vendors.get(int(obj_dev['vendor_id']), None)
                if not vendor:
                    continue

                # add to database
                mdref = ComponentRef(appstream_id=appstream_id,
                                     version=version,
                                     date=date,
                                     md=md,
                                     status=obj_ver.get('status', None),
                                     release_tag=release_tag,
                                     url=url,
                                     name=name,
                                     vendor=vendor,
                                     vendor_partner=g.user.vendor,
                                     protocol=protocol)
                db.session.add(mdref)
        except KeyError as e:
            return _json_error('JSON {} invalid: {}'.format(obj_dev, str(e)))

    # commit new mdrefs
    db.session.commit()

    return _json_success()
Example #3
0
def route_report():
    """ Upload a report """

    # only accept form data
    if request.method != 'POST':
        return _json_error('only POST supported')

    # parse both content types, either application/json or multipart/form-data
    signature = None
    if request.data:
        payload = request.data.decode('utf8')
    elif request.form:
        data = request.form.to_dict()
        if 'payload' not in data:
            return _json_error('No payload in multipart/form-data')
        payload = data['payload']
        if 'signature' in data:
            signature = data['signature']
    else:
        return _json_error('No data')

    # find user and verify
    crt = None
    if signature:
        try:
            info = _pkcs7_signature_info(signature, check_rc=False)
        except IOError as e:
            return _json_error('Signature invalid: %s' % str(e))
        if 'serial' not in info:
            return _json_error('Signature invalid, no signature')
        crt = db.session.query(Certificate).filter(
            Certificate.serial == info['serial']).first()
        if crt:
            try:
                _pkcs7_signature_verify(crt, payload, signature)
            except IOError as _:
                return _json_error('Signature did not validate')

    # parse JSON data
    try:
        item = json.loads(payload)
    except ValueError as e:
        return _json_error('No JSON object could be decoded: ' + str(e))

    # check we got enough data
    for key in ['ReportVersion', 'MachineId', 'Reports', 'Metadata']:
        if not key in item:
            return _json_error('invalid data, expected %s' % key)
        if item[key] is None:
            return _json_error('missing data, expected %s' % key)

    # parse only this version
    if item['ReportVersion'] != 2:
        return _json_error('report version not supported')

    # add each firmware report
    machine_id = item['MachineId']
    reports = item['Reports']
    if len(reports) == 0:
        return _json_error('no reports included')
    metadata = item['Metadata']
    if len(metadata) == 0:
        return _json_error('no metadata included')

    msgs = []
    uris = []
    for report in reports:
        for key in ['Checksum', 'UpdateState', 'Metadata']:
            if not key in report:
                return _json_error('invalid data, expected %s' % key)
            if report[key] is None:
                return _json_error('missing data, expected %s' % key)

        # flattern the report including the per-machine and per-report metadata
        data = metadata
        for key in report:
            # don't store some data
            if key in [
                    'Created', 'Modified', 'BootTime', 'UpdateState',
                    'DeviceId', 'UpdateState', 'DeviceId', 'Checksum'
            ]:
                continue
            if key == 'Metadata':
                md = report[key]
                for md_key in md:
                    data[md_key] = md[md_key]
                continue
            # allow array of strings for any of the keys
            if isinstance(report[key], list):
                data[key] = ','.join(report[key])
            else:
                data[key] = report[key]

        # try to find the checksum (which might not exist on this server)
        fw = db.session.query(Firmware).filter(
            Firmware.checksum_signed_sha1 == report['Checksum']).first()
        if not fw:
            fw = db.session.query(Firmware).filter(
                Firmware.checksum_signed_sha256 == report['Checksum']).first()
        if not fw:
            msgs.append('%s did not match any known firmware archive' %
                        report['Checksum'])
            continue

        # cannot report this failure
        if fw.do_not_track:
            msgs.append('%s will not accept reports' % report['Checksum'])
            continue

        # update the device checksums if there is only one component
        if crt and crt.user.check_acl(
                '@qa') and 'ChecksumDevice' in data and len(fw.mds) == 1:
            md = fw.md_prio
            found = False

            # fwupd v1.2.6 sends an array of strings, before that just a string
            checksums = data['ChecksumDevice']
            if not isinstance(checksums, list):
                checksums = [checksums]

            # does the submitted checksum already exist as a device checksum
            for checksum in checksums:
                for csum in md.device_checksums:
                    if csum.value == checksum:
                        found = True
                        break
                if found:
                    continue
                _event_log('added device checksum %s to firmware %s' %
                           (checksum, md.fw.checksum_upload_sha1))
                if _is_sha1(checksum):
                    md.device_checksums.append(Checksum(checksum, 'SHA1'))
                elif _is_sha256(checksum):
                    md.device_checksums.append(Checksum(checksum, 'SHA256'))

        # find any matching report
        issue_id = 0
        if report['UpdateState'] == 3:
            issue = _find_issue_for_report_data(data, fw)
            if issue:
                issue_id = issue.issue_id
                msgs.append('The failure is a known issue')
                uris.append(issue.url)

        # update any old report
        r = db.session.query(Report).\
                        filter(Report.checksum == report['Checksum']).\
                        filter(Report.machine_id == machine_id).first()
        if r:
            msgs.append('%s replaces old report' % report['Checksum'])
            r.state = report['UpdateState']
            for e in r.attributes:
                db.session.delete(e)
        else:
            # save a new report in the database
            r = Report(machine_id=machine_id,
                       firmware_id=fw.firmware_id,
                       issue_id=issue_id,
                       state=report['UpdateState'],
                       checksum=report['Checksum'])

        # update the firmware so that the QA user does not have to wait 24h
        if r.state == 2:
            fw.report_success_cnt += 1
        elif r.state == 3:
            if r.issue_id:
                fw.report_issue_cnt += 1
            else:
                fw.report_failure_cnt += 1

        # update the LVFS user
        if crt:
            r.user_id = crt.user_id

        # save all the report entries
        for key in data:
            r.attributes.append(ReportAttribute(key=key, value=data[key]))
        db.session.add(r)

    # all done
    db.session.commit()

    # put messages and URIs on one line
    return _json_success(msg='; '.join(msgs) if msgs else None,
                         uri='; '.join(uris) if uris else None)
Example #4
0
def route_report():
    """ Upload a report """

    # only accept form data
    if request.method != 'POST':
        return _json_error('only POST supported')

    # parse both content types, either application/json or multipart/form-data
    signature = None
    if request.data:
        payload = request.data.decode('utf8')
    elif request.form:
        data = request.form.to_dict()
        if 'payload' not in data:
            return _json_error('No payload in multipart/form-data')
        payload = data['payload']
        if 'signature' in data:
            signature = data['signature']
    else:
        return _json_error('No data')

    # find user and verify
    if signature:
        try:
            info = _pkcs7_signature_info(signature, check_rc=False)
        except IOError as e:
            return _json_error('Signature invalid: %s' % str(e))
        if 'serial' not in info:
            return _json_error('Signature invalid, no signature')

    # parse JSON data
    try:
        item = json.loads(payload)
    except ValueError as e:
        return _json_error('No JSON object could be decoded: ' + str(e))

    # check we got enough data
    for key in ['ReportVersion', 'MachineId', 'Metadata']:
        if not key in item:
            return _json_error('invalid data, expected %s' % key)
        if item[key] is None:
            return _json_error('missing data, expected %s' % key)

    # parse only this version
    if item['ReportVersion'] != 2:
        return _json_error('report version not supported')

    # add each firmware report
    machine_id = item['MachineId']
    if not _is_sha256(machine_id):
        return _json_error('MachineId invalid, expected SHA256')
    hsireports = item.get('HsiReports', [])
    sec_attrs = item.get('SecurityAttributes', [])
    if not hsireports and not sec_attrs:
        return _json_error('no hsireports included')
    metadata = item['Metadata']
    if not metadata:
        return _json_error('no metadata included')

    if sec_attrs:

        # required metadata for this report type
        for key in ['HostProduct', 'HostFamily', 'HostVendor', 'HostSku']:
            if not key in metadata:
                return _json_error('invalid data, expected %s' % key)
            if metadata[key] is None:
                return _json_error('missing data, expected %s' % key)

        # check attrs
        for sec_attr in sec_attrs:
            for key in ['AppstreamId', 'HsiResult']:
                if not key in sec_attr:
                    return _json_error('invalid data, expected %s' % key)
                if sec_attr[key] is None:
                    return _json_error('missing data, expected %s' % key)

        # update any old report
        rpt = db.session.query(HsiReport)\
                        .filter(HsiReport.machine_id == machine_id).first()
        if rpt:
            db.session.delete(rpt)

        # construct a single string
        distro = '{} {} ({})'.format(metadata.get('DistroId', 'Unknown'),
                                     metadata.get('DistroVersion', 'Unknown'),
                                     metadata.get('DistroVariant', 'Unknown'))

        # save a new report in the database
        host_security_parts = metadata['HostSecurityId'].split(' ')
        rpt = HsiReport(
            payload=payload,  # in case we want to extract
            signature=signature,  # if we want to match the user
            machine_id=machine_id,
            distro=distro,
            kernel_cmdline=metadata.get('KernelCmdline'),
            kernel_version=metadata.get('KernelVersion'),
            host_product=metadata['HostProduct'],
            host_vendor=metadata['HostVendor'],
            host_family=metadata['HostFamily'],
            host_sku=metadata['HostSku'],
            host_security_id=host_security_parts[0],
            host_security_version=host_security_parts[1][2:-1])
        for sec_attr in sec_attrs:
            flags = sec_attr.get('Flags', [])
            attr = HsiReportAttr(appstream_id=sec_attr['AppstreamId'],
                                 hsi_result=sec_attr['HsiResult'],
                                 is_runtime='runtime-issue' in flags,
                                 is_success='success' in flags,
                                 is_obsoleted='obsoleted' in flags)
            rpt.attrs.append(attr)
        db.session.add(rpt)

    # all done
    db.session.commit()
    return _json_success()