Skip to content

Commit 94ad0d2

Browse files
Update proxy
1 parent 6073327 commit 94ad0d2

2 files changed

Lines changed: 317 additions & 10 deletions

File tree

examples/httpProxy.js

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
(function() {
2+
var global = window;
3+
if (global._encodedRW) {
4+
console.log('/!\\ ignoring encoded ' + location.pathname);
5+
return;
6+
}
7+
global._encodedRW = true;
8+
9+
var p = location.pathname;
10+
var i = p.indexOf('/', 4);
11+
var base = p.substring(4, i);
12+
var opts = p.substring(i + 1, p.indexOf('/', i + 1));
13+
console.log('base path is ' + base + '/' + opts + ' (' + location.pathname + ')');
14+
15+
function b64ec(c) {
16+
return c === '-' ? '+' : (c === '_' ? '/' : '');
17+
}
18+
function b64e(s) {
19+
return btoa(s).replace(/[-_=]/g, b64ec);
20+
}
21+
function b64dc(c) {
22+
return c === '+' ? '-' : (c === '/' ? '_' : '');
23+
}
24+
function b64d(s) {
25+
return atob(s.replace(/[+\/]/g, b64dc));
26+
}
27+
function encodeHref(href) {
28+
var u = URL.parse(href);
29+
if (u) {
30+
if (u.protocol === 'https:' || u.protocol === 'http:') {
31+
var b = b64e(u.protocol + '//' + u.host);
32+
if (b !== base && opts.indexOf('o') !== -1) {
33+
return '/RW/static/not-found';
34+
}
35+
return '/RW/' + b + '/' + opts + u.pathname + u.search + u.hash;
36+
}
37+
} else if (href.charAt(0) === '/') {
38+
if (href.charAt(1) === '/') {
39+
var d = b64d(base);
40+
var i = d.indexOf(':');
41+
if (i) {
42+
return encodeHref(d.substring(0, i + 1) + href)
43+
}
44+
} else if (href.lastIndexOf('/RW/', 0) !== 0) {
45+
return '/RW/' + base + '/' + opts + href;
46+
}
47+
}
48+
return null;
49+
}
50+
function proxyHref(href, fallback) {
51+
var e = encodeHref('' + href);
52+
if (e) {
53+
console.log('replacing fetch ' + href + ' => ' + e);
54+
return e;
55+
}
56+
return fallback;
57+
}
58+
function blockTagName(tagName) {
59+
if (tagName) {
60+
var n = tagName.toUpperCase();
61+
if (n === 'SCRIPT' || n === 'IFRAME') {
62+
console.log('blocking element creation ' + n);
63+
return 'rw-' + tagName;
64+
}
65+
}
66+
return tagName;
67+
}
68+
function transformHtml(content) {
69+
if (content) {
70+
var c = content.toUpperCase();
71+
// TODO rewrite HTML when possible
72+
if (c.indexOf('HREF=') > 0 || c.indexOf('SRC=') > 0) {
73+
console.log('blocking HTML ' + content);
74+
return '';
75+
}
76+
}
77+
return content;
78+
}
79+
80+
/*
81+
* Wrapping fetch and XMLHttpRequest to proxy URLs
82+
*/
83+
if (global.fetch) {
84+
var rawFetch = global.fetch;
85+
global.fetch = function(resource, options) {
86+
return rawFetch(proxyHref(resource instanceof Request ? resource.url : resource, resource), options);
87+
};
88+
}
89+
if (global.XMLHttpRequest) {
90+
var rawXHROpen = global.XMLHttpRequest.prototype.open;
91+
global.XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
92+
return rawXHROpen.call(this, method, proxyHref(url, url), async, user, password);
93+
};
94+
}
95+
96+
/*
97+
* Wrapping various element creation
98+
*/
99+
if (global.Document) {
100+
var rawCreateElement = global.Document.prototype.createElement;
101+
global.Document.prototype.createElement = function(tagName, options) {
102+
return rawCreateElement.call(this, blockTagName(tagName), options);
103+
};
104+
var rawCreateElementNS = global.Document.prototype.createElementNS;
105+
global.Document.prototype.createElementNS = function(nsUri, tagName, options) {
106+
return rawCreateElementNS.call(this, nsUri, blockTagName(tagName), options);
107+
};
108+
var rawInsertAdjacentHTML = global.Document.prototype.insertAdjacentHTML;
109+
global.Document.prototype.insertAdjacentHTML = function(position, input) {
110+
return rawInsertAdjacentHTML.call(this, position, transformHtml(input));
111+
};
112+
var rawWrite = global.Document.prototype.write;
113+
global.Document.prototype.write = function() {
114+
var args = [];
115+
for (var i = 0; i < arguments.length; i++) {
116+
args.push(transformHtml(arguments[i]));
117+
}
118+
rawWrite.apply(this, args);
119+
};
120+
}
121+
if (global.Element && Object.getOwnPropertyDescriptor) {
122+
var rawInner = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
123+
if (rawInner && rawInner.set) {
124+
Object.defineProperty(Element.prototype, 'innerHTML', {
125+
configurable: true,
126+
enumerable: rawInner.enumerable,
127+
get: function () {
128+
return rawInner.get.call(this);
129+
},
130+
set: function (html) {
131+
rawInner.set.call(this, transformHtml(html));
132+
}
133+
});
134+
}
135+
}
136+
137+
if (global.MutationObserver) {
138+
function getUrlAttributeName(node) {
139+
var n = node.nodeName.toUpperCase();
140+
// the original resource may have already been loaded
141+
if (n === 'SCRIPT' || n === 'IFRAME' || n === 'SOURCE' || n === 'TRACK' || n === 'EMBED') {
142+
return 'src';
143+
} else if (n === 'IMG') {
144+
var srcset = node.getAttribute('srcset');
145+
if (srcset) {
146+
node.removeAttribute('srcset');
147+
if (!node.hasAttribute('src')) {
148+
var i = srcset.indexOf(' ');
149+
if (i < 0) {
150+
i = srcset.indexOf(',');
151+
}
152+
if (i > 0) {
153+
node.setAttribute('src', srcset.substring(0, i));
154+
}
155+
}
156+
}
157+
return 'src';
158+
} else if (n === 'A' || n === 'LINK' || n === 'AREA' || n === 'BASE') {
159+
return 'href';
160+
} else if (n === 'FORM') {
161+
return 'action';
162+
}
163+
return null;
164+
}
165+
function processAttribute(node, name) {
166+
var value = node.getAttribute(name);
167+
if (value) {
168+
var v = encodeHref(value);
169+
if (v) {
170+
console.log('replacing ' + node.nodeName + '.' + name + ': ' + value + ' => ' + v);
171+
node.setAttribute(name, v);
172+
}
173+
}
174+
}
175+
function rewriteElement(node) {
176+
if (node.nodeName.toUpperCase().lastIndexOf('RW-', 0) === 0 && rawCreateElement) {
177+
console.log('blocked element detected ' + node.nodeName);
178+
var e = rawCreateElement.call(global.document, node.nodeName.substring(3));
179+
if (node.attributes) {
180+
for (var ai = 0; ai < node.attributes.length; ai++) {
181+
var attr = node.attributes[ai];
182+
var value = attr.value;
183+
var t = attr.name.toLowerCase();
184+
if (t === 'href' || t === 'src') {
185+
var v = encodeHref(value);
186+
if (v) {
187+
console.log('replacing ' + node.nodeName + '.' + attr.name + ': ' + value + ' => ' + v);
188+
value = v;
189+
}
190+
}
191+
e.setAttribute(attr.name, value);
192+
}
193+
}
194+
while (node.firstChild) {
195+
e.appendChild(node.firstChild);
196+
}
197+
if (node.parentNode) {
198+
node.parentNode.replaceChild(e, node);
199+
return true;
200+
}
201+
}
202+
return false;
203+
}
204+
function processElement(node) {
205+
if (rewriteElement(node)) {
206+
return;
207+
}
208+
var t = getUrlAttributeName(node);
209+
if (t) {
210+
processAttribute(node, t);
211+
}
212+
for (var i = 0; i < node.childElementCount; i++) {
213+
processElement(node.children[i]);
214+
}
215+
}
216+
function observerCb(mutationList, observer) {
217+
for (var i = 0; i < mutationList.length; i++) {
218+
var mutation = mutationList[i];
219+
if (mutation.type === 'childList') {
220+
for (var j = 0; j < mutation.addedNodes.length; j++) {
221+
var addedNode = mutation.addedNodes[j];
222+
if (addedNode.nodeType === 1) {
223+
processElement(addedNode);
224+
}
225+
}
226+
} else if (mutation.type === 'attributes') {
227+
var t = mutation.attributeName.toLowerCase();
228+
if (t === 'href' || t === 'src') {
229+
processAttribute(mutation.target, t);
230+
}
231+
}
232+
}
233+
}
234+
console.log('connecting observer');
235+
var observer = new MutationObserver(observerCb);
236+
observer.observe(document.getRootNode(), {attributes: true, childList: true, subtree: true});
237+
document.addEventListener('DOMContentLoaded', function() {
238+
setTimeout(function() {
239+
console.log('disconnecting observer');
240+
let mutationList = observer.takeRecords();
241+
observer.disconnect();
242+
if (mutationList.length > 0) {
243+
observerCb(mutationList);
244+
}
245+
}, 500);
246+
});
247+
}
248+
})();

examples/httpProxy.lua

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ local HttpMessage = require('jls.net.http.HttpMessage')
99
local HttpHeaders = require('jls.net.http.HttpHeaders')
1010
local HttpExchange = require('jls.net.http.HttpExchange')
1111
local ProxyHttpHandler = require('jls.net.http.handler.ProxyHttpHandler')
12+
local FileHttpHandler = require('jls.net.http.handler.FileHttpHandler')
1213
local Http1 = require('jls.net.http.Http1')
1314
local Url = require('jls.net.Url')
15+
local File = require('jls.io.File')
1416
local StreamHandler = require('jls.io.StreamHandler')
1517
local BufferedStreamHandler = require('jls.io.streams.BufferedStreamHandler')
1618
local tables = require('jls.util.tables')
@@ -163,12 +165,20 @@ local function encodeHref(href, base, opts)
163165
if url:getProtocol() == 'https' or url:getProtocol() == 'http' then
164166
local b = base64:encode(formatBaseUrl(url))
165167
if b ~= base and string.find(opts, 'o', 1, true) then
166-
return '/not-found'
168+
return '/RW/static/not-found'
167169
end
168-
return '/r/'..b..'/'..opts..url:getFile()
170+
return '/RW/'..b..'/'..opts..url:getFile()
169171
end
170172
elseif string.find(href, '^/') then
171-
return '/r/'..base..'/'..opts..href
173+
if string.find(href, '^//') then
174+
local d = base64:decodeSafe(base)
175+
local p = string.match(d, '^([^:]+):')
176+
if p then
177+
return encodeHref(p..href, base, opts)
178+
end
179+
else
180+
return '/RW/'..base..'/'..opts..href
181+
end
172182
end
173183
return href
174184
end
@@ -217,15 +227,36 @@ local RewriteProxyHandler = class.create(ProxyHttpHandler, function(handler, sup
217227
end))
218228
end
219229

230+
local function findTag(data, name, before)
231+
local namePattern = string.gsub(name, '%a', function(a)
232+
return '['..string.lower(a)..string.upper(a)..']'
233+
end)
234+
local s, e = string.find(data, '<'..namePattern..'>')
235+
if not s then
236+
s, e = string.find(data, '<'..namePattern..'%s[^>]*>')
237+
end
238+
if s then
239+
if before then
240+
return s - 1
241+
end
242+
return e
243+
end
244+
end
245+
220246
local function transformHtml(data, base, opts)
221247
local function urlToQuery(n, v, s)
222248
local m = string.lower(n)
223-
if m == 'href' or m == 'src' then
249+
if m == 'href' or m == 'src' or m == 'action' then
224250
v = encodeHref(v, base, opts)
251+
elseif m == 'srcset' then
252+
string.gsub(data, '[^,]+', function(w)
253+
w = string.gsub(w, '^%s+', '')
254+
return encodeHref(w, base, opts)
255+
end)
225256
end
226257
return n..'='..s..v..s
227258
end
228-
return (string.gsub(data, '<%s*(%w+)([^>]*)>', function(tag, atts)
259+
local d = string.gsub(data, '<%s*(%w+)([^>]*)>', function(tag, atts)
229260
local m = string.gsub(atts, '%s+$', '')
230261
local s = ''
231262
if string.sub(m, #m) == '/' then
@@ -235,12 +266,17 @@ local RewriteProxyHandler = class.create(ProxyHttpHandler, function(handler, sup
235266
local t = string.lower(tag)
236267
if t == 'script' and string.find(opts, 's', 1, true) then
237268
m = ' type="text/plain"'
238-
elseif t == 'a' or t == 'img' or t == 'link' or t == 'script' then
269+
elseif t == 'a' or t == 'link' or t == 'area' or t == 'base' or t == 'img' or t == 'script' or t == 'iframe' then
239270
m = string.gsub(m, '(%w+)%s*=%s*"([^"]+)(")', urlToQuery)
240271
m = string.gsub(m, "(%w+)%s*=%s*'([^']+)(')", urlToQuery)
241272
end
242273
return '<'..tag..m..s..'>'
243-
end))
274+
end)
275+
local i = findTag(d, 'head') or findTag(d, 'html') or findTag(d, 'script', true) or findTag(d, 'body')
276+
if i then
277+
d = string.sub(d, 1, i)..'<script src="/RW/static/observe.js"></script>'..string.sub(d, i + 1)
278+
end
279+
return d
244280
end
245281

246282
function handler:adaptResponseStreamHandler(exchange, sh)
@@ -270,8 +306,8 @@ local RewriteProxyHandler = class.create(ProxyHttpHandler, function(handler, sup
270306
if not transform then
271307
return sh
272308
end
273-
response:setContentLength()
274-
response:setHeader('transfer-encoding')
309+
response:setContentLength(nil)
310+
response:setHeader('transfer-encoding', nil)
275311
return BufferedStreamHandler:new(StreamHandler:new(function(err, data)
276312
if err then
277313
return sh:onError(err)
@@ -302,6 +338,10 @@ local RewriteProxyHandler = class.create(ProxyHttpHandler, function(handler, sup
302338

303339
end)
304340

341+
local RESOURCE_MAP = {
342+
['observe.js'] = File:new('examples/httpProxy.js'):readAll()
343+
}
344+
305345
local CONFIG_SCHEMA = {
306346
title = 'HTTP proxy',
307347
type = 'object',
@@ -447,7 +487,26 @@ httpServer:bind(config.server.address, config.server.port):next(function()
447487
end)
448488

449489
local proxyHandler = RewriteProxyHandler:new()
450-
httpServer:createContext('/r/(%w+)/(%w+)(.*)', proxyHandler)
490+
httpServer:createContext('/RW/(%w+)/(%w+)(.*)', proxyHandler)
491+
httpServer:createContext('/RW/static/not-found', function(exchange)
492+
HttpExchange.notFound(exchange)
493+
end)
494+
httpServer:createContext('/RW/static/(.+)', function(exchange)
495+
local n = exchange:getRequestArguments()
496+
local c = RESOURCE_MAP[n]
497+
if c then
498+
local response = exchange:getResponse()
499+
response:setStatusCode(200, 'OK')
500+
response:setContentType(FileHttpHandler.guessContentType(n))
501+
response:setCacheControl(43200)
502+
response:setContentLength(#c)
503+
if exchange:getRequestMethod() == 'GET' then
504+
response:setBody(c)
505+
end
506+
else
507+
HttpExchange.notFound(exchange)
508+
end
509+
end)
451510
else
452511
local proxyHandler = ProxyHandler:new(config.proxy)
453512
httpServer:createContext('(.*)', proxyHandler)

0 commit comments

Comments
 (0)