A collection of notebooks for interacting with data. https://ristretto.codeberg.page/
2024-10-22 07:00:23 +00:00
.vscode rename transform to image-filters 2024-02-07 16:29:20 -08:00
build add one dark theme and make it an option in code-edit 2024-07-18 22:17:07 -07:00
drafts expand only one node in file list; add drafts folder 2024-07-20 20:13:41 -07:00
.gitignore work on library build script 2023-11-27 23:47:41 -08:00
_welcome.md _welcome: improve formatting, use markdown-inspired tags in JSON for content 2024-07-27 20:35:47 -07:00
app-content.md apply array.from first to generator output 2024-07-18 23:02:15 -07:00
app-redirect-dialog.md add app redirect dialog 2024-03-08 22:47:07 -08:00
app-view.md app-view, notebook-view, loader: apply changes to dependencies in other tabs 2024-08-27 21:56:08 -07:00
auth.md auth: put sign in form in separate file 2024-06-07 10:01:25 -07:00
autocomplete.md start data cards example 2024-03-01 22:39:29 -08:00
base64.md add base64 with example; empty auth notebook 2024-05-28 22:04:16 -07:00
blank-page.md blank-page: add element with filename on drop 2024-07-28 16:59:10 -07:00
build-docker.md add dockerfile and instructions for deno/node docker container 2023-11-24 23:58:06 -08:00
build-libraries.md add one dark theme and make it an option in code-edit 2024-07-18 22:17:07 -07:00
build.md rename explore to app-view so explore can be moved out 2024-07-06 19:16:47 -07:00
bundle-libraries.md add one dark theme and make it an option in code-edit 2024-07-18 22:17:07 -07:00
client-server-simulator.md add back/forward support to client server simulator 2024-05-30 12:58:14 -07:00
code-data-environment.md add code-data-environment doc 2024-10-08 00:31:17 -07:00
code-edit-new.md code-edit-new: fix example view 2024-09-21 11:16:41 -07:00
code-edit-styles.md start code-edit-styles notebook 2024-05-14 22:29:48 -07:00
code-edit.md customize scroll bars in code editor 2024-07-19 17:29:04 -07:00
codemirror-bundle.md add one dark theme and make it an option in code-edit 2024-07-18 22:17:07 -07:00
color-picker.md color-picker: add event and color property; add example and css variable for thumb border 2024-07-23 21:03:25 -07:00
colors.json.md add thumbnail for shapes; make thumbnails load when item is shown 2023-11-21 19:09:39 -08:00
container-frame.md container-frame: add loader and import it in order to load app 2024-10-11 22:38:51 -07:00
data-cards.md use adoptedStyleSheets in data-cards 2024-07-30 14:42:58 -07:00
deno-vm-instance.md add deno-vm-instance walkthrough 2024-07-15 00:01:27 -07:00
dev.md split code editor into separate file; support editing source of notebook 2024-02-06 15:29:04 -08:00
editable-data-table.md editable-data-table: minor refactor 2024-09-10 09:58:36 -07:00
example-notebook.md example-notebook: edit with notebook-view 2024-08-11 12:16:41 -07:00
file-cards.md add icon licenses 2024-08-03 14:41:20 -07:00
file-tree.md file-tree: add TODOs 2024-09-21 14:20:04 -07:00
files.json.md add file tree 2024-04-29 12:31:22 -07:00
font.woff2.md add font example 2024-03-05 22:26:35 -08:00
forms.md move components to separate files 2024-02-01 18:12:56 -08:00
heading.md heading: add description 2024-08-27 18:46:03 -07:00
histogram.md add histogram example 2024-02-07 20:52:26 -08:00
host.md host: go back to using src w/ data URL for inner frame 2024-09-28 11:58:34 -07:00
image-filters.md make grayscale in image filters full grayscale 2024-02-07 22:42:58 -08:00
image.png.md integrate code editor component (CodeMirror) 2024-01-29 23:54:37 -08:00
intro.md get link working 2024-07-18 20:41:39 -07:00
json-tree.md add file tree 2024-04-29 12:31:22 -07:00
LICENSE Add license 2024-07-21 00:08:16 +00:00
list.md add icon licenses 2024-08-03 14:41:20 -07:00
loader.md app-view, notebook-view, loader: apply changes to dependencies in other tabs 2024-08-27 21:56:08 -07:00
markdown-code-blocks.md markdown-code-blocks: comment code; fix issues; begin tests 2024-07-14 15:51:35 -07:00
menu-nav.md add tab-styles with different variations on tabs; start other notebooks 2024-05-13 23:50:00 -07:00
menu-styles.md add tab-styles with different variations on tabs; start other notebooks 2024-05-13 23:50:00 -07:00
menu.md menu: clean up box shadow 2024-07-20 18:33:33 -07:00
network-isolated-iframe-playground.md edit network-isolated-iframe-playground 2024-10-04 22:54:49 -07:00
notebook-view.dev.md notebook-view: base markdown parsing on starting with non-empty line; group code blocks; add dev notebook 2024-07-31 14:57:46 -07:00
notebook-view.md notebook-view: remove if (false) 2024-08-27 23:15:53 -07:00
outline-text-edit.md fix file extension 2024-07-11 19:29:41 -07:00
palette.md make examples load using same loader used by list.md editor 2024-02-09 16:40:48 -08:00
planets.csv.md add csv and table example 2024-02-27 17:33:31 -08:00
prosemirror-bundle.md add one dark theme and make it an option in code-edit 2024-07-18 22:17:07 -07:00
prosemirror-codemirror-tabbed-view.md start on prosemirror-codemirror tabbed view 2024-07-12 23:13:30 -07:00
prosemirror-custom-schema.md remove console.log 2024-07-09 19:30:04 -07:00
prosemirror-key-value-blocks.md prosemirror-key-value-blocks: clean up appearance 2024-07-19 19:52:31 -07:00
prosemirror-key-value.md prosemirror-key-value: add key value that has no extra places for the caret to go 2024-07-03 18:06:49 -07:00
README.md use data uri instead of srcdoc to get it working on safari 2024-03-07 19:26:09 -08:00
reconnect-example.md add component reconnect example 2024-05-13 16:46:50 -07:00
rich-text-edit.md remove console.log 2024-07-09 19:30:04 -07:00
run-build.js build: exclude notes from build 2024-05-06 12:03:17 -07:00
shapes.md make examples load using same loader used by list.md editor 2024-02-09 16:40:48 -08:00
split-pane.md split-pane: support horizontal splits; offset with starting position 2024-07-20 13:53:36 -07:00
storage.md storage: fix error 2024-05-17 19:41:19 -07:00
stream-match.md stream-match: start on utility to match something inside a stream 2024-07-22 16:02:00 -07:00
styles.md add style to file-tree; add styles file 2024-05-19 23:42:59 -07:00
tab-styles.md tab-styles: add color picker 2024-07-09 20:46:17 -07:00
tabbed.md notebook-view, tabbed: support includeFiles 2024-08-17 19:45:38 -07:00
table-filters.md add tab-styles with different variations on tabs; start other notebooks 2024-05-13 23:50:00 -07:00
table-nav.md add tab-styles with different variations on tabs; start other notebooks 2024-05-13 23:50:00 -07:00
table-pagination.md add tab-styles with different variations on tabs; start other notebooks 2024-05-13 23:50:00 -07:00
table-styles.md add tab-styles with different variations on tabs; start other notebooks 2024-05-13 23:50:00 -07:00
table.md table: add data to example 2024-05-15 20:05:44 -07:00
tabs-draggable.md add icon licenses 2024-08-03 14:41:20 -07:00
tabs-new.md tabs-new: dispatch event when clicking selected preview tab 2024-08-23 18:58:51 -07:00
tabs.md add icon licenses 2024-08-03 14:41:20 -07:00
test.md test: show example 2024-05-15 23:17:35 -07:00
tiny-delta.md add tiny-delta for finding delta from one string or Uint8Array to another 2024-08-10 18:25:39 -07:00
todo.md start data cards example 2024-03-01 22:39:29 -08:00
tutorial-view.md start tutorial-view for a step by step tutorial 2024-08-26 10:37:32 -07:00
website.md add icon licenses 2024-08-03 14:41:20 -07:00
websocket-shared-doc.md websocket-shared-doc: run AppServer in iframe 2024-07-17 15:29:50 -07:00
wiki-response.json.md add json example 2024-04-08 15:10:58 -05:00
workflow.md add workflow doc 2024-10-13 00:27:19 -07:00

Ristretto

A notebook container and a collection of notebooks for interacting with data.

Introduction

When users browse the web, if they browse a lot of sites, they put trust in the browser. Browser tabs are sandboxed in such a way that they are not allowed to access some things their browser has access to, like files on their computer, and when something breaks through the sandbox, that is considered a security vulnerability.

However, you are cautioned against typing, pasting, or uploading your private information to sites that you don't trust.

There is a lot of software out there. People are careful when they download programs, but people browse the web more freely, including on code notebook and interactive playground sites.

This is intended to provide a place where people can play more freely with private data. This way, you can try some code for graphing, visualizing, or searching your data. No guarantees are provided, and it is at your own risk. However, hopefully the container in ristretto will enable more expirementation.

The notebook collection has an assortment of tools for interacting with data, such as editors, visualization tools, transformation utilities, and search interfaces.

The Notebook Container

This runs notebook code under a Content-Security-Policy and a sandboxed iFrame, which combine to prevent data from being leaked without user interaction.

The code setting up the Content-Security-Policy and the sandboxed iFrame is very small, so it can quickly be inspected. It consists of:

  • a top-level web page, index.html, that has a the most broad Content-Security-Policy, that allows for downloading notebooks
  • a middle iframe, frame.html, which takes care of setting up the sandboxed iframe, and has a Content-Security-Policy that prevents all network requests - the ones it needs are accessed through the parent iframe via postMessage
  • a sandboxed srcdoc iframe under which the notebook code is run – This has allow-scripts but not allow-same-origin, so notebook code can run but it can't access localStorage, so each tab is kept separate

This doesn't allow anything except copying and pasting. Navigating to URLs is also blocked through a Content Security Policy. This way, a page can't trick a user into clicking on a link. To load data from a file, use copy and paste. This can also work for binary data through Base64. Another container will provide a way to upload and download files directly.

A Content-Security-Policy can be set through a header or through a meta tag. When headers can be reliably set, a header is preferred. However, this is run on the Pages platforms of code hosts, so the code and how it gets updated is more transparent.

All the sandbox does to load the code is have the outer frame download a giant Markdown file containing the notebook collection, and send it from the middle iframe to the inner srcdoc sandboxed iframe with some code to extract and run the entry point.

The inner srcdoc sandboxed iframe can have sandboxed iframes within it, to keep parts of the notebooks separate. How deeply nested they can be depends on the browser.

It also uses postMessage to allow the notebook to set the title of the web page.

The Notebook Collection

The Notebook Collection is the files inside this repository, and their output. These are built into a giant Markdown file.

A script will be added to check that the content of the giant Markdown file matches the content and build output of the little Markdown files in the repository.

Implementation

This consists of three files:

  • index.html - this is the top-level iframe, which has an iframe containing csp.html
  • frame.html - this contains a strict CSP, and has a srcdoc iframe, under which notebook code runs. It gets data from the parent and sends it to the child srcdoc iframe. Both itself and the srcdoc iframe are subject to the more strict CSP, so if there is work that could be done with either frame.html or index.html, it is done in frame.html.
  • notebook.md - This contains the notebook code. It is a giant Markdown file which contains the notebook collection for Ristretto, wrapped using fenced code blocks with more backquotes than the notebooks they contain.

The outer frame, index.html, has a CSP that allows it to access notebook.md, and to run inline and eval code. The middle frame, frame.html, is a non-sandboxed iframe that has a CSP that allows it and the inner frame to run inline and eval code, but not to access any local or network data, that isn't passed in through postMessage. The inner frame, a srcdoc iframe that has its code for starting up in frame.html, is a sandboxed iframe that has allow-scripts but not allow-same-origin.

index.html

<!doctype html>
<html>
  <head>
    <meta http-equiv="Content-Security-Policy" content="default-src data: 'unsafe-inline' 'unsafe-eval'; connect-src https://ristretto.codeberg.page/notebook.md; frame-src https://ristretto.codeberg.page/frame.html">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title></title>
<style type="text/css">
body {
  height: 100vh;
  margin: 0;
  display: flex;
  align-items: stretch;
  flex-direction: column;
}
iframe {
  flex-grow: 1;
  border: 0;
}
</style>
  </head>
  <body>
    <iframe id="frame" src="/frame.html"></iframe>
<script type="module">
const frame = document.getElementById('frame')
frame.addEventListener('load', async () => {
  const data = new Uint8Array(await (await fetch('/notebook.md')).arrayBuffer())
  frame.contentWindow.postMessage(['notebook', data], '*', [data.buffer])
})
</script>
  </body>
</html>

frame.html

<!doctype html>
<html>
  <head>
    <meta http-equiv="Content-Security-Policy" content="default-src data: 'unsafe-inline' 'unsafe-eval'; connect-src 'none'">
    <title></title>
<style type="text/css">
body {
  height: 100vh;
  margin: 0;
  display: flex;
  align-items: stretch;
  flex-direction: column;
}
iframe {
  flex-grow: 1;
  border: 0;
}
</style>
  </head>
  <body>
<script type="module">
addEventListener('message', e => {
  if (e.data[0] === 'notebook') {
    const iframe = document.createElement('iframe')
    iframe.sandbox = 'allow-scripts'
    iframe.addEventListener('load', async () => {
      const data = e.data[1]
      iframe.contentWindow.postMessage(['notebook', data], '*', [data.buffer])
    })
    const re = new RegExp('(?:^|\\n)\\s*\\n`entry.js`\\n\\s*\\n```.*?\\n(.*?)```\\s*(?:\\n|$)', 's')
    iframe.src = `
<!doctype html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
  </head>
  <body>
<script type="module">
addEventListener('message', e => {
  if (e.data[0] === 'notebook') {
    window.__source = new TextDecoder().decode(e.data[1])
    const re = new RegExp(${JSON.stringify(re.source)}, ${JSON.stringify(re.flags)})
    const entrySrc = window.__source.match(re)[1]
    const script = document.createElement('script')
    script.type = 'module'
    script.textContent = entrySrc
    document.body.append(script)
  }
})
<-script>
  </body>
</html>
    `.trim().replace('-script', '/script')
    iframe.src = `data:text/html;base64,${btoa(src.trim())}`
    // iframe.srcdoc = src.trim()]
    document.body.replaceChildren(iframe)
  }
})
</script>
  </body>
</html>

Here is an example notebook.md that has buttons to attempt to do some things. It can be checked following these instructions:

  • Place the three files (index.html, frame.html, and notebook.md) in a directory
  • replace https://ristretto.codeberg.page with the URL where it will be running (such as http://localhost:4000)
  • Run a web server on the directory like python -m http.server 4000
  • Opening it up and inspecting it in different browsers. You can check the server logs and the logs in the web inspectors of the browsers to make sure attempts to access other pages are blocked.

notebook.md

# Ristretto Container Demo

This is a demo that runs right inside the container, and tries some actions that are protected against by the iframe sandbox and CSP.

`entry.js`

```js
class RunCodeBlock extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    this.nameEl = document.createElement('h2')
    this.htmlEl = document.createElement('textarea')
    this.htmlEl.readOnly = true
    this.jsEl = document.createElement('textarea')
    this.jsEl.readOnly = true
    this.outputEl = document.createElement('div')
    const heading = text => {
      const el = document.createElement('h3')
      el.innerText = text
      return el
    }
    this.shadowRoot.append(
      this.nameEl,
      heading('HTML'),
      this.htmlEl,
      heading('JavaScript'),
      this.jsEl,
      heading('Output'),
      this.outputEl,
    )
  }

  connectedCallback() {
    const style = document.createElement('style')
    style.textContent = `
      :host {
        display: 'flex';
        flex-direction: column;
        gap: 5px;
        margin: 10px;
      }
      textarea {
        font-family: monospace;
        width: 500px;
        height: 200px;
      }
    `
    this.shadowRoot.append(style)
  }

  get name() {
    return this.nameEl.innerText
  }

  set name(value) {
    this.nameEl.innerText = value
  }

  set html(value) {
    this.htmlEl.value = value
  }

  get html() {
    return this.htmlEl.value
  }

  set js(value) {
    this.jsEl.value = value
  }

  get js() {
    return this.jsEl.value
  }

  async run() {
    if (this.html !== undefined) {
      this.outputEl.innerHTML = this.html
    }
    if (this.js) {
      const fn = (await import(
        `data:text/javascript;base64,${btoa(this.js)}`
      )).default
      if (typeof fn === 'function') {
        fn(this.outputEl)
      } else {
        console.error("Didn't get function from data import")
      }
    }
  }
}

customElements.define('run-code-block', RunCodeBlock)

function unindent(s) {
  const spaces = Math.min(20, ...([...s.matchAll(/^([ ]+)\S/gm)].map(m => m[1].length)))
  return s.replaceAll(new RegExp(`^${' '.repeat(spaces)}`, 'gm'), '')
}

async function run() {
  const runCodeBlocks = [
    {
      title: 'Navigate to same site with query string',
      html: `
        <a href="https://ristretto.codeberg.page/notebook.md?data=private">
          Link
        </a>
      `,
    },
    {
      title: 'Navigate to another site',
      html: `
        <a href="https://justatest.requestcatcher.com/test?data=private">
          Link
        </a>
      `,
    },
    {
      title: 'Make a request to the same site',
      html: `
        <p>
          <button class="send">
            Send
          </button>
          <span style="margin: 0 10px">
            Status/Error:
          </span>
          <span class="output">Ready</span>
        </p>
      `,
      js: `
        export default el => {
          const outputEl = el.querySelector('.output')
          el.querySelector('.send').addEventListener(
            'click',
            async () => {
              let resp, e
              outputEl.innerText = 'Sending'
              try {
                resp = await fetch('https://ristretto.codeberg.page/notebook.md?data=private')
              } catch (err) {
                e = err
              }
              outputEl.innerText = (e ?? resp.status).toString()
            }
          )
        }
      `,
    },
    {
      title: 'Add a style with a URL',
      html: `
        <p>
          <button class="add">
            Add
          </button>
          <div class="test" style="min-width: 50px; min-height: 50px"></div>
        </p>
      `,
      js: `
        export default el => {
          el.querySelector('.add').addEventListener(
            'click',
            async () => {
              const style = document.createElement('style')
              style.textContent = \`
                .test {
                  background-image: url('http://placekitten.com/200/300');
                  border: 5px solid blue;
                }
              \`
              el.appendChild(style)
            }
          )
        }
      `,
    },
  ]
  const codeBlocks = runCodeBlocks.map(({title, html, js}) => {
    const el = document.createElement('run-code-block')
    el.title = title
    el.html = unindent(html ?? '').trim()
    el.js = unindent(js ?? '').trim()
    return el
  })
  document.body.append(...codeBlocks)
  for (const codeBlock of codeBlocks) {
    codeBlock.run()
  }
}

run()
```

If you try clicking the links or download in a new browser, you can see that they don't load.

A strict CSP is necessary for these to pass. If you allow direct fetch access to even one file URL from the inner iframe, it can send info from inside the inner iframe in the query string (the CSP checks the path but not the query string). This would likely be a trusted server, but it's still against the goals of this sandbox.

Having the data in the sandbox stay in the sandbox, unless the user copies or downloads it, depends on the browser being secure. Be sure to use an up-to-date browser.

Also the data:, 'unsafe-inline', and 'unsafe-eval' are needed for notebook features, but make it so some browser vulnerabilities that have happened recently would occur here. For instance, font vulnerability and image vulnerability. So it's important to be careful.

These shouldn't allow for external access, but it needs to be investigated more fully.