Skip to content

Commit

Permalink
feat: 1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Zekfad committed Jun 30, 2023
1 parent 6f31fe0 commit 60a7062
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 116 deletions.
19 changes: 11 additions & 8 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
SERVER_SCHEME = https
SERVER_HOST = example.com
SERVER_PORT = 443
SERVER_USERNAME = user
SERVER_PASSWORD = passw0rd
LOCAL_BIND_IP = 127.0.0.1
LOCAL_PORT = 8080
HTTP_PROXY = http://username:pa$$w0rd@example.com:1337/
REMOTE_URI = https://username:pa$$w0rd@example.com:443/
REMOTE_SCHEME = https
REMOTE_USERNAME = user
REMOTE_PASSWORD = passw0rd
REMOTE_HOST = example.com
REMOTE_PORT = 443
LOCAL_URI = http://user:passw0rd@127.0.0.1:8080/
LOCAL_SCHEME = http
LOCAL_USERNAME = user
LOCAL_PASSWORD = passw0rd
HTTP_PROXY = http://username:pa$$w0rd@example.com:1337/
LOCAL_HOST = 127.0.0.1
LOCAL_PORT = 8080
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# 1.2.0

- Add `Access-Control-Expose-Headers` header if none is provided by server.
- Rename environment variables:
`SERVER_SCHEME` -> `REMOTE_SCHEME`
`SERVER_HOST` -> `REMOTE_HOST`
`SERVER_USERNAME` -> `REMOTE_USERNAME`
`SERVER_PASSWORD` -> `REMOTE_PASSWORD`
`SERVER_POST` -> `REMOTE_POST`
`LOCAL_BIND_IP` -> `LOCAL_HOST`
- Now you can use just `LOCAL_URI` and `REMOTE_URI` same as `HTTP_PROXY`,
this simplifies usage from console.

# 1.1.2

- Add support for internal redirects.
Expand Down
40 changes: 33 additions & 7 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,60 @@ It will help you to keep private credentials out of build artifacts.

## Features

* Target server authentication
* Remote server authentication
* Local server authentication
* Spoofing referrer
* Referrer spoofing (pretend that request came from target origin)
* CORS bypass
* HTTP proxy support
* Adds `Access-Control-Expose-Headers` to allow browser inspect headers.

## Usage

### From console

You only need to provide `REMOTE_URI` and optionally `LOCAL_URI` (defaults to
`http://127.0.0.1:8080`)

```shell
REMOTE_URI=http://localhost:8080/
LOCAL_URI=http://127.0.0.1:8081/
```

### Via config (`.env` file)
Use environmental variables or `.env` file in working directory or pass it's
location as first argument:

```dotenv
# Remote HTTP(S) server
## Remote HTTP(S) server
# Remote server URI (preferred)
REMOTE_URI = https://username:pa$$w0rd@example.com:443/
# Or exploded URI
SERVER_SCHEME = https
SERVER_HOST = example.com
SERVER_PORT = 443
# Remote server HTTP Basic auth (optional)
SERVER_USERNAME = user
SERVER_PASSWORD = passw0rd
# Local HTTP server
LOCAL_BIND_IP = 127.0.0.1
REMOTE_USERNAME = user
REMOTE_PASSWORD = passw0rd
## Local HTTP server
# Local server URI (preferred)
LOCAL_URI = http://user:passw0rd@127.0.0.1:8080/
# Or exploded URI
LOCAL_HOST = 127.0.0.1
LOCAL_PORT = 8080
# Local server HTTP Basic auth (optional)
LOCAL_USERNAME = user
LOCAL_PASSWORD = passw0rd
# HTTP proxy (optional)
HTTP_PROXY = http://username:pa$$w0rd@example.com:1337/
```

## Notes

Keep in mind that if you have local server authentication, you won't be able
to send authentication details to a remote server through a mirror.

Expand Down
120 changes: 39 additions & 81 deletions bin/main.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import 'dart:convert';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:dotenv/dotenv.dart' as dotenv;
import 'package:dev_mirror/config.dart';
import 'package:dev_mirror/secure_compare.dart';
import 'package:dev_mirror/uri_basic_auth.dart';
import 'package:dev_mirror/uri_credentials.dart';
import 'package:dev_mirror/uri_has_origin.dart';
import 'package:uri/uri.dart';


/// Invalid string URI.
const _invalidUri = '::Not valid URI::';
const headersNotToForwardToRemote = [
HttpHeaders.hostHeader,
];
Expand All @@ -22,103 +23,56 @@ const headersToSpoofBeforeForwardFromRemote = [
];


extension UriHasOrigin on Uri {
bool get hasOrigin => (scheme == 'http' || scheme == 'https') && host != '';
}

/// List of additional root CA
final List<String> trustedRoots = [
[72, 80, 78, 151, 76, 13, 172, 91, 92, 212, 118, 200, 32, 34, 116, 178, 76, 140, 113, 114], // DST Root CA X3
].map(String.fromCharCodes).toList();

bool secureCompare(String a, String b) {
if(a.codeUnits.length != b.codeUnits.length)
return false;

var r = 0;
for(var i = 0; i < a.codeUnits.length; i++) {
r |= a.codeUnitAt(i) ^ b.codeUnitAt(i);
}
return r == 0;
}

/// Returns environment variable or `.env` variable
String? getEnv(String variable) =>
Platform.environment[variable] ?? dotenv.env[variable];

/// Adds CORS headers to [response]
void addCORSHeaders(HttpRequest request, HttpResponse response) {
final refererUri = Uri.tryParse(
request.headers[HttpHeaders.refererHeader]?.singleOrNull ?? _invalidUri,
request.headers[HttpHeaders.refererHeader]?.singleOrNull ?? '::INVALID::',
);
response.headers
..add(
..set(
HttpHeaders.accessControlAllowOriginHeader,
(refererUri != null && refererUri.hasOrigin)
? refererUri.origin
: '*',
)
..add(
..set(
HttpHeaders.accessControlAllowMethodsHeader,
request.headers[HttpHeaders.accessControlRequestMethodHeader]?.join(',')
?? '*',
)
..add(
..set(
HttpHeaders.accessControlAllowHeadersHeader,
request.headers[HttpHeaders.accessControlRequestHeadersHeader]?.join(',')
?? 'authorization,*',
)
..add(
..set(
HttpHeaders.accessControlAllowCredentialsHeader,
'true',
)
..set(
HttpHeaders.accessControlExposeHeadersHeader,
request.headers[HttpHeaders.accessControlExposeHeadersHeader]?.join(',')
?? 'authorization,*',
);
}

void main(List<String> arguments) async {
final dotEnvFile = arguments.firstOrNull ?? '.env';
if (File.fromUri(Uri.file(dotEnvFile)).existsSync())
dotenv.load(dotEnvFile);

// Local server bind settings
final localBindIp = InternetAddress.tryParse(getEnv('LOCAL_BIND_IP') ?? '') ?? InternetAddress.loopbackIPv4;
final localPort = int.tryParse(getEnv('LOCAL_PORT') ?? '') ?? 8080;

// Local auth
final localUsername = getEnv('LOCAL_USERNAME');
final localPassword = getEnv('LOCAL_PASSWORD');
final localBasicAuth = (localUsername == null || localPassword == null) ? null
: 'Basic ${base64Encode(utf8.encode('$localUsername:$localPassword'))}';
final localBaseUrl = 'http://${localBindIp.host}:$localPort';

// Remote server
final remoteScheme = getEnv('SERVER_SCHEME') ?? 'https';
final remoteHost = getEnv('SERVER_HOST') ?? 'example.com';
final remotePort = int.tryParse(getEnv('SERVER_PORT') ?? (remoteScheme == 'https' ? '443' : '')) ?? 80;
final config = Config.load(dotEnvFile);

// Remote server auth
final remoteUsername = getEnv('SERVER_USERNAME');
final remotePassword = getEnv('SERVER_PASSWORD');
final remoteBasicAuth = (remoteUsername == null || remotePassword == null) ? null
: 'Basic ${base64Encode(utf8.encode('$remoteUsername:$remotePassword'))}';
final serverBaseUrl = '$remoteScheme://$remoteHost${![ 'http', 'https', ].contains(remoteScheme) ? remotePort : ''}';

// HTTP proxy
final httpProxy = Uri.tryParse(getEnv('HTTP_PROXY') ?? _invalidUri);
final httpProxyCredentialsMatch = httpProxy == null ? null
: RegExp(r'^(?<username>.+?):(?<password>.+?)$').firstMatch(httpProxy.userInfo);
final httpProxyUsername = httpProxyCredentialsMatch?.namedGroup('username');
final httpProxyPassword = httpProxyCredentialsMatch?.namedGroup('password');
final httpProxyCredentials = (httpProxyUsername == null || httpProxyPassword == null) ? null
: HttpClientBasicCredentials(httpProxyUsername, httpProxyPassword);

stdout.write('Starting mirror server $localBaseUrl -> $serverBaseUrl');
if (localBasicAuth != null)
stdout.write('Starting mirror server ${config.local} -> ${config.remote}');
if (config.local.basicAuth != null)
stdout.write(' [Local auth]');
if (remoteBasicAuth != null)
if (config.remote.basicAuth != null)
stdout.write(' [Remote auth auto-fill]');
if (httpProxy != null) {
if (config.proxy != null) {
stdout.write(' [Through HTTP proxy]');
if (httpProxy.scheme != 'http') {
if (config.proxy!.scheme != 'http') {
stdout.writeln(' [Error]');
stderr.writeln('Proxy URI must be valid.');
return;
Expand All @@ -127,7 +81,7 @@ void main(List<String> arguments) async {

late final HttpServer server;
try {
server = await HttpServer.bind(localBindIp, localPort);
server = await HttpServer.bind(config.local.host, config.local.port);
} catch(error) {
stdout.writeln(' [Error]');
stderr
Expand All @@ -143,16 +97,17 @@ void main(List<String> arguments) async {
trustedRoots.contains(String.fromCharCodes(cert.sha1));

// Apply HTTP proxy
if (httpProxy != null) {
if (httpProxyCredentials != null) {
if (config.proxy case final Uri proxy) {
final credentials = proxy.httpClientCredentials;
if (credentials != null) {
client.addProxyCredentials(
httpProxy.host,
httpProxy.port,
proxy.host,
proxy.port,
'Basic',
httpProxyCredentials,
credentials,
);
}
client.findProxy = (uri) => 'PROXY ${httpProxy.host}:${httpProxy.port}';
client.findProxy = (uri) => 'PROXY ${proxy.host}:${proxy.port}';
}

var requestId = 0;
Expand All @@ -162,13 +117,12 @@ void main(List<String> arguments) async {

final response = request.response;

addCORSHeaders(request, response);

// Handle preflight
if (
request.method.toUpperCase() == 'OPTIONS' &&
request.headers[HttpHeaders.accessControlRequestMethodHeader] != null
) {
addCORSHeaders(request, response);
stdout.writeln('[$requestId] Preflight handled.');
response
..contentLength = 0
Expand All @@ -177,6 +131,7 @@ void main(List<String> arguments) async {
return;
}

final localBasicAuth = config.local.basicAuth;
if (localBasicAuth != null) {
final _userAuth = request.headers[HttpHeaders.authorizationHeader]?.singleOrNull;
if (_userAuth == null || !secureCompare(_userAuth, localBasicAuth)) {
Expand All @@ -192,9 +147,9 @@ void main(List<String> arguments) async {
}

final remoteUri = (UriBuilder.fromUri(request.uri)
..scheme = remoteScheme
..host = remoteHost
..port = remotePort
..scheme = config.remote.scheme
..host = config.remote.host
..port = config.remote.port
).build();

stdout.writeln('[$requestId] Forwarding: ${request.method} $remoteUri');
Expand All @@ -205,6 +160,7 @@ void main(List<String> arguments) async {
requestToRemote.followRedirects = false;

// Remote server auth
final remoteBasicAuth = config.remote.basicAuth;
if (remoteBasicAuth != null)
requestToRemote.headers.add(HttpHeaders.authorizationHeader, remoteBasicAuth);

Expand All @@ -216,7 +172,7 @@ void main(List<String> arguments) async {
requestToRemote.headers.add(
headerName,
headerValues.map(
(value) => value.replaceAll(localBaseUrl, serverBaseUrl),
(value) => value.replaceAll(config.local.toString(), config.remote.toString()),
),
);
else
Expand All @@ -242,14 +198,15 @@ void main(List<String> arguments) async {
response.headers.add(
headerName,
headerValues.map(
(value) => value.replaceAll(serverBaseUrl, localBaseUrl),
(value) => value.replaceAll(config.remote.toString(), config.local.toString()),
),
);
// Add headers as-is
else
response.headers.add(headerName, headerValues);
});
response.statusCode = remoteResponse.statusCode;
addCORSHeaders(request, response);

// Pipe remote response
remoteResponse
Expand Down Expand Up @@ -277,6 +234,7 @@ void main(List<String> arguments) async {
..writeln('[$requestId] Mirror error:')
..writeln(_error);

addCORSHeaders(request, response);
response
..statusCode = HttpStatus.internalServerError
..headers.contentType = ContentType.text
Expand Down
Loading

0 comments on commit 60a7062

Please sign in to comment.