10
\$\begingroup\$

Context

I'm trying to build a system in which a tool (the Client) will generate a header to be used as part of an HTTP request from the user's browser.

  • The user should be able to choose their own implementation of the Client.
  • The user should not have to install a plugin or extension to their browser.

Summary

A 3rd party will serve a small wrapper (the Shim) which will keep track of where to load the Client from. It will store this in the browser's IndexedDB under its own origin.

The Shim and the Client will be loaded in iframes of their own origin, so that they (and the Host website) can only access each-other's functionality through the defined methods (based on MessageChannels and postMessage() calls).

Parties, Prerogatives, and Restrictions

  • The Host website is at www.host.com.
  • They shouldn't be able to know what implementation of the Client is in use,
  • nor should they be able to affect the Client in any way except by requesting header values.
  • (As it stands the Host is able to suggest a default Client; when I actually build the set-Client-address tool for the Shim, I'll get rid of this.)
  • The Shim is served from www.shim.com.
  • They're assumed to be trustworthy in the sense that people know what code they're serving. (Even this isn't ideal; subresource integrity checks would be great if they worked on iframes.)
  • Their code obviously knows who the Client is, and can see the requests and responses as they're passed back and forth, but all of this stays on the user's machine as far as the Shim is concerned.
  • They can only affect the client by requesting the header values.
  • The Client is served from www.client.com.
  • At present this is being passed in as a default, but in practice it should have been saved into the IndexedDB on the user's machine belonging to the www.shim.com origin. (It will get put there by some other tool I haven't written yet.)
  • It's fine for the client to know who the Host is; I may even add that as an explicit property of each request.
  • There's an image or some other resource that the Host wants to load on their page, but requesting that image requires a custom Receipts-Receipt HTTPS header, the value for which needs to come from the Client. Let's suppose this image is at https://www.target.com/target.png, but it could just as easily be in the same origin as the Host website itself.

Code

www.host.com/host.html

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>Host</title>
    <script 
        src="https://www.shim.com/shim.js"
        data-default="https://www.client.com/client.html">
    </script>
    <script>
        _page_loaded = ()=>{
            window.FOTR.fetch(
                new Request('https://www.target.com/target.png')
                ).then((response)=>{
                    return response.blob();
                }).then((b)=>{
                    const i = window.document.createElement('img');
                    i.src = URL.createObjectURL(b);
                    document.getElementById("testTarget").appendChild(i);
                });
        };
    </script>
  </head>
  <body onload="_page_loaded()">
    <h1>Host</h1>
    <p>Lorem ipsum dolor sit amet.</p>
    <p id="testTarget"></p>
    <p>Text continues.</p>
  </body>
</html>

www.shim.com/shim.js

((toolName)=>{
    if(typeof window[toolName] === 'undefined'){

        const loaderUtilities = { //This is exactly the same between shim.html and shim.js.
            
            domReady: new Promise((resolve, reject)=>{
                if(document.readyState === "loading"){
                    document.addEventListener('DOMContentLoaded', resolve);
                }
                else{
                    resolve();
                }   // add error handler?
            }),
            
            origin: (uri)=>{
                const parser = window.document.createElement('a');
                parser.href = uri;
                return `${parser.protocol}//${parser.host}`;
            },
            
            loadTool: (uri)=>{
                return new Promise((resolve, reject)=>{
                    const tag = window.document.createElement('iframe');
                    tag.src = uri;
                    tag.width = 0;
                    tag.height = 0;
                    tag.style = "visibility: hidden";
                    window.addEventListener("message",
                        (e)=>{
                            if(e.origin == loaderUtilities.origin(uri)){ //is it possible to refine the origin check?
                                resolve(e.data);
                            }
                        }, 
                        false);
                    loaderUtilities.domReady.then(()=>{
                        document.body.appendChild(tag);
                    });
                });   // add error handler?
            },
            
            requestOverPort: (port, resource)=>{
                return new Promise((resolve, reject)=>{
                    const disposableChannel = new MessageChannel();
                    disposableChannel.port1.onmessage = (e)=>{
                        resolve(e.data);
                        disposableChannel.port1.close();
                    };
                    port.postMessage(
                        {
                            resource: resource,
                            port: disposableChannel.port2
                        },
                        [disposableChannel.port2]);
                });
            },
        };
            
        const defaultClient = document.currentScript.getAttribute("data-default") || '';
        const gotClientPort = loaderUtilities.loadTool(`https://www.shim.com/shim.html#${defaultClient}`);

        window[toolName] = {
            fetch: (request)=>{
                return gotClientPort
                    .then((clientPort)=>{
                        return loaderUtilities.requestOverPort(
                            clientPort,
                            {
                                url: request.url,
                                method: request.method
                            });
                    })
                    .then((receipt)=>{
                        return window.fetch(
                            request,
                            {
                                headers: new Headers({ 'Receipts-Receipt': receipt }),
                            });
                    });
            }
        }

    }
})(document.currentScript.getAttribute("data-name") || "FOTR")

www.shim.com/shim.html

<!DOCTYPE html>
<html>
  <head>
    <title>The FOTR Shim</title>
    <meta charset="UTF-8">
    <script type="text/javascript">

        const loaderUtilities = { //This is exactly the same between shim.html and shim.js.
            
            domReady: new Promise((resolve, reject)=>{
                if(document.readyState === "loading"){
                    document.addEventListener('DOMContentLoaded', resolve);
                }
                else{
                    resolve();
                }   // add error handler?
            }),
            
            origin: (uri)=>{
                const parser = window.document.createElement('a');
                parser.href = uri;
                return `${parser.protocol}//${parser.host}`;
            },
            
            loadTool: (uri)=>{
                return new Promise((resolve, reject)=>{
                    const tag = window.document.createElement('iframe');
                    tag.src = uri;
                    tag.width = 0;
                    tag.height = 0;
                    tag.style = "visibility: hidden";
                    window.addEventListener("message",
                        (e)=>{
                            if(e.origin == loaderUtilities.origin(uri)){ //is it possible to refine the origin check?
                                resolve(e.data);
                            }
                        }, 
                        false);
                    loaderUtilities.domReady.then(()=>{
                        document.body.appendChild(tag);
                    });
                });   // add error handler?
            },
            
            requestOverPort: (port, resource)=>{
                return new Promise((resolve, reject)=>{
                    const disposableChannel = new MessageChannel();
                    disposableChannel.port1.onmessage = (e)=>{
                        resolve(e.data);
                        disposableChannel.port1.close();
                    };
                    port.postMessage(
                        {
                            resource: resource,
                            port: disposableChannel.port2
                        },
                        [disposableChannel.port2]);
                });
            },
        };
        
        const indexedDBPromise = (request)=>{
            return new Promise(
                (resolve, reject)=>{
                    request.onsuccess = (e)=>{
                        resolve(e.target.result);
                    };
                    request.onerror = (e)=>{
                        reject(e.target.error);
                    };
                });
        };

        const defaultClient = window.location.href.split('#')[1] || '';
        const objectStoreName = "chosen_clients";

        const openDB = window.indexedDB.open("FOTR", 1);
        openDB.onupgradeneeded = (e)=>{
            e.target.result.createObjectStore(objectStoreName);
        };

        indexedDBPromise(openDB)
            .then(
                (db)=>{
                    const tx = db.transaction(objectStoreName, "readonly");
                    tx.oncomplete = ()=>{
                        db.close();
                    };
                    return indexedDBPromise(tx.objectStore(objectStoreName).getAll());
                })//handle open-db error?
            .then(
                (db_result)=>{
                    const clientURI = db_result.uri || defaultClient;
                    return loaderUtilities.loadTool(clientURI);
                })//handle db-read error?
            .then(
                (innerPort)=>{
                    const outerChannel = new MessageChannel();
                    outerChannel.port1.onmessage = (e)=>{
                        const newPort = e.data.port;
                        const request = e.data.resource;
                        console.log(`Forwarding request for [${request.method}]${request.url}.`);
                        loaderUtilities.requestOverPort(innerPort, request)
                            .then((response)=>{
                                console.log(`Forwarding requested value "${response}" for [${request.method}]${request.url}`);
                                newPort.postMessage(response);
                                newPort.close();
                            });
                    };
                    window.parent.postMessage(outerChannel.port2, '*', [outerChannel.port2]);
                });//handle tool-load error?

    </script>
  </head>
  <body></body>
</html>

www.client.com/client.html

<!DOCTYPE html>
<html>
  <head>
    <title>The stupidest client.</title>
    <meta charset="UTF-8">
    <script type="text/javascript">
      const pipe = new MessageChannel();
      const work = (request)=>{
          return "555";
      };
      pipe.port1.onmessage = (e)=>{
          const newPort = e.data.port;
          const request = e.data.resource;
          const response = work(request);
          console.log(`Receipt requested for [${request.method}]${request.url}; returning "${response}"`);
          newPort.postMessage(response);
          newPort.close();
      };
      window.parent.postMessage(pipe.port2, '*', [pipe.port2]); //should I be more restrictive of the recipient?
    </script>
  </head>
  <body</body>
</html>

Comments

  • The above works, in the sense that the image loads and the request for that image has the correct custom header.
  • The Client presented is just a test rig that always returns '555'.

Question

Mostly I'm concerned with the security and usability of the Shim.

  • Is the usage of iframes, Promises, indexedDB, and inter-frame messaging trustworthy? Am I doing them correctly?
  • Does this Shim system provide the protections described in "Parties, Prerogatives, and Restrictions", insofar as any modern web-browser is secure?
  • How should I approach error-handling?
  • What else should I do to test this system?
\$\endgroup\$
2
  • \$\begingroup\$ Is this for advertising in websites? \$\endgroup\$
    – konijn
    May 12, 2020 at 13:09
  • \$\begingroup\$ I haven't been working on this for months. The context at the time was a micro-transactions system, but this particular piece of the system could be adapted for use for lots of different things. \$\endgroup\$ May 12, 2020 at 14:51

1 Answer 1

4
\$\begingroup\$

From a medium review;

  • host.html

    • Not a big fan of inline script tags
    • Not sure why there are 2 <script> tags, I would merge them
    • You seem to practice a bit of Spartan Programming
      • i is usually kept for integers, I would go for img or even image
      • b just blocks my reading, I would go with blob
    • JavaScript code should follow lowerCamelCase, so _page_loaded -> pageLoaded
  • shim.js

    • It's unclear, why that is, it could use either an additional comment as to why or the removal of the duplicate functionality //This is exactly the same between shim.html and shim.js.
    • It's funny, you keep switching between double and single quotes for string constants. I advise single quotes nowadays because it saves on pressing the shift key.
    • This is pure style, I would not have inlined that function there: window.addEventListener("message",
    • Your code is suffering a bit from the Arrow Anti Pattern, see previous

The rest does not require comments for me. This looks overall very understandable & maintainable.

For your questions;

  • Is the usage of iframes, Promises, indexedDB, and inter-frame messaging trustworthy? Am I doing them correctly? -> This looks fine to me

  • Does this Shim system provide the protections described in "Parties, Prerogatives, and Restrictions", insofar as any modern web browser is secure? -> To your point, it is secure enough, not any less secure than the browser itself

The other 2 questions are a stretch for CodeReview, however, if you ever post this code with error handling, then I promise I won't wait 3 years to review it ;)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.