init
This commit is contained in:
commit
4724d62eac
|
@ -0,0 +1,59 @@
|
|||
# HTTP Server Online
|
||||
|
||||
Start a local HTTP server without any tools, just open a web page.
|
||||
|
||||
## Try It Online
|
||||
|
||||
https://http-server.etherdream.com
|
||||
|
||||
https://user-images.githubusercontent.com/1072787/138898490-e4de9326-1715-415b-bbb6-3a377faaf618.mp4
|
||||
|
||||
## Custom 404
|
||||
|
||||
If `/path/to/foo` does not exist, we will try:
|
||||
|
||||
* `/path/to/404.html`
|
||||
|
||||
* `/path/404.html`
|
||||
|
||||
* `/404.html`
|
||||
|
||||
* return `404 Not Found`
|
||||
|
||||
## Index Page
|
||||
|
||||
For `/path/to/`, we will try:
|
||||
|
||||
* `/path/to/index.html`
|
||||
|
||||
* 404.html (`/path/to/404.html`, `/path/404.html`, `/404.html`)
|
||||
|
||||
* Directory Indexing
|
||||
|
||||
## Trailing Slash
|
||||
|
||||
If `/path/to` is a directory, we will redirect to `/path/to/`.
|
||||
|
||||
## Stop
|
||||
|
||||
Access `/?stop` to stop the server.
|
||||
|
||||
## Limitations
|
||||
|
||||
* Your browser must support `File System Access API` and `Service Worker API`
|
||||
|
||||
* The target website can not use `Service Worker API` because it is already occupied
|
||||
|
||||
* The target website can only be accessed in the same browser and session
|
||||
|
||||
## TODO
|
||||
|
||||
This is just a toy that took a few hours, and more interesting features will be added later.
|
||||
|
||||
* Tunnel (expose local server to the Internet)
|
||||
|
||||
* P2P (HTTP over WebRTC)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,73 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>HTTP Server</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<style>
|
||||
#btnOpenDir {
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#txtErr {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button id="btnOpenDir" disabled>Select Web Directory</button>
|
||||
<div id="txtErr"></div>
|
||||
<div id="divCmd" hidden>
|
||||
<div>Stop Server: <a id="linkStop" href="/?stop" target="_blank"></a></div>
|
||||
</div>
|
||||
<script>
|
||||
const sw = navigator.serviceWorker
|
||||
|
||||
btnOpenDir.onclick = async() => {
|
||||
try {
|
||||
var dirHandle = await showDirectoryPicker()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (location.search === '?stop') {
|
||||
history.pushState(null, '', '/')
|
||||
}
|
||||
sw.controller.postMessage(dirHandle)
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
txtErr.textContent = msg
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!self.showDirectoryPicker) {
|
||||
showError('File System Access API is not supported')
|
||||
return
|
||||
}
|
||||
if (!sw) {
|
||||
showError('Service Worker API is not supported')
|
||||
return
|
||||
}
|
||||
if (!sw.controller) {
|
||||
try {
|
||||
await sw.register('sw.js')
|
||||
} catch (err) {
|
||||
showError(err.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
sw.onmessage = (e) => {
|
||||
if (e.data === 'GOT') {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
btnOpenDir.disabled = false
|
||||
divCmd.hidden = false
|
||||
linkStop.textContent = linkStop.href
|
||||
}
|
||||
main()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,235 @@
|
|||
'use strict'
|
||||
let mStopFlag = true
|
||||
let mRootDirHandle
|
||||
|
||||
|
||||
function formatSize(n) {
|
||||
let i = 0
|
||||
while (n >= 1024) {
|
||||
n /= 1024
|
||||
i++
|
||||
}
|
||||
if (i === 0) {
|
||||
return n + 'B'
|
||||
}
|
||||
return n.toFixed(1) + 'kMGTP'[i - 1]
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return str
|
||||
.replace(/&|<|>/g, s => '&#' + s.charCodeAt(0) + ';')
|
||||
.replace(/\s/g, ' ')
|
||||
}
|
||||
|
||||
async function listDir(dirHandle, dirPath) {
|
||||
const DIR_PREFIX = '\x00' // for sort
|
||||
const keys = []
|
||||
const sizeMap = {}
|
||||
|
||||
if (dirPath !== '/') {
|
||||
keys[0] = DIR_PREFIX + '..'
|
||||
}
|
||||
|
||||
for await (const [name, handle] of dirHandle) {
|
||||
if (handle.kind === 'file') {
|
||||
keys.push(name)
|
||||
const file = await handle.getFile()
|
||||
sizeMap[name] = file.size
|
||||
} else {
|
||||
keys.push(DIR_PREFIX + name)
|
||||
}
|
||||
}
|
||||
|
||||
const tableRows = keys.sort().map(key => {
|
||||
let icon, size, name
|
||||
|
||||
if (key.startsWith(DIR_PREFIX)) {
|
||||
icon = '📂'
|
||||
size = ''
|
||||
name = key.substr(1) + '/'
|
||||
} else {
|
||||
icon = '📄'
|
||||
size = formatSize(sizeMap[key])
|
||||
name = key
|
||||
}
|
||||
return `\
|
||||
<tr>
|
||||
<td class="icon">${icon}</td>
|
||||
<td class="size">${size}</td>
|
||||
<td class="name"><a href="${escape(name)}">${escHtml(name)}</a></td>
|
||||
</tr>`
|
||||
})
|
||||
|
||||
const now = new Date().toLocaleString()
|
||||
const html = `\
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Index of ${escHtml(dirPath)}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<style>
|
||||
td {
|
||||
font-family: monospace;
|
||||
}
|
||||
td.size {
|
||||
text-align: right;
|
||||
width: 4em;
|
||||
}
|
||||
td.name {
|
||||
padding-left: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of ${escHtml(dirPath)}</h1>
|
||||
<table>
|
||||
${tableRows.join('\n')}
|
||||
</table>
|
||||
<br>
|
||||
<address>Powered by Service Worker (${now})</address>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return new Response(html, {
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function find404(dirHandles) {
|
||||
for (const dirHandle of dirHandles.reverse()) {
|
||||
const fileHandle = await getSubFile(dirHandle, '404.html')
|
||||
if (fileHandle) {
|
||||
const file = await fileHandle.getFile()
|
||||
return new Response(file.stream(), {
|
||||
status: 404,
|
||||
headers: {
|
||||
'content-type': file.type,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function make404() {
|
||||
return new Response('404 Not Found', {
|
||||
status: 404,
|
||||
})
|
||||
}
|
||||
|
||||
async function getSubDir(dirHandle, dirName) {
|
||||
try {
|
||||
return await dirHandle.getDirectoryHandle(dirName)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
async function getSubFile(dirHandle, fileName) {
|
||||
try {
|
||||
return await dirHandle.getFileHandle(fileName)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
async function getSubFileOrDir(dirHandle, fileName) {
|
||||
return await getSubFile(dirHandle, fileName) ||
|
||||
await getSubDir(dirHandle, fileName)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @param {Request} req
|
||||
*/
|
||||
async function respond(url, req) {
|
||||
if (url.search === '?stop' && req.mode === 'navigate') {
|
||||
console.log('[sw] stop server')
|
||||
mStopFlag = true
|
||||
return Response.redirect('/')
|
||||
}
|
||||
const dirNames = unescape(url.pathname).replace(/^\/+/, '').split(/\/+/)
|
||||
const fileName = dirNames.pop()
|
||||
const dirHandles = [mRootDirHandle]
|
||||
let dirHandle = mRootDirHandle
|
||||
let dirPath = '/'
|
||||
|
||||
for (const dir of dirNames) {
|
||||
dirHandle = await getSubDir(dirHandle, dir)
|
||||
if (!dirHandle) {
|
||||
return await find404(dirHandles) || make404()
|
||||
}
|
||||
dirHandles.push(dirHandle)
|
||||
dirPath += `${dir}/`
|
||||
}
|
||||
|
||||
const handle = await getSubFileOrDir(dirHandle, fileName || 'index.html')
|
||||
if (!handle) {
|
||||
const res = await find404(dirHandles)
|
||||
if (res) {
|
||||
return res
|
||||
}
|
||||
return fileName
|
||||
? make404()
|
||||
: listDir(dirHandle, dirPath)
|
||||
}
|
||||
|
||||
if (handle.kind === 'directory') {
|
||||
return Response.redirect(dirPath + fileName + '/')
|
||||
}
|
||||
|
||||
/** @type {File} */
|
||||
let file = await handle.getFile()
|
||||
|
||||
/** @type {ResponseInit} */
|
||||
const resOpt = {
|
||||
headers: {
|
||||
'content-type': file.type,
|
||||
},
|
||||
}
|
||||
|
||||
const range = req.headers.get('range')
|
||||
if (range) {
|
||||
// only consider `bytes=begin-end` or `bytes=begin-`
|
||||
const m = range.match(/bytes=(\d+)-(\d*)/)
|
||||
if (m) {
|
||||
const size = file.size
|
||||
const begin = +m[1]
|
||||
const end = +m[2] || size
|
||||
|
||||
file = file.slice(begin, end)
|
||||
resOpt.status = 206
|
||||
resOpt.headers['content-range'] = `bytes ${begin}-${end-1}/${size}`
|
||||
}
|
||||
}
|
||||
|
||||
resOpt.headers['content-length'] = file.size
|
||||
return new Response(file.stream(), resOpt)
|
||||
}
|
||||
|
||||
onfetch = (e) => {
|
||||
if (mStopFlag) {
|
||||
return
|
||||
}
|
||||
console.assert(mRootDirHandle)
|
||||
|
||||
const req = e.request
|
||||
const url = new URL(req.url)
|
||||
if (url.origin !== location.origin) {
|
||||
return
|
||||
}
|
||||
e.respondWith(respond(url, req))
|
||||
}
|
||||
|
||||
onmessage = (e) => {
|
||||
const path = new URL(e.source.url).pathname
|
||||
if (path === '/' || path === '/index.html') {
|
||||
mRootDirHandle = e.data
|
||||
mStopFlag = false
|
||||
e.source.postMessage('GOT')
|
||||
}
|
||||
}
|
||||
|
||||
onactivate = () => {
|
||||
clients.claim()
|
||||
}
|
Loading…
Reference in New Issue