"chrome:" URIs have no host; check for that, and substitute "hohost" instead
[k8imago.git] / code / modules / tamper.js
blob2f43ec5286f93250ec93b09db5bb544814abe7e8
1 /* coded by Ketmar // Invisible Vector (psyc://ketmar.no-ip.org/~Ketmar)
2 * Understanding is not required. Only obedience.
4 * This program is free software. It comes without any warranty, to
5 * the extent permitted by applicable law. You can redistribute it
6 * and/or modify it under the terms of the Do What The Fuck You Want
7 * To Public License, Version 2, as published by Sam Hocevar. See
8 * http://www.wtfpl.net/txt/copying/ for more details.
9 */
10 // traffic tamper
11 var EXPORTED_SYMBOLS = [
12 "ImgDetectListener"
15 let {utils:Cu, classes:Cc, interfaces:Ci, results:Cr} = Components;
18 //////////////////////////////////////////////////////////////////////////////
19 Cu.import("resource://gre/modules/Services.jsm");
21 Cu.import("chrome://k8-imago-code/content/modules/utils.js");
22 Cu.import(MODULE_PATH+"debuglog.js");
23 Cu.import(MODULE_PATH+"prefs.js");
24 Cu.import(MODULE_PATH+"detector.js");
25 Cu.import(MODULE_PATH+"hazard.js");
26 Cu.import(MODULE_PATH+"stoplist.js");
29 //////////////////////////////////////////////////////////////////////////////
30 // this listener will collect first several kb of image, detect it's type and
31 // size, and then will decide what to do with it (image, not size)
32 let tmpid = 0;
35 function ImgDetectListener (isoctet, blacklisted, doSizeBlock) {
36 this.olst = null;
37 this.receivedData = ""; // string, incoming data; will become 0
38 this.cancelled = false;
39 this.piping = false;
40 this.stopSent = false;
41 this.octets = !!isoctet;
42 this.blacklisted = !!blacklisted;
43 this.maxLength = PREFS.maxLength;
44 this.checker = new FormatChecker();
45 this.id = tmpid++;
46 this.imageInfo = null;
47 this.doSizeBlock = !!doSizeBlock;
48 this.rule = null;
49 this.mainURI = null; // URI of the main window
53 ImgDetectListener.prototype = {
54 get debugLogOpt () (PREFS.debugLog),
56 get minWidthOpt () (this.rule ? this.rule.minWidth : PREFS.minWidth),
57 get minHeightOpt () (this.rule ? this.rule.minHeight : PREFS.minHeight),
58 get maxWidthOpt () (this.rule ? this.rule.maxWidth : PREFS.maxWidth),
59 get maxHeightOpt () (this.rule ? this.rule.maxHeight : PREFS.maxHeight),
60 get showPlaceholderOpt () (this.rule ? this.rule.showPlaceholder : PREFS.showPlaceholder),
61 get allowUnknownFormatsOpt () (this.rule ? this.rule.allowUnknownFormats : PREFS.allowUnknownFormats),
62 get maxLengthOpt () (this.rule ? this.rule.maxLength : PREFS.maxLength),
63 get allowFirstPartyImagesOpt () (this.rule ? this.rule.allowFirstPartyImages : PREFS.allowFirstPartyImages),
66 log: function () {
67 if (!this.debugLogOpt) return;
68 if (arguments.length) {
69 let s = ""+this.id+":: ";
70 for (let idx = 0; idx < arguments.length; ++idx) s += arguments[idx];
71 Services.console.logStringMessage(s);
75 logError: function () {
76 if (!this.debugLogOpt) return;
77 if (arguments.length) {
78 let s = ""+this.id+":: ";
79 for (let idx = 0; idx < arguments.length; ++idx) s += arguments[idx];
80 Cu.reportError(s);
84 _markImage: function (request) {
85 let srcchan = request.QueryInterface(Ci.nsIChannel);
86 let url = srcchan.URI.spec;
87 let dw = getDomWindowForChannel(srcchan);
88 if (dw) {
89 //Services.console.logStringMessage("==================== <"+url+">");
90 let title;
91 if (this.imageInfo) {
92 title = this.imageInfo.name+" "+(this.imageInfo.valid ? ""+this.imageInfo.width+"x"+this.imageInfo.height : "<INVALID>");
93 } else {
94 title = (this.blacklisted ? "<BLACKLISTED>" : "<INVALID>");
96 //for (let img of dw.document.querySelectorAll("img:not([k8imago-mark])[src=\""+cssEscape(url)+"\"]")) {
97 for (let img of dw.document.querySelectorAll("img")) {
98 //for (let iidx = 0; iidx < dw.document.images.length; ++iidx) {
99 //let img = dw.document.images[iidx];
100 if (img.src == url) {
101 img.setAttribute("k8imago-saved-title", (img.getAttribute("title")||""));
102 img.setAttribute("k8imago-saved-alt", (img.getAttribute("alt")||""));
103 img.setAttribute("title", title);
104 img.setAttribute("alt", title);
105 img.setAttribute("k8imago-mark", "tan");
106 //if (PREFS.debugLog) conlog("!!! setting title for <"+url+">");
107 //Services.console.logStringMessage("!!! setting title for <"+url+">");
110 else {
111 Services.console.logStringMessage("!!! SKIPPING title for <"+img.src+">");
115 //Services.console.logStringMessage("=======================================================");
118 else {
119 Services.console.logStringMessage("+++***+++ NO DOM WINDOW FOR <"+url+">");
124 // cancel request
125 _cancel: function (request, context, total, data) {
126 if (typeof(total) !== "number") total = "???";
127 // abort this request
128 this.cancelled = true;
129 this.receivedData = null; // just in case
130 this.checker = null;
131 this._markImage(request);
132 if (this.debugLogOpt) {
133 let srcchan = request.QueryInterface(Ci.nsIChannel);
134 let url = srcchan.URI.spec;
135 this.log("[", url, "] total: ", total, ": CANCELLING!");
137 if (typeof(data) === "string") {
138 if (this.showPlaceholderOpt) {
139 this.log(" sending HAZARD");
140 this.olst.onDataAvailable(request, context, createInputStreamFromString(data), 0, data.length);
141 } else {
142 this.log(" sending HAZARD is blocked");
144 this.stopSent = true;
145 this.olst.onStopRequest(request, context, 0);
147 // just in case that double cancel will throw
148 try {
149 request.cancel(Cr.NS_BINDING_ABORTED);
150 } catch (e) {}
153 _pipe: function (request, context) {
154 this.piping = true;
155 this.receivedData = null;
156 this.checker = null;
159 url: function (request) {
160 if (!request) return "<no-url>";
161 let srcchan = request.QueryInterface(Ci.nsIChannel);
162 if (!srcchan) return "<no-url>";
163 return srcchan.URI.spec;
166 // object initialization finished
167 inited: function () {
170 QueryInterface: function (aIID) {
171 if (aIID.equals(Ci.nsIStreamListener) || aIID.equals(Ci.nsISupports)) return this;
172 throw Components.results.NS_NOINTERFACE;
175 onDataAvailable: function (request, context, inputStream, offset, count) {
176 if (this.cancelled) return;
178 if (!this.piping && this.debugLogOpt) this.log("[", this.url(request), "] offset=", offset, "; count=", count);
180 // if we aren't piping...
181 if (!this.piping) {
182 // ...and there's no more formats...
183 if (this.checker.done) {
184 // abort or pipe
185 if (!this.allowUnknownFormatsOpt) {
186 this._cancel(request, context, offset+count, hazardPNG);
187 return;
189 // send collected data
190 if (this.receivedData) {
191 if (this.receivedData.length) {
192 if (offset == 0) this.logError("WTF000?! offset is zero, but we have receivedData!");
193 this.olst.onDataAvailable(request, context, createInputStreamFromString(this.receivedData), 0, this.receivedData.length);
194 if (offset != this.receivedData.length) this.logError("WTF001?! offset is not equal to receivedData length! offset=", offset, "; length=", this.receivedData.length);
196 } else if (offset > 0) {
197 this.logError("WTF002?! offset is not zero, but we have no receivedData!");
199 this._pipe(request, context);
200 // go on, do piping
204 // pipe data if we are piping
205 if (this.piping) {
206 // check if we hit the limit
207 if (this.doSizeBlock) {
208 let srcchan = request.QueryInterface(Ci.nsIChannel);
209 if (this.maxTotalBytesForDocOpt > 0 && !this.octets) {
210 let dw = getDomWindowForChannel(srcchan);
211 let isize = getByteCounterForDomWindow(dw)+count;
212 if (isize >= this.maxTotalBytesForDocOpt) {
213 if (this.debugLogOpt) conlog("*** [", httpChannel.URI.spec, "]: blocked due to bytes limit: isize="+isize+"; max="+this.maxTotalBytesForDocOpt+"; count="+count);
214 this._cancel(request, context, offset+count);
215 return;
217 setByteCounterForDomWindow(dw, isize);
220 // piping data
221 if (this.maxLength > 0 && !this.octets) {
222 if (offset+count > this.maxLength) {
223 this._cancel(request, context, offset+count);
224 return;
227 this.olst.onDataAvailable(request, context, inputStream, offset, count);
228 return;
231 // collecting header data and analyzing image formats
232 let ist = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
233 ist.setInputStream(inputStream);
235 // read up to `this.checker.maxHeaderBytes` kb
236 let maxb = this.checker.maxHeaderBytes;
237 if (this.receivedData.length+count < maxb) {
238 this.log(" reading ", count, " bytes (maxb=", maxb, ")");
239 this.receivedData += ist.readBytes(count);
240 count = 0;
241 } else {
242 let left = maxb-this.receivedData.length;
243 if (left > count) {
244 Cu.reportError("internal error (something is heavily fucked, #000)");
245 throw new Error("internal error (something is heavily fucked, #000)");
247 if (left > 0) {
248 this.receivedData += ist.readBytes(left);
249 count -= left;
251 this.log(" read ", left, " bytes, ", count, " bytes left");
254 let res = this.checker.process(this.receivedData);
255 this.log(" checker returned `", res, "`, done is: ", this.checker.done);
256 if (res === true) {
257 // no successfull detection yet, need more spice
258 if (this.receivedData.length >= maxb) this._cancel(request, context, offset+count, hazardPNG);
259 return;
261 if (res !== null) {
262 // detected something
263 this.imageInfo = res;
264 if (!res.valid) {
265 this._cancel(request, context, offset+count, hazardPNG);
266 return;
268 this.log(" image: ", res.name, ": ", res.width, "x", res.height);
269 // valid image, check dimensions
270 if (res.width < this.minWidthOpt || res.height < this.minHeightOpt ||
271 res.width > this.maxWidthOpt || res.height > this.maxHeightOpt)
273 this.log(" allowed size: min=", this.minWidthOpt, "x", this.minHeightOpt, "; max=", this.maxWidthOpt, "x", this.maxHeightOpt);
274 this._cancel(request, context, offset+count, hazardPNG);
275 return;
277 } else {
278 // no more formats
279 if (!this.allowUnknownFormatsOpt) {
280 this.log(" unknown format");
281 this._cancel(request, context, offset+count, hazardPNG);
282 return;
286 // here we got something good, transfer collected header and start piping
287 this.olst.onDataAvailable(request, context, createInputStreamFromString(this.receivedData), 0, this.receivedData.length);
288 // pass what is left
289 if (count > 0) {
290 offset += this.receivedData.length;
291 this.olst.onDataAvailable(request, context, inputStream, offset, count);
293 this._pipe(request, context);
296 onStartRequest: function (request, context) {
297 if (this.debugLogOpt) this.log("onStartRequest() [", this.url(request), "]");
298 this.olst.onStartRequest(request, context);
299 if (this.blacklisted) {
300 this.log("image is blacklisted");
301 this._cancel(request, context, 0, hazardPNG);
302 return;
306 onStopRequest: function (request, context, statusCode) {
307 if (this.debugLogOpt) this.log("onStopRequest() [", this.url(request), "]: status code=", statusCode);
308 //this.log("[", this.url(request), "]: status code=", statusCode);
309 if (!this.stopSent && this.receivedData !== null) {
310 // wow, we have some buffered data, check and send it!
311 let res = this.checker.process(this.receivedData);
312 if (typeof(res) === "object") {
313 if (!res.valid) {
314 this._cancel(request, context, 0, hazardPNG);
315 return;
317 if (res.width < this.minWidthOpt || res.height < this.minHeightOpt ||
318 res.width > this.maxWidthOpt || res.height > this.maxHeightOpt) {
319 this._cancel(request, context, 0, hazardPNG);
320 return;
322 // ok
323 } else {
324 if (!this.allowUnknownFormatsOpt) {
325 this._cancel(request, context, 0, hazardPNG);
326 return;
329 this.olst.onDataAvailable(request, context, createInputStreamFromString(this.receivedData), 0, this.receivedData.length);
331 this.receivedData = null; // just in case
332 this.checker = null;
333 if (!this.stopSent) {
334 this.olst.onStopRequest(request, context, statusCode);