/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// See
// https://wiki.mozilla.org/Security/Features/Application_Reputation_Design_Doc
// for a description of Chrome's implementation of this feature.
#include "ApplicationReputation.h"
#include "chrome/common/safe_browsing/csd.pb.h"

#include "nsIArray.h"
#include "nsIApplicationReputation.h"
#include "nsIChannel.h"
#include "nsIHttpChannel.h"
#include "nsIIOService.h"
#include "nsIObserverService.h"
#include "nsISimpleEnumerator.h"
#include "nsIStreamListener.h"
#include "nsIStringStream.h"
#include "nsITimer.h"
#include "nsIUploadChannel2.h"
#include "nsIURI.h"
#include "nsIURL.h"
#include "nsIUrlClassifierDBService.h"
#include "nsIURLFormatter.h"
#include "nsIX509Cert.h"
#include "nsIX509CertDB.h"

#include "mozilla/BasePrincipal.h"
#include "mozilla/Components.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/LoadContext.h"
#include "mozilla/Preferences.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/glean/ReputationserviceMetrics.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/intl/LocaleService.h"

#include "nsCOMPtr.h"
#include "nsDebug.h"
#include "nsDependentSubstring.h"
#include "nsError.h"
#include "nsLocalFileCommon.h"
#include "nsNetCID.h"
#include "nsReadableUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"

#include "nsIContentPolicy.h"
#include "nsICryptoHash.h"
#include "nsILoadInfo.h"
#include "nsContentUtils.h"
#include "nsWeakReference.h"
#include "nsIRedirectHistoryEntry.h"

#include "ApplicationReputationTelemetryUtils.h"

using mozilla::BasePrincipal;
using mozilla::OriginAttributes;
using mozilla::Preferences;
using mozilla::TimeStamp;
using mozilla::intl::LocaleService;
using safe_browsing::ClientDownloadRequest;
using safe_browsing::ClientDownloadRequest_CertificateChain;
using safe_browsing::ClientDownloadRequest_Resource;
using safe_browsing::ClientDownloadRequest_SignatureInfo;

// Preferences that we need to initialize the query.
#define PREF_SB_APP_REP_URL "browser.safebrowsing.downloads.remote.url"
#define PREF_SB_MALWARE_ENABLED "browser.safebrowsing.malware.enabled"
#define PREF_SB_DOWNLOADS_ENABLED "browser.safebrowsing.downloads.enabled"
#define PREF_SB_DOWNLOADS_REMOTE_ENABLED \
  "browser.safebrowsing.downloads.remote.enabled"
#define PREF_SB_DOWNLOADS_REMOTE_TIMEOUT \
  "browser.safebrowsing.downloads.remote.timeout_ms"
#define PREF_DOWNLOAD_BLOCK_TABLE "urlclassifier.downloadBlockTable"
#define PREF_DOWNLOAD_ALLOW_TABLE "urlclassifier.downloadAllowTable"

// Preferences that are needed to action the verdict.
#define PREF_BLOCK_DANGEROUS \
  "browser.safebrowsing.downloads.remote.block_dangerous"
#define PREF_BLOCK_DANGEROUS_HOST \
  "browser.safebrowsing.downloads.remote.block_dangerous_host"
#define PREF_BLOCK_POTENTIALLY_UNWANTED \
  "browser.safebrowsing.downloads.remote.block_potentially_unwanted"
#define PREF_BLOCK_UNCOMMON \
  "browser.safebrowsing.downloads.remote.block_uncommon"

// MOZ_LOG=ApplicationReputation:5
mozilla::LazyLogModule ApplicationReputationService::prlog(
    "ApplicationReputation");
#define LOG(args) \
  MOZ_LOG(ApplicationReputationService::prlog, mozilla::LogLevel::Debug, args)
#define LOG_ENABLED() \
  MOZ_LOG_TEST(ApplicationReputationService::prlog, mozilla::LogLevel::Debug)

/**
 * Our detection of executable/binary files uses 3 lists:
 * - kNonBinaryExecutables (below)
 * - kBinaryFileExtensions (below)
 * - sExecutableExts (in nsLocalFileCommon)
 *
 * On Windows, the `sExecutableExts` list is used to determine whether files
 * count as executable. For executable files, we will not offer an "open with"
 * option when downloading, only "save as".
 *
 * On all platforms, the combination of these lists is used to determine
 * whether files should be subject to application reputation checks.
 * Specifically, all files with extensions that:
 * - are in kBinaryFileExtensions, or
 * - are in sExecutableExts **and not in kNonBinaryExecutables**
 *
 * will be subject to checks.
 *
 * There are tests that verify that these lists are sorted and that extensions
 * never appear in both the sExecutableExts and kBinaryFileExtensions lists.
 *
 * When adding items to any lists:
 * - please prefer adding to sExecutableExts unless it is imperative users can
 *   (potentially automatically!) open such files with a helper application
 *   without first saving them (and that outweighs any associated risk).
 * - if adding executable items that shouldn't be submitted to apprep servers,
 *   add them to sExecutableExts and also to kNonBinaryExecutables.
 * - always add an associated comment in the kBinaryFileExtensions list. Add
 *   a commented-out entry with an `exec` annotation if you add the actual
 *   entry in sExecutableExts.
 *
 * When removing items please consider whether items should still be in the
 * sExecutableExts list even if removing them from the kBinaryFileExtensions
 * list, and vice versa.
 *
 * Note that there is a GTest that does its best to check some of these
 * invariants that you'll likely need to update if you're modifying these
 * lists.
 */

// Items that are in sExecutableExts but shouldn't be submitted for application
// reputation checks.
/* static */
const char* const ApplicationReputationService::kNonBinaryExecutables[] = {
    // clang-format off
    ".ad",
    ".afploc",
    ".air",
    ".atloc",
    ".ftploc",
    ".terminal",
    // clang-format on
};

// Items that should be submitted for application reputation checks that users
// are able to open immediately (without first saving and then finding the
// file). If users shouldn't be able to open them immediately, add to
// sExecutableExts instead (see also the docstring comment above!).
/* static */
const char* const ApplicationReputationService::kBinaryFileExtensions[] = {
    // Originally extracted from the "File Type Policies" Chrome extension
    // Items listed with an `exec` comment are in the sExecutableExts list in
    // nsLocalFileCommon.h .
    //".001",
    //".7z",
    //".ace",
    //".accda", exec       // MS Access database
    //".accdb", exec       // MS Access database
    //".accde", exec       // MS Access database
    //".accdr", exec       // MS Access database
    ".action",  // Mac script
    //".ad", exec // Windows
    //".ade", exec  // MS Access
    //".adp", exec // MS Access
    //".air", exec // Adobe AIR installer; excluded from apprep checks.
    ".apk",  // Android package
    //".app", exec  // Executable application
    ".applescript",
    //".application", exec // MS ClickOnce
    //".appref-ms", exec // MS ClickOnce
    //".appx", exec
    //".appxbundle", exec
    //".arc",
    //".arj",
    ".as",  // Mac archive
    //".asp", exec  // Windows Server script
    ".asx",  // Windows Media Player
    //".b64",
    //".balz",
    //".bas", exec  // Basic script
    ".bash",  // Linux shell
    //".bat", exec  // Windows shell
    //".bhx",
    ".bin",
    ".btapp",      // uTorrent and Transmission
    ".btinstall",  // uTorrent and Transmission
    ".btkey",      // uTorrent and Transmission
    ".btsearch",   // uTorrent and Transmission
    ".btskin",     // uTorrent and Transmission
    ".bz",         // Linux archive (bzip)
    ".bz2",        // Linux archive (bzip2)
    ".bzip2",      // Linux archive (bzip2)
    ".cab",        // Windows archive
    ".caction",    // Automator action
    ".cdr",        // Mac disk image
    //".cer", exec // Signed certificate file
    ".cfg",  // Windows
    ".chi",  // Windows Help
    //".chm", exec // Windows Help
    ".class",  // Java
    //".cmd", exec // Windows executable
    //".com", exec // Windows executable
    //".command", exec  // Mac script
    ".configprofile",  // Configuration file for Apple systems
    ".cpgz",           // Mac archive
    ".cpi",            // Control Panel Item. Executable used for adding icons
                       // to Control Panel
    //".cpio",
    //".cpl", exec  // Windows executable
    //".crt", exec  // Windows signed certificate
    ".crx",  // Chrome extensions
    ".csh",  // Linux shell
    //".csv",
    ".dart",        // Mac disk image
    ".dc42",        // Apple DiskCopy Image
    ".deb",         // Linux package
    ".definition",  // Automator action
    ".desktop",     // A shortcut that runs other files
    //".der", exec  // Signed certificate
    ".dex",    // Android
    ".dht",    // HTML
    ".dhtm",   // HTML
    ".dhtml",  // HTML
    //".diagcab", exec // Executable windows archive, like .cab
    ".diskcopy42",  // Apple DiskCopy Image
    ".dll",         // Windows executable
    ".dmg",         // Mac disk image
    ".dmgpart",     // Mac disk image
    ".doc",         // MS Office
    ".docb",        // MS Office
    ".docm",        // MS Word
    ".docx",        // MS Word
    ".dot",         // MS Word
    ".dotm",        // MS Word
    ".dott",        // MS Office
    ".dotx",        // MS Word
    ".drv",         // Windows driver
    ".dvdr",        // Mac Disk image
    ".dylib",       // Mach object dynamic library file
    ".efi",         // Firmware
    ".eml",         // MS Outlook
    //".exe", exec // Windows executable
    //".fat",
    //".fileloc", exec  // Apple finder internet location data file
    ".fon",  // Windows font
    //".fxp", exec // MS FoxPro
    ".gadget",  // Windows
    //".gif",
    ".grp",   // Windows
    ".gz",    // Linux archive (gzip)
    ".gzip",  // Linux archive (gzip)
    ".hfs",   // Mac disk image
    //".hlp", exec // Windows Help
    ".hqx",  // Mac archive
    //".hta", exec // HTML trusted application
    ".htm",
    ".html",
    ".htt",  // MS HTML template
    //".ica",
    ".img",      // Mac disk image
    ".imgpart",  // Mac disk image
    //".inf", exec // Windows installer
    //".inetloc", exec  // Apple finder internet location data file
    ".ini",  // Generic config file
    //".ins", exec // IIS config
    ".internetconnect",  // Configuration file for Apple system
    //".inx", // InstallShield
    ".iso",  // CD image
             //".isp", exec // IIS config
             //".isu", // InstallShield
             //".jar", exec // Java
#ifndef MOZ_ESR
//".jnlp", exec // Java
#endif
    //".job", // Windows
    //".jpg",
    //".jpeg",
    //".js", exec  // JavaScript script
    //".jse", exec // JScript
    ".ksh",  // Linux shell
    //".lha",
    //".library-ms", exec // Windows
    //".lnk", exec // Windows
    ".local",  // Windows
    //".lpaq1",
    //".lpaq5",
    //".lpaq8",
    //".lzh",
    //".lzma",
    //".mad", exec  // MS Access
    //".maf", exec  // MS Access
    //".mag", exec  // MS Access
    //".mam", exec  // MS Access
    ".manifest",  // Windows
    //".maq", exec  // MS Access
    //".mar", exec  // MS Access
    //".mas", exec  // MS Access
    //".mat", exec  // MS Access
    //".mau", exec  // Media attachment
    //".mav", exec  // MS Access
    //".maw", exec  // MS Access
    //".mda", exec  // MS Access
    //".mdb", exec  // MS Access
    //".mde", exec  // MS Access
    //".mdt", exec  // MS Access
    //".mdw", exec  // MS Access
    //".mdz", exec  // MS Access
    ".mht",    // MS HTML
    ".mhtml",  // MS HTML
    ".mim",    // MS Mail
    //".mkv",
    ".mmc",           // MS Office
    ".mobileconfig",  // Configuration file for Apple systems
    ".mof",           // Windows
    //".mov",
    //".mp3",
    //".mp4",
    ".mpkg",  // Mac installer
    //".msc", exec  // Windows executable
    ".msg",  // MS Outlook
    //".msh", exec  // Windows shell
    //".msh1", exec // Windows shell
    //".msh1xml", exec  // Windows shell
    //".msh2", exec // Windows shell
    //".msh2xml", exec // Windows shell
    //".mshxml", exec // Windows
    //".msi", exec  // Windows installer
    //".msix", exec // Windows installer
    //".msixbundle", exec // Windows installer
    //".msp", exec  // Windows installer
    //".mst", exec  // Windows installer
    ".ndif",            // Mac disk image
    ".networkconnect",  // Configuration file for Apple systems
    //".ntfs", // 7z
    ".ocx",  // ActiveX
    //".ops", exec  // MS Office
    ".osas",  // AppleScript
    ".osax",  // AppleScript
    //".out", // Linux binary
    ".oxt",  // OpenOffice extension, can execute arbitrary code
    //".package",
    //".paf", // PortableApps package
    //".paq8f",
    //".paq8jd",
    //".paq8l",
    //".paq8o",
    ".partial",  // Downloads
    ".pax",      // Mac archive
    //".pcd", exec     // Microsoft Visual Test
    ".pdf",  // Adobe Acrobat
    //".pea",
    ".pet",  // Linux package
    //".pif", exec // Windows
    ".pkg",  // Mac installer
    ".pl",   // Perl script
    //".plg", exec // MS Visual Studio
    //".png",
    ".pot",   // MS PowerPoint
    ".potm",  // MS PowerPoint
    ".potx",  // MS PowerPoint
    ".ppam",  // MS PowerPoint
    ".pps",   // MS PowerPoint
    ".ppsm",  // MS PowerPoint
    ".ppsx",  // MS PowerPoint
    ".ppt",   // MS PowerPoint
    ".pptm",  // MS PowerPoint
    ".pptx",  // MS PowerPoint
    //".prf", exec // MS Outlook
    //".prg", exec // Windows
    ".ps1",     // Windows shell
    ".ps1xml",  // Windows shell
    ".ps2",     // Windows shell
    ".ps2xml",  // Windows shell
    ".psc1",    // Windows shell
    ".psc2",    // Windows shell
    //".pst", exec // MS Outlook
    ".pup",  // Linux package
    ".py",   // Python script
    ".pyc",  // Python binary
    ".pyd",  // Equivalent of a DLL, for python libraries
    ".pyo",  // Compiled python code
    ".pyw",  // Python GUI
    //".quad",
    //".r00",
    //".r01",
    //".r02",
    //".r03",
    //".r04",
    //".r05",
    //".r06",
    //".r07",
    //".r08",
    //".r09",
    //".r10",
    //".r11",
    //".r12",
    //".r13",
    //".r14",
    //".r15",
    //".r16",
    //".r17",
    //".r18",
    //".r19",
    //".r20",
    //".r21",
    //".r22",
    //".r23",
    //".r24",
    //".r25",
    //".r26",
    //".r27",
    //".r28",
    //".r29",
    //".rar",
    ".rb",  // Ruby script
    //".reg", exec  // Windows Registry
    ".rels",  // MS Office
    //".rgs", // Windows Registry
    ".rpm",  // Linux package
    ".rtf",  // MS Office
    //".run", // Linux shell
    //".scf", exec         // Windows shell
    ".scpt",   // AppleScript
    ".scptd",  // AppleScript
    //".scr", exec         // Windows
    //".sct", exec         // Windows shell
    //".search-ms", exec         // Windows Saved Search
    ".seplugin",  // AppleScript
    ".service",   // Systemd service unit file
    //".settingcontent-ms", exec // Windows settings
    ".sh",    // Linux shell
    ".shar",  // Linux shell
    //".shb", exec         // Windows
    //".shs", exec         // Windows shell
    ".sht",           // HTML
    ".shtm",          // HTML
    ".shtml",         // HTML
    ".sldm",          // MS PowerPoint
    ".sldx",          // MS PowerPoint
    ".slk",           // MS Excel
    ".slp",           // Linux package
    ".smi",           // Mac disk image
    ".sparsebundle",  // Mac disk image
    ".sparseimage",   // Mac disk image
    ".spl",           // Adobe Flash
    //".squashfs",
    ".svg",
    ".swf",   // Adobe Flash
    ".swm",   // Windows Imaging
    ".sys",   // Windows
    ".tar",   // Linux archive
    ".taz",   // Linux archive (bzip2)
    ".tbz",   // Linux archive (bzip2)
    ".tbz2",  // Linux archive (bzip2)
    ".tcsh",  // Linux shell
    //".tif",
    ".tgz",  // Linux archive (gzip)
    //".toast", // Roxio disk image
    ".torrent",  // Bittorrent
    ".tpz",      // Linux archive (gzip)
    //".txt",
    ".txz",  // Linux archive (xz)
    ".tz",   // Linux archive (gzip)
    //".u3p", // U3 Smart Apps
    ".udf",   // MS Excel
    ".udif",  // Mac disk image
    //".url", exec  // Windows
    //".uu",
    //".uue",
    //".vb", exec  // Visual Basic script
    //".vbe", exec // Visual Basic script
    //".vbs", exec // Visual Basic script
    //".vbscript", // Visual Basic script
    //".vdx", exec // MS Visio
    ".vhd",   // Windows virtual hard drive
    ".vhdx",  // Windows virtual hard drive
    ".vmdk",  // VMware virtual disk
    //".vsd", exec  // MS Visio
    //".vsdm", exec // MS Visio
    //".vsdx", exec // MS Visio
    //".vsmacros", exec  // MS Visual Studio
    //".vss",  exec  // MS Visio
    //".vssm", exec  // MS Visio
    //".vssx", exec  // MS Visio
    //".vst",  exec  // MS Visio
    //".vstm", exec  // MS Visio
    //".vstx", exec  // MS Visio
    //".vsw",  exec  // MS Visio
    //".vsx",  exec  // MS Visio
    //".vtx",  exec  // MS Visio
    //".wav",
    //".webloc",  // MacOS website location file
    //".webp",
    ".website",   // Windows
    ".wflow",     // Automator action
    ".wim",       // Windows Imaging
    ".workflow",  // Mac Automator
    //".wrc", // FreeArc archive
    //".ws",  exec  // Windows script
    ".wsb",  // Windows Sandbox configuration
    //".wsc", exec  // Windows script
    //".wsf", exec  // Windows script
    //".wsh", exec  // Windows script
    ".xar",   // MS Excel
    ".xbap",  // XAML Browser Application
    ".xht",
    ".xhtm",
    ".xhtml",
    ".xip",   // Mac archive
    ".xla",   // MS Excel
    ".xlam",  // MS Excel
    ".xldm",  // MS Excel
    //".xll", exec  // MS Excel
    ".xlm",   // MS Excel
    ".xls",   // MS Excel
    ".xlsb",  // MS Excel
    ".xlsm",  // MS Excel
    ".xlsx",  // MS Excel
    ".xlt",   // MS Excel
    ".xltm",  // MS Excel
    ".xltx",  // MS Excel
    ".xlw",   // MS Excel
    ".xml",   // MS Excel
    ".xnk",   // MS Exchange
    //".xrm-ms", exec // Windows
    ".xsd",  // XML schema definition
    ".xsl",  // XML Stylesheet
    //".xxe",
    ".xz",     // Linux archive (xz)
    ".z",      // InstallShield
#ifdef XP_WIN  // disable on Mac/Linux, see 1167493
    ".zip",    // Generic archive
#endif
    ".zipx",  // WinZip
              //".zpaq",
};

static const char* const kMozNonBinaryExecutables[] = {
    ".001", ".7z",   ".ace",  ".arc",   ".arj",    ".b64",      ".balz",
    ".bhx", ".cpio", ".fat",  ".lha",   ".lpaq1",  ".lpaq5",    ".lpaq8",
    ".lzh", ".lzma", ".ntfs", ".paq8f", ".paq8jd", ".paq8l",    ".paq8o",
    ".pea", ".quad", ".r00",  ".r01",   ".r02",    ".r03",      ".r04",
    ".r05", ".r06",  ".r07",  ".r08",   ".r09",    ".r10",      ".r11",
    ".r12", ".r13",  ".r14",  ".r15",   ".r16",    ".r17",      ".r18",
    ".r19", ".r20",  ".r21",  ".r22",   ".r23",    ".r24",      ".r25",
    ".r26", ".r27",  ".r28",  ".r29",   ".rar",    ".squashfs", ".uu",
    ".uue", ".wrc",  ".xxe",  ".zpaq",  ".toast",
};

static const char* const kSafeFileExtensions[] = {
    ".jpg",  ".jpeg", ".mp3",      ".mp4",  ".png",  ".csv",  ".ica",
    ".gif",  ".txt",  ".package",  ".tif",  ".webp", ".mkv",  ".wav",
    ".mov",  ".paf",  ".vbscript", ".ad",   ".inx",  ".isu",  ".job",
    ".rgs",  ".u3p",  ".out",      ".run",  ".bmp",  ".css",  ".ehtml",
    ".flac", ".ico",  ".jfif",     ".m4a",  ".m4v",  ".mpeg", ".mpg",
    ".oga",  ".ogg",  ".ogm",      ".ogv",  ".opus", ".pjp",  ".pjpeg",
    ".svgz", ".text", ".tiff",     ".weba", ".webm", ".xbm",
};

enum class LookupType { AllowlistOnly, BlocklistOnly, BothLists };

// Define the reasons that download protection service accepts or blocks this
// download. This is now used for telemetry purposes and xpcshell test. Please
// also update the xpcshell-test if a reason is added.
//
// LocalWhitelist       : URL is found in the local whitelist
// LocalBlocklist       : URL is found in the local blocklist
// NonBinary            : The downloaded non-binary file is not found in the
// local blocklist VerdictSafe          : Remote lookup reports the download is
// safe VerdictUnknown       : Remote lookup reports unknown, we treat this as a
// safe download VerdictDangerous     : Remote lookup reports the download is
// dangerous VerdictDangerousHost : Remote lookup reports the download is from a
// dangerous host VerdictUnwanted      : Remote lookup reports the download is
// potentially unwatned VerdictUncommon      : Remote lookup reports the
// download is uncommon VerdictUnrecognized  : The verdict type from remote
// lookup is not defined in the csd.proto DangerousPrefOff     : The download is
// dangerous, but the corresponding preference is off DangerousHostPrefOff : The
// download is from a dangerous host, but the corresponding preference is off
// UnwantedPrefOff      : The download is potentially unwanted, but the
// corresponding preference is off UncommonPrefOff      : The download us
// uncommon, but the coressponding preference is off NetworkError         :
// There is an error while requesting remote lookup RemoteLookupDisabled :
// Remote lookup is disabled or the remote lookup URL is empty InternalError :
// An unexpected internal error DPDisabled           : Download protection is
// disabled
using Reason = mozilla::glean::application_reputation::ReasonLabel;

class PendingDBLookup;

// A single use class private to ApplicationReputationService encapsulating an
// nsIApplicationReputationQuery and an nsIApplicationReputationCallback. Once
// created by ApplicationReputationService, it is guaranteed to call mCallback.
// This class is private to ApplicationReputationService.
class PendingLookup final : public nsIStreamListener,
                            public nsITimerCallback,
                            public nsINamed,
                            public nsIObserver,
                            public nsSupportsWeakReference {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIREQUESTOBSERVER
  NS_DECL_NSISTREAMLISTENER
  NS_DECL_NSITIMERCALLBACK
  NS_DECL_NSINAMED
  NS_DECL_NSIOBSERVER

  // Constructor and destructor.
  PendingLookup(nsIApplicationReputationQuery* aQuery,
                nsIApplicationReputationCallback* aCallback);

  // Start the lookup. The lookup may have 2 parts: local and remote. In the
  // local lookup, PendingDBLookups are created to query the local allow and
  // blocklists for various URIs associated with this downloaded file. In the
  // event that no results are found, a remote lookup is sent to the Application
  // Reputation server.
  nsresult StartLookup();

 private:
  ~PendingLookup();

  friend class PendingDBLookup;

  // Telemetry states.
  // Status of the remote response (valid or not).
  enum SERVER_RESPONSE_TYPES {
    SERVER_RESPONSE_VALID = 0,
    SERVER_RESPONSE_FAILED = 1,
    SERVER_RESPONSE_INVALID = 2,
  };

  // The target filename for the downloaded file.
  nsCString mFileName;

  // True if extension of this file matches any extension in the
  // kBinaryFileExtensions or sExecutableExts list.
  bool mIsBinaryFile;

  // Number of blocklist and allowlist hits we have seen.
  uint32_t mBlocklistCount;
  uint32_t mAllowlistCount;

  // The query containing metadata about the downloaded file.
  nsCOMPtr<nsIApplicationReputationQuery> mQuery;

  // The callback with which to report the verdict.
  nsCOMPtr<nsIApplicationReputationCallback> mCallback;

  // An array of strings created from certificate information used to whitelist
  // the downloaded file.
  nsTArray<nsCString> mAllowlistSpecs;
  // The source URI of the download (i.e. final URI after any redirects).
  nsTArray<nsCString> mAnylistSpecs;
  // The referrer and possibly any redirects.
  nsTArray<nsCString> mBlocklistSpecs;

  // When we started this query
  TimeStamp mStartTime;

  // The channel used to talk to the remote lookup server
  nsCOMPtr<nsIChannel> mChannel;

  // Timer to abort this lookup if it takes too long
  nsCOMPtr<nsITimer> mTimeoutTimer;

  // A protocol buffer for storing things we need in the remote request. We
  // store the resource chain (redirect information) as well as signature
  // information extracted using the Windows Authenticode API, if the binary is
  // signed.
  ClientDownloadRequest mRequest;

  // The response from the application reputation query. This is read in chunks
  // as part of our nsIStreamListener implementation and may contain embedded
  // NULLs.
  nsCString mResponse;

  // The clock records the start time of a remote lookup request, used by
  // telemetry.
  PRIntervalTime mTelemetryRemoteRequestStartMs;

  // Returns the type of download binary for the file.
  ClientDownloadRequest::DownloadType GetDownloadType(
      const nsACString& aFilename);

  // Clean up and call the callback. PendingLookup must not be used after this
  // function is called.
  nsresult OnComplete(uint32_t aVerdict, Reason aReason, nsresult aRv);

  // Wrapper function for nsIStreamListener.onStopRequest to make it easy to
  // guarantee calling the callback
  nsresult OnStopRequestInternal(nsIRequest* aRequest, nsresult aResult,
                                 uint32_t& aVerdict, Reason& aReason);

  // Return the hex-encoded hash of the whole URI.
  nsresult GetSpecHash(nsACString& aSpec, nsACString& hexEncodedHash);

  // Strip url parameters, fragments, and user@pass fields from the URI spec
  // using nsIURL. Hash data URIs and return blob URIs unfiltered.
  nsresult GetStrippedSpec(nsIURI* aUri, nsACString& spec);

  // Escape '/' and '%' in certificate attribute values.
  nsCString EscapeCertificateAttribute(const nsACString& aAttribute);

  // Escape ':' in fingerprint values.
  nsCString EscapeFingerprint(const nsACString& aAttribute);

  // Generate whitelist strings for the given certificate pair from the same
  // certificate chain.
  nsresult GenerateWhitelistStringsForPair(nsIX509Cert* certificate,
                                           nsIX509Cert* issuer);

  // Generate whitelist strings for the given certificate chain, which starts
  // with the signer and may go all the way to the root cert.
  nsresult GenerateWhitelistStringsForChain(
      const ClientDownloadRequest_CertificateChain& aChain);

  // For signed binaries, generate strings of the form:
  // http://sb-ssl.google.com/safebrowsing/csd/certificate/
  //   <issuer_cert_sha1_fingerprint>[/CN=<cn>][/O=<org>][/OU=<unit>]
  // for each (cert, issuer) pair in each chain of certificates that is
  // associated with the binary.
  nsresult GenerateWhitelistStrings();

  // Parse the XPCOM certificate lists and stick them into the protocol buffer
  // version.
  nsresult ParseCertificates(
      const nsTArray<nsTArray<nsTArray<uint8_t>>>& aSigArray);

  // Adds the redirects to mBlocklistSpecs to be looked up.
  nsresult AddRedirects(nsIArray* aRedirects);

  // Helper function to ensure that we call PendingLookup::LookupNext or
  // PendingLookup::OnComplete.
  nsresult DoLookupInternal();

  // Looks up all the URIs that may be responsible for allowlisting or
  // blocklisting the downloaded file. These URIs may include whitelist strings
  // generated by certificates verifying the binary as well as the target URI
  // from which the file was downloaded.
  nsresult LookupNext();

  // Sends a query to the remote application reputation service. Returns NS_OK
  // on success.
  nsresult SendRemoteQuery();

  // Helper function to ensure that we always call the callback.
  nsresult SendRemoteQueryInternal(Reason& aReason);
};

// A single-use class for looking up a single URI in the safebrowsing DB. This
// class is private to PendingLookup.
class PendingDBLookup final : public nsIUrlClassifierCallback {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIURLCLASSIFIERCALLBACK

  // Constructor and destructor
  explicit PendingDBLookup(PendingLookup* aPendingLookup);

  // Look up the given URI in the safebrowsing DBs, optionally on both the allow
  // list and the blocklist. If there is a match, call
  // PendingLookup::OnComplete. Otherwise, call PendingLookup::LookupNext.
  nsresult LookupSpec(const nsACString& aSpec, const LookupType& aLookupType);

 private:
  ~PendingDBLookup();

  // The download appeared on the allowlist, blocklist, or no list (and thus
  // could trigger a remote query.
  enum LIST_TYPES {
    ALLOW_LIST = 0,
    BLOCK_LIST = 1,
    NO_LIST = 2,
  };

  nsCString mSpec;
  LookupType mLookupType;
  RefPtr<PendingLookup> mPendingLookup;
  nsresult LookupSpecInternal(const nsACString& aSpec);
};

NS_IMPL_ISUPPORTS(PendingDBLookup, nsIUrlClassifierCallback)

PendingDBLookup::PendingDBLookup(PendingLookup* aPendingLookup)
    : mLookupType(LookupType::BothLists), mPendingLookup(aPendingLookup) {
  LOG(("Created pending DB lookup [this = %p]", this));
}

PendingDBLookup::~PendingDBLookup() {
  LOG(("Destroying pending DB lookup [this = %p]", this));
  mPendingLookup = nullptr;
}

nsresult PendingDBLookup::LookupSpec(const nsACString& aSpec,
                                     const LookupType& aLookupType) {
  LOG(("Checking principal %s [this=%p]", PromiseFlatCString(aSpec).get(),
       this));
  mSpec = aSpec;
  mLookupType = aLookupType;
  nsresult rv = LookupSpecInternal(aSpec);
  if (NS_FAILED(rv)) {
    nsAutoCString errorName;
    mozilla::GetErrorName(rv, errorName);
    LOG(("Error in LookupSpecInternal() [rv = %s, this = %p]", errorName.get(),
         this));
    return mPendingLookup->LookupNext();  // ignore this lookup and move to next
  }
  // LookupSpecInternal has called nsIUrlClassifierCallback.lookup, which is
  // guaranteed to call HandleEvent.
  return rv;
}

nsresult PendingDBLookup::LookupSpecInternal(const nsACString& aSpec) {
  nsresult rv;

  nsCOMPtr<nsIURI> uri;
  nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
  rv = ios->NewURI(aSpec, nullptr, nullptr, getter_AddRefs(uri));
  NS_ENSURE_SUCCESS(rv, rv);

  OriginAttributes attrs;
  nsCOMPtr<nsIPrincipal> principal =
      BasePrincipal::CreateContentPrincipal(uri, attrs);
  if (!principal) {
    return NS_ERROR_FAILURE;
  }

  // Check local lists to see if the URI has already been whitelisted or
  // blacklisted.
  LOG(("Checking DB service for principal %s [this = %p]", mSpec.get(), this));
  nsCOMPtr<nsIUrlClassifierDBService> dbService =
      mozilla::components::UrlClassifierDB::Service(&rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString tables;
  nsAutoCString allowlist;
  Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, allowlist);
  if ((mLookupType != LookupType::BlocklistOnly) && !allowlist.IsEmpty()) {
    tables.Append(allowlist);
  }
  nsAutoCString blocklist;
  Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, blocklist);
  if ((mLookupType != LookupType::AllowlistOnly) && !blocklist.IsEmpty()) {
    if (!tables.IsEmpty()) {
      tables.Append(',');
    }
    tables.Append(blocklist);
  }
  if (principal->IsSystemPrincipal()) {
    return mPendingLookup->LookupNext();
  }
  return dbService->Lookup(principal, tables, this);
}

NS_IMETHODIMP
PendingDBLookup::HandleEvent(const nsACString& tables) {
  // HandleEvent is guaranteed to call either:
  // 1) PendingLookup::OnComplete if the URL matches the blocklist, or
  // 2) PendingLookup::LookupNext if the URL does not match the blocklist.
  // Blocklisting trumps allowlisting.
  nsAutoCString blockList;
  Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, blockList);
  if ((mLookupType != LookupType::AllowlistOnly) && !blockList.IsEmpty() &&
      FindInReadable(blockList, tables)) {
    mPendingLookup->mBlocklistCount++;
    mozilla::glean::application_reputation::local.AccumulateSingleSample(
        BLOCK_LIST);
    LOG(("Found principal %s on blocklist [this = %p]", mSpec.get(), this));
    return mPendingLookup->OnComplete(
        nsIApplicationReputationService::VERDICT_DANGEROUS,
        Reason::eLocalblocklist, NS_OK);
  }

  nsAutoCString allowList;
  Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, allowList);
  if ((mLookupType != LookupType::BlocklistOnly) && !allowList.IsEmpty() &&
      FindInReadable(allowList, tables)) {
    mPendingLookup->mAllowlistCount++;
    mozilla::glean::application_reputation::local.AccumulateSingleSample(
        ALLOW_LIST);
    LOG(("Found principal %s on allowlist [this = %p]", mSpec.get(), this));
    // Don't call onComplete, since blocklisting trumps allowlisting
    return mPendingLookup->LookupNext();
  }

  LOG(("Didn't find principal %s on any list [this = %p]", mSpec.get(), this));
  mozilla::glean::application_reputation::local.AccumulateSingleSample(NO_LIST);
  return mPendingLookup->LookupNext();
}

NS_IMPL_ISUPPORTS(PendingLookup, nsIStreamListener, nsIRequestObserver,
                  nsIObserver, nsISupportsWeakReference, nsITimerCallback,
                  nsINamed)

PendingLookup::PendingLookup(nsIApplicationReputationQuery* aQuery,
                             nsIApplicationReputationCallback* aCallback)
    : mIsBinaryFile(false),
      mBlocklistCount(0),
      mAllowlistCount(0),
      mQuery(aQuery),
      mCallback(aCallback) {
  LOG(("Created pending lookup [this = %p]", this));
}

PendingLookup::~PendingLookup() {
  LOG(("Destroying pending lookup [this = %p]", this));
}

static const char* const kDmgFileExtensions[] = {
    ".cdr",          ".dart",        ".dc42",  ".diskcopy42",
    ".dmg",          ".dmgpart",     ".dvdr",  ".img",
    ".imgpart",      ".iso",         ".ndif",  ".smi",
    ".sparsebundle", ".sparseimage", ".toast", ".udif",
};

static const char* const kRarFileExtensions[] = {
    ".r00", ".r01", ".r02", ".r03", ".r04", ".r05", ".r06", ".r07",
    ".r08", ".r09", ".r10", ".r11", ".r12", ".r13", ".r14", ".r15",
    ".r16", ".r17", ".r18", ".r19", ".r20", ".r21", ".r22", ".r23",
    ".r24", ".r25", ".r26", ".r27", ".r28", ".r29", ".rar",
};

static const char* const kZipFileExtensions[] = {
    ".zip",   // Generic archive
    ".zipx",  // WinZip
};

static const char* GetFileExt(const nsACString& aFilename,
                              const char* const aFileExtensions[],
                              const size_t aLength) {
  for (size_t i = 0; i < aLength; ++i) {
    if (StringEndsWith(aFilename, nsDependentCString(aFileExtensions[i]),
                       nsCaseInsensitiveCStringComparator)) {
      return aFileExtensions[i];
    }
  }
  return nullptr;
}

static const char* GetFileExt(const nsACString& aFilename) {
#define _GetFileExt(_f, _l) GetFileExt(_f, _l, std::size(_l))
  const char* ext = _GetFileExt(
      aFilename, ApplicationReputationService::kBinaryFileExtensions);
  if (ext == nullptr &&
      !_GetFileExt(aFilename,
                   ApplicationReputationService::kNonBinaryExecutables)) {
    ext = _GetFileExt(aFilename, sExecutableExts);
  }
  return ext;
}

// Returns true if the file extension matches one in the given array.
static bool IsFileType(const nsACString& aFilename,
                       const char* const aFileExtensions[],
                       const size_t aLength) {
  return GetFileExt(aFilename, aFileExtensions, aLength) != nullptr;
}

static bool IsBinary(const nsACString& aFilename) {
  return IsFileType(
             aFilename, ApplicationReputationService::kBinaryFileExtensions,
             std::size(ApplicationReputationService::kBinaryFileExtensions)) ||
         (!IsFileType(
              aFilename, ApplicationReputationService::kNonBinaryExecutables,
              std::size(ApplicationReputationService::kNonBinaryExecutables)) &&
          IsFileType(aFilename, sExecutableExts, std::size(sExecutableExts)));
}

ClientDownloadRequest::DownloadType PendingLookup::GetDownloadType(
    const nsACString& aFilename) {
  MOZ_ASSERT(IsBinary(aFilename));

  // Extensions are matched case-insensitively, so compare against a lowercased
  // copy of the filename.
  nsAutoCString fileName(aFilename);
  ToLowerCase(fileName);

  // From
  // https://cs.chromium.org/chromium/src/chrome/common/safe_browsing/download_protection_util.cc?l=17
  if (StringEndsWith(fileName, ".zip"_ns)) {
    return ClientDownloadRequest::ZIPPED_EXECUTABLE;
  } else if (StringEndsWith(fileName, ".apk"_ns)) {
    return ClientDownloadRequest::ANDROID_APK;
  } else if (StringEndsWith(fileName, ".app"_ns) ||
             StringEndsWith(fileName, ".applescript"_ns) ||
             StringEndsWith(fileName, ".cdr"_ns) ||
             StringEndsWith(fileName, ".dart"_ns) ||
             StringEndsWith(fileName, ".dc42"_ns) ||
             StringEndsWith(fileName, ".diskcopy42"_ns) ||
             StringEndsWith(fileName, ".dmg"_ns) ||
             StringEndsWith(fileName, ".dmgpart"_ns) ||
             StringEndsWith(fileName, ".dvdr"_ns) ||
             StringEndsWith(fileName, ".img"_ns) ||
             StringEndsWith(fileName, ".imgpart"_ns) ||
             StringEndsWith(fileName, ".iso"_ns) ||
             StringEndsWith(fileName, ".mpkg"_ns) ||
             StringEndsWith(fileName, ".ndif"_ns) ||
             StringEndsWith(fileName, ".osas"_ns) ||
             StringEndsWith(fileName, ".osax"_ns) ||
             StringEndsWith(fileName, ".pkg"_ns) ||
             StringEndsWith(fileName, ".scpt"_ns) ||
             StringEndsWith(fileName, ".scptd"_ns) ||
             StringEndsWith(fileName, ".seplugin"_ns) ||
             StringEndsWith(fileName, ".smi"_ns) ||
             StringEndsWith(fileName, ".sparsebundle"_ns) ||
             StringEndsWith(fileName, ".sparseimage"_ns) ||
             StringEndsWith(fileName, ".toast"_ns) ||
             StringEndsWith(fileName, ".udif"_ns)) {
    return ClientDownloadRequest::MAC_EXECUTABLE;
  }

  return ClientDownloadRequest::WIN_EXECUTABLE;  // default to Windows binaries
}

nsresult PendingLookup::LookupNext() {
  // We must call LookupNext or SendRemoteQuery upon return.
  // Look up all of the URLs that could allow or block this download.
  // Blocklist first.

  // If a url is in blocklist we should call PendingLookup::OnComplete directly.
  MOZ_ASSERT(mBlocklistCount == 0);

  nsCString spec;
  if (!mAnylistSpecs.IsEmpty()) {
    // Check the source URI only.
    spec = mAnylistSpecs.PopLastElement();
    RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));

    // We don't need to check whitelist if the file is not a binary file.
    auto type =
        mIsBinaryFile ? LookupType::BothLists : LookupType::BlocklistOnly;
    return lookup->LookupSpec(spec, type);
  }

  if (!mBlocklistSpecs.IsEmpty()) {
    // Check the referrer and redirect chain.
    spec = mBlocklistSpecs.PopLastElement();
    RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));
    return lookup->LookupSpec(spec, LookupType::BlocklistOnly);
  }

  // Now that we've looked up all of the URIs against the blocklist,
  // if any of mAnylistSpecs or mAllowlistSpecs matched the allowlist,
  // go ahead and pass.
  if (mAllowlistCount > 0) {
    return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
                      Reason::eLocalwhitelist, NS_OK);
  }

  MOZ_ASSERT_IF(!mIsBinaryFile, mAllowlistSpecs.Length() == 0);

  // Only binary signatures remain.
  if (!mAllowlistSpecs.IsEmpty()) {
    spec = mAllowlistSpecs.PopLastElement();
    LOG(("PendingLookup::LookupNext: checking %s on allowlist", spec.get()));
    RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));
    return lookup->LookupSpec(spec, LookupType::AllowlistOnly);
  }

  if (!mFileName.IsEmpty()) {
    if (IsBinary(mFileName)) {
      mozilla::glean::application_reputation::binary_type
          .EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
                       eBinaryfile)
          .Add();
    } else if (IsFileType(mFileName, kSafeFileExtensions,
                          std::size(kSafeFileExtensions))) {
      mozilla::glean::application_reputation::binary_type
          .EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
                       eNonbinaryfile)
          .Add();
    } else if (IsFileType(mFileName, kMozNonBinaryExecutables,
                          std::size(kMozNonBinaryExecutables))) {
      mozilla::glean::application_reputation::binary_type
          .EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
                       eMoznonbinaryfile)
          .Add();
    } else {
      mozilla::glean::application_reputation::binary_type
          .EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
                       eUnknownfile)
          .Add();
    }
  } else {
    mozilla::glean::application_reputation::binary_type
        .EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
                     eMissingfilename)
        .Add();
  }

  if (IsFileType(mFileName, kDmgFileExtensions,
                 std::size(kDmgFileExtensions))) {
    mozilla::glean::application_reputation::binary_archive
        .EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
                     eDmgfile)
        .Add();
  } else if (IsFileType(mFileName, kRarFileExtensions,
                        std::size(kRarFileExtensions))) {
    mozilla::glean::application_reputation::binary_archive
        .EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
                     eRarfile)
        .Add();
  } else if (IsFileType(mFileName, kZipFileExtensions,
                        std::size(kZipFileExtensions))) {
    mozilla::glean::application_reputation::binary_archive
        .EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
                     eZipfile)
        .Add();
  } else if (mIsBinaryFile) {
    mozilla::glean::application_reputation::binary_archive
        .EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
                     eOtherbinaryfile)
        .Add();
  }

  // There are no more URIs to check against local list. If the file is
  // not eligible for remote lookup, bail.
  if (!mIsBinaryFile) {
    LOG(("Not eligible for remote lookups [this=%p]", this));
    return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
                      Reason::eNonbinaryfile, NS_OK);
  }

  nsresult rv = SendRemoteQuery();
  if (NS_FAILED(rv)) {
    return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
                      Reason::eInternalerror, rv);
  }
  return NS_OK;
}

nsCString PendingLookup::EscapeCertificateAttribute(
    const nsACString& aAttribute) {
  // Escape '/' because it's a field separator, and '%' because Chrome does
  nsCString escaped;
  escaped.SetCapacity(aAttribute.Length());
  for (unsigned int i = 0; i < aAttribute.Length(); ++i) {
    if (aAttribute.Data()[i] == '%') {
      escaped.AppendLiteral("%25");
    } else if (aAttribute.Data()[i] == '/') {
      escaped.AppendLiteral("%2F");
    } else if (aAttribute.Data()[i] == ' ') {
      escaped.AppendLiteral("%20");
    } else {
      escaped.Append(aAttribute.Data()[i]);
    }
  }
  return escaped;
}

nsCString PendingLookup::EscapeFingerprint(const nsACString& aFingerprint) {
  // Google's fingerprint doesn't have colons
  nsCString escaped;
  escaped.SetCapacity(aFingerprint.Length());
  for (unsigned int i = 0; i < aFingerprint.Length(); ++i) {
    if (aFingerprint.Data()[i] != ':') {
      escaped.Append(aFingerprint.Data()[i]);
    }
  }
  return escaped;
}

nsresult PendingLookup::GenerateWhitelistStringsForPair(
    nsIX509Cert* certificate, nsIX509Cert* issuer) {
  // The whitelist paths have format:
  // http://sb-ssl.google.com/safebrowsing/csd/certificate/<issuer_cert_fingerprint>[/CN=<cn>][/O=<org>][/OU=<unit>]
  // Any of CN, O, or OU may be omitted from the whitelist entry. Unfortunately
  // this is not publicly documented, but the Chrome implementation can be found
  // here:
  // https://code.google.com/p/chromium/codesearch#search/&q=GetCertificateWhitelistStrings
  nsCString whitelistString(
      "http://sb-ssl.google.com/safebrowsing/csd/certificate/");

  nsString fingerprint;
  nsresult rv = issuer->GetSha1Fingerprint(fingerprint);
  NS_ENSURE_SUCCESS(rv, rv);
  whitelistString.Append(EscapeFingerprint(NS_ConvertUTF16toUTF8(fingerprint)));

  nsString commonName;
  rv = certificate->GetCommonName(commonName);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!commonName.IsEmpty()) {
    whitelistString.AppendLiteral("/CN=");
    whitelistString.Append(
        EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(commonName)));
  }

  nsString organization;
  rv = certificate->GetOrganization(organization);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!organization.IsEmpty()) {
    whitelistString.AppendLiteral("/O=");
    whitelistString.Append(
        EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(organization)));
  }

  nsString organizationalUnit;
  rv = certificate->GetOrganizationalUnit(organizationalUnit);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!organizationalUnit.IsEmpty()) {
    whitelistString.AppendLiteral("/OU=");
    whitelistString.Append(
        EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(organizationalUnit)));
  }
  LOG(("Whitelisting %s", whitelistString.get()));

  mAllowlistSpecs.AppendElement(whitelistString);
  return NS_OK;
}

nsresult PendingLookup::GenerateWhitelistStringsForChain(
    const safe_browsing::ClientDownloadRequest_CertificateChain& aChain) {
  // We need a signing certificate and an issuer to construct a whitelist
  // entry.
  if (aChain.element_size() < 2) {
    return NS_OK;
  }

  // Get the signer.
  nsresult rv;
  nsCOMPtr<nsIX509CertDB> certDB = do_GetService(NS_X509CERTDB_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIX509Cert> signer;
  nsTArray<uint8_t> signerBytes;
  signerBytes.AppendElements(aChain.element(0).certificate().data(),
                             aChain.element(0).certificate().size());
  rv = certDB->ConstructX509(signerBytes, getter_AddRefs(signer));
  NS_ENSURE_SUCCESS(rv, rv);

  for (int i = 1; i < aChain.element_size(); ++i) {
    // Get the issuer.
    nsCOMPtr<nsIX509Cert> issuer;
    nsTArray<uint8_t> issuerBytes;
    issuerBytes.AppendElements(aChain.element(i).certificate().data(),
                               aChain.element(i).certificate().size());
    rv = certDB->ConstructX509(issuerBytes, getter_AddRefs(issuer));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = GenerateWhitelistStringsForPair(signer, issuer);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

nsresult PendingLookup::GenerateWhitelistStrings() {
  for (int i = 0; i < mRequest.signature().certificate_chain_size(); ++i) {
    nsresult rv = GenerateWhitelistStringsForChain(
        mRequest.signature().certificate_chain(i));
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

nsresult PendingLookup::AddRedirects(nsIArray* aRedirects) {
  uint32_t length = 0;
  aRedirects->GetLength(&length);
  LOG(("ApplicationReputation: Got %u redirects", length));
  nsCOMPtr<nsISimpleEnumerator> iter;
  nsresult rv = aRedirects->Enumerate(getter_AddRefs(iter));
  NS_ENSURE_SUCCESS(rv, rv);

  bool hasMoreRedirects = false;
  rv = iter->HasMoreElements(&hasMoreRedirects);
  NS_ENSURE_SUCCESS(rv, rv);

  while (hasMoreRedirects) {
    nsCOMPtr<nsISupports> supports;
    rv = iter->GetNext(getter_AddRefs(supports));
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIRedirectHistoryEntry> redirectEntry =
        do_QueryInterface(supports, &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIPrincipal> principal;
    rv = redirectEntry->GetPrincipal(getter_AddRefs(principal));
    auto* basePrin = BasePrincipal::Cast(principal);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIURI> uri;
    rv = basePrin->GetURI(getter_AddRefs(uri));
    NS_ENSURE_SUCCESS(rv, rv);

    // Add the spec to our list of local lookups. The most recent redirect is
    // the last element.
    nsCString spec;
    rv = GetStrippedSpec(uri, spec);
    NS_ENSURE_SUCCESS(rv, rv);
    mBlocklistSpecs.AppendElement(spec);
    LOG(("ApplicationReputation: Appending redirect %s\n", spec.get()));

    // Store the redirect information in the remote request.
    ClientDownloadRequest_Resource* resource = mRequest.add_resources();
    resource->set_url(spec.get());
    resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT);

    rv = iter->HasMoreElements(&hasMoreRedirects);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

nsresult PendingLookup::StartLookup() {
  mStartTime = TimeStamp::Now();
  nsresult rv = DoLookupInternal();
  if (NS_FAILED(rv)) {
    return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
                      Reason::eInternalerror, NS_OK);
  }
  return rv;
}

nsresult PendingLookup::GetSpecHash(nsACString& aSpec,
                                    nsACString& hexEncodedHash) {
  nsCOMPtr<nsICryptoHash> cryptoHash;
  nsresult rv =
      NS_NewCryptoHash(nsICryptoHash::SHA256, getter_AddRefs(cryptoHash));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = cryptoHash->Update(
      reinterpret_cast<const uint8_t*>(aSpec.BeginReading()), aSpec.Length());
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString binaryHash;
  rv = cryptoHash->Finish(false, binaryHash);
  NS_ENSURE_SUCCESS(rv, rv);

  // This needs to match HexEncode() in Chrome's
  // src/base/strings/string_number_conversions.cc
  static const char* const hex = "0123456789ABCDEF";
  hexEncodedHash.SetCapacity(2 * binaryHash.Length());
  for (size_t i = 0; i < binaryHash.Length(); ++i) {
    auto c = static_cast<unsigned char>(binaryHash[i]);
    hexEncodedHash.Append(hex[(c >> 4) & 0x0F]);
    hexEncodedHash.Append(hex[c & 0x0F]);
  }

  return NS_OK;
}

nsresult PendingLookup::GetStrippedSpec(nsIURI* aUri, nsACString& escaped) {
  if (NS_WARN_IF(!aUri)) {
    return NS_ERROR_INVALID_ARG;
  }

  nsresult rv;
  rv = aUri->GetScheme(escaped);
  NS_ENSURE_SUCCESS(rv, rv);

  if (escaped.EqualsLiteral("blob")) {
    aUri->GetSpec(escaped);
    LOG(
        ("PendingLookup::GetStrippedSpec(): blob URL left unstripped as '%s' "
         "[this = %p]",
         PromiseFlatCString(escaped).get(), this));
    return NS_OK;
  }

  if (escaped.EqualsLiteral("data")) {
    // Replace URI with "data:<everything before comma>,SHA256(<whole URI>)"
    aUri->GetSpec(escaped);
    int32_t comma = escaped.FindChar(',');
    if (comma > -1 &&
        static_cast<nsCString::size_type>(comma) < escaped.Length() - 1) {
      MOZ_ASSERT(comma > 4, "Data URIs start with 'data:'");
      nsAutoCString hexEncodedHash;
      rv = GetSpecHash(escaped, hexEncodedHash);
      if (NS_SUCCEEDED(rv)) {
        escaped.Truncate(comma + 1);
        escaped.Append(hexEncodedHash);
      }
    }

    LOG(
        ("PendingLookup::GetStrippedSpec(): data URL stripped to '%s' [this = "
         "%p]",
         PromiseFlatCString(escaped).get(), this));
    return NS_OK;
  }

  // If aURI is not an nsIURL, we do not want to check the lists or send a
  // remote query.
  nsCOMPtr<nsIURL> url = do_QueryInterface(aUri, &rv);
  if (NS_FAILED(rv)) {
    LOG(
        ("PendingLookup::GetStrippedSpec(): scheme '%s' is not supported [this "
         "= %p]",
         PromiseFlatCString(escaped).get(), this));
    return rv;
  }

  nsCString temp;
  rv = url->GetHostPort(temp);
  NS_ENSURE_SUCCESS(rv, rv);

  escaped.AppendLiteral("://");
  escaped.Append(temp);

  rv = url->GetFilePath(temp);
  NS_ENSURE_SUCCESS(rv, rv);

  // nsIUrl.filePath starts with '/'
  escaped.Append(temp);

  LOG(("PendingLookup::GetStrippedSpec(): URL stripped to '%s' [this = %p]",
       PromiseFlatCString(escaped).get(), this));
  return NS_OK;
}

nsresult PendingLookup::DoLookupInternal() {
  // We want to check the target URI, its referrer, and associated redirects
  // against the local lists.
  nsCOMPtr<nsIURI> uri;
  nsresult rv = mQuery->GetSourceURI(getter_AddRefs(uri));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCString sourceSpec;
  rv = GetStrippedSpec(uri, sourceSpec);
  NS_ENSURE_SUCCESS(rv, rv);

  mAnylistSpecs.AppendElement(sourceSpec);

  ClientDownloadRequest_Resource* resource = mRequest.add_resources();
  resource->set_url(sourceSpec.get());
  resource->set_type(ClientDownloadRequest::DOWNLOAD_URL);

  nsCOMPtr<nsIReferrerInfo> referrerInfo;
  (void)mQuery->GetReferrerInfo(getter_AddRefs(referrerInfo));
  nsCOMPtr<nsIURI> referrer;
  // It is quite possible that referrer header is omitted due to security reason
  // (for example navigation from https-> http). Hence we should use the
  // original referrer which has not applied referrer policy yet, to make sure
  // we don't mistakenly allow unsafe download.
  if (referrerInfo) {
    referrer = referrerInfo->GetOriginalReferrer();
  }

  if (referrer) {
    nsCString referrerSpec;
    rv = GetStrippedSpec(referrer, referrerSpec);
    NS_ENSURE_SUCCESS(rv, rv);
    mBlocklistSpecs.AppendElement(referrerSpec);
    resource->set_referrer(referrerSpec.get());
  }

  nsCOMPtr<nsIArray> redirects;
  rv = mQuery->GetRedirects(getter_AddRefs(redirects));
  if (redirects) {
    AddRedirects(redirects);
  } else {
    LOG(("ApplicationReputation: Got no redirects [this=%p]", this));
  }

  rv = mQuery->GetSuggestedFileName(mFileName);
  if (NS_SUCCEEDED(rv) && !mFileName.IsEmpty()) {
    mIsBinaryFile = IsBinary(mFileName);
    LOG(("Suggested filename: %s [binary = %d, this = %p]", mFileName.get(),
         mIsBinaryFile, this));
  } else {
    nsAutoCString errorName;
    mozilla::GetErrorName(rv, errorName);
    LOG(("No suggested filename [rv = %s, this = %p]", errorName.get(), this));
    mFileName.Truncate();
  }

  // We can skip parsing certificate for non-binary files because we only
  // check local block list for them.
  if (mIsBinaryFile) {
    nsTArray<nsTArray<nsTArray<uint8_t>>> sigArray;
    rv = mQuery->GetSignatureInfo(sigArray);
    NS_ENSURE_SUCCESS(rv, rv);

    if (!sigArray.IsEmpty()) {
      rv = ParseCertificates(sigArray);
      NS_ENSURE_SUCCESS(rv, rv);
    }

    rv = GenerateWhitelistStrings();
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Start the call chain.
  return LookupNext();
}

nsresult PendingLookup::OnComplete(uint32_t aVerdict, Reason aReason,
                                   nsresult aRv) {
  if (NS_FAILED(aRv)) {
    nsAutoCString errorName;
    mozilla::GetErrorName(aRv, errorName);
    LOG(
        ("Failed sending remote query for application reputation "
         "[rv = %s, this = %p]",
         errorName.get(), this));
  }

  if (mTimeoutTimer) {
    mTimeoutTimer->Cancel();
    mTimeoutTimer = nullptr;
  }

  bool shouldBlock = true;
  switch (aVerdict) {
    case nsIApplicationReputationService::VERDICT_DANGEROUS:
      if (!Preferences::GetBool(PREF_BLOCK_DANGEROUS, true)) {
        shouldBlock = false;
        aReason = Reason::eDangerousprefoff;
      }
      break;
    case nsIApplicationReputationService::VERDICT_UNCOMMON:
      if (!Preferences::GetBool(PREF_BLOCK_UNCOMMON, true)) {
        shouldBlock = false;
        aReason = Reason::eUncommonprefoff;
      }
      break;
    case nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED:
      if (!Preferences::GetBool(PREF_BLOCK_POTENTIALLY_UNWANTED, true)) {
        shouldBlock = false;
        aReason = Reason::eUnwantedprefoff;
      }
      break;
    case nsIApplicationReputationService::VERDICT_DANGEROUS_HOST:
      if (!Preferences::GetBool(PREF_BLOCK_DANGEROUS_HOST, true)) {
        shouldBlock = false;
        aReason = Reason::eDangeroushostprefoff;
      }
      break;
    default:
      shouldBlock = false;
      break;
  }

  mozilla::glean::application_reputation::reason.EnumGet(aReason).Add();
  mozilla::glean::application_reputation::should_block
      .EnumGet(
          static_cast<mozilla::glean::application_reputation::ShouldBlockLabel>(
              shouldBlock))
      .Add();

  double t = (TimeStamp::Now() - mStartTime).ToMilliseconds();
  LOG(("Application Reputation verdict is %u, obtained in %f ms [this = %p]",
       aVerdict, t, this));
  if (shouldBlock) {
    LOG(("Application Reputation check failed, blocking bad binary [this = %p]",
         this));
  } else {
    LOG(("Application Reputation check passed [this = %p]", this));
  }

  nsresult res = mCallback->OnComplete(shouldBlock, aRv, aVerdict);
  return res;
}

nsresult PendingLookup::ParseCertificates(
    const nsTArray<nsTArray<nsTArray<uint8_t>>>& aSigArray) {
  // Binaries may be signed by multiple chains of certificates. If there are no
  // chains, the binary is unsigned (or we were unable to extract signature
  // information on a non-Windows platform)

  // Each chain may have multiple certificates.
  for (const auto& certList : aSigArray) {
    safe_browsing::ClientDownloadRequest_CertificateChain* certChain =
        mRequest.mutable_signature()->add_certificate_chain();
    for (const auto& cert : certList) {
      // Add this certificate to the protobuf to send remotely.
      certChain->add_element()->set_certificate(cert.Elements(), cert.Length());
    }
  }
  if (mRequest.signature().certificate_chain_size() > 0) {
    mRequest.mutable_signature()->set_trusted(true);
  }
  return NS_OK;
}

nsresult PendingLookup::SendRemoteQuery() {
  MOZ_ASSERT(!IsFileType(
      mFileName, ApplicationReputationService::kNonBinaryExecutables,
      std::size(ApplicationReputationService::kNonBinaryExecutables)));
  Reason reason = Reason::eNotset;
  nsresult rv = SendRemoteQueryInternal(reason);
  if (NS_FAILED(rv)) {
    return OnComplete(nsIApplicationReputationService::VERDICT_SAFE, reason,
                      rv);
  }
  // SendRemoteQueryInternal has fired off the query and we call OnComplete in
  // the nsIStreamListener.onStopRequest.
  return rv;
}

nsresult PendingLookup::SendRemoteQueryInternal(Reason& aReason) {
  auto scopeExit = mozilla::MakeScopeExit([&aReason]() {
    if (aReason == Reason::eNotset) {
      aReason = Reason::eInternalerror;
    }
  });

  // If we aren't supposed to do remote lookups, bail.
  if (!Preferences::GetBool(PREF_SB_DOWNLOADS_REMOTE_ENABLED, false)) {
    LOG(("Remote lookups are disabled [this = %p]", this));
    aReason = Reason::eRemotelookupdisabled;
    return NS_ERROR_NOT_AVAILABLE;
  }
  // If the remote lookup URL is empty or absent, bail.
  nsString serviceUrl;
  nsCOMPtr<nsIURLFormatter> formatter(
      do_GetService("@mozilla.org/toolkit/URLFormatterService;1"));
  if (!formatter ||
      NS_FAILED(formatter->FormatURLPref(
          NS_ConvertASCIItoUTF16(PREF_SB_APP_REP_URL), serviceUrl)) ||
      serviceUrl.IsEmpty() || u"about:blank"_ns.Equals(serviceUrl)) {
    LOG(("Remote lookup URL is empty or absent [this = %p]", this));
    aReason = Reason::eRemotelookupdisabled;
    return NS_ERROR_NOT_AVAILABLE;
  }

  LOG(("Sending remote query for application reputation [this = %p]", this));
  // We did not find a local result, so fire off the query to the
  // application reputation service.
  nsCOMPtr<nsIURI> uri;
  nsresult rv;
  rv = mQuery->GetSourceURI(getter_AddRefs(uri));
  NS_ENSURE_SUCCESS(rv, rv);
  nsCString spec;
  rv = GetStrippedSpec(uri, spec);
  NS_ENSURE_SUCCESS(rv, rv);
  mRequest.set_url(spec.get());

  uint32_t fileSize;
  rv = mQuery->GetFileSize(&fileSize);
  NS_ENSURE_SUCCESS(rv, rv);
  mRequest.set_length(fileSize);
  // We have no way of knowing whether or not a user initiated the
  // download. Set it to true to lessen the chance of false positives.
  mRequest.set_user_initiated(true);

  nsCString locale;
  rv = LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale);
  NS_ENSURE_SUCCESS(rv, rv);
  mRequest.set_locale(locale.get());
  nsCString sha256Hash;
  rv = mQuery->GetSha256Hash(sha256Hash);
  NS_ENSURE_SUCCESS(rv, rv);
  mRequest.mutable_digests()->set_sha256(
      std::string(sha256Hash.Data(), sha256Hash.Length()));
  mRequest.set_file_basename(mFileName.get());
  mRequest.set_download_type(GetDownloadType(mFileName));

  if (mRequest.signature().trusted()) {
    LOG(
        ("Got signed binary for remote application reputation check "
         "[this = %p]",
         this));
  } else {
    LOG(
        ("Got unsigned binary for remote application reputation check "
         "[this = %p]",
         this));
  }

  // Serialize the protocol buffer to a string. This can only fail if we are
  // out of memory, or if the protocol buffer req is missing required fields
  // (only the URL for now).
  std::string serialized;
  if (!mRequest.SerializeToString(&serialized)) {
    return NS_ERROR_UNEXPECTED;
  }

  if (LOG_ENABLED()) {
    nsAutoCString serializedStr(serialized.c_str(), serialized.length());
    serializedStr.ReplaceSubstring("\0"_ns, "\\0"_ns);

    LOG(("Serialized protocol buffer [this = %p]: (length=%zd) %s", this,
         serializedStr.Length(), serializedStr.get()));
  }

  // Set the input stream to the serialized protocol buffer
  nsCOMPtr<nsIStringInputStream> sstream =
      do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = sstream->CopyData(serialized.c_str(), serialized.length());
  NS_ENSURE_SUCCESS(rv, rv);

  // Set up the channel to transmit the request to the service.
  nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
  rv = ios->NewChannel(NS_ConvertUTF16toUTF8(serviceUrl), nullptr, nullptr,
                       nullptr,  // aLoadingNode
                       nsContentUtils::GetSystemPrincipal(),
                       nullptr,  // aTriggeringPrincipal
                       nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
                       nsIContentPolicy::TYPE_OTHER, getter_AddRefs(mChannel));
  NS_ENSURE_SUCCESS(rv, rv);

  mChannel->SetLoadFlags(nsIChannel::LOAD_BYPASS_URL_CLASSIFIER);

  nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
  mozilla::OriginAttributes attrs;
  attrs.mFirstPartyDomain.AssignLiteral(NECKO_SAFEBROWSING_FIRST_PARTY_DOMAIN);
  loadInfo->SetOriginAttributes(attrs);

  nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel, &rv));
  NS_ENSURE_SUCCESS(rv, rv);
  (void)httpChannel;

  // Upload the protobuf to the application reputation service.
  nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(mChannel, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = uploadChannel->ExplicitSetUploadStream(
      sstream, "application/octet-stream"_ns, serialized.size(), "POST"_ns);
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t timeoutMs =
      Preferences::GetUint(PREF_SB_DOWNLOADS_REMOTE_TIMEOUT, 10000);
  NS_NewTimerWithCallback(getter_AddRefs(mTimeoutTimer), this, timeoutMs,
                          nsITimer::TYPE_ONE_SHOT);

  mTelemetryRemoteRequestStartMs = PR_IntervalNow();

  rv = mChannel->AsyncOpen(this);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

NS_IMETHODIMP
PendingLookup::Notify(nsITimer* aTimer) {
  LOG(("Remote lookup timed out [this = %p]", this));
  MOZ_ASSERT(aTimer == mTimeoutTimer);
  mozilla::glean::application_reputation::remote_lookup_timeout
      .EnumGet(mozilla::glean::application_reputation::
                   RemoteLookupTimeoutLabel::eTrue)
      .Add();
  mChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL);
  mTimeoutTimer->Cancel();
  return NS_OK;
}

NS_IMETHODIMP
PendingLookup::GetName(nsACString& aName) {
  aName.AssignLiteral("PendingLookup");
  return NS_OK;
}

///////////////////////////////////////////////////////////////////////////////
// nsIObserver implementation
NS_IMETHODIMP
PendingLookup::Observe(nsISupports* aSubject, const char* aTopic,
                       const char16_t* aData) {
  if (!strcmp(aTopic, "quit-application")) {
    if (mTimeoutTimer) {
      mTimeoutTimer->Cancel();
      mTimeoutTimer = nullptr;
    }
    if (mChannel) {
      mChannel->Cancel(NS_ERROR_ABORT);
    }
  }
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// nsIStreamListener
static nsresult AppendSegmentToString(nsIInputStream* inputStream,
                                      void* closure, const char* rawSegment,
                                      uint32_t toOffset, uint32_t count,
                                      uint32_t* writeCount) {
  nsAutoCString* decodedData = static_cast<nsAutoCString*>(closure);
  decodedData->Append(rawSegment, count);
  *writeCount = count;
  return NS_OK;
}

NS_IMETHODIMP
PendingLookup::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
                               uint64_t offset, uint32_t count) {
  uint32_t read;
  return aStream->ReadSegments(AppendSegmentToString, &mResponse, count, &read);
}

NS_IMETHODIMP
PendingLookup::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }

NS_IMETHODIMP
PendingLookup::OnStopRequest(nsIRequest* aRequest, nsresult aResult) {
  NS_ENSURE_STATE(mCallback);

  if (aResult != NS_ERROR_NET_TIMEOUT_EXTERNAL) {
    mozilla::glean::application_reputation::remote_lookup_timeout
        .EnumGet(mozilla::glean::application_reputation::
                     RemoteLookupTimeoutLabel::eFalse)
        .Add();

    MOZ_ASSERT(mTelemetryRemoteRequestStartMs > 0);
    int32_t msecs = PR_IntervalToMilliseconds(PR_IntervalNow() -
                                              mTelemetryRemoteRequestStartMs);

    MOZ_ASSERT(msecs >= 0);
    mozilla::glean::application_reputation::remote_lookup_response_time
        .AccumulateRawDuration(mozilla::TimeDuration::FromMilliseconds(msecs));
  }

  uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE;
  Reason reason = Reason::eNotset;
  nsresult rv = OnStopRequestInternal(aRequest, aResult, verdict, reason);
  OnComplete(verdict, reason, rv);
  return rv;
}

nsresult PendingLookup::OnStopRequestInternal(nsIRequest* aRequest,
                                              nsresult aResult,
                                              uint32_t& aVerdict,
                                              Reason& aReason) {
  auto scopeExit = mozilla::MakeScopeExit([&aReason]() {
    // If |aReason| is not set while exiting, there must be an error.
    if (aReason == Reason::eNotset) {
      aReason = Reason::eNetworkerror;
    }
  });

  if (NS_FAILED(aResult)) {
    mozilla::glean::application_reputation::server.AccumulateSingleSample(
        SERVER_RESPONSE_FAILED);
    mozilla::glean::application_reputation::server_2
        .EnumGet(NSErrorToLabel(aResult))
        .Add();
    return aResult;
  }

  nsresult rv;
  nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
  if (NS_FAILED(rv)) {
    mozilla::glean::application_reputation::server.AccumulateSingleSample(
        SERVER_RESPONSE_FAILED);
    mozilla::glean::application_reputation::server_2
        .EnumGet(mozilla::glean::application_reputation::Server2Label::
                     eFailgetchannel)
        .Add();
    return rv;
  }

  uint32_t status = 0;
  rv = channel->GetResponseStatus(&status);
  if (NS_FAILED(rv)) {
    mozilla::glean::application_reputation::server.AccumulateSingleSample(
        SERVER_RESPONSE_FAILED);
    mozilla::glean::application_reputation::server_2
        .EnumGet(mozilla::glean::application_reputation::Server2Label::
                     eFailgetresponse)
        .Add();
    return rv;
  }

  if (status != 200) {
    mozilla::glean::application_reputation::server.AccumulateSingleSample(
        SERVER_RESPONSE_FAILED);
    mozilla::glean::application_reputation::server_2
        .EnumGet(HTTPStatusToLabel(status))
        .Add();
    return NS_ERROR_NOT_AVAILABLE;
  }

  std::string buf(mResponse.Data(), mResponse.Length());
  safe_browsing::ClientDownloadResponse response;
  if (!response.ParseFromString(buf)) {
    LOG(("Invalid protocol buffer response [this = %p]: %s", this,
         buf.c_str()));
    mozilla::glean::application_reputation::server.AccumulateSingleSample(
        SERVER_RESPONSE_INVALID);
    return NS_ERROR_CANNOT_CONVERT_DATA;
  }

  mozilla::glean::application_reputation::server.AccumulateSingleSample(
      SERVER_RESPONSE_VALID);
  mozilla::glean::application_reputation::server_2
      .EnumGet(
          mozilla::glean::application_reputation::Server2Label::eResponsevalid)
      .Add();

  // Clamp responses 0-7, we only know about 0-4 for now.
  mozilla::glean::application_reputation::server_verdict.AccumulateSingleSample(
      std::min<uint32_t>(response.verdict(), 7));
  const char* ext = GetFileExt(mFileName);
  mozilla::glean::application_reputation::server_verdict_2.Get(
      nsCString(ext),
      VerdictToLabel(std::min<uint32_t>(response.verdict(), 7)));
  switch (response.verdict()) {
    case safe_browsing::ClientDownloadResponse::DANGEROUS:
      aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS;
      aReason = Reason::eVerdictdangerous;
      break;
    case safe_browsing::ClientDownloadResponse::DANGEROUS_HOST:
      aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS_HOST;
      aReason = Reason::eVerdictdangeroushost;
      break;
    case safe_browsing::ClientDownloadResponse::POTENTIALLY_UNWANTED:
      aVerdict = nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED;
      aReason = Reason::eVerdictunwanted;
      break;
    case safe_browsing::ClientDownloadResponse::UNCOMMON:
      aVerdict = nsIApplicationReputationService::VERDICT_UNCOMMON;
      aReason = Reason::eVerdictuncommon;
      break;
    case safe_browsing::ClientDownloadResponse::UNKNOWN:
      aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
      aReason = Reason::eVerdictunknown;
      break;
    case safe_browsing::ClientDownloadResponse::SAFE:
      aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
      aReason = Reason::eVerdictsafe;
      break;
    default:
      // Treat everything else as safe
      aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
      aReason = Reason::eVerdictunrecognized;
      break;
  }

  return NS_OK;
}

NS_IMPL_ISUPPORTS(ApplicationReputationService, nsIApplicationReputationService)

ApplicationReputationService*
    ApplicationReputationService::gApplicationReputationService = nullptr;

already_AddRefed<ApplicationReputationService>
ApplicationReputationService::GetSingleton() {
  if (!gApplicationReputationService) {
    // Note: This is cleared in the new ApplicationReputationService destructor.
    gApplicationReputationService = new ApplicationReputationService();
  }
  return do_AddRef(gApplicationReputationService);
}

ApplicationReputationService::ApplicationReputationService() {
  LOG(("Application reputation service started up"));
}

ApplicationReputationService::~ApplicationReputationService() {
  LOG(("Application reputation service shutting down"));
  MOZ_ASSERT(gApplicationReputationService == this);
  gApplicationReputationService = nullptr;
}

NS_IMETHODIMP
ApplicationReputationService::QueryReputation(
    nsIApplicationReputationQuery* aQuery,
    nsIApplicationReputationCallback* aCallback) {
  LOG(("Starting application reputation check [query=%p]", aQuery));
  NS_ENSURE_ARG_POINTER(aQuery);
  NS_ENSURE_ARG_POINTER(aCallback);

  nsresult rv = QueryReputationInternal(aQuery, aCallback);
  if (NS_FAILED(rv)) {
    Reason reason = rv == NS_ERROR_NOT_AVAILABLE ? Reason::eDpdisabled
                                                 : Reason::eInternalerror;

    mozilla::glean::application_reputation::reason.EnumGet(reason).Add();
    mozilla::glean::application_reputation::should_block
        .EnumGet(
            mozilla::glean::application_reputation::ShouldBlockLabel::eFalse)
        .Add();

    aCallback->OnComplete(false, rv,
                          nsIApplicationReputationService::VERDICT_SAFE);
  }
  return NS_OK;
}

nsresult ApplicationReputationService::QueryReputationInternal(
    nsIApplicationReputationQuery* aQuery,
    nsIApplicationReputationCallback* aCallback) {
  // If malware checks aren't enabled, don't query application reputation.
  if (!Preferences::GetBool(PREF_SB_MALWARE_ENABLED, false)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  if (!Preferences::GetBool(PREF_SB_DOWNLOADS_ENABLED, false)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsCOMPtr<nsIURI> uri;
  nsresult rv = aQuery->GetSourceURI(getter_AddRefs(uri));
  NS_ENSURE_SUCCESS(rv, rv);
  // Bail if the URI hasn't been set.
  NS_ENSURE_STATE(uri);

  // Create a new pending lookup and start the call chain.
  RefPtr<PendingLookup> lookup(new PendingLookup(aQuery, aCallback));

  // Add an observer for shutdown
  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();
  if (!observerService) {
    return NS_ERROR_FAILURE;
  }

  observerService->AddObserver(lookup, "quit-application", true);
  return lookup->StartLookup();
}

nsresult ApplicationReputationService::IsBinary(const nsACString& aFileName,
                                                bool* aBinary) {
  *aBinary = ::IsBinary(aFileName);
  return NS_OK;
}

nsresult ApplicationReputationService::IsExecutable(const nsACString& aFileName,
                                                    bool* aExecutable) {
  *aExecutable =
      ::IsFileType(aFileName, sExecutableExts, std::size(sExecutableExts));
  return NS_OK;
}
