This commit is contained in:
zjcqoo 2021-10-26 22:43:00 +08:00
commit 4724d62eac
3 changed files with 367 additions and 0 deletions

59
README.md Normal file
View File

@ -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

73
index.html Normal file
View File

@ -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>

235
sw.js Normal file
View File

@ -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, '&nbsp;')
}
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()
}