summaryrefslogtreecommitdiffstats
path: root/toolkit/components/url-classifier/UrlClassifierRemoteSettingsService.sys.mjs
blob: 42f8cf272a098275056a24d1ffaf9abe76495c15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/* 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/. */

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
});

const COLLECTION_NAME = "tracking-protection-lists";

// SafeBrowsing protocol parameters.
export const SBRS_UPDATE_MINIMUM_DELAY = 21600; // Minimum delay before polling again in seconds

export function UrlClassifierRemoteSettingsService() {}
UrlClassifierRemoteSettingsService.prototype = {
  classID: Components.ID("{1980624c-c50b-4b46-a91c-dfaba7792706}"),
  QueryInterface: ChromeUtils.generateQI([
    "nsIUrlClassifierRemoteSettingsService",
  ]),

  _initialized: false,

  // Entries that are retrieved from RemoteSettings.get(). keyed by the table name.
  _entries: {},

  async lazyInit() {
    if (this._initialized) {
      return;
    }

    let rs = lazy.RemoteSettings(COLLECTION_NAME);
    // Bug 1750191: Notify listmanager to trigger an update instead
    // of polling data periodically.
    rs.on("sync", async event => {
      let {
        data: { current },
      } = event;
      this._onUpdateEntries(current);
    });

    this._initialized = true;

    let entries;
    try {
      entries = await rs.get();
    } catch (e) {}

    this._onUpdateEntries(entries || []);
  },

  _onUpdateEntries(aEntries) {
    aEntries.map(entry => {
      this._entries[entry.Name] = entry;
    });
  },

  // Parse the update request. See UrlClassifierListManager.jsm makeUpdateRequest
  // for more details about how we build the update request.
  //
  // @param aRequest the request payload of the update request
  // @return array The array of requested tables. Each item in the array is composed
  //               with [table name, chunk numner]
  _parseRequest(aRequest) {
    let lines = aRequest.split("\n");
    let requests = [];
    for (let line of lines) {
      let fields = line.split(";");
      let chunkNum = fields[1]?.match(/(?<=a:).*/);
      requests.push([fields[0], chunkNum]);
    }
    return requests;
  },

  async _getLists(aRequest, aListener) {
    await this.lazyInit();

    let rs = lazy.RemoteSettings(COLLECTION_NAME);
    let payload = "n:" + SBRS_UPDATE_MINIMUM_DELAY + "\n";

    let requests = this._parseRequest(aRequest);
    for (let request of requests) {
      let [reqTableName, reqChunkNum] = request;
      let entry = this._entries[reqTableName];
      if (!entry?.attachment) {
        continue;
      }

      // If the request version is the same as what we have in Remote Settings,
      // we are up-to-date now.
      if (entry.Version == reqChunkNum) {
        continue;
      }

      let downloadError = false;
      try {
        // SafeBrowsing maintains its own files, so we can remove the downloaded
        // files after SafeBrowsing processes the data.
        let buffer = await rs.attachments.downloadAsBytes(entry);
        let bytes = new Uint8Array(buffer);
        let strData = "";
        for (let i = 0; i < bytes.length; i++) {
          strData += String.fromCharCode(bytes[i]);
        }

        // Construct the payload
        payload += "i:" + reqTableName + "\n";
        payload += strData;
      } catch (e) {
        downloadError = true;
      }

      if (downloadError) {
        try {
          aListener.onStartRequest(null);
          aListener.onStopRequest(null, Cr.NS_ERROR_FAILURE);
        } catch (e) {}
        return;
      }
    }

    // Send the update response over stream listener interface
    let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
      Ci.nsIStringInputStream
    );
    stream.setData(payload, payload.length);

    aListener.onStartRequest(null);
    aListener.onDataAvailable(null, stream, 0, payload.length);
    aListener.onStopRequest(null, Cr.NS_OK);
  },

  fetchList(aPayload, aListener) {
    this._getLists(aPayload, aListener);
  },

  clear() {
    this._initialized = false;
    this._entries = {};
  },
};