Skip to content

Commit 23aabda

Browse files
committed
feat: add http proxy support
1 parent 64c53c1 commit 23aabda

5 files changed

Lines changed: 118 additions & 69 deletions

File tree

.env.sample

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
SERVER_SCHEME = https
2-
SERVER_HOST = example.com
3-
SERVER_PORT = 443
4-
SERVER_USERNAME = user
5-
SERVER_PASSWORD = passw0rd
6-
LOCAL_BIND_IP = 127.0.0.1
7-
LOCAL_PORT = 8080
8-
LOCAL_USERNAME = user
9-
LOCAL_PASSWORD = passw0rd
1+
SERVER_SCHEME = https
2+
SERVER_HOST = example.com
3+
SERVER_PORT = 443
4+
SERVER_USERNAME = user
5+
SERVER_PASSWORD = passw0rd
6+
LOCAL_BIND_IP = 127.0.0.1
7+
LOCAL_PORT = 8080
8+
LOCAL_USERNAME = user
9+
LOCAL_PASSWORD = passw0rd
10+
HTTP_PROXY = http://username:pa$$w0rd@example.com:1337/

ReadMe.md

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,42 @@
1-
# dev-mirror
2-
3-
Simple mirror (proxy) server for private APIs testing.
4-
It will help you to keep private credentials out of build artifacts.
5-
6-
## Features
7-
8-
* Target server authentication
9-
* Local server authentication
10-
* Spoofing referrer
11-
* CORS bypass
12-
13-
## Usage
14-
15-
Use environmental variables or `.env` file in working directory:
16-
```dotenv
17-
# Remote HTTP(S) server
18-
SERVER_SCHEME = https
19-
SERVER_HOST = example.com
20-
SERVER_PORT = 443
21-
# Remote server HTTP Basic auth
22-
SERVER_USERNAME = user
23-
SERVER_PASSWORD = passw0rd
24-
# Local HTTP server
25-
LOCAL_BIND_IP = 127.0.0.1
26-
LOCAL_PORT = 8080
27-
# Local server HTTP Basic auth
28-
LOCAL_USERNAME = user
29-
LOCAL_PASSWORD = passw0rd
30-
```
31-
32-
Keep in mind, that if you have local server authentication, you wont be able
33-
to send authentication details to remote server though the mirror.
34-
35-
That means, that configuration where remote server requires authentication and
36-
mirror has no remote credentials, but has local authentication is invalid.
37-
To fix this add remote credentials (recommended), or disable local
38-
authentication.
1+
# dev-mirror
2+
3+
Simple mirror (proxy) server for private APIs testing.
4+
It will help you to keep private credentials out of build artifacts.
5+
6+
## Features
7+
8+
* Target server authentication
9+
* Local server authentication
10+
* Spoofing referrer
11+
* CORS bypass
12+
* HTTP proxy support
13+
14+
## Usage
15+
16+
Use environmental variables or `.env` file in working directory or pass it's
17+
location as first argument:
18+
```dotenv
19+
# Remote HTTP(S) server
20+
SERVER_SCHEME = https
21+
SERVER_HOST = example.com
22+
SERVER_PORT = 443
23+
# Remote server HTTP Basic auth (optional)
24+
SERVER_USERNAME = user
25+
SERVER_PASSWORD = passw0rd
26+
# Local HTTP server
27+
LOCAL_BIND_IP = 127.0.0.1
28+
LOCAL_PORT = 8080
29+
# Local server HTTP Basic auth (optional)
30+
LOCAL_USERNAME = user
31+
LOCAL_PASSWORD = passw0rd
32+
# HTTP proxy (optional)
33+
HTTP_PROXY = http://username:pa$$w0rd@example.com:1337/
34+
```
35+
36+
Keep in mind that if you have local server authentication, you won't be able
37+
to send authentication details to a remote server through a mirror.
38+
39+
That means that configuration in which a remote server requires authentication
40+
and a mirror has no remote credentials but has local authentication is invalid.
41+
To fix this, add remote credentials (recommended), or disable local
42+
authentication.

bin/main.dart

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,65 @@ void addCORSHeaders(HttpRequest request) {
3030
}
3131

3232
void main(List<String> arguments) async {
33-
if (File.fromUri(Uri.file('.env')).existsSync())
34-
dotenv.load();
33+
String dotEnvFile = arguments.firstOrNull ?? '.env';
34+
if (File.fromUri(Uri.file(dotEnvFile)).existsSync())
35+
dotenv.load(dotEnvFile);
3536

37+
// Local server
3638
final InternetAddress localIp = InternetAddress.tryParse(dotenv.env['LOCAL_BIND_IP'] ?? '') ?? InternetAddress.loopbackIPv4;
3739
final int localPort = int.tryParse(dotenv.env['LOCAL_PORT'] ?? '') ?? 8080;
40+
// Local auth
41+
final String? localUsername = dotenv.env['LOCAL_USERNAME'];
42+
final String? localPassword = dotenv.env['LOCAL_PASSWORD'];
43+
final String? localBasicAuth = (localUsername != null && localPassword != null)
44+
? 'Basic ${base64Encode(utf8.encode('$localUsername:$localPassword'))}'
45+
: null;
46+
final String localBaseUrl = 'http://${localIp.host}:$localPort';
47+
48+
// Remote server
3849
final String serverScheme = dotenv.env['SERVER_SCHEME'] ?? 'https';
3950
final String serverHost = dotenv.env['SERVER_HOST'] ?? 'example.com';
4051
final int serverPort = int.tryParse(dotenv.env['SERVER_PORT'] ?? (serverScheme == 'https' ? '443' : '')) ?? 80;
52+
// Server auth
4153
final String? serverUsername = dotenv.env['SERVER_USERNAME'];
4254
final String? serverPassword = dotenv.env['SERVER_PASSWORD'];
4355
final String? serverBasicAuth = (serverUsername != null && serverPassword != null)
4456
? 'Basic ${base64Encode(utf8.encode('$serverUsername:$serverPassword'))}'
4557
: null;
4658
final String serverBaseUrl = '$serverScheme://$serverHost${![ 'http', 'https', ].contains(serverScheme) ? serverPort : ''}';
47-
final String? localUsername = dotenv.env['LOCAL_USERNAME'];
48-
final String? localPassword = dotenv.env['LOCAL_PASSWORD'];
49-
final String? localBasicAuth = (localUsername != null && localPassword != null)
50-
? 'Basic ${base64Encode(utf8.encode('$localUsername:$localPassword'))}'
59+
60+
final Uri? httpProxy = Uri.tryParse(dotenv.env['HTTP_PROXY'] ?? '::Not valid URI::');
61+
final RegExpMatch? match = httpProxy != null
62+
? RegExp(r'^(?<username>.+?):(?<password>.+?)$')
63+
.firstMatch(httpProxy.userInfo)
64+
: null;
65+
final String? proxyUsername = match?.namedGroup('username');
66+
final String? proxyPassword = match?.namedGroup('password');
67+
final HttpClientBasicCredentials? httpProxyCredentials = (proxyUsername != null && proxyPassword != null)
68+
? HttpClientBasicCredentials(proxyUsername, proxyPassword)
5169
: null;
52-
final String localBaseUrl = 'http://${localIp.host}:$localPort';
5370

5471
stdout.write('Starting mirror server $localBaseUrl -> $serverBaseUrl');
5572
if (localBasicAuth != null)
5673
stdout.write(' [Local auth]');
5774
if (serverBasicAuth != null)
5875
stdout.write(' [Remote auth auto-fill]');
76+
if (httpProxy != null) {
77+
stdout.write(' [Through HTTP proxy]');
78+
if (httpProxy.scheme != 'http') {
79+
stdout.writeln(' [Error]');
80+
stderr.writeln('Proxy URI must be valid.');
81+
return;
82+
}
83+
}
5984

6085
late final HttpServer server;
6186
try {
6287
server = await HttpServer.bind(localIp, localPort);
6388
} catch(error) {
6489
stdout.writeln(' [Error]');
65-
stderr.writeln('Error unable to bind server.');
90+
stderr.writeln('Error unable to bind server:');
91+
stderr.writeln(error);
6692
return;
6793
}
6894
stdout.writeln(' [Done]');
@@ -71,12 +97,28 @@ void main(List<String> arguments) async {
7197
return trustedCert.contains(String.fromCharCodes(cert.sha1));
7298
};
7399

100+
// HTTP proxy
101+
if (httpProxy != null) {
102+
if (httpProxyCredentials != null) {
103+
client.addProxyCredentials(
104+
httpProxy.host,
105+
httpProxy.port,
106+
'Basic',
107+
httpProxyCredentials
108+
);
109+
}
110+
client.findProxy = (uri) => 'PROXY ${httpProxy.host}:${httpProxy.port}';
111+
}
112+
74113
server.listen((HttpRequest request) {
75114
addCORSHeaders(request);
76115
final HttpResponse response = request.response;
77116

78117
// preflight
79-
if (request.method == 'OPTIONS' && request.headers['access-control-request-method'] != null) {
118+
if (
119+
request.method == 'OPTIONS' &&
120+
request.headers[HttpHeaders.accessControlRequestMethodHeader] != null
121+
) {
80122
response
81123
..contentLength = 0
82124
..statusCode = HttpStatus.ok
@@ -85,11 +127,11 @@ void main(List<String> arguments) async {
85127
}
86128

87129
if (localBasicAuth != null) {
88-
final String? _userAuth = request.headers['authorization']?.singleOrNull;
130+
final String? _userAuth = request.headers[HttpHeaders.authorizationHeader]?.singleOrNull;
89131
if (_userAuth == null || _userAuth != localBasicAuth) {
90132
response
91133
..statusCode = HttpStatus.unauthorized
92-
..headers.add('WWW-Authenticate', 'Basic realm=Protected')
134+
..headers.add(HttpHeaders.wwwAuthenticateHeader, 'Basic realm=Protected')
93135
..headers.contentType = ContentType.text
94136
..write('PROXY///ERROR///UNAUTHORIZED')
95137
..close();
@@ -111,12 +153,13 @@ void main(List<String> arguments) async {
111153
.openUrl(request.method, targetUri)
112154
.then((HttpClientRequest proxyRequest) async {
113155
if (serverBasicAuth != null)
114-
proxyRequest.headers.add('Authorization', serverBasicAuth);
115-
request.headers.forEach((name, values) {
156+
proxyRequest.headers.add(HttpHeaders.authorizationHeader, serverBasicAuth);
157+
request.headers.forEach((String name, List<String> values) {
116158
if (![
117-
'host',
118-
].contains(name))
119-
if (name == 'referer')
159+
// Headers to skip
160+
HttpHeaders.hostHeader,
161+
].contains(name)) {
162+
if (name == HttpHeaders.refererHeader)
120163
proxyRequest.headers.add(
121164
name,
122165
values.map(
@@ -125,6 +168,7 @@ void main(List<String> arguments) async {
125168
);
126169
else
127170
proxyRequest.headers.add(name, values);
171+
}
128172
});
129173
if (request.contentLength > 0)
130174
await proxyRequest.addStream(request);
@@ -134,9 +178,9 @@ void main(List<String> arguments) async {
134178
stdout.write(' [${proxyResponse.statusCode}]');
135179
proxyResponse.headers.forEach((name, values) {
136180
if (![
137-
'connection',
138-
'content-length',
139-
'content-encoding',
181+
HttpHeaders.connectionHeader,
182+
HttpHeaders.contentLengthHeader,
183+
HttpHeaders.contentEncodingHeader,
140184
].contains(name))
141185
response.headers.add(name, values);
142186
});

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ packages:
1414
name: collection
1515
url: "https://pub.dartlang.org"
1616
source: hosted
17-
version: "1.15.0"
17+
version: "1.16.0"
1818
dotenv:
1919
dependency: "direct main"
2020
description:

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: dev_mirror
22
description: Development proxy for accessing private APIs.
3-
version: 1.0.1
3+
version: 1.1.0
44
homepage: https://github.com/Zekfad/dev-mirror
55

66
environment:
77
sdk: '>=2.15.1 <3.0.0'
88

99
dependencies:
10-
collection: ^1.15.0
10+
collection: ^1.16.0
1111
dotenv: ^3.0.0
1212
uri: ^1.0.0
1313
dev_dependencies:

0 commit comments

Comments
 (0)