-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.ts
133 lines (112 loc) · 4.39 KB
/
server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { ServerConfig } from "./config.ts";
import { GeminiRequest } from "./request.ts";
import { ResponseBuilder } from "./response.ts";
import { LineEndingError, ProxyRefusedError, GoneError, RedirectError } from "./errors.ts";
import { Logger, getLogger } from "https://deno.land/std/log/mod.ts";
import { StatusCode } from "./status.ts";
/// A Denoscuri virtual server, which receives and services incoming requests
export class Server {
/// The configuration for this server
config: ServerConfig;
/// This server's logger
logger: Logger;
/// Creates a new server from the given config
constructor(c: ServerConfig) {
this.config = c;
this.logger = getLogger(this.config.hostname);
}
/// Starts the server
public async start(): Promise<void> {
// Create a TLS socket
const listener = Deno.listenTls({
hostname: this.config.hostname,
port: this.config.port,
certFile: this.config.certFile,
keyFile: this.config.keyFile
});
this.logger.info("♊ Listening on " + this.config.hostname + ":" + this.config.port);
// Loop until the server is killed
while (true) {
try {
for await (const conn of listener) {
this.handle_connection(conn);
}
} catch (err) {
if (err.name === 'BadResource') {
// Ugly but expected when a connection is closed
} else {
// Because this error occurs outside of the connection servicing code
// we cannot send a response to it
this.logger.critical(`Unexpected error in Server::start ${err}`);
}
}
}
}
/// Receives a request and then responds appropriately
async handle_connection(conn: Deno.Conn) {
// A request can only be 1024 bytes plus two bytes for CRLF. Technically,
// this allows for a 1025 byte request if requireCRLF is false in the server
// config, but such a server is already noncompliant with the standard :)
let buffer = new Uint8Array(1026);
const count = await conn.read(buffer);
// Nothing received. Don't reply.
if (!count) return;
// Get the actual request contents
const msg = buffer.subarray(0, count);
// Parse the request then build a response, including if an error occurred
const res = await this.parse_request(msg)
.then(async (req) => this.goner_check(req))
.then(async (req) => this.redirect_check(req))
.then(async (req) => ResponseBuilder.buildFromPath(this.config.documentRoot, req.path))
.catch(async (e) => {
if (e.code === StatusCode.PERMANENT_FAILURE) {
this.logger.critical(`Internal error: ${e.error}`);
}
return ResponseBuilder.buildFromError(e);
});
this.logger.debug(`Sending response header: ${JSON.stringify(res.header)}`);
// Write the response
await conn.write(res.encode());
// Close the connection
conn.close();
}
/// Parses the incoming request into a GeminiRequest object
async parse_request(buffer: Uint8Array): Promise<GeminiRequest> {
// Decode the request as UTF-8
const req_str = new TextDecoder("utf-8").decode(buffer);
this.logger.debug(() => `Received request: ${req_str.replace("\r", "\\r").replace("\n", "\\n")}`);
// Check for valid line ending in request
if ((this.config.requireCRLF && req_str.substr(-2) !== "\r\n")
|| req_str.substr(-1) !== "\n") {
throw new LineEndingError(this.config.requireCRLF ? "\\r\\n" : "\\n");
}
// Create the actual request object
const req = new GeminiRequest(req_str);
// Check for unsupported proxying
if (req.hostname !== this.config.hostname || req.protocol !== "gemini:") {
throw new ProxyRefusedError(req_str);
}
// Return the request
return req;
}
/// Checks if the incoming request is for a resource marked Gone
async goner_check(req: GeminiRequest): Promise<GeminiRequest> {
if (this.config.goners.length === 0) return req;
const goner = this.config.goners.filter((g) => req.path.startsWith(g));
if (goner.length !== 0) {
throw new GoneError(req.path);
} else {
return req;
}
}
async redirect_check(req: GeminiRequest): Promise<GeminiRequest> {
for (const k in this.config.redirects) {
if (req.path.startsWith(k)) {
const r = this.config.redirects[k];
const path = req.path.replace(k, r.destination);
throw new RedirectError(path, r.permanent);
}
}
return req;
}
}