Created
September 17, 2020 11:32
-
-
Save kdy1/fee62b5f2e6dcc4639e2f16277978ccd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const encoder = new TextEncoder(); | |
function encode(input) { | |
return encoder.encode(input); | |
} | |
const decoder = new TextDecoder(); | |
function decode(input) { | |
return decoder.decode(input); | |
} | |
const DEFAULT_BUF_SIZE = 4096; | |
const MIN_BUF_SIZE = 16; | |
const MAX_CONSECUTIVE_EMPTY_READS = 100; | |
const CR = "\r".charCodeAt(0); | |
const LF = "\n".charCodeAt(0); | |
class BufferFullError extends Error { | |
name = "BufferFullError"; | |
constructor(partial){ | |
super("Buffer full"); | |
this.partial = partial; | |
} | |
} | |
class BufReader { | |
r = 0; | |
w = 0; | |
eof = false; | |
static create(r, size = DEFAULT_BUF_SIZE) { | |
return r instanceof BufReader ? r : new BufReader(r, size); | |
} | |
constructor(rd1, size1 = DEFAULT_BUF_SIZE){ | |
if (size1 < MIN_BUF_SIZE) { | |
size1 = MIN_BUF_SIZE; | |
} | |
this._reset(new Uint8Array(size1), rd1); | |
} | |
size() { | |
return this.buf.byteLength; | |
} | |
buffered() { | |
return this.w - this.r; | |
} | |
async _fill() { | |
if (this.r > 0) { | |
this.buf.copyWithin(0, this.r, this.w); | |
this.w -= this.r; | |
this.r = 0; | |
} | |
if (this.w >= this.buf.byteLength) { | |
throw Error("bufio: tried to fill full buffer"); | |
} | |
for(let i = MAX_CONSECUTIVE_EMPTY_READS; i > 0; i--){ | |
const rr = await this.rd.read(this.buf.subarray(this.w)); | |
if (rr === null) { | |
this.eof = true; | |
return; | |
} | |
assert(rr >= 0, "negative read"); | |
this.w += rr; | |
if (rr > 0) { | |
return; | |
} | |
} | |
throw new Error(`No progress after ${MAX_CONSECUTIVE_EMPTY_READS} read() calls`); | |
} | |
reset(r) { | |
this._reset(this.buf, r); | |
} | |
_reset(buf, rd) { | |
this.buf = buf; | |
this.rd = rd; | |
this.eof = false; | |
} | |
async read(p) { | |
let rr = p.byteLength; | |
if (p.byteLength === 0) return rr; | |
if (this.r === this.w) { | |
if (p.byteLength >= this.buf.byteLength) { | |
const rr1 = await this.rd.read(p); | |
const nread = rr1 ?? 0; | |
assert(nread >= 0, "negative read"); | |
return rr1; | |
} | |
this.r = 0; | |
this.w = 0; | |
rr = await this.rd.read(this.buf); | |
if (rr === 0 || rr === null) return rr; | |
assert(rr >= 0, "negative read"); | |
this.w += rr; | |
} | |
const copied = copyBytes(this.buf.subarray(this.r, this.w), p, 0); | |
this.r += copied; | |
return copied; | |
} | |
async readFull(p) { | |
let bytesRead = 0; | |
while(bytesRead < p.length){ | |
try { | |
const rr = await this.read(p.subarray(bytesRead)); | |
if (rr === null) { | |
if (bytesRead === 0) { | |
return null; | |
} else { | |
throw new PartialReadError(); | |
} | |
} | |
bytesRead += rr; | |
} catch (err) { | |
err.partial = p.subarray(0, bytesRead); | |
throw err; | |
} | |
} | |
return p; | |
} | |
async readByte() { | |
while(this.r === this.w){ | |
if (this.eof) return null; | |
await this._fill(); | |
} | |
const c = this.buf[this.r]; | |
this.r++; | |
return c; | |
} | |
async readString(delim) { | |
if (delim.length !== 1) { | |
throw new Error("Delimiter should be a single character"); | |
} | |
const buffer = await this.readSlice(delim.charCodeAt(0)); | |
if (buffer === null) return null; | |
return new TextDecoder().decode(buffer); | |
} | |
async readLine() { | |
let line; | |
try { | |
line = await this.readSlice(LF); | |
} catch (err) { | |
let { partial: partial1 } = err; | |
assert(partial1 instanceof Uint8Array, "bufio: caught error from `readSlice()` without `partial` property"); | |
if (!(err instanceof BufferFullError)) { | |
throw err; | |
} | |
if (!this.eof && partial1.byteLength > 0 && partial1[partial1.byteLength - 1] === CR) { | |
assert(this.r > 0, "bufio: tried to rewind past start of buffer"); | |
this.r--; | |
partial1 = partial1.subarray(0, partial1.byteLength - 1); | |
} | |
return { | |
line: partial1, | |
more: !this.eof | |
}; | |
} | |
if (line === null) { | |
return null; | |
} | |
if (line.byteLength === 0) { | |
return { | |
line, | |
more: false | |
}; | |
} | |
if (line[line.byteLength - 1] == LF) { | |
let drop = 1; | |
line = line.subarray(0, line.byteLength - drop); | |
} | |
return { | |
line, | |
more: false | |
}; | |
} | |
async readSlice(delim) { | |
let s = 0; | |
let slice; | |
while(true){ | |
let i = this.buf.subarray(this.r + s, this.w).indexOf(delim); | |
if (i >= 0) { | |
i += s; | |
slice = this.buf.subarray(this.r, this.r + i + 1); | |
this.r += i + 1; | |
break; | |
} | |
if (this.eof) { | |
if (this.r === this.w) { | |
return null; | |
} | |
slice = this.buf.subarray(this.r, this.w); | |
this.r = this.w; | |
break; | |
} | |
if (this.buffered() >= this.buf.byteLength) { | |
this.r = this.w; | |
const newbuf = this.buf.slice(0); | |
this.buf = newbuf; | |
throw new BufferFullError(oldbuf); | |
} | |
s = this.w - this.r; | |
try { | |
await this._fill(); | |
} catch (err) { | |
err.partial = slice; | |
throw err; | |
} | |
} | |
return slice; | |
} | |
async peek(n) { | |
if (n < 0) { | |
throw Error("negative count"); | |
} | |
let avail = this.w - this.r; | |
while(avail < n && avail < this.buf.byteLength && !this.eof){ | |
try { | |
await this._fill(); | |
} catch (err) { | |
err.partial = this.buf.subarray(this.r, this.w); | |
throw err; | |
} | |
avail = this.w - this.r; | |
} | |
if (avail === 0 && this.eof) { | |
return null; | |
} else if (avail < n && this.eof) { | |
return this.buf.subarray(this.r, this.r + avail); | |
} else if (avail < n) { | |
throw new BufferFullError(this.buf.subarray(this.r, this.w)); | |
} | |
return this.buf.subarray(this.r, this.r + n); | |
} | |
} | |
class AbstractBufBase { | |
usedBufferBytes = 0; | |
err = null; | |
size() { | |
return this.buf.byteLength; | |
} | |
available() { | |
return this.buf.byteLength - this.usedBufferBytes; | |
} | |
buffered() { | |
return this.usedBufferBytes; | |
} | |
} | |
class BufWriter extends AbstractBufBase { | |
static create(writer, size = DEFAULT_BUF_SIZE) { | |
return writer instanceof BufWriter ? writer : new BufWriter(writer, size); | |
} | |
constructor(writer1, size2 = DEFAULT_BUF_SIZE){ | |
super(); | |
this.writer = writer1; | |
if (size2 <= 0) { | |
size2 = DEFAULT_BUF_SIZE; | |
} | |
this.buf = new Uint8Array(size2); | |
} | |
reset(w) { | |
this.err = null; | |
this.usedBufferBytes = 0; | |
this.writer = w; | |
} | |
async flush() { | |
if (this.err !== null) throw this.err; | |
if (this.usedBufferBytes === 0) return; | |
try { | |
await Deno.writeAll(this.writer, this.buf.subarray(0, this.usedBufferBytes)); | |
} catch (e) { | |
this.err = e; | |
throw e; | |
} | |
this.buf = new Uint8Array(this.buf.length); | |
this.usedBufferBytes = 0; | |
} | |
async write(data) { | |
if (this.err !== null) throw this.err; | |
if (data.length === 0) return 0; | |
let totalBytesWritten = 0; | |
let numBytesWritten = 0; | |
while(data.byteLength > this.available()){ | |
if (this.buffered() === 0) { | |
try { | |
numBytesWritten = await this.writer.write(data); | |
} catch (e) { | |
this.err = e; | |
throw e; | |
} | |
} else { | |
numBytesWritten = copyBytes(data, this.buf, this.usedBufferBytes); | |
this.usedBufferBytes += numBytesWritten; | |
await this.flush(); | |
} | |
data = data.subarray(numBytesWritten); | |
} | |
numBytesWritten = copyBytes(data, this.buf, this.usedBufferBytes); | |
this.usedBufferBytes += numBytesWritten; | |
totalBytesWritten += numBytesWritten; | |
return totalBytesWritten; | |
} | |
} | |
class BufWriterSync extends AbstractBufBase { | |
static create(writer, size = DEFAULT_BUF_SIZE) { | |
return writer instanceof BufWriterSync ? writer : new BufWriterSync(writer, size); | |
} | |
constructor(writer2, size3 = DEFAULT_BUF_SIZE){ | |
super(); | |
this.writer = writer2; | |
if (size3 <= 0) { | |
size3 = DEFAULT_BUF_SIZE; | |
} | |
this.buf = new Uint8Array(size3); | |
} | |
reset(w) { | |
this.err = null; | |
this.usedBufferBytes = 0; | |
this.writer = w; | |
} | |
flush() { | |
if (this.err !== null) throw this.err; | |
if (this.usedBufferBytes === 0) return; | |
try { | |
Deno.writeAllSync(this.writer, this.buf.subarray(0, this.usedBufferBytes)); | |
} catch (e) { | |
this.err = e; | |
throw e; | |
} | |
this.buf = new Uint8Array(this.buf.length); | |
this.usedBufferBytes = 0; | |
} | |
writeSync(data) { | |
if (this.err !== null) throw this.err; | |
if (data.length === 0) return 0; | |
let totalBytesWritten = 0; | |
let numBytesWritten = 0; | |
while(data.byteLength > this.available()){ | |
if (this.buffered() === 0) { | |
try { | |
numBytesWritten = this.writer.writeSync(data); | |
} catch (e) { | |
this.err = e; | |
throw e; | |
} | |
} else { | |
numBytesWritten = copyBytes(data, this.buf, this.usedBufferBytes); | |
this.usedBufferBytes += numBytesWritten; | |
this.flush(); | |
} | |
data = data.subarray(numBytesWritten); | |
} | |
numBytesWritten = copyBytes(data, this.buf, this.usedBufferBytes); | |
this.usedBufferBytes += numBytesWritten; | |
totalBytesWritten += numBytesWritten; | |
return totalBytesWritten; | |
} | |
} | |
function createLPS(pat) { | |
const lps = new Uint8Array(pat.length); | |
lps[0] = 0; | |
let prefixEnd = 0; | |
let i = 1; | |
while(i < lps.length){ | |
if (pat[i] == pat[prefixEnd]) { | |
prefixEnd++; | |
lps[i] = prefixEnd; | |
i++; | |
} else if (prefixEnd === 0) { | |
lps[i] = 0; | |
i++; | |
} else { | |
prefixEnd = pat[prefixEnd - 1]; | |
} | |
} | |
return lps; | |
} | |
async function* readDelim(reader, delim) { | |
const delimLen = delim.length; | |
const delimLPS = createLPS(delim); | |
let inputBuffer = new Deno.Buffer(); | |
const inspectArr = new Uint8Array(Math.max(1024, delimLen + 1)); | |
let inspectIndex = 0; | |
let matchIndex = 0; | |
while(true){ | |
const result = await reader.read(inspectArr); | |
if (result === null) { | |
yield inputBuffer.bytes(); | |
return; | |
} | |
if (result < 0) { | |
return; | |
} | |
const sliceRead = inspectArr.subarray(0, result); | |
await Deno.writeAll(inputBuffer, sliceRead); | |
let sliceToProcess = inputBuffer.bytes(); | |
while(inspectIndex < sliceToProcess.length){ | |
if (sliceToProcess[inspectIndex] === delim[matchIndex]) { | |
inspectIndex++; | |
matchIndex++; | |
} else { | |
if (matchIndex === 0) { | |
inspectIndex++; | |
} else { | |
matchIndex = delimLPS[matchIndex - 1]; | |
} | |
} | |
} | |
inputBuffer = new Deno.Buffer(sliceToProcess); | |
} | |
} | |
function deferred() { | |
let methods; | |
const promise = new Promise((resolve, reject)=>{ | |
}); | |
return Object.assign(promise, methods); | |
} | |
class MuxAsyncIterator { | |
iteratorCount = 0; | |
yields = []; | |
throws = []; | |
signal = deferred(); | |
add(iterator) { | |
++this.iteratorCount; | |
this.callIteratorNext(iterator); | |
} | |
async callIteratorNext(iterator) { | |
try { | |
const { value , done } = await iterator.next(); | |
if (done) { | |
--this.iteratorCount; | |
} else { | |
this.yields.push({ | |
iterator, | |
value | |
}); | |
} | |
} catch (e) { | |
this.throws.push(e); | |
} | |
this.signal.resolve(); | |
} | |
async *iterate() { | |
while(this.iteratorCount > 0){ | |
await this.signal; | |
for(let i = 0; i < this.yields.length; i++){ | |
const { iterator , value } = this.yields[i]; | |
yield value; | |
this.callIteratorNext(iterator); | |
} | |
if (this.throws.length) { | |
for (const e of this.throws){ | |
throw e; | |
} | |
this.throws.length = 0; | |
} | |
this.yields.length = 0; | |
this.signal = deferred(); | |
} | |
} | |
[Symbol.asyncIterator]() { | |
return this.iterate(); | |
} | |
} | |
const deferred1 = deferred, MuxAsyncIterator1 = MuxAsyncIterator; | |
export class ServerRequest { | |
done = deferred1(); | |
_contentLength = undefined; | |
get contentLength() { | |
if (this._contentLength === undefined) { | |
const cl = this.headers.get("content-length"); | |
if (cl) { | |
this._contentLength = parseInt(cl); | |
if (Number.isNaN(this._contentLength)) { | |
this._contentLength = null; | |
} | |
} else { | |
this._contentLength = null; | |
} | |
} | |
return this._contentLength; | |
} | |
_body = null; | |
get body() { | |
if (!this._body) { | |
if (this.contentLength != null) { | |
this._body = bodyReader(this.contentLength, this.r); | |
} else { | |
const transferEncoding = this.headers.get("transfer-encoding"); | |
if (transferEncoding != null) { | |
const parts = transferEncoding.split(",").map((e)=>e.trim().toLowerCase() | |
); | |
assert(parts.includes("chunked"), 'transfer-encoding must include "chunked" if content-length is not set'); | |
this._body = chunkedBodyReader(this.headers, this.r); | |
} else { | |
this._body = emptyReader(); | |
} | |
} | |
} | |
return this._body; | |
} | |
async respond(r) { | |
let err; | |
try { | |
await writeResponse(this.w, r); | |
} catch (e) { | |
try { | |
this.conn.close(); | |
} catch { | |
} | |
err = e; | |
} | |
this.done.resolve(err); | |
if (err) { | |
throw err; | |
} | |
} | |
finalized = false; | |
async finalize() { | |
if (this.finalized) return; | |
const body = this.body; | |
const buf = new Uint8Array(1024); | |
while(await body.read(buf) !== null){ | |
} | |
this.finalized = true; | |
} | |
} | |
export class Server { | |
closing = false; | |
connections = []; | |
constructor(listener){ | |
this.listener = listener; | |
} | |
close() { | |
this.closing = true; | |
this.listener.close(); | |
for (const conn of this.connections){ | |
try { | |
conn.close(); | |
} catch (e) { | |
if (!(e instanceof Deno.errors.BadResource)) { | |
throw e; | |
} | |
} | |
} | |
} | |
async *iterateHttpRequests(conn) { | |
const reader = new BufReader(conn); | |
const writer3 = new BufWriter(conn); | |
while(!this.closing){ | |
let request; | |
try { | |
request = await readRequest(conn, reader); | |
} catch (error) { | |
if (error instanceof Deno.errors.InvalidData || error instanceof Deno.errors.UnexpectedEof) { | |
await writeResponse(writer3, { | |
status: 400, | |
body: encode(`${error.message}\r\n\r\n`) | |
}); | |
} | |
break; | |
} | |
if (request === null) { | |
break; | |
} | |
request.w = writer3; | |
yield request; | |
const responseError = await request.done; | |
if (responseError) { | |
this.untrackConnection(request.conn); | |
return; | |
} | |
await request.finalize(); | |
} | |
this.untrackConnection(conn); | |
try { | |
conn.close(); | |
} catch (e) { | |
} | |
} | |
trackConnection(conn) { | |
this.connections.push(conn); | |
} | |
untrackConnection(conn) { | |
const index = this.connections.indexOf(conn); | |
if (index !== -1) { | |
this.connections.splice(index, 1); | |
} | |
} | |
async *acceptConnAndIterateHttpRequests(mux) { | |
if (this.closing) return; | |
let conn; | |
try { | |
conn = await this.listener.accept(); | |
} catch (error) { | |
if (error instanceof Deno.errors.BadResource || error instanceof Deno.errors.InvalidData || error instanceof Deno.errors.UnexpectedEof) { | |
return mux.add(this.acceptConnAndIterateHttpRequests(mux)); | |
} | |
throw error; | |
} | |
this.trackConnection(conn); | |
mux.add(this.acceptConnAndIterateHttpRequests(mux)); | |
yield* this.iterateHttpRequests(conn); | |
} | |
[Symbol.asyncIterator]() { | |
const mux = new MuxAsyncIterator1(); | |
mux.add(this.acceptConnAndIterateHttpRequests(mux)); | |
return mux.iterate(); | |
} | |
} | |
export function _parseAddrFromStr(addr) { | |
let url; | |
try { | |
const host = addr.startsWith(":") ? `0.0.0.0${addr}` : addr; | |
url = new URL(`http://${host}`); | |
} catch { | |
throw new TypeError("Invalid address."); | |
} | |
if (url.username || url.password || url.pathname != "/" || url.search || url.hash) { | |
throw new TypeError("Invalid address."); | |
} | |
return { | |
hostname: url.hostname, | |
port: url.port === "" ? 80 : Number(url.port) | |
}; | |
} | |
export function serve(addr) { | |
if (typeof addr === "string") { | |
addr = _parseAddrFromStr(addr); | |
} | |
const listener1 = Deno.listen(addr); | |
return new Server(listener1); | |
} | |
export async function listenAndServe(addr, handler) { | |
const server = serve(addr); | |
for await (const request of server){ | |
handler(request); | |
} | |
} | |
export function serveTLS(options) { | |
const tlsOptions = { | |
...options, | |
transport: "tcp" | |
}; | |
const listener1 = Deno.listenTls(tlsOptions); | |
return new Server(listener1); | |
} | |
export async function listenAndServeTLS(options, handler) { | |
const server = serveTLS(options); | |
for await (const request of server){ | |
handler(request); | |
} | |
} | |
function concat(origin, b) { | |
const output = new Uint8Array(origin.length + b.length); | |
output.set(origin, 0); | |
output.set(b, origin.length); | |
return output; | |
} | |
function copyBytes(src, dst, off = 0) { | |
off = Math.max(0, Math.min(off, dst.byteLength)); | |
const dstBytesAvailable = dst.byteLength - off; | |
if (src.byteLength > dstBytesAvailable) { | |
src = src.subarray(0, dstBytesAvailable); | |
} | |
dst.set(src, off); | |
return src.byteLength; | |
} | |
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/g; | |
function str(buf) { | |
if (buf == null) { | |
return ""; | |
} else { | |
return decode(buf); | |
} | |
} | |
function charCode(s) { | |
return s.charCodeAt(0); | |
} | |
class TextProtoReader { | |
constructor(r){ | |
this.r = r; | |
} | |
async readLine() { | |
const s = await this.readLineSlice(); | |
if (s === null) return null; | |
return str(s); | |
} | |
async readMIMEHeader() { | |
const m = new Headers(); | |
let line; | |
let buf = await this.r.peek(1); | |
if (buf === null) { | |
return null; | |
} else if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { | |
line = await this.readLineSlice(); | |
} | |
buf = await this.r.peek(1); | |
if (buf === null) { | |
throw new Deno.errors.UnexpectedEof(); | |
} else if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { | |
throw new Deno.errors.InvalidData(`malformed MIME header initial line: ${str(line)}`); | |
} | |
while(true){ | |
const kv = await this.readLineSlice(); | |
if (kv === null) throw new Deno.errors.UnexpectedEof(); | |
if (kv.byteLength === 0) return m; | |
let i = kv.indexOf(charCode(":")); | |
if (i < 0) { | |
throw new Deno.errors.InvalidData(`malformed MIME header line: ${str(kv)}`); | |
} | |
const key = str(kv.subarray(0, i)); | |
if (key == "") { | |
continue; | |
} | |
i++; | |
while(i < kv.byteLength && (kv[i] == charCode(" ") || kv[i] == charCode("\t"))){ | |
i++; | |
} | |
const value = str(kv.subarray(i)).replace(invalidHeaderCharRegex, encodeURI); | |
try { | |
m.append(key, value); | |
} catch { | |
} | |
} | |
} | |
async readLineSlice() { | |
let line; | |
while(true){ | |
const r1 = await this.r.readLine(); | |
if (r1 === null) return null; | |
const { line: l , more } = r1; | |
if (!line && !more) { | |
if (this.skipSpace(l) === 0) { | |
return new Uint8Array(0); | |
} | |
return l; | |
} | |
line = line ? concat(line, l) : l; | |
if (!more) { | |
break; | |
} | |
} | |
return line; | |
} | |
skipSpace(l) { | |
let n = 0; | |
for(let i = 0; i < l.length; i++){ | |
if (l[i] === charCode(" ") || l[i] === charCode("\t")) { | |
continue; | |
} | |
n++; | |
} | |
return n; | |
} | |
} | |
class DenoStdInternalError extends Error { | |
constructor(message){ | |
super(message); | |
this.name = "DenoStdInternalError"; | |
} | |
} | |
function assert(expr, msg = "") { | |
if (!expr) { | |
throw new DenoStdInternalError(msg); | |
} | |
} | |
var Status; | |
(function(Status1) { | |
Status1[Status1["Continue"] = 100] = "Continue"; | |
Status1[Status1["SwitchingProtocols"] = 101] = "SwitchingProtocols"; | |
Status1[Status1["Processing"] = 102] = "Processing"; | |
Status1[Status1["EarlyHints"] = 103] = "EarlyHints"; | |
Status1[Status1["OK"] = 200] = "OK"; | |
Status1[Status1["Created"] = 201] = "Created"; | |
Status1[Status1["Accepted"] = 202] = "Accepted"; | |
Status1[Status1["NonAuthoritativeInfo"] = 203] = "NonAuthoritativeInfo"; | |
Status1[Status1["NoContent"] = 204] = "NoContent"; | |
Status1[Status1["ResetContent"] = 205] = "ResetContent"; | |
Status1[Status1["PartialContent"] = 206] = "PartialContent"; | |
Status1[Status1["MultiStatus"] = 207] = "MultiStatus"; | |
Status1[Status1["AlreadyReported"] = 208] = "AlreadyReported"; | |
Status1[Status1["IMUsed"] = 226] = "IMUsed"; | |
Status1[Status1["MultipleChoices"] = 300] = "MultipleChoices"; | |
Status1[Status1["MovedPermanently"] = 301] = "MovedPermanently"; | |
Status1[Status1["Found"] = 302] = "Found"; | |
Status1[Status1["SeeOther"] = 303] = "SeeOther"; | |
Status1[Status1["NotModified"] = 304] = "NotModified"; | |
Status1[Status1["UseProxy"] = 305] = "UseProxy"; | |
Status1[Status1["TemporaryRedirect"] = 307] = "TemporaryRedirect"; | |
Status1[Status1["PermanentRedirect"] = 308] = "PermanentRedirect"; | |
Status1[Status1["BadRequest"] = 400] = "BadRequest"; | |
Status1[Status1["Unauthorized"] = 401] = "Unauthorized"; | |
Status1[Status1["PaymentRequired"] = 402] = "PaymentRequired"; | |
Status1[Status1["Forbidden"] = 403] = "Forbidden"; | |
Status1[Status1["NotFound"] = 404] = "NotFound"; | |
Status1[Status1["MethodNotAllowed"] = 405] = "MethodNotAllowed"; | |
Status1[Status1["NotAcceptable"] = 406] = "NotAcceptable"; | |
Status1[Status1["ProxyAuthRequired"] = 407] = "ProxyAuthRequired"; | |
Status1[Status1["RequestTimeout"] = 408] = "RequestTimeout"; | |
Status1[Status1["Conflict"] = 409] = "Conflict"; | |
Status1[Status1["Gone"] = 410] = "Gone"; | |
Status1[Status1["LengthRequired"] = 411] = "LengthRequired"; | |
Status1[Status1["PreconditionFailed"] = 412] = "PreconditionFailed"; | |
Status1[Status1["RequestEntityTooLarge"] = 413] = "RequestEntityTooLarge"; | |
Status1[Status1["RequestURITooLong"] = 414] = "RequestURITooLong"; | |
Status1[Status1["UnsupportedMediaType"] = 415] = "UnsupportedMediaType"; | |
Status1[Status1["RequestedRangeNotSatisfiable"] = 416] = "RequestedRangeNotSatisfiable"; | |
Status1[Status1["ExpectationFailed"] = 417] = "ExpectationFailed"; | |
Status1[Status1["Teapot"] = 418] = "Teapot"; | |
Status1[Status1["MisdirectedRequest"] = 421] = "MisdirectedRequest"; | |
Status1[Status1["UnprocessableEntity"] = 422] = "UnprocessableEntity"; | |
Status1[Status1["Locked"] = 423] = "Locked"; | |
Status1[Status1["FailedDependency"] = 424] = "FailedDependency"; | |
Status1[Status1["TooEarly"] = 425] = "TooEarly"; | |
Status1[Status1["UpgradeRequired"] = 426] = "UpgradeRequired"; | |
Status1[Status1["PreconditionRequired"] = 428] = "PreconditionRequired"; | |
Status1[Status1["TooManyRequests"] = 429] = "TooManyRequests"; | |
Status1[Status1["RequestHeaderFieldsTooLarge"] = 431] = "RequestHeaderFieldsTooLarge"; | |
Status1[Status1["UnavailableForLegalReasons"] = 451] = "UnavailableForLegalReasons"; | |
Status1[Status1["InternalServerError"] = 500] = "InternalServerError"; | |
Status1[Status1["NotImplemented"] = 501] = "NotImplemented"; | |
Status1[Status1["BadGateway"] = 502] = "BadGateway"; | |
Status1[Status1["ServiceUnavailable"] = 503] = "ServiceUnavailable"; | |
Status1[Status1["GatewayTimeout"] = 504] = "GatewayTimeout"; | |
Status1[Status1["HTTPVersionNotSupported"] = 505] = "HTTPVersionNotSupported"; | |
Status1[Status1["VariantAlsoNegotiates"] = 506] = "VariantAlsoNegotiates"; | |
Status1[Status1["InsufficientStorage"] = 507] = "InsufficientStorage"; | |
Status1[Status1["LoopDetected"] = 508] = "LoopDetected"; | |
Status1[Status1["NotExtended"] = 510] = "NotExtended"; | |
Status1[Status1["NetworkAuthenticationRequired"] = 511] = "NetworkAuthenticationRequired"; | |
})(Status || (Status = { | |
})); | |
const STATUS_TEXT = new Map([ | |
[ | |
Status.Continue, | |
"Continue" | |
], | |
[ | |
Status.SwitchingProtocols, | |
"Switching Protocols" | |
], | |
[ | |
Status.Processing, | |
"Processing" | |
], | |
[ | |
Status.EarlyHints, | |
"Early Hints" | |
], | |
[ | |
Status.OK, | |
"OK" | |
], | |
[ | |
Status.Created, | |
"Created" | |
], | |
[ | |
Status.Accepted, | |
"Accepted" | |
], | |
[ | |
Status.NonAuthoritativeInfo, | |
"Non-Authoritative Information" | |
], | |
[ | |
Status.NoContent, | |
"No Content" | |
], | |
[ | |
Status.ResetContent, | |
"Reset Content" | |
], | |
[ | |
Status.PartialContent, | |
"Partial Content" | |
], | |
[ | |
Status.MultiStatus, | |
"Multi-Status" | |
], | |
[ | |
Status.AlreadyReported, | |
"Already Reported" | |
], | |
[ | |
Status.IMUsed, | |
"IM Used" | |
], | |
[ | |
Status.MultipleChoices, | |
"Multiple Choices" | |
], | |
[ | |
Status.MovedPermanently, | |
"Moved Permanently" | |
], | |
[ | |
Status.Found, | |
"Found" | |
], | |
[ | |
Status.SeeOther, | |
"See Other" | |
], | |
[ | |
Status.NotModified, | |
"Not Modified" | |
], | |
[ | |
Status.UseProxy, | |
"Use Proxy" | |
], | |
[ | |
Status.TemporaryRedirect, | |
"Temporary Redirect" | |
], | |
[ | |
Status.PermanentRedirect, | |
"Permanent Redirect" | |
], | |
[ | |
Status.BadRequest, | |
"Bad Request" | |
], | |
[ | |
Status.Unauthorized, | |
"Unauthorized" | |
], | |
[ | |
Status.PaymentRequired, | |
"Payment Required" | |
], | |
[ | |
Status.Forbidden, | |
"Forbidden" | |
], | |
[ | |
Status.NotFound, | |
"Not Found" | |
], | |
[ | |
Status.MethodNotAllowed, | |
"Method Not Allowed" | |
], | |
[ | |
Status.NotAcceptable, | |
"Not Acceptable" | |
], | |
[ | |
Status.ProxyAuthRequired, | |
"Proxy Authentication Required" | |
], | |
[ | |
Status.RequestTimeout, | |
"Request Timeout" | |
], | |
[ | |
Status.Conflict, | |
"Conflict" | |
], | |
[ | |
Status.Gone, | |
"Gone" | |
], | |
[ | |
Status.LengthRequired, | |
"Length Required" | |
], | |
[ | |
Status.PreconditionFailed, | |
"Precondition Failed" | |
], | |
[ | |
Status.RequestEntityTooLarge, | |
"Request Entity Too Large" | |
], | |
[ | |
Status.RequestURITooLong, | |
"Request URI Too Long" | |
], | |
[ | |
Status.UnsupportedMediaType, | |
"Unsupported Media Type" | |
], | |
[ | |
Status.RequestedRangeNotSatisfiable, | |
"Requested Range Not Satisfiable" | |
], | |
[ | |
Status.ExpectationFailed, | |
"Expectation Failed" | |
], | |
[ | |
Status.Teapot, | |
"I'm a teapot" | |
], | |
[ | |
Status.MisdirectedRequest, | |
"Misdirected Request" | |
], | |
[ | |
Status.UnprocessableEntity, | |
"Unprocessable Entity" | |
], | |
[ | |
Status.Locked, | |
"Locked" | |
], | |
[ | |
Status.FailedDependency, | |
"Failed Dependency" | |
], | |
[ | |
Status.TooEarly, | |
"Too Early" | |
], | |
[ | |
Status.UpgradeRequired, | |
"Upgrade Required" | |
], | |
[ | |
Status.PreconditionRequired, | |
"Precondition Required" | |
], | |
[ | |
Status.TooManyRequests, | |
"Too Many Requests" | |
], | |
[ | |
Status.RequestHeaderFieldsTooLarge, | |
"Request Header Fields Too Large" | |
], | |
[ | |
Status.UnavailableForLegalReasons, | |
"Unavailable For Legal Reasons" | |
], | |
[ | |
Status.InternalServerError, | |
"Internal Server Error" | |
], | |
[ | |
Status.NotImplemented, | |
"Not Implemented" | |
], | |
[ | |
Status.BadGateway, | |
"Bad Gateway" | |
], | |
[ | |
Status.ServiceUnavailable, | |
"Service Unavailable" | |
], | |
[ | |
Status.GatewayTimeout, | |
"Gateway Timeout" | |
], | |
[ | |
Status.HTTPVersionNotSupported, | |
"HTTP Version Not Supported" | |
], | |
[ | |
Status.VariantAlsoNegotiates, | |
"Variant Also Negotiates" | |
], | |
[ | |
Status.InsufficientStorage, | |
"Insufficient Storage" | |
], | |
[ | |
Status.LoopDetected, | |
"Loop Detected" | |
], | |
[ | |
Status.NotExtended, | |
"Not Extended" | |
], | |
[ | |
Status.NetworkAuthenticationRequired, | |
"Network Authentication Required" | |
], | |
]); | |
function emptyReader() { | |
return { | |
read (_) { | |
return Promise.resolve(null); | |
} | |
}; | |
} | |
function bodyReader(contentLength, r1) { | |
let totalRead = 0; | |
let finished = false; | |
async function read(buf) { | |
if (finished) return null; | |
let result; | |
const remaining = contentLength - totalRead; | |
if (remaining >= buf.byteLength) { | |
result = await r1.read(buf); | |
} else { | |
const readBuf = buf.subarray(0, remaining); | |
result = await r1.read(readBuf); | |
} | |
if (result !== null) { | |
totalRead += result; | |
} | |
finished = totalRead === contentLength; | |
return result; | |
} | |
return { | |
read | |
}; | |
} | |
function chunkedBodyReader(h, r1) { | |
const tp = new TextProtoReader(r1); | |
let finished = false; | |
const chunks = []; | |
async function read(buf) { | |
if (finished) return null; | |
const [chunk] = chunks; | |
if (chunk) { | |
const chunkRemaining = chunk.data.byteLength - chunk.offset; | |
const readLength = Math.min(chunkRemaining, buf.byteLength); | |
for(let i = 0; i < readLength; i++){ | |
buf[i] = chunk.data[chunk.offset + i]; | |
} | |
chunk.offset += readLength; | |
if (chunk.offset === chunk.data.byteLength) { | |
chunks.shift(); | |
if (await tp.readLine() === null) { | |
throw new Deno.errors.UnexpectedEof(); | |
} | |
} | |
return readLength; | |
} | |
const line = await tp.readLine(); | |
if (line === null) throw new Deno.errors.UnexpectedEof(); | |
const [chunkSizeString] = line.split(";"); | |
const chunkSize = parseInt(chunkSizeString, 16); | |
if (Number.isNaN(chunkSize) || chunkSize < 0) { | |
throw new Error("Invalid chunk size"); | |
} | |
if (chunkSize > 0) { | |
if (chunkSize > buf.byteLength) { | |
let eof = await r1.readFull(buf); | |
if (eof === null) { | |
throw new Deno.errors.UnexpectedEof(); | |
} | |
const restChunk = new Uint8Array(chunkSize - buf.byteLength); | |
eof = await r1.readFull(restChunk); | |
if (eof === null) { | |
throw new Deno.errors.UnexpectedEof(); | |
} else { | |
chunks.push({ | |
offset: 0, | |
data: restChunk | |
}); | |
} | |
return buf.byteLength; | |
} else { | |
const bufToFill = buf.subarray(0, chunkSize); | |
const eof = await r1.readFull(bufToFill); | |
if (eof === null) { | |
throw new Deno.errors.UnexpectedEof(); | |
} | |
if (await tp.readLine() === null) { | |
throw new Deno.errors.UnexpectedEof(); | |
} | |
return chunkSize; | |
} | |
} else { | |
assert(chunkSize === 0); | |
if (await r1.readLine() === null) { | |
throw new Deno.errors.UnexpectedEof(); | |
} | |
await readTrailers(h, r1); | |
finished = true; | |
return null; | |
} | |
} | |
return { | |
read | |
}; | |
} | |
function isProhibidedForTrailer(key) { | |
const s = new Set([ | |
"transfer-encoding", | |
"content-length", | |
"trailer" | |
]); | |
return s.has(key.toLowerCase()); | |
} | |
async function readTrailers(headers, r1) { | |
const trailers = parseTrailer(headers.get("trailer")); | |
if (trailers == null) return; | |
const trailerNames = [ | |
...trailers.keys() | |
]; | |
const tp = new TextProtoReader(r1); | |
const result = await tp.readMIMEHeader(); | |
if (result == null) { | |
throw new Deno.errors.InvalidData("Missing trailer header."); | |
} | |
const undeclared = [ | |
...result.keys() | |
].filter((k)=>!trailerNames.includes(k) | |
); | |
if (undeclared.length > 0) { | |
throw new Deno.errors.InvalidData(`Undeclared trailers: ${Deno.inspect(undeclared)}.`); | |
} | |
for (const [k, v] of result){ | |
headers.append(k, v); | |
} | |
const missingTrailers = trailerNames.filter((k1)=>!result.has(k1) | |
); | |
if (missingTrailers.length > 0) { | |
throw new Deno.errors.InvalidData(`Missing trailers: ${Deno.inspect(missingTrailers)}.`); | |
} | |
headers.delete("trailer"); | |
} | |
function parseTrailer(field) { | |
if (field == null) { | |
return undefined; | |
} | |
const trailerNames = field.split(",").map((v)=>v.trim().toLowerCase() | |
); | |
if (trailerNames.length === 0) { | |
throw new Deno.errors.InvalidData("Empty trailer header."); | |
} | |
const prohibited = trailerNames.filter((k)=>isProhibidedForTrailer(k) | |
); | |
if (prohibited.length > 0) { | |
throw new Deno.errors.InvalidData(`Prohibited trailer names: ${Deno.inspect(prohibited)}.`); | |
} | |
return new Headers(trailerNames.map((key)=>[ | |
key, | |
"" | |
] | |
)); | |
} | |
async function writeChunkedBody(w, r1) { | |
const writer3 = BufWriter.create(w); | |
for await (const chunk of Deno.iter(r1)){ | |
if (chunk.byteLength <= 0) continue; | |
const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); | |
const end = encoder.encode("\r\n"); | |
await writer3.write(start); | |
await writer3.write(chunk); | |
await writer3.write(end); | |
} | |
const endChunk = encoder.encode("0\r\n\r\n"); | |
await writer3.write(endChunk); | |
} | |
async function writeTrailers(w, headers, trailers) { | |
const trailer = headers.get("trailer"); | |
if (trailer === null) { | |
throw new TypeError("Missing trailer header."); | |
} | |
const transferEncoding = headers.get("transfer-encoding"); | |
if (transferEncoding === null || !transferEncoding.match(/^chunked/)) { | |
throw new TypeError(`Trailers are only allowed for "transfer-encoding: chunked", got "transfer-encoding: ${transferEncoding}".`); | |
} | |
const writer3 = BufWriter.create(w); | |
const trailerNames = trailer.split(",").map((s)=>s.trim().toLowerCase() | |
); | |
const prohibitedTrailers = trailerNames.filter((k)=>isProhibidedForTrailer(k) | |
); | |
if (prohibitedTrailers.length > 0) { | |
throw new TypeError(`Prohibited trailer names: ${Deno.inspect(prohibitedTrailers)}.`); | |
} | |
const undeclared = [ | |
...trailers.keys() | |
].filter((k)=>!trailerNames.includes(k) | |
); | |
if (undeclared.length > 0) { | |
throw new TypeError(`Undeclared trailers: ${Deno.inspect(undeclared)}.`); | |
} | |
for (const [key, value] of trailers){ | |
await writer3.write(encoder.encode(`${key}: ${value}\r\n`)); | |
} | |
await writer3.write(encoder.encode("\r\n")); | |
await writer3.flush(); | |
} | |
async function writeResponse(w, r1) { | |
const protoMajor = 1; | |
const protoMinor = 1; | |
const statusCode = r1.status || 200; | |
const statusText = STATUS_TEXT.get(statusCode); | |
const writer3 = BufWriter.create(w); | |
if (!statusText) { | |
throw new Deno.errors.InvalidData("Bad status code"); | |
} | |
if (!r1.body) { | |
r1.body = new Uint8Array(); | |
} | |
if (typeof r1.body === "string") { | |
r1.body = encoder.encode(r1.body); | |
} | |
let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; | |
const headers = r1.headers ?? new Headers(); | |
if (r1.body && !headers.get("content-length")) { | |
if (r1.body instanceof Uint8Array) { | |
out += `content-length: ${r1.body.byteLength}\r\n`; | |
} else if (!headers.get("transfer-encoding")) { | |
out += "transfer-encoding: chunked\r\n"; | |
} | |
} | |
for (const [key, value] of headers){ | |
out += `${key}: ${value}\r\n`; | |
} | |
out += `\r\n`; | |
const header = encoder.encode(out); | |
const n = await writer3.write(header); | |
assert(n === header.byteLength); | |
if (r1.body instanceof Uint8Array) { | |
const n1 = await writer3.write(r1.body); | |
assert(n1 === r1.body.byteLength); | |
} else if (headers.has("content-length")) { | |
const contentLength = headers.get("content-length"); | |
assert(contentLength != null); | |
const bodyLength = parseInt(contentLength); | |
const n1 = await Deno.copy(r1.body, writer3); | |
assert(n1 === bodyLength); | |
} else { | |
await writeChunkedBody(writer3, r1.body); | |
} | |
if (r1.trailers) { | |
const t = await r1.trailers(); | |
await writeTrailers(writer3, headers, t); | |
} | |
await writer3.flush(); | |
} | |
function parseHTTPVersion(vers) { | |
switch(vers){ | |
case "HTTP/1.1": | |
return [ | |
1, | |
1 | |
]; | |
case "HTTP/1.0": | |
return [ | |
1, | |
0 | |
]; | |
default: | |
{ | |
const Big = 1000000; | |
if (!vers.startsWith("HTTP/")) { | |
break; | |
} | |
const dot = vers.indexOf("."); | |
if (dot < 0) { | |
break; | |
} | |
const majorStr = vers.substring(vers.indexOf("/") + 1, dot); | |
const major = Number(majorStr); | |
if (!Number.isInteger(major) || major < 0 || major > Big) { | |
break; | |
} | |
const minorStr = vers.substring(dot + 1); | |
const minor = Number(minorStr); | |
if (!Number.isInteger(minor) || minor < 0 || minor > Big) { | |
break; | |
} | |
return [ | |
major, | |
minor | |
]; | |
} | |
} | |
throw new Error(`malformed HTTP version ${vers}`); | |
} | |
async function readRequest(conn, bufr) { | |
const tp = new TextProtoReader(bufr); | |
const firstLine = await tp.readLine(); | |
if (firstLine === null) return null; | |
const headers = await tp.readMIMEHeader(); | |
if (headers === null) throw new Deno.errors.UnexpectedEof(); | |
const req = new ServerRequest(); | |
req.conn = conn; | |
req.r = bufr; | |
[req.method, req.url, req.proto] = firstLine.split(" ", 3); | |
[req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); | |
req.headers = headers; | |
fixLength(req); | |
return req; | |
} | |
function fixLength(req) { | |
const contentLength = req.headers.get("Content-Length"); | |
if (contentLength) { | |
const arrClen = contentLength.split(","); | |
if (arrClen.length > 1) { | |
const distinct = [ | |
...new Set(arrClen.map((e)=>e.trim() | |
)) | |
]; | |
if (distinct.length > 1) { | |
throw Error("cannot contain multiple Content-Length headers"); | |
} else { | |
req.headers.set("Content-Length", distinct[0]); | |
} | |
} | |
const c = req.headers.get("Content-Length"); | |
if (req.method === "HEAD" && c && c !== "0") { | |
throw Error("http: method cannot contain a Content-Length"); | |
} | |
if (c && req.headers.has("transfer-encoding")) { | |
throw new Error("http: Transfer-Encoding and Content-Length cannot be send together"); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment