136 lines
5.1 KiB
JavaScript
136 lines
5.1 KiB
JavaScript
/*
|
|
* Copyright (C) 2020 - present Instructure, Inc.
|
|
*
|
|
* This file is part of Canvas.
|
|
*
|
|
* Canvas is free software: you can redistribute it and/or modify it under
|
|
* the terms of the GNU Affero General Public License as published by the Free
|
|
* Software Foundation, version 3 of the License.
|
|
*
|
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
* details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License along
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/* Hosted Canvas uses its own Inst-FS service as a file service to host and
|
|
* serve user-uploaded files. This service however is served from an
|
|
* Instructure domain name distinct from Canvas; "fixing" this is not feasible
|
|
* because of the preponderance of "vanity domains", where Canvas is delivered
|
|
* on a school's domain. This leads to the domain being treated as a
|
|
* third-party despite being an integral part of the Canvas "ecosystem".
|
|
*
|
|
* Aggressive third-party tracking prevention by Safari 13.1+ breaks Inst-FS'
|
|
* ability to use cookies to authenticate users' access to Canvas files. This
|
|
* service worker allows the Canvas front-end to assert Inst-FS authorization
|
|
* via HTTP headers instead of relying on browser cookies.
|
|
*
|
|
* However, it's preferrable to authenticate via cookie when possible, since
|
|
* this allows Inst-FS to further redirect the user to a cookie-authenticated
|
|
* CDN endpoint. As such, this ServiceWorker is only expected to be installed
|
|
* for Safari 13.1+.
|
|
*/
|
|
|
|
// Ensure we take effect as quickly as possible.
|
|
// eslint-disable-next-line no-restricted-globals
|
|
self.addEventListener('install', function (event) {
|
|
// The promise that skipWaiting() returns can be safely ignored.
|
|
// eslint-disable-next-line no-restricted-globals
|
|
self.skipWaiting()
|
|
})
|
|
|
|
// eslint-disable-next-line no-restricted-globals
|
|
self.addEventListener('fetch', function (event) {
|
|
if (eligibleRequest(event.request)) {
|
|
event.respondWith(fetchFile(event.request))
|
|
}
|
|
})
|
|
|
|
function eligibleRequest(request) {
|
|
const url = new URL(request.url)
|
|
return (
|
|
request.method === 'GET' &&
|
|
request.mode !== 'navigate' &&
|
|
!imagePreviewWithoutInstfs(url) &&
|
|
eligiblePath(url.pathname)
|
|
)
|
|
}
|
|
|
|
// most files links we care about look like this regex fragment with a context
|
|
// of some sort in front
|
|
const commonFilePath = `/files/[^/]+/(preview|download(.\\w+)?)`
|
|
|
|
function contextFilePath(context) {
|
|
return new RegExp(`^/${context}/[^/]+${commonFilePath}$`)
|
|
}
|
|
|
|
function imagePreviewWithoutInstfs(url) {
|
|
// Image preview without inst-fs (files domain)
|
|
// causes some CORS issues. We ignore that path here.
|
|
const previewPath = new RegExp(`/files/[^/]+/preview(.\\w+)?`)
|
|
return (
|
|
previewPath.test(url.pathname) &&
|
|
url.searchParams.has('instfs') &&
|
|
url.searchParams.get('instfs') == 'false'
|
|
)
|
|
}
|
|
|
|
const eligiblePaths = [
|
|
contextFilePath(`accounts`),
|
|
contextFilePath(`assessment_questions`),
|
|
contextFilePath(`assignments`),
|
|
contextFilePath(`courses`),
|
|
contextFilePath(`groups`),
|
|
contextFilePath(`quizzes/quiz_submissions`),
|
|
contextFilePath(`quiz_statistics`),
|
|
contextFilePath(`users`),
|
|
|
|
// without a context is also valid
|
|
new RegExp(`^${commonFilePath}$`),
|
|
|
|
// these are some outliers that can still benefit from the service worker
|
|
new RegExp(`^/images/thumbnails/(show/)?[^/]+/[^/]+$`),
|
|
new RegExp(`^/api/lti/assignments/[^/]+/submissions/[^/]+/attachment/[^/]+$`)
|
|
]
|
|
|
|
function eligiblePath(pathname) {
|
|
return eligiblePaths.some(function (candidate) {
|
|
return candidate.test(pathname)
|
|
})
|
|
}
|
|
|
|
function fetchFile(request) {
|
|
// by providing this header, we're asking canvas to try and give us the
|
|
// external location of the canvas file, with a short-lived authentication
|
|
// token for the location, rather than redirecting us to it. if canvas can do
|
|
// that, it will return that location in a JSON body as well as echoing back
|
|
// the header so we can know it was honored.
|
|
//
|
|
// if the header is not included in the response, we just use the response
|
|
// directly, successful or not.
|
|
//
|
|
// otherwise, we request the provided location, use the provided token in the
|
|
// Authorization header. the external service is expected to accept this
|
|
// token in lieu of a session cookie to authenticate the user.
|
|
//
|
|
// for non-inst-fs this will still redirect to s3, so we need cors mode, but
|
|
// we only want to send credentials to canvas.
|
|
const mode = 'cors'
|
|
const credentials = 'same-origin'
|
|
const headers = new Headers({'X-Canvas-File-Location': 'True'})
|
|
return fetch(request, {mode, credentials, headers}).then(function (response) {
|
|
if (response.ok && response.headers.has('X-Canvas-File-Location')) {
|
|
return response.text().then(function (body) {
|
|
const {location, token} = JSON.parse(body)
|
|
const instfsHeaders = new Headers({Authorization: `Bearer ${token}`})
|
|
return fetch(location, {headers: instfsHeaders})
|
|
})
|
|
} else {
|
|
return response
|
|
}
|
|
})
|
|
}
|