File size: 6,762 Bytes
30c32c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
204
205
206
207
208
209
210
211
212
213
const uid = require('./uid');

// probably good
const generateQuadUid = () => uid() + uid() + uid() + uid();

// idk i just copied this lol
const none = "'none'";
const featurePolicy = {
    'accelerometer': none,
    'ambient-light-sensor': none,
    'battery': none,
    'camera': none,
    'display-capture': none,
    'document-domain': none,
    'encrypted-media': none,
    'fullscreen': none,
    'geolocation': none,
    'gyroscope': none,
    'magnetometer': none,
    'microphone': none,
    'midi': none,
    'payment': none,
    'picture-in-picture': none,
    'publickey-credentials-get': none,
    'speaker-selection': none,
    'usb': none,
    'vibrate': none,
    'vr': none,
    'screen-wake-lock': none,
    'web-share': none,
    'interest-cohort': none
};

// idk i just copied this lol
const generateAllow = () => Object.entries(featurePolicy)
    .map(([name, permission]) => `${name} ${permission}`)
    .join('; ');

const createFrame = () => {
    const element = document.createElement("iframe");
    const frameId = generateQuadUid(); // this is how we differentiate iframe messages from other messages
    // hopefully pm doesnt do sonme weird stuff that makes this not work lol
    // console.log(frameId); // remove later lol
    element.dataset.id = frameId;
    element.style.display = "none";
    element.setAttribute('aria-hidden', 'true');
    // allow modals so people can use alert & stuff
    element.sandbox = 'allow-scripts allow-modals';
    element.allow = generateAllow();
    document.body.append(element);
    return element;
};

const origin = window.origin;

/**
 * vscode give me autofill
 * @param {MessageEvent} event 
 * @param {HTMLIFrameElement} iframe 
 * @param {Function} removeHandler 
 * @returns nothing
 */
const messageHandler = (event, iframe, removeHandler) => new Promise(resolve => {
    // console.log(event.origin) // remove later
    // this might not work first try cuz idk what event.origin is
    // if (event.origin !== iframe.contentDocument.location.origin) return; 
    // yea event origin is just location
    // console.log(event.origin, origin)
    // why is event.origin null
    // ok we arent checking origin because its just null for some reason
    // if (event.origin !== origin) return;
    // console.log(event.data.payload)
    if (!event.data.payload) return;

    // console.log({ payload: event.data.payload.id, iframe: iframe.dataset.id })
    if (event.data.payload.id !== iframe.dataset.id) return;
    const data = event.data.payload;

    window.removeEventListener('message', removeHandler);
    try {
        const url = iframe.src;
        // delete object url
        URL.revokeObjectURL(url);
    } catch {
        // honestly idk how this could fail im just doing this incase
        // something stupid happens and people cant use eval anymore
        console.warn('failed to revoke url of iframe sandboxed eval');
    }
    iframe.remove();

    // send back data
    resolve(data);
});

/**
 * generates a string that can be placed into the iframe src
 * @param {string} code the code
 * @returns the code that can be placed into the eval in the iframe src
 */
const prepareCodeForEval = (code) => {
    const escaped = JSON.stringify(code);
    // when the html encounters a closing script tag, itll end the script
    // so just put a backslash before it and it should be fine
    const scriptEscaped = escaped.replaceAll('<\/script>', '<\\/script>');
    return scriptEscaped;
}

const generateEvaluateSrc = (code, frame) => {
    // this puts some funny stuff in the iframe src
    // so that it actually works
    const runnerCode = `(async () => {
    let result = null;
    let success = true;
    try {
        // techincally eval can also postMessage
        // and also modify success & result probably
        // but theres no real reason to prevent it
        // nor does the user have any reason to do it
        result = await eval(${prepareCodeForEval(code)});
    } catch (err) {
        success = false;
        result = err;
    }

    const parent = window.parent;
    const origin = '*';
    // console.log(result,success);
    console.log(origin);

    try {
        parent.postMessage({
            payload: {
                success: success,
                value: result,
                id: ${JSON.stringify(frame.dataset.id)}
            },
        }, origin);
    } catch (topLevelError) {
        // couldnt clone likely
        try {
            parent.postMessage({
                payload: {
                    success: success,
                    value: JSON.stringify(result),
                    id: ${JSON.stringify(frame.dataset.id)}
                },
            }, origin);
        } catch (err) {
            // ok we cant stringify it just error
            parent.postMessage({
                payload: {
                    success: false,
                    value: [String(topLevelError), String(err)].join("; "),
                    id: ${JSON.stringify(frame.dataset.id)}
                },
            }, origin);
        }
    }
})();`;

    const html = [
        '<!DOCTYPE html>',
        '<html lang="en-US">',
        // the html head isnt required i just think its sily to add and shouldnt affect anything
        '<head>',
        '<title>the an one of an iframe</title>',
        '</head>',
        // same story with adding actual elements
        '<body>',
        '<h1><p>epic computing in progress...</p></h1>',
        // removed for being not cool!
        // '<img src="https://media.tenor.com/jXQiJUuqfM8AAAAd/type-emoji.gif">',
        '<script>',
        runnerCode,
        '</script>',
        '</body>',
        '</html>'
    ].join("\n");

    const blob = new Blob([html], { type: 'text/html;charset=UTF-8' });
    const url = URL.createObjectURL(blob);

    return url;
};

class SandboxRunner {
    static execute(code) {
        return new Promise(resolve => {
            const frame = createFrame();
            /**
             * please vscode show me the autofill
             * @param {MessageEvent} e 
             */
            const trueHandler = e => {
                // this code is weird but we need to remove
                // event handler ladter
                // console.log(e); // debug
                messageHandler(e, frame, trueHandler).then(payload => {
                    // console.log(payload)
                    resolve({
                        success: payload.success,
                        value: payload.value
                    });
                });
            };
            window.addEventListener('message', trueHandler);
            frame.src = generateEvaluateSrc(code, frame);
        });
    }
}

module.exports = SandboxRunner;