File: capture.js

package info (click to toggle)
lightbeam 3.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 2,344 kB
  • sloc: makefile: 18; sh: 6
file content (203 lines) | stat: -rw-r--r-- 5,679 bytes parent folder | download
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/*
 * Listens for HTTP request responses, sending first- and
 * third-party requests to storage.
 */
const capture = {
  init() {
    this.addListeners();
  },

  addListeners() {
    // listen for each HTTP response
    this.queue = [];
    browser.webRequest.onResponseStarted.addListener(
      response => {
        const eventDetails = {
          type: 'sendThirdParty',
          data: response
        };
        this.queue.push(eventDetails);
        this.processNextEvent();
      },
      { urls: ['<all_urls>'] }
    );
    // listen for tab updates
    browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
      const eventDetails = {
        type: 'sendFirstParty',
        data: {
          tabId,
          changeInfo,
          tab
        }
      };
      this.queue.push(eventDetails);
      this.processNextEvent();
    });
  },

  // Process each HTTP request or tab page load in order,
  // so that async reads/writes to IndexedDB
  // (via sendFirstParty and sendThirdParty) won't miss data
  // The 'ignore' boolean ensures processNextEvent is only
  // executed when the previous call to processNextEvent
  // has completed.
  async processNextEvent(ignore = false) {
    if (this.processingQueue && !ignore) {
      return;
    }
    if (this.queue.length >= 1) {
      try {
        const nextEvent = this.queue.shift();
        this.processingQueue = true;
        switch (nextEvent.type) {
          case 'sendFirstParty':
            await this.sendFirstParty(
              nextEvent.data.tabId,
              nextEvent.data.changeInfo,
              nextEvent.data.tab
            );
            break;
          case 'sendThirdParty':
            await this.sendThirdParty(nextEvent.data);
            break;
          default:
            throw new Error(
              'An event must be of type sendFirstParty or sendThirdParty.'
            );
        }
      } catch (e) {
        // eslint-disable-next-line no-console
        console.warn('Exception found in queue process', e);
      }
      this.processNextEvent(true);
    } else {
      this.processingQueue = false;
    }
  },

  // Returns true if the request should be stored, otherwise false.
  // info could be a tab (from setFirstParty) or a
  // response (from setThirdParty) object
  async shouldStore(info) {
    const tabId = info.id || info.tabId;
    let documentUrl, privateBrowsing;
    // Ignore container tabs as we need to store them correctly
    //  showing a simpler graph just for default means we won't confuse users
    //  into thinking isolation has broken
    const defaultCookieStore = 'firefox-default';
    if ('cookieStoreId' in info && info.cookieStoreId !== defaultCookieStore) {
      return false;
    }
    if (this.isVisibleTab(tabId)) {
      const tab = await this.getTab(tabId);
      if (!tab) {
        return;
      }
      if (tab.cookieStoreId !== defaultCookieStore) {
        return false;
      }
      documentUrl = new URL(tab.url);
      privateBrowsing = tab.incognito;
    } else {
      // if we were not able to check the cookie store
      // lets drop this for paranoia sake.
      if (!('cookieStoreId' in info)) {
        return false;
      }
      // browser.tabs.get throws an error for nonvisible tabs (tabId = -1)
      // but some non-visible tabs can make third party requests,
      // ex: Service Workers
      documentUrl = new URL(info.originUrl);
      privateBrowsing = false;
    }

    // ignore about:*, moz-extension:*
    // also ignore private browsing tabs
    if (
      documentUrl.protocol !== 'about:' &&
      documentUrl.protocol !== 'moz-extension:' &&
      !privateBrowsing
    ) {
      return true;
    }
    return false;
  },

  isVisibleTab(tabId) {
    return tabId !== browser.tabs.TAB_ID_NONE;
  },

  async getTab(tabId) {
    let tab;
    try {
      tab = await browser.tabs.get(tabId);
    } catch (e) {
      // Lets ignore tabs we can't get hold of (likely have closed)
      return;
    }
    return tab;
  },

  // capture third party requests
  async sendThirdParty(response) {
    // console.log('response', response);
    if (!response.originUrl) {
      // originUrl is undefined for the first request from the browser to the
      // first party site
      return;
    }

    // @todo figure out why Web Extensions sometimes gives
    // undefined for response.originUrl
    const originUrl = response.originUrl ? new URL(response.originUrl) : '';
    const targetUrl = new URL(response.url);
    let firstPartyUrl;
    if (this.isVisibleTab(response.tabId)) {
      const tab = await this.getTab(response.tabId);
      if (!tab) {
        return;
      }
      firstPartyUrl = new URL(tab.url);
    } else {
      firstPartyUrl = new URL(response.originUrl);
    }

    if (
      firstPartyUrl.hostname &&
      targetUrl.hostname !== firstPartyUrl.hostname &&
      (await this.shouldStore(response))
    ) {
      const data = {
        target: targetUrl.hostname,
        origin: originUrl.hostname,
        requestTime: response.timeStamp,
        firstParty: false
      };
      await store.setThirdParty(
        firstPartyUrl.hostname,
        targetUrl.hostname,
        data
      );
    }
  },

  // capture first party requests
  async sendFirstParty(tabId, changeInfo, tab) {
    const documentUrl = new URL(tab.url);
    if (
      documentUrl.hostname &&
      tab.status === 'complete' &&
      (await this.shouldStore(tab))
    ) {
      const data = {
        faviconUrl: tab.favIconUrl,
        firstParty: true,
        requestTime: Date.now()
      };
      await store.setFirstParty(documentUrl.hostname, data);
    }
  }
};

capture.init();