This repository has been archived by the owner on May 6, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
proxy.js
154 lines (135 loc) · 4.73 KB
/
proxy.js
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { parse } from "https://deno.land/std/flags/mod.ts";
import { serve, serveTls } from "https://deno.land/std/http/mod.ts";
const { h, help, certFile, keyFile, auth } = parse(Deno.args)
const HELP = `Deno https proxy serve 1.0.0
USAGE:
proxy [OPTIONS]
OPTIONS:
-h, --help Print help information
--certFile=<certFilePath> Assign certFile to enable HTTPS server, must use with --keyFile
--keyFile=<keyFilePath> Assign keyFile to enable HTTPS server, must use with --certFile
--auth=<authFilePath> Assign authorization file to enable web basic authorization`
if(h || help) {
console.log(HELP)
Deno.exit(0)
}
const params = {
hostname: '::',
certFile,
keyFile,
}
certFile ? serveTls(handle, params) : serve(handle, params)
const authString = auth ? Deno.readTextFileSync(auth) : ''
const users = authString.match(/^(\w|:)+/gm) || []
if(auth) {
certFile || console.warn('[Warning] pass basic authorization on HTTP is unsafely, please consider use HTTPS')
console.info(`load ${users.length} valid user`)
}
/**
* return string with bracket if it is ipv6
* example: 127.0.0.1 → 127.0.0.1
* ::1 → [::1]
* @param {string} addr ip address
* @return {string} for ipv4, will return itself. for ipv6, it returns bracket wrapped.
*/
function wrapIPv6 (addr){
return addr.includes(':') ? `[${addr}]` : addr
}
/**
* check header this request whether proxy by self
* @param {Headers} headers request's header
* @param {string} by serve proxy by field
*/
function isSelfRequest(headers, by) {
return headers.get('forwarded')?.split(',').some(forwarded => {
for(const s of forwarded.split(';')) {
const [key, value] = s.split('=')
if(key === 'by' && value === by) {
return true
}
}
return false
})
}
/**
* handle upgrade connect
* @param {Request} request
* @return {Promise<Response>}
*/
async function handleConnect(request) {
const url = new URL(request.url)
/** @type { Deno.TcpConn } */
let destConn
try {
destConn = await Deno.connect({ hostname: url.hostname, port: Number(url.port || '443') })
}
catch (e) {
// Honestly, I don't know why it was error here
return
}
const p = Deno.upgradeHttp(request);
(async () => {
const [conn] = await p
conn.readable.pipeTo(destConn.writable).catch(() => {})
destConn.readable.pipeTo(conn.writable).catch(() => {})
})().then(() => {})
return new Response(null, { status: 200, statusText: 'Connection Established' })
}
const AUTH_RESPONSE_INIT = { status: 407, headers: {'proxy-authenticate': 'basic realm="proxy"'}}
/**
* handle HTTP Request
* @param {Request} request
* @param {Deno.NetAddr} localAddr
* @param {Deno.NetAddr} remoteAddr
* @return {Promise<Response>}
*/
async function handle(request, { localAddr, remoteAddr }) {
const by = `${wrapIPv6(localAddr.hostname)}:${localAddr.port}`;
const headers = new Headers(request.headers)
/* prevent self-cycle */
if(isSelfRequest(headers, by)) {
return new Response('It work!')
}
/* authorize */
if(auth) {
const authorization = request.headers.get('proxy-authorization')
if(!authorization) return new Response(null, AUTH_RESPONSE_INIT)
let [, credentials] = authorization.split(' ')
credentials = atob(credentials)
if(!users.includes(credentials)) return new Response(null, AUTH_RESPONSE_INIT)
}
if(request.method === 'CONNECT') return handleConnect(request)
// add proxy header
const forwarded = {
by,
for: `${wrapIPv6(remoteAddr.hostname)}:${remoteAddr.port}`,
host: request.headers.get('host'),
proto: certFile ? 'https' : 'http',
}
headers.append('forwarded', Object.entries(forwarded).map(record => record.join('=')).join(';'))
// deal hop-by-hop header
headers.delete('keep-alive')
headers.delete('transfer-encoding')
headers.delete('te')
headers.delete('trailer')
headers.delete('upgrade')
headers.delete('upgrade')
headers.delete('proxy-authorization')
headers.delete('proxy-authenticate')
const connection = headers.get('connection')
if (connection && connection !== 'close') {
headers.delete('connection')
connection.split(',').forEach(header => headers.delete(header))
}
return fetch(request.url.replace(/^https:/, 'http:'), {
headers,
method: request.method,
body: request.body,
mode: request.mode,
credentials: request.credentials,
cache: request.cache,
redirect: request.redirect,
referrer: request.referrer,
integrity: request.integrity,
});
}