worker-pastebin/src/index.coffee

235 lines
7.0 KiB
CoffeeScript

import * as util from './util'
import * as crypto from './crypto'
import S3 from './aws/s3'
import config from '../config.json'
import configInsec from '../config.insecure.json'
import indexHtml from '../worker/index.html'
FRONTEND_PATHS = [
'/', '/paste/text', '/paste/binary',
'/paste/text/', '/paste/binary/'
]
FRONTEND_SHA256 = null
s3 = new S3 config
main = ->
addEventListener 'fetch', (event) =>
event.respondWith handleRequest event
buildInvalidResponse = (msg) ->
if not msg
msg = "Invalid Request"
new Response msg,
status: 400
buildFrontendResponse = (req) ->
if req.headers.has "if-none-match"
if req.headers.get("if-none-match") == "W/" + FRONTEND_SHA256
# Skip this response if the frontend was not updated
return new Response null,
status: 304
new Response indexHtml,
status: 200
headers:
'content-type': 'text/html'
'etag': "W/" + FRONTEND_SHA256
handleRequest = (event) ->
# Ensure we have a SHA256 value of frontend first
# This will be used as ETag
if not FRONTEND_SHA256
FRONTEND_SHA256 = crypto.hex await crypto.SHA256 indexHtml
# Handle request for static home page first
if event.request.method == "GET"
parsedURL = new URL event.request.url
if parsedURL.pathname in FRONTEND_PATHS
return buildFrontendResponse event.request
# Validate file name first, since this is shared logic
file = util.getFileName event.request.url
if not file
return buildInvalidResponse()
# Handle PUT and GET separately
if event.request.method == "PUT"
handlePUT event.request, file
else if event.request.method == "GET"
handleGET event.request, file
else
buildInvalidResponse()
handlePUT = (req, file) ->
if not util.validateLength req
return buildInvalidResponse "Maximum upload size: " + util.MAX_UPLOAD_SIZE
if file.length > util.MAX_FILENAME_LENGTH
return buildInvalidResponse "File name too long (max #{util.MAX_FILENAME_LENGTH})"
if file != configInsec.remote_fetch_magic
handleClientUpload req, file
else
# If the file name is magic, we fetch content from remote
handleRemoteFetch req
generateID = ->
id = null
path = null
loop
id = util.randomID()
path = util.idToPath id
files = await s3.listObjects
prefix: path
break if !files.Contents or files.Contents.length == 0
[id, path]
handleClientUpload = (req, file) ->
# Generate a valid ID first
[id, path] = await generateID()
path = path + "/" + file
len = req.headers.get "content-length"
# Upload the file to S3
try
await s3.putObject path, req.body, # Expiration should be configured on S3 side
ContentType: req.headers.get "content-type"
ContentLength: len
catch err
console.log err
return buildInvalidResponse err
# Simply return the path in body
new Response "/paste/" + id,
status: 200
handleGET = (req, file) ->
path = util.idToPath file
# Find the file first, because ID is only the path part
# We still need the real file name
files = await s3.listObjects
prefix: path
if not files.Contents or files.Contents.length == 0
return new Response "Not Found",
status: 404
else if req.url.endsWith "crypt"
# We need frontend to handle encrypted files
# The key is passed after the hash ('#'), unavailable to server
return buildFrontendResponse req
# The full path to the original file
fullPath = files.Contents[0].Key
fileName = fullPath.split '/'
.pop()
# Build options and downlaod the file from origin
options = {}
# Handle range header
if req.headers.has "range"
options["range"] = req.headers.get "range"
resp = await s3.getObject fullPath, options
if not resp.ok
return new Response "Something went wrong",
status: resp.status
# If the content is text, and the user is using a browser
# show frontend code viewer
if not req.url.endsWith 'original'
isText = util.isText resp.headers.get 'content-type'
isBrowser = util.isBrowser req
if isText and isBrowser
return buildFrontendResponse req
# Build response headers
headers =
'content-length': resp.headers.get 'content-length'
'accept-ranges': 'bytes'
# TODO: handle text/* with a code viewer of some sort
'content-type': resp.headers.get 'content-type'
# Prevent executing random HTML / XML by treating all text as `text/plain`
if headers['content-type'].startsWith 'text/'
headers['content-type'] = 'text/plain'
# Add content-disposition header to indicate file name
inline = util.shouldShowInline headers['content-type']
headers['content-disposition'] =
(if inline then 'inline;' else 'attachment;') + ' filename*=' + encodeURIComponent fileName
# Handle ranged resposes
if resp.headers.has 'content-range'
headers['content-range'] = resp.headers.get 'content-range'
new Response resp.body,
status: resp.status
headers: headers
handleRemoteFetch = (req) ->
# We support fetching files from a remote URL
# given that the length is within a separate constraint
# Determine if we are actually getting an URL first
if req.headers.get("content-type") != "text/plain"
return buildInvalidResponse "Invalid URL"
if !req.headers.has("content-length") or req.headers.get("content-length") >= 1024
return buildInvalidResponse "Invalid URL"
# Get the URL
url = await req.text()
# Validate URL first
try
url = new URL url
throw "WTF" if url.protocol != "http:" and url.protocol != "https:"
catch err
return buildInvalidResponse "Invalid URL"
# Request the remote content
remote_res = await fetch url,
redirect: "follow"
if remote_res.status != 200
return buildInvalidResponse "Remote server returned error status"
# Check the length first
# TODO: Maybe we need to limit another length for remote fetch pastes
if !remote_res.headers.has("content-length") or remote_res.headers.get("content-length") > util.MAX_UPLOAD_SIZE
return buildInvalidResponse "Remote file too large"
# Try to determine the file name
path_splitted = url.pathname.split '/'
fileName = path_splitted[path_splitted.length - 1]
if remote_res.headers.has "content-disposition"
dispos = remote_res.headers.get "content-disposition"
for item of dispos.split ';'
item = item.trim()
if item.startsWith("filename=") or item.startsWith("filename*=")
fileName = item.replace "filename=", ""
.replace "filename*=", ""
.replace "\"", ""
break
console.log fileName
if fileName.length > util.MAX_FILENAME_LENGTH
return buildInvalidResponse "Remote file name too long"
# Begin uploading
[id, path] = await generateID()
path = path + "/" + fileName
len = remote_res.headers.get "content-length"
try
await s3.putObject path, remote_res.body,
ContentType: remote_res.headers.get "content-type"
ContentLength: len
catch err
console.log err
return buildInvalidResponse err
# Simply return the path in body
new Response "/paste/" + id,
status: 200
export default main