Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions my_modules/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ aliasRoutes.forEach(dep => {
if (dep.url.includes('*')) { // for routes with wildcards
router.get(dep.url, (req, res) => { // req.params[0] contains the wildcard path
const filePath = path.join(__dirname, dep.loc, req.params[0])
res.setHeader('Access-Control-Allow-Origin', '*')
res.sendFile(filePath)
})
} else { // for exact routes
router.get(dep.url, (req, res) => res.sendFile(path.join(__dirname, dep.loc)))
router.get(dep.url, (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.sendFile(path.join(__dirname, dep.loc))
})
}
})

Expand Down Expand Up @@ -133,7 +137,8 @@ router.get('/templates/*', (req, res) => {
res.send(html)
})
})
} else { // it’s a file → serve it directly
} else { // it's a file → serve it directly
res.setHeader('Access-Control-Allow-Origin', '*')
res.sendFile(requestedPath)
}
})
Expand Down
5 changes: 3 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ if (process.env.CURTAIN) {
app.use('/api', utils.corsGate)
app.use(ROUTES)
app.use(GITHUB)
app.use(express.static(`${__dirname}/www`))
app.use('/docs', express.static(`${__dirname}/docs`))
const staticCORS = (res) => res.setHeader('Access-Control-Allow-Origin', '*')
app.use(express.static(`${__dirname}/www`, { setHeaders: staticCORS }))
app.use('/docs', express.static(`${__dirname}/docs`, { setHeaders: staticCORS }))
app.use(ERRORS.notFound)
app.use(ERRORS.errorHandler)

Expand Down
2 changes: 2 additions & 0 deletions www/core/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ nn.on('load', async () => {
utils.loaderUpdate('ready')
// setup custom renderer to catch errors (see on "message" below)
utils.setCustomRenderer(null)
// sandbox for security
NNE.iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-modals allow-pointer-lock')
// ...check URL for params, && fade out load screen when ready
if (utils.checkURL() === 'none') utils.loadDefault()
})
Expand Down
2 changes: 1 addition & 1 deletion www/core/netitor
12 changes: 10 additions & 2 deletions www/core/utils-convo.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,17 @@ window.CONVOS['utils-misc'] = (self) => {
}, {
before: () => { if (NNW.layout === 'welcome') NNW.layout = 'dock-left' },
id: 'agree-to-fork',
content: 'How exciting! In order to create your own remix of this project I\'m going to "<a href="https://guides.github.com/activities/forking/" target="_blank">fork</a>" it to your GitHub. Forking creates an associated copy onto your account. Sounds good?',
content: 'How exciting! In order to create your own remix of this project I\'m going to "<a href="https://guides.github.com/activities/forking/" target="_blank">fork</a>" it to your GitHub. Forking creates an associated copy in your account. One thing to keep in mind is that I\'ll be downloading this project\'s code and running it in your browser, so make sure you trust the creator of this repo. Sounds good?',
options: {
'let\'s do it!': (e) => utils.forkRepo(),
'I trust it, let\'s do it!': (e) => utils.forkRepo(),
'trust?': (e) => e.goTo('trust-code'),
'oh, never mind': (e) => e.hide()
}
}, {
id: 'trust-code',
content: 'You didn\'t write this code, so it\'s important to trust the person who did. This is true anytime you run someone else\'s code in your environment, this could mean importing a library someone else wrote into your own project, installing a new extention in your browser or even installing an app you downloaded to your computer. If you didn\'t write the code, always make sure it\'s coming from a tusted source!',
options: {
'I trust it, let\'s fork!': (e) => utils.forkRepo(),
'oh, never mind': (e) => e.hide()
}
}, {
Expand Down
16 changes: 11 additions & 5 deletions www/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,11 @@ window.utils = {

function bgmovement (e) {
// send mousemovments back up to netnet
// '*' required: iframe may be sandboxed (null origin)
window.top.postMessage({
type: 'netnet-bg',
data: { x: e.x, y: e.y }
})
}, '*')
}

function reduceMotion () {
Expand All @@ -209,7 +210,7 @@ window.utils = {
NNE.iframe.contentWindow.postMessage({
type: 'netnet',
data: { x: e.x, y: e.y, nomotion }
})
}, '*')
},

forkRepo: () => {
Expand Down Expand Up @@ -756,9 +757,14 @@ window.utils = {
// main.js listens for these errors + sends them to 'code-review' widget
setCustomRenderer: (base, proxy) => {
const errMsgr = `<script>
window.onerror = function (message, source, lineno) {
window.parent.postMessage({ type: 'iframe-error', message, source, lineno }, '*')
}
window.addEventListener('error', function (e) {
if (e.target && e.target !== window) {
var src = e.target.src || e.target.href || ''
if (src) window.parent.postMessage({ type: 'iframe-error', src: src }, '*')
} else {
window.parent.postMessage({ type: 'iframe-error', message: e.message, source: e.filename, lineno: e.lineno }, '*')
}
}, true)
</script>`
if (!base) {
NNE.customRender = function (event) { event.update(errMsgr + event.code) }
Expand Down
13 changes: 13 additions & 0 deletions www/widgets/code-review/convo.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ window.CONVOS['code-review'] = (self) => {
},
// console error convos
{
id: 'sandbox-security-error',
content: 'It looks like you\'re trying to use browser storage (like <code>localStorage</code>, <code>sessionStorage</code>, or <code>indexedDB</code>). In this studio your sketches get rendered into a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox" target="_blank">sandboxed</a> iframe for security, but that also means browser storage APIs won\'t work in sketches. If you want to use browser storage, try switching to a <b>Project</b>!',
options: {
'ok, got it': (e) => e.hide(),
'what\'s a project?': (e) => e.goTo('sandbox-security-error-project')
}
}, {
id: 'sandbox-security-error-project',
content: 'Unlike sketches, a <b>Project</b> can have multiple files and can be published to the Web using GitHub. Click on my face to open the <b>Coding Menu</b> and connect your GitHub account to start creating projects. When a project renders its result, the iframe (the rendered output) isn\'t sandboxed, so browser storage APIs like <code>localStorage</code> work as expected.',
options: {
'ok thanks': (e) => e.hide()
}
}, {
id: 'custom-renderer-error',
content: `<i>Your browser passed me this error</i>:<br><span style="font-family: fira-code, inconsolata, monospace">${self.error.message}</span>`,
options: {
Expand Down
122 changes: 106 additions & 16 deletions www/widgets/code-review/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class CodeReview extends Widget {
this.tempCode = null
this.issues = [] // issues netnet catches via linting
this.error = {} // last error passed by browser console
this._resourceErrors = [] // failed resource URLs from iframe capture listener
this._runtimeError = null // last JS runtime error; persists across review() calls
this._createHTML()

this.on('open', () => this._opened())
Expand All @@ -21,18 +23,53 @@ class CodeReview extends Widget {
Convo.load(this.key, () => { this.convos = window.CONVOS[this.key](this) })
}

/*
NOTE: this has gone through a lot of upates/changes over time, it may need
some refactoring at some point, when we do that, lets use this to test:

http://localhost:8001/?layout=dock-left#code/eJxlkrFy3CAQhvt7ih0a6cY29B5JTaq4sIvzC3CwkkgEyLCy7saTd88iObE9bgSz+///foCac7TX7nAAaJwfICfTipFozvdKresqnYkh27M00attr+aErw5XtY6OUNlXe3e5THIOgwA9USu8y9mFAZzXA4qOk0u4hjFh3wpj5eB6ATauYYratoLSwjLCTP+LjdI7ktcudFLKVTviTN41aqvtwH3SHj+YGbksfWRCTBuy6Bq1y3ZLNsnN1AFsVADlRAQlEloebxaPgeTLgul6wgkNxVRXpV0dN32PZMa6Unp2anA0Lme1ZEyBB1S38AYmoeUEp6d8D1Xm8l1MbmA//NkTACSNGOoEbQdJ/sox1MevLatJl26ZK10ImJ7xQgz4cHp6lJkSX4Xrr5vuw2p0QcNifPturc5TNL/RMhXcAP6j2T7c0dOJj8oPJjPST0JfC3/9oUncgniOXnxS9zHWx/dXVe/3eWjU9h/9BaGZtzM=

for security purposes, we've updated the way the render iframe works so that
it's now sandboxed (see main.js) which had some side-effects and required
updates to this widget and how it handled these special error cases. If we
update things in the future we need to first confirm that the sandbox is
still in place, the fetch request in the test sketch should update <main>
with the "blocked" message (NOT a GitHub payload)

we should also see 4 error markers in this sketch:
1. a CORS error on line 3 for the <img>
2. a warning about download attributes on line 5
3. a mixed-content warning for the <iframe> on line 9
4. a localStorage warning on line 18

also, if we address the localStorage error we should see:
5. a browser/console reference error for foo() on line 20

*/

review (obj = {}) {
// update issues passed to .review({ issues }) from main.js
if (obj.issues) this.issues = obj.issues
// check for any custom issues
this._checkForMixedContent()
// mark all linted issues
if (obj.issues) {
this.issues = obj.issues
this._resourceErrors = [] // code changed — clear stale resource errors
this._runtimeError = null // code changed — clear stale runtime error
}
const c = WIDGETS['student-session']
? WIDGETS['student-session'].getData('chattiness') : null
if (c && c !== 'low') this._markIssues(this.issues, true)
// clear markers before _updateError adds its runtime error marker so it isn't wiped
if (c && c !== 'low') NNE.clearMarkers(['orange', 'red'])
else NNE.marker(null)
// check for any custom issues
this._checkForMixedContent()
this._checkForDownloadLinks()
// update errors passed to .review({ error }) from main.js
// must run before _checkForResourceErrors (may push to this._resourceErrors)
this._updateError(obj.error)
this._checkForResourceErrors()
// mark all linted issues (false = don't clear, _applyRuntimeMarker handles that)
if (c && c !== 'low') this._markIssues(this.issues, false)
// re-apply runtime error marker last so _markIssues can't clear it
this._applyRuntimeMarker()
// update this code review widget's view
this._reviewIssues()
}
Expand Down Expand Up @@ -60,12 +97,17 @@ class CodeReview extends Widget {
const c = ss ? ss.getData('chattiness') : null
// event object from 'onerror' event injected into iframe either via
// utils.setCustomRenderer (sketch) or files-db-service-worker.js (project)
if (event?.data?.type === 'iframe-error' && event.data.src) {
// resource load failure (img, script, link) — queue for _checkForResourceErrors
this._resourceErrors.push(event.data.src)
return
}
if (c && c !== 'low' && event?.data && event?.data.type === 'iframe-error') {
// these are the number of lines added to the top of the file before rendering
// (see errMsgr in utils.setCustomRenderer)
const diff = 4
const diff = 9
const message = event.data.message
let file = event.data.source.includes('.') ? event.data.source : null
let file = event.data.source?.includes('.') ? event.data.source : null
let line = event.data.lineno - diff

// if project, ensure only showing errors on current file
Expand All @@ -75,18 +117,26 @@ class CodeReview extends Widget {
if (!file.endsWith('.html')) line += diff
}

// add error markers (aka _markErrors)
const m = NNE.marker(line, 'red', () => {
// _explainError
this.error = { message, line, file }
this.convos = window.CONVOS[this.key](this)
window.convo = new Convo(this.convos, 'custom-renderer-error')
})
m.setAttribute('title', message)
m.dataset.err = JSON.stringify({ message, line, file })
// detect sandbox security errors (localStorage, sessionStorage, indexedDB, etc.)
const isSandboxErr = message.includes('allow-same-origin') || message.toLowerCase().includes('sandboxed')
// store so review() can re-apply the marker after _markIssues clears markers
this._runtimeError = { message, line, file, isSandboxErr }
}
}

_applyRuntimeMarker () {
if (!this._runtimeError) return
const { message, line, file, isSandboxErr } = this._runtimeError
const convoId = isSandboxErr ? 'sandbox-security-error' : 'custom-renderer-error'
const m = NNE.marker(line, 'red', () => {
this.error = { message, line, file }
this.convos = window.CONVOS[this.key](this)
window.convo = new Convo(this.convos, convoId)
})
m.setAttribute('title', message)
m.dataset.err = JSON.stringify({ message, line, file })
}

_findErrors () { // list console error objects
return Array.from(document.querySelectorAll('.netitor-gutter-marker'))
.filter(ele => ele.dataset.err)
Expand Down Expand Up @@ -164,6 +214,27 @@ class CodeReview extends Widget {

// •.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸ CUSTOM ERRORS

_checkForResourceErrors () {
if (!this._resourceErrors.length) return
const lines = NNE.cm.getValue().split('\n')
this._resourceErrors.forEach(src => {
const parts = src.split('/')
const filename = parts[parts.length - 1]
lines.forEach((str, i) => {
if (!str.includes(src)) return
this.issues.push({
type: 'error',
language: 'html',
message: 'Failed to load: ' + src,
friendly: 'The file <b>' + filename + '</b> failed to load, it may be blocked by the server\'s <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">CORS policy</a> or the URL may be incorrect.',
line: i + 1,
col: 0
})
})
})
this._resourceErrors = []
}

_checkForMixedContent () {
if (NNE.language !== 'html') return
const before = ['src="', 'href="']
Expand All @@ -187,6 +258,25 @@ class CodeReview extends Widget {
}
}

_checkForDownloadLinks () {
if (NNE.language !== 'html') return
if (WIDGETS['project-files']?.projectData?.name) return // sandbox removed for projects
const lines = NNE.code.split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const LINE = lines[i]
if (LINE.includes('<a') && /\bdownload\b/.test(LINE)) {
this.issues.push({
type: 'warning',
language: 'html',
message: 'download attribute blocked by sandbox',
friendly: 'It seems you\'re using the <code>download</code> attribute on an anchor tag. In this studio your sketches get rendered into a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox" target="_blank">sandboxed</a> iframe for security, which prevents file downloads from being triggered.',
line: i + 1,
col: LINE.indexOf('download')
})
}
}
}

// •.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*•.¸¸¸.•*

_createHTML () {
Expand Down
4 changes: 4 additions & 0 deletions www/widgets/project-files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@ class ProjectFiles extends Widget {
NNE.code = ''
NNE.language = 'html'
NNE.wrap = WIDGETS['student-session'].getData('wrap') === 'true'
// restore sandbox now that we're back in sketch mode
NNE.iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-modals allow-pointer-lock')
// close widget
this.close()
}
Expand Down Expand Up @@ -1950,6 +1952,8 @@ class ProjectFiles extends Widget {
console.error('ProjectFiles: no service worker support in this browser')
return
}
// project files need same-origin iframe so the SW can intercept requests
NNE.iframe.removeAttribute('sandbox')

if (this.log) console.log('ProjectFiles: service worker loading')

Expand Down
6 changes: 5 additions & 1 deletion www/widgets/tutorial-maker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ class TutorialMaker extends Widget {
// quit hyper video player so it runs the "reset"
if (this.hvp) this.hvp.quit()
this.hvp = null
// restore sandbox now that we're back in sketch mode
NNE.iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-modals allow-pointer-lock')
}

// ........................... update data ...................................
Expand Down Expand Up @@ -296,7 +298,7 @@ class TutorialMaker extends Widget {

_openPopup (type, payload) {
const url = 'widgets/tutorial-maker/popups/index.html'
this.popup = window.open(url, 'tut-mkr-widget', 'width=485,height=167')
this.popup = window.open(url, 'tut-mkr-widget', 'width=485,height=234')
// keep an eye on the pop up to see if it closed
this.popupWatcher = setInterval(() => {
if (this.popup && this.popup.closed) {
Expand Down Expand Up @@ -330,6 +332,8 @@ class TutorialMaker extends Widget {
_setCustomRenderer () {
// custom renderer so that the iframe gets resolved by the ServiceWorker
const ready = navigator.serviceWorker.controller && this.hvp?.data
// tutorial files need same-origin iframe so the SW can intercept requests
if (ready) NNE.iframe.removeAttribute('sandbox')
NNE.customRender = (eve) => {
if (ready) {
eve.iframe.src = `/TUTORIAL_MAKER/${this.hvp.data.id}/index.html`
Expand Down
2 changes: 1 addition & 1 deletion www/widgets/tutorial-maker/popups/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<section id="start" class="overlay">
<p>
Welcome to the Tutorial Maker! This widget can be used to create your own interactive netnet tutorials. To learn how take a look at the <a id="docs-link" href="#" target="_blank">Interactive Tutorial docs</a>.
Welcome to the Tutorial Maker! This widget can be used to load and/or create custom interactive netnet tutorials. To learn how create your own take a look at the <a id="docs-link" href="#" target="_blank">Interactive Tutorial docs</a>. If you're going to open a tutorial someone else made, make sure you trust the source because tutorials can run arbitrary code in your netnet environment.
</p>
<button id="new" class="pill-btn pill-btn--secondary">new</button>
<button id="open" class="pill-btn pil-btn--secondary">open</button>
Expand Down