Skip to content

Commit e30b4eb

Browse files
committed
0.2.0 - add local mitm service
Add a service `ServiceDownloader` which allows the MITM served by the local device.
1 parent ac8c972 commit e30b4eb

File tree

10 files changed

+536
-9
lines changed

10 files changed

+536
-9
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
2. Add the default downloader component `Downloader`. It is implemented by using `StreamSaver.js`.
1313
3. Add the cross-origin support when serving the data to clients (e.g. the browser).
1414
4. Add the external URL proxy support for fetching the cross-origin resources.
15+
5. Add a service `ServiceDownloader` which allows the MITM served by the local device.
1516

1617
#### :wrench: Fix
1718

dash_file_cache/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
# Import frequently-used classes.
3434
from .caches import CachePlain, CacheQueue, CacheFile
35-
from .services import ServiceData
35+
from .services import ServiceData, ServiceDownloader
3636
from .components import PlainDownloader, Downloader
3737

3838
__all__ = (
@@ -46,6 +46,7 @@
4646
"CacheQueue",
4747
"CacheFile",
4848
"ServiceData",
49+
"ServiceDownloader",
4950
"PlainDownloader",
5051
"Downloader",
5152
)

dash_file_cache/services/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,20 @@
2424
# Import sub-modules.
2525
from . import utilities
2626
from . import reqstream
27+
from . import downloader
2728
from . import data
2829

2930
from .data import ServiceData
30-
31-
__all__ = ("utilities", "reqstream", "data", "ServiceData")
31+
from .downloader import ServiceDownloader
32+
33+
__all__ = (
34+
"utilities",
35+
"reqstream",
36+
"downloader",
37+
"data",
38+
"ServiceData",
39+
"ServiceDownloader",
40+
)
3241

3342
# Set this local module as the prefered one
3443
__path__ = extend_path(__path__, __name__)

dash_file_cache/services/data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ def serve(
438438
rself = self
439439

440440
class ViewCachedData(flask.views.MethodView):
441-
"""Service for the raw HTML page"""
441+
"""Service for the cached data."""
442442

443443
init_every_request: ClassVar[bool] = False
444444

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<!--
2+
mitm.html is the lite "man in the middle"
3+
4+
This is only meant to signal the opener's messageChannel to
5+
the service worker - when that is done this mitm can be closed
6+
but it's better to keep it alive since this also stops the sw
7+
from restarting
8+
9+
The service worker is capable of intercepting all request and fork their
10+
own "fake" response - wish we are going to craft
11+
when the worker then receives a stream then the worker will tell the opener
12+
to open up a link that will start the download
13+
14+
Acquired from GitHub
15+
jimmywarting/StreamSaver.js
16+
https://github.com/jimmywarting/StreamSaver.js
17+
18+
Licensed by
19+
MIT License
20+
-->
21+
<script>
22+
// This will prevent the sw from restarting
23+
let keepAlive = () => {
24+
keepAlive = () => {};
25+
var ping =
26+
location.href.substr(0, location.href.lastIndexOf("/")) + "/ping";
27+
var interval = setInterval(() => {
28+
if (sw) {
29+
sw.postMessage("ping");
30+
} else {
31+
fetch(ping).then((res) => res.text(!res.ok && clearInterval(interval)));
32+
}
33+
}, 10000);
34+
};
35+
36+
// message event is the first thing we need to setup a listner for
37+
// don't want the opener to do a random timeout - instead they can listen for
38+
// the ready event
39+
// but since we need to wait for the Service Worker registration, we store the
40+
// message for later
41+
let messages = [];
42+
window.onmessage = (evt) => messages.push(evt);
43+
44+
let sw = null;
45+
let scope = "";
46+
47+
function registerWorker() {
48+
return navigator.serviceWorker
49+
.getRegistration("./")
50+
.then((swReg) => {
51+
return (
52+
swReg || navigator.serviceWorker.register("sw.js", {scope: "./"})
53+
);
54+
})
55+
.then((swReg) => {
56+
const swRegTmp = swReg.installing || swReg.waiting;
57+
58+
scope = swReg.scope;
59+
60+
return (
61+
(sw = swReg.active) ||
62+
new Promise((resolve) => {
63+
swRegTmp.addEventListener(
64+
"statechange",
65+
(fn = () => {
66+
if (swRegTmp.state === "activated") {
67+
swRegTmp.removeEventListener("statechange", fn);
68+
sw = swReg.active;
69+
resolve();
70+
}
71+
})
72+
);
73+
})
74+
);
75+
});
76+
}
77+
78+
// Now that we have the Service Worker registered we can process messages
79+
function onMessage(event) {
80+
let {data, ports, origin} = event;
81+
82+
// It's important to have a messageChannel, don't want to interfere
83+
// with other simultaneous downloads
84+
if (!ports || !ports.length) {
85+
throw new TypeError("[StreamSaver] You didn't send a messageChannel");
86+
}
87+
88+
if (typeof data !== "object") {
89+
throw new TypeError("[StreamSaver] You didn't send a object");
90+
}
91+
92+
// the default public service worker for StreamSaver is shared among others.
93+
// so all download links needs to be prefixed to avoid any other conflict
94+
data.origin = origin;
95+
96+
// if we ever (in some feature versoin of streamsaver) would like to
97+
// redirect back to the page of who initiated a http request
98+
data.referrer = data.referrer || document.referrer || origin;
99+
100+
// pass along version for possible backwards compatibility in sw.js
101+
data.streamSaverVersion = new URLSearchParams(location.search).get(
102+
"version"
103+
);
104+
105+
if (data.streamSaverVersion === "1.2.0") {
106+
console.warn("[StreamSaver] please update streamsaver");
107+
}
108+
109+
/** @since v2.0.0 */
110+
if (!data.headers) {
111+
console.warn(
112+
"[StreamSaver] pass `data.headers` that you would like to pass along to the service worker\nit should be a 2D array or a key/val object that fetch's Headers api accepts"
113+
);
114+
} else {
115+
// test if it's correct
116+
// should thorw a typeError if not
117+
new Headers(data.headers);
118+
}
119+
120+
/** @since v2.0.0 */
121+
if (typeof data.filename === "string") {
122+
console.warn(
123+
"[StreamSaver] You shouldn't send `data.filename` anymore. It should be included in the Content-Disposition header option"
124+
);
125+
// Do what File constructor do with fileNames
126+
data.filename = data.filename.replace(/\//g, ":");
127+
}
128+
129+
/** @since v2.0.0 */
130+
if (data.size) {
131+
console.warn(
132+
"[StreamSaver] You shouldn't send `data.size` anymore. It should be included in the content-length header option"
133+
);
134+
}
135+
136+
/** @since v2.0.0 */
137+
if (data.readableStream) {
138+
console.warn(
139+
"[StreamSaver] You should send the readableStream in the messageChannel, not throught mitm"
140+
);
141+
}
142+
143+
/** @since v2.0.0 */
144+
if (!data.pathname) {
145+
console.warn(
146+
"[StreamSaver] Please send `data.pathname` (eg: /pictures/summer.jpg)"
147+
);
148+
data.pathname = Math.random().toString().slice(-6) + "/" + data.filename;
149+
}
150+
151+
// remove all leading slashes
152+
data.pathname = data.pathname.replace(/^\/+/g, "");
153+
154+
// remove protocol
155+
let org = origin.replace(/(^\w+:|^)\/\//, "");
156+
157+
// set the absolute pathname to the download url.
158+
data.url = new URL(`${scope + org}/${data.pathname}`).toString();
159+
160+
if (!data.url.startsWith(`${scope + org}/`)) {
161+
throw new TypeError("[StreamSaver] bad `data.pathname`");
162+
}
163+
164+
// This sends the message data as well as transferring
165+
// messageChannel.port2 to the service worker. The service worker can
166+
// then use the transferred port to reply via postMessage(), which
167+
// will in turn trigger the onmessage handler on messageChannel.port1.
168+
169+
const transferable = data.readableStream
170+
? [ports[0], data.readableStream]
171+
: [ports[0]];
172+
173+
if (!(data.readableStream || data.transferringReadable)) {
174+
keepAlive();
175+
}
176+
177+
return sw.postMessage(data, transferable);
178+
}
179+
180+
if (window.opener) {
181+
// The opener can't listen to onload event, so we need to help em out!
182+
// (telling them that we are ready to accept postMessage's)
183+
window.opener.postMessage("StreamSaver::loadedPopup", "*");
184+
}
185+
186+
if (navigator.serviceWorker) {
187+
registerWorker().then(() => {
188+
window.onmessage = onMessage;
189+
messages.forEach(window.onmessage);
190+
});
191+
}
192+
193+
// FF v102 just started to supports transferable streams, but still needs to ping sw.js
194+
// even tough the service worker dose not have to do any kind of work and listen to any
195+
// messages... #305
196+
keepAlive();
197+
</script>

0 commit comments

Comments
 (0)