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()
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()
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)
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()