Chrome extension doing a download can't always specify file extension? - google-chrome-extension

I'm trying to make a Chrome extension that thru chrome.downloads.download can initiate a download and specify the filename of the resulting local file -- in particular, specifying the file extension of the downloaded result.
If the server advertises a content-type of application/octet-stream for this file, this works fine.
However if the server advertises some other content-type (for example application/zip), then the download object is set to have that MIME type, and the downloaded file name is forced to have a related extension (for example ".zip") instead of the one I specified.
I've tried using onHeadersReceived to change the content-type in the incoming headers, forcing it to application/octet-stream, but the download object still ends up having the original MIME type and the file extension is still forced.
Using chrome.downloads.onDeterminingFilename to "suggest" my desired filename+extension also does not help prevent the forced extension. I've disabled all other extensions and also checked to see if onActionIgnored ever fires (it does not). I finally added logging for all other webRequest callbacks to see if there's any other behavior that could explain the difference between the successful and failed cases, but this content-type/mimetype issue seems to be the culprit.
This is with an unpacked extension loaded into Chrome 84.0.4147.89 on Linux (elementary OS, an Ubuntu variant). I'll get into more details about the extension code below, but I'm thinking there's probably just something about the flow of how download objects are created which either a) makes doing this impossible or b) means that I need to do it in some other way.
Thanks for any help!
Here's the manifest of my test extension:
{
"name": "Test DL Rename",
"version": "1.0",
"manifest_version": 2,
"description": "Add a file extension to the name of a download.",
"background": {
"scripts": ["background.js"],
"persistent": true
},
"permissions": [
"contextMenus",
"downloads",
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
}
Here's the background.js with all the debugging chattiness stripped out:
const CONTEXT_MENU_ID = "TEST_DL_RENAME";
// add our thing to the context menu for links
chrome.contextMenus.create(
{
id: CONTEXT_MENU_ID,
title: "Test DL Rename",
contexts: ["link"]
}
);
// test download-renaming by adding ".qz" extension to downloaded file
chrome.contextMenus.onClicked.addListener(
function(info, tab) {
if (info.menuItemId !== CONTEXT_MENU_ID) {
return;
}
filename = info.linkUrl.substring(info.linkUrl.lastIndexOf('/') + 1);
qzFilename = filename + ".qz";
console.log("specifying name for download: " + qzFilename);
chrome.downloads.download(
{
url: info.linkUrl,
filename: qzFilename,
conflictAction: "uniquify"
},
function(downloadId) {
console.log("started download ID " + downloadId);
}
);
}
);
// test setting content-type on a received download
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
// if doing this for real, we'd track which URLs we actually want to change
// for now just change anything that is a zipfile
if (details.url.split('.').pop() != "zip") {
return {};
}
for (var i = 0; i < details.responseHeaders.length; i++) {
if (details.responseHeaders[i].name.toLowerCase() == "content-type"){
console.log("forcing content-type to application/octet-stream");
details.responseHeaders[i].value = "application/octet-stream";
break;
}
}
return {
responseHeaders: details.responseHeaders
};
},
{
urls: ["<all_urls>"]
},
["blocking", "responseHeaders"]
);
And these are the files I'm using to run my tests currently. If I right-click on "test with octet-stream MIME type" and choosing "Test DL Rename", that results in a download named "test.zip.qz" as desired. Right-clicking on "test with zip MIME type" results in "test-mime.zip.zip" rather than "test-mime.zip.qz".
test with octet-stream MIME type
test with zip MIME type

Looks like an intended restriction in Chrome's handling of downloads to ensure "safety", which you can contest on https://crbug.com by advocating your use case.
Meanwhile, download the blob yourself and change its type:
chrome.contextMenus.onClicked.addListener(async ({linkUrl: url}) => {
const blob = await (await fetch(url)).blob();
const typedBlob = blob.type === 'application/octet-stream' ? blob :
new Blob([blob], {type: 'application/octet-stream'});
chrome.downloads.download({
url: URL.createObjectURL(typedBlob),
filename: url.substring(url.lastIndexOf('/') + 1) + '.qz',
conflictAction: 'uniquify',
});
});
P.S. Now that you don't need webRequest API you can use "persistent": false in manifest.json (FWIW there's a way though to use both at the same time by putting webRequest into optional_permissions, see the documentation).

Related

Downloading a large Blob to local file in ManifestV3 service worker

I have a logging mechanism in place that saves the logs into an array. And I need a way to download the logs into a file.
I had this previously working (on manifest v2) with
const url = URL.createObjectURL(new Blob(reallyLongString, { type: 'text/plain' }));
const filename = 'logs.txt';
chrome.downloads.download({url, filename});
Now I am migrating to manifest v3 and since manifest v3 does not have URL.createObjectURL, you cannot create a url to pass to chrome.downloads.download
Instead it is possible to create a Blob URL using something like
const url = `data:text/plain,${reallyLongString}`;
const filename = 'logs.txt';
chrome.downloads.download({url, filename});
The problem is that chrome.downloads.download seems to have a limit on the number of characters passed in the url argument, and the downloaded file only contains a small part of the string.
So what would be a way to overcome this limitation?
Hopefully, a way to download Blob directly in service worker will be implemented in https://crbug.com/1224027.
Workaround via an extension page
Here's the algorithm:
Use an already opened page such as popup or options
Otherwise, inject an iframe into any page that we have access to
Otherwise, open a new minimized window
async function downloadBlob(blob, name, destroyBlob = true) {
// When `destroyBlob` parameter is true, the blob is transferred instantly,
// but it's unusable in SW afterwards, which is fine as we made it only to download
const send = async (dst, close) => {
dst.postMessage({blob, name, close}, destroyBlob ? [await blob.arrayBuffer()] : []);
};
// try an existing page/frame
const [client] = await self.clients.matchAll({type: 'window'});
if (client) return send(client);
const WAR = chrome.runtime.getManifest().web_accessible_resources;
const tab = WAR?.some(r => r.resources?.includes('downloader.html'))
&& (await chrome.tabs.query({url: '*://*/*'})).find(t => t.url);
if (tab) {
chrome.scripting.executeScript({
target: {tabId: tab.id},
func: () => {
const iframe = document.createElement('iframe');
iframe.src = chrome.runtime.getURL('downloader.html');
iframe.style.cssText = 'display:none!important';
document.body.appendChild(iframe);
}
});
} else {
chrome.windows.create({url: 'downloader.html', state: 'minimized'});
}
self.addEventListener('message', function onMsg(e) {
if (e.data === 'sendBlob') {
self.removeEventListener('message', onMsg);
send(e.source, !tab);
}
});
}
downloader.html:
<script src=downloader.js></script>
downloader.js, popup.js, options.js, and other scripts for extension pages (not content scripts):
navigator.serviceWorker.ready.then(swr => swr.active.postMessage('sendBlob'));
navigator.serviceWorker.onmessage = async e => {
if (e.data.blob) {
await chrome.downloads.download({
url: URL.createObjectURL(e.data.blob),
filename: e.data.name,
});
}
if (e.data.close) {
window.close();
}
}
manifest.json:
"web_accessible_resources": [{
"matches": ["<all_urls>"],
"resources": ["downloader.html"],
"use_dynamic_url": true
}]
Warning! Since "use_dynamic_url": true is not yet implemented don't add web_accessible_resources if you don't want to make your extension detectable by web pages.
Workaround via Offscreen document
Soon there'll be another workaround: chrome.offscreen.createDocument instead of chrome.windows.create to start an invisible DOM page where we can call URL.createObjectURL, pass the result back to SW that will use it for chrome.downloads.download.

How can I modify response headers using webRequest.onHeadersRecieved?

I'm writing an extension that gives a Chrome (and Firefox) user the ability to selectively modify response headers. Mainly it's for QA people to test that the front-end handles error modes properly (e.g. if a given route returns a 500, then the correct dialog displays).
This implementation uses the Chrome webRequest API.I know that the extension is loading, and I can block requests with listener1. However, listener2 does not behave as expected, as it neither modifies the status code nor does it add the josh header. I have verified this in Chrome's network monitor tab. Both filters work, as far as it goes (I was testing with an explicit https filter because I know the webRequest API doesn't work with file:// urls.)
As part of my troubleshooting process, I've disabled all other Chrome extensions, so I'm pretty sure nothing else is modifying headers.
let count = 0;
function listener1(details){
return {cancel: true};
}
function listener2(details){
console.log(count++, details);
if (!details) return;
for (let i = 0; i < details.responseHeaders.length; i++){
if (details.responseHeaders[i].name === 'status'){
details.responseHeaders[i].value = '500';
break;
}
}
details.responseHeaders.push({name: 'josh', value: 'count: ' + count});
// Response is of type "Blocking Response"
return {responseHeaders: details.responseHeaders};
}
const filter1 = '<all_urls>';
const filter2 = 'https://www.google.com/'
chrome.webRequest.onHeadersReceived.addListener(listener2, { urls: [filter2] }, ['blocking', 'responseHeaders']);
Here is my manifest:
{
"name": "Server Error Simulation Extension",
"description" : "Base Level Extension",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_popup": "popup.html",
"default_icon": "logo.png"
},
"background": {
"persistent": true,
"scripts": ["background.js"]
},
"permissions": [
"activeTab",
"storage",
"webRequest",
"webRequestBlocking",
"*://*.com/"
]
}
Note that I'm only wanting to modify headers, so this Chrome bug about modifying response bodies doesn't apply. Also, Firefox copied this API from Chrome so I've tagged Firefox as well.
Partial answer: HTTP status is not a header, according to the RFC 2616 which defines HTTP 1.1. Status is it's own thing, called a Status-Line. And it does look different, textually, since it doesn't have a key: prefix unlike everything else in the header.
As for why my custom header doesn't show up in network monitor, I'm not sure. Maybe it's a bug in the devtools, or something else.
In any event, whether or not its officially a header, the Chrome webRequest API exposes it as a "status" header, and I believe it should support modification in a way consistent with other headers.

chrome-extension: How to detect and retrieve url from tab when redirect happens?

Just started on chrome - extension development. I'm trying to retrieve the url from a newly created tab but my expected url comes from the server as response with 302 status code and browser redirects to it. I tried with chrome.tabs.onUpdated.addListener() but its not detecting the URL with 302. I searched in chrome.tabs API and i don't see any events for redirect. What is the best way to detect if any redirection is happening in the tab and get the url from that object?
My use case is, when user clicks on one button, it opens a new tab with a specific url and since this url is SSO protected which will go through SSO dance and finally goes to target page.During SSO dance there are 3 redirects happening before serving the target page and i'm trying to detect and retrieve one of the url in the redirect process.
P.S: This is my first question and i'm excited to be part of this community. Thanks in advance.
Update1: I tried with chrome.webRequest.onBeforeRedirect.addListener
manifest.json
{
"name": "CatBlock",
"version": "1.0",
"description": "I can't has cheezburger!",
"permissions": ["alarms", "webRequest", "webRequestBlocking", "activeTab", "tabs",
"https://*/*"],
"background": {
"scripts": ["background.js"]
},
"manifest_version": 2
}
background.js
chrome.webRequest.onBeforeRequest.addListener(
function(info) {
console.log("Cat intercepteddd: " + info.url);
});
chrome.webRequest.onBeforeRedirect.addListener(
function(info) {
console.log("Cat intercepteddd: " + info.url);
});
chrome.webRequest.onResponseStarted.addListener(
function(info) {
console.log("Cat intercepteddd: " + info.url);
});
const responseListener = function(details) {
const headers = details.responseHeaders;
for (let i = 0; i < headers.length; i++) {
if (headers[i].name == "location") {
console.log("redirectURL:" + headers[i].value);
}
}
return { responseHeaders: newHeaders};
};
chrome.webRequest.onHeadersReceived.addListener(
responseListener,{urls: [URL_TO_DETECT]}, ["blocking", "responseHeaders"]);
Usually 303 redirects will be caught on the response headers, use the above code to get the redirect url from response header, make sure to have the code in background.js

http requests not working chrome extension

I am developing a chrome extension that requires corss domain XHR. So I need to make get requests to a server and get some text out of it. I am currently loading the unpacked extension from my computer. The script doesn't seem to be working.
Here is my manifest.json:
{
"name": "My extension",
"version": "1.1",
"manifest_version": 2,
"description": "Testing http requests",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"permissions": [
"http://*/"
]
}
And here is the script that performs the get request (from this tutorial):
function showresponse(){
var query = document.getElementById("query").value;
var url = "http://blah.com/search.php?term="+query;
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
console.log("hello world");
document.getElementById("container").innerHTML = xhr.responseText;
}
}
xhr.send(null);
}
The id's etc are set according to my popup.html file and that's set up correctly and it includes this js file containing the showresponse() function definition.
I also tried packaging my extension to get a myextension.crx file after reading this question and I tried opening the file in my browser, but chrome doesn't allow installing the extension from localhost or unknown servers for security reasons I suppose.
My question is how do I make a cross domain XHR in a chrome extension?
Also the response from the get request to the server is actually an html document and I need to filter some text out of the returned html tags. As I am making a query to a php script, can I receive and therefore play around with the html output if I make a get request in javascript?
How do I go about acheiving this?

Upload a File in a Google Chrome Extension

I'm writing an extension for Chrome, and I need to upload a file from the page the user is currently on to my server to be processed, I cannot figure out how to upload the file though. I considered just passing the link to the server and having the server download the file, however if the site requires authentication this will not work. Is it possible to upload a file via a Chrome extension to my server?
I've recently developed a Chrome extension which retrieves content from a page, and sends it to the server.
The following approach was used:
File downloads: Get the src property of an <img> element, for example.
Fetch the file from the Cache - use XMLHttpRequest from the background page.
Use a Web Worker in the background page to handle the upload.
Side note, to take the checksum of the image, Crypto-JS: MD5 can be used. Example (where xhr is the XMLHttpRequest object with responseType set to arraybuffer, see Worker demo):
var md5sum = Crypto.MD5( new Uint8Array(xhr.response) );
Full example
Content script
// Example: Grab the first <img> from the document if it exists.
var img = document.images[0];
if (img) {
// Send the target of the image:
chrome.runtime.sendMessage({method: 'postUrl', url: img.src});
}
Background script (with Worker)
chrome.runtime.onMessage.addListener(function(request) {
if (request.method == 'postUrl') {
var worker = new Worker('worker.js');
worker.postMessage(request.url);
}
});
Web Worker
// Define the FormData object for the Web worker:
importScripts('xhr2-FormData.js')
// Note: In a Web worker, the global object is called "self" instead of "window"
self.onmessage = function(event) {
var resourceUrl = event.data; // From the background page
var xhr = new XMLHttpRequest();
xhr.open('GET', resourceUrl, true);
// Response type arraybuffer - XMLHttpRequest 2
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
if (xhr.status == 200) {
nextStep(xhr.response);
}
};
xhr.send();
};
function nextStep(arrayBuffer) {
var xhr = new XMLHttpRequest();
// Using FormData polyfill for Web workers!
var fd = new FormData();
fd.append('server-method', 'upload');
// The native FormData.append method ONLY takes Blobs, Files or strings
// The FormData for Web workers polyfill can also deal with array buffers
fd.append('file', arrayBuffer);
xhr.open('POST', 'http://YOUR.DOMAIN.HERE/posturl.php', true);
// Transmit the form to the server
xhr.send(fd);
};
FormData for Web workers POLYFILL
Web workers do not natively support the FormData object, used to transmit multipart/form-data forms. That's why I've written a polyfill for it. This code has to be included in the Web worker, using importScripts('xhr2-FormData.js').
The source code of the polyfill is available at https://gist.github.com/Rob--W/8b5adedd84c0d36aba64
Manifest file:
{
"name": "Rob W - Demo: Scraping images and posting data",
"version": "1.0",
"manifest_version": 2,
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["contentscript.js"]
}
],
"background": {
"scripts": ["background.js"]
},
"permissions": ["http://*/*", "https://*/*"]
}
Relevant documentation
Message passing Google Chrome Extensions
chrome.runtime.onMessage Google Chrome Extensions
XMLHttpRequest Level 2 W3c specification
FormData (XHR2) MDN
ArrayBuffer responses (XHR2) HTML5 rocks (note: arraybuffer responses are deprecated in favor of typed arrays, the polyfill has been updated to reflect this change)
The simplest solutions seems to be for your extension to send the file's URI to your server, and then your server-side code will download it from the page into the server and process it.
Create a server-side script like http://mysite.com/process.php?uri=[file's URI goes here] that will process the given file. Use AJAX to call this URL (more info at http://code.google.com/chrome/extensions/xhr.html ). The script will return the processed file, which you could then use in your extension.
You should checkout the following:
chrome.extension.sendRequest() and chrome.extension.onRequest()
You can read more about them here: http://code.google.com/chrome/extensions/messaging.html
Basically you will setup the page on the server to watch for the Chrome extension, and once they connect you will need to have a javascript that will do the upload task for you.
I haven't tested this out, but it may get you where you need to be. Also you may want to read the Long-lived connections section.
Goodluck

Resources