Skip to content

Commit 028a018

Browse files
committed
Grout: An Ngrok Alternative
1 parent e713752 commit 028a018

File tree

5 files changed

+245
-44
lines changed

5 files changed

+245
-44
lines changed

README.md

Lines changed: 118 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@
7171
- [End-to-End Encryption](#end-to-end-encryption)
7272
- [TLS Interception](#tls-interception)
7373
- [TLS Interception With Docker](#tls-interception-with-docker)
74+
- [GROUT (NGROK Alternative)](#grout-ngrok-alternative)
75+
- [How Grout works](#how-grout-works)
76+
- [Self-hosted Grout](#self-hosted-grout)
7477
- [Proxy Over SSH Tunnel](#proxy-over-ssh-tunnel)
7578
- [Proxy Remote Requests Locally](#proxy-remote-requests-locally)
7679
- [Proxy Local Requests Remotely](#proxy-local-requests-remotely)
@@ -138,6 +141,7 @@
138141
[//]: # (DO-NOT-REMOVE-docs-badges-END)
139142

140143
# Features
144+
- [A drop-in alternative to `ngrok`](#grout-ngrok-alternative)
141145
- Fast & Scalable
142146

143147
- Scale up by using all available cores on the system
@@ -1290,6 +1294,76 @@ with TLS Interception:
12901294
}
12911295
```
12921296

1297+
# GROUT (NGROK Alternative)
1298+
1299+
`grout` is a drop-in alternative to `ngrok` that comes packaged within `proxy.py`
1300+
1301+
```console
1302+
grout
1303+
NAME:
1304+
grout - securely tunnel local files, folders and services to public URLs
1305+
1306+
USAGE:
1307+
grout route [name]
1308+
1309+
DESCRIPTION:
1310+
grout exposes local networked services behinds NATs and firewalls to the
1311+
public internet over a secure tunnel. Share local folders, directories and websites,
1312+
build/test webhook consumers and self-host personal services to public URLs.
1313+
1314+
EXAMPLES:
1315+
Share Files and Folders:
1316+
grout C:\path\to\folder # Share a folder on your system
1317+
grout /path/to/folder # Share a folder on your system
1318+
grout /path/to/folder --basic-auth user:pass # Add authentication for shared folder
1319+
grout /path/to/photo.jpg # Share a specific file on your system
1320+
1321+
Expose HTTP, HTTPS and Websockets:
1322+
grout http://localhost:9090 # Expose HTTP service running on port 9090
1323+
grout https://localhost:8080 # Expose HTTPS service running on port 8080
1324+
grout https://localhost:8080 --path /worker/ # Expose only certain paths of HTTPS service on port 8080
1325+
grout https://localhost:8080 --basic-auth u:p # Add authentication for exposed HTTPS service on port 8080
1326+
1327+
Expose TCP Services:
1328+
grout tcp://:6379 # Expose Redis service running locally on port 6379
1329+
grout tcp://:22 # Expose SSH service running locally on port 22
1330+
1331+
Custom URLs:
1332+
grout https://localhost:8080 abhinavsingh # Custom URL for HTTPS service running on port 8080
1333+
grout tcp://:22 abhinavsingh # Custom URL for SSH service running locally on port 22
1334+
1335+
Custom Domains:
1336+
grout tcp://:5432 abhinavsingh.domain.tld # Custom URL for Postgres service running locally on port 5432
1337+
1338+
Self-hosted solutions:
1339+
grout tcp://:5432 abhinavsingh.my.server # Custom URL for Postgres service running locally on port 5432
1340+
1341+
SUPPORT:
1342+
Write to us at support@jaxl.com
1343+
1344+
Privacy policy and Terms & conditions
1345+
https://jaxl.com/privacy/
1346+
1347+
Created by Jaxl™
1348+
https://jaxl.io
1349+
```
1350+
1351+
## How Grout works
1352+
1353+
- `grout` infrastructure has 2 components: client and server
1354+
- `grout` client has 2 components: a thin and a thick client
1355+
- `grout` thin client is part of open source `proxy.py` (BSD 3-Clause License)
1356+
- `grout` thick client and servers are hosted at [jaxl.io](https://jaxl.io)
1357+
and a copyright of [Jaxl Innovations Private Limited](https://jaxl.com)
1358+
- `grout` server has 3 components: a registry server, a reverse proxy server and a tunnel server
1359+
1360+
## Self-Hosted `grout`
1361+
1362+
- `grout` thick client and servers can also be hosted on your GCP, AWS, Cloud infrastructures
1363+
- With a self-hosted version, your traffic flows through the network you control and trust
1364+
- `grout` developers at [jaxl.io](https://jaxl.io) provides GCP, AWS, Docker images for self-hosted solutions
1365+
- Please drop an email at [support@jaxl.com](mailto:support@jaxl.com) to get started.
1366+
12931367
# Proxy Over SSH Tunnel
12941368

12951369
**This is a WIP and may not work as documented**
@@ -2340,12 +2414,17 @@ To run standalone benchmark for `proxy.py`, use the following command from repo
23402414

23412415
```console
23422416
proxy -h
2343-
usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
2417+
usage: -m [-h] [--enable-proxy-protocol] [--threadless] [--threaded]
2418+
[--num-workers NUM_WORKERS] [--enable-events] [--enable-conn-pool]
2419+
[--key-file KEY_FILE] [--cert-file CERT_FILE]
2420+
[--client-recvbuf-size CLIENT_RECVBUF_SIZE]
2421+
[--server-recvbuf-size SERVER_RECVBUF_SIZE]
2422+
[--max-sendbuf-size MAX_SENDBUF_SIZE] [--timeout TIMEOUT]
2423+
[--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
23442424
[--tunnel-username TUNNEL_USERNAME]
23452425
[--tunnel-ssh-key TUNNEL_SSH_KEY]
23462426
[--tunnel-ssh-key-passphrase TUNNEL_SSH_KEY_PASSPHRASE]
2347-
[--tunnel-remote-port TUNNEL_REMOTE_PORT] [--threadless]
2348-
[--threaded] [--num-workers NUM_WORKERS] [--enable-events]
2427+
[--tunnel-remote-port TUNNEL_REMOTE_PORT]
23492428
[--local-executor LOCAL_EXECUTOR] [--backlog BACKLOG]
23502429
[--hostname HOSTNAME] [--hostnames HOSTNAMES [HOSTNAMES ...]]
23512430
[--port PORT] [--ports PORTS [PORTS ...]] [--port-file PORT_FILE]
@@ -2357,10 +2436,6 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
23572436
[--basic-auth BASIC_AUTH] [--enable-ssh-tunnel]
23582437
[--work-klass WORK_KLASS] [--pid-file PID_FILE] [--openssl OPENSSL]
23592438
[--data-dir DATA_DIR] [--ssh-listener-klass SSH_LISTENER_KLASS]
2360-
[--enable-proxy-protocol] [--enable-conn-pool] [--key-file KEY_FILE]
2361-
[--cert-file CERT_FILE] [--client-recvbuf-size CLIENT_RECVBUF_SIZE]
2362-
[--server-recvbuf-size SERVER_RECVBUF_SIZE]
2363-
[--max-sendbuf-size MAX_SENDBUF_SIZE] [--timeout TIMEOUT]
23642439
[--disable-http-proxy] [--disable-headers DISABLE_HEADERS]
23652440
[--ca-key-file CA_KEY_FILE] [--ca-cert-dir CA_CERT_DIR]
23662441
[--ca-cert-file CA_CERT_FILE] [--ca-file CA_FILE]
@@ -2378,10 +2453,45 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
23782453
[--filtered-client-ips FILTERED_CLIENT_IPS]
23792454
[--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
23802455

2381-
proxy.py v2.4.4rc6.dev172+ge1879403.d20240425
2456+
proxy.py v2.4.4rc6.dev191+gef5a8922
23822457

23832458
options:
23842459
-h, --help show this help message and exit
2460+
--enable-proxy-protocol
2461+
Default: False. If used, will enable proxy protocol.
2462+
Only version 1 is currently supported.
2463+
--threadless Default: True. Enabled by default on Python 3.8+ (mac,
2464+
linux). When disabled a new thread is spawned to
2465+
handle each client connection.
2466+
--threaded Default: False. Disabled by default on Python < 3.8
2467+
and windows. When enabled a new thread is spawned to
2468+
handle each client connection.
2469+
--num-workers NUM_WORKERS
2470+
Defaults to number of CPU cores.
2471+
--enable-events Default: False. Enables core to dispatch lifecycle
2472+
events. Plugins can be used to subscribe for core
2473+
events.
2474+
--enable-conn-pool Default: False. (WIP) Enable upstream connection
2475+
pooling.
2476+
--key-file KEY_FILE Default: None. Server key file to enable end-to-end
2477+
TLS encryption with clients. If used, must also pass
2478+
--cert-file.
2479+
--cert-file CERT_FILE
2480+
Default: None. Server certificate to enable end-to-end
2481+
TLS encryption with clients. If used, must also pass
2482+
--key-file.
2483+
--client-recvbuf-size CLIENT_RECVBUF_SIZE
2484+
Default: 128 KB. Maximum amount of data received from
2485+
the client in a single recv() operation.
2486+
--server-recvbuf-size SERVER_RECVBUF_SIZE
2487+
Default: 128 KB. Maximum amount of data received from
2488+
the server in a single recv() operation.
2489+
--max-sendbuf-size MAX_SENDBUF_SIZE
2490+
Default: 64 KB. Maximum amount of data to flush in a
2491+
single send() operation.
2492+
--timeout TIMEOUT Default: 10.0. Number of seconds after which an
2493+
inactive connection must be dropped. Inactivity is
2494+
defined by no data sent or received by the client.
23852495
--tunnel-hostname TUNNEL_HOSTNAME
23862496
Default: None. Remote hostname or IP address to which
23872497
SSH tunnel will be established.
@@ -2397,17 +2507,6 @@ options:
23972507
--tunnel-remote-port TUNNEL_REMOTE_PORT
23982508
Default: 8899. Remote port which will be forwarded
23992509
locally for proxy.
2400-
--threadless Default: True. Enabled by default on Python 3.8+ (mac,
2401-
linux). When disabled a new thread is spawned to
2402-
handle each client connection.
2403-
--threaded Default: False. Disabled by default on Python < 3.8
2404-
and windows. When enabled a new thread is spawned to
2405-
handle each client connection.
2406-
--num-workers NUM_WORKERS
2407-
Defaults to number of CPU cores.
2408-
--enable-events Default: False. Enables core to dispatch lifecycle
2409-
events. Plugins can be used to subscribe for core
2410-
events.
24112510
--local-executor LOCAL_EXECUTOR
24122511
Default: 1. Enabled by default. Use 0 to disable. When
24132512
enabled acceptors will make use of local (same
@@ -2463,30 +2562,6 @@ options:
24632562
--ssh-listener-klass SSH_LISTENER_KLASS
24642563
Default: proxy.core.ssh.listener.SshTunnelListener. An
24652564
implementation of BaseSshTunnelListener
2466-
--enable-proxy-protocol
2467-
Default: False. If used, will enable proxy protocol.
2468-
Only version 1 is currently supported.
2469-
--enable-conn-pool Default: False. (WIP) Enable upstream connection
2470-
pooling.
2471-
--key-file KEY_FILE Default: None. Server key file to enable end-to-end
2472-
TLS encryption with clients. If used, must also pass
2473-
--cert-file.
2474-
--cert-file CERT_FILE
2475-
Default: None. Server certificate to enable end-to-end
2476-
TLS encryption with clients. If used, must also pass
2477-
--key-file.
2478-
--client-recvbuf-size CLIENT_RECVBUF_SIZE
2479-
Default: 128 KB. Maximum amount of data received from
2480-
the client in a single recv() operation.
2481-
--server-recvbuf-size SERVER_RECVBUF_SIZE
2482-
Default: 128 KB. Maximum amount of data received from
2483-
the server in a single recv() operation.
2484-
--max-sendbuf-size MAX_SENDBUF_SIZE
2485-
Default: 64 KB. Maximum amount of data to flush in a
2486-
single send() operation.
2487-
--timeout TIMEOUT Default: 10.0. Number of seconds after which an
2488-
inactive connection must be dropped. Inactivity is
2489-
defined by no data sent or received by the client.
24902565
--disable-http-proxy Default: False. Whether to disable
24912566
proxy.HttpProxyPlugin.
24922567
--disable-headers DISABLE_HEADERS

check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
# Ensure all python files start with licensing information
4646
for py_file in ALL_PY_FILES:
47-
if py_file.is_file() and py_file.name != '_scm_version.py':
47+
if py_file.is_file() and py_file.name not in ('_scm_version.py', 'grout.py'):
4848
with open(py_file, 'rb') as f:
4949
code = f.read(len(PY_FILE_PREFIX))
5050
if code != PY_FILE_PREFIX:

proxy/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
from .grout import grout
1112
from .proxy import Proxy, main, sleep_loop, entry_point
1213
from .testing import TestCase
1314

1415

1516
__all__ = [
17+
# Grout entry point. See
18+
# https://jaxl.io/
19+
'grout',
1620
# PyPi package entry_point. See
1721
# https://github.com/abhinavsingh/proxy.py#from-command-line-when-installed-using-pip
1822
'entry_point',

proxy/grout.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Copyright (c) 2010-present by Jaxl Innovations Private Limited.
4+
5+
All rights reserved.
6+
7+
Redistribution and use in source and binary forms,
8+
with or without modification, is strictly prohibited.
9+
"""
10+
11+
import os
12+
import sys
13+
import gzip
14+
import json
15+
import time
16+
import socket
17+
import getpass
18+
import argparse
19+
from typing import Any, Dict, Tuple, Optional, cast
20+
21+
from .http.codes import httpStatusCodes
22+
from .http.client import client
23+
from .http.methods import httpMethods
24+
from .common.plugins import Plugins
25+
from .common.version import __version__
26+
from .common.constants import HTTPS_PROTO
27+
28+
29+
def grout() -> None: # noqa: C901
30+
default_grout_tld = os.environ.get('JAXL_DEFAULT_GROUT_TLD', 'jaxl.io')
31+
32+
def _clear_line() -> None:
33+
print('\r' + ' ' * 60, end='', flush=True)
34+
35+
def _env(scheme: bytes, host: bytes, port: int) -> Optional[Dict[str, Any]]:
36+
response = client(
37+
scheme=scheme,
38+
host=host,
39+
port=port,
40+
path=b'/env/',
41+
method=httpMethods.BIND,
42+
body='v={0}&u={1}&h={2}'.format(
43+
__version__,
44+
os.environ.get('USER', getpass.getuser()),
45+
socket.gethostname(),
46+
).encode(),
47+
)
48+
if response:
49+
if (
50+
response.code is not None
51+
and int(response.code) == httpStatusCodes.OK
52+
and response.body is not None
53+
):
54+
return cast(
55+
Dict[str, Any],
56+
json.loads(
57+
(
58+
gzip.decompress(response.body).decode()
59+
if response.has_header(b'content-encoding')
60+
and response.header(b'content-encoding') == b'gzip'
61+
else response.body.decode()
62+
),
63+
),
64+
)
65+
if response.code is None:
66+
_clear_line()
67+
print('\r\033[91mUnable to fetch\033[0m', end='', flush=True)
68+
else:
69+
_clear_line()
70+
print(
71+
'\r\033[91mError code {0}\033[0m'.format(
72+
response.code.decode(),
73+
),
74+
end='',
75+
flush=True,
76+
)
77+
else:
78+
_clear_line()
79+
print('\r\033[91mUnable to connect\033[0m')
80+
return None
81+
82+
def _parse() -> Tuple[str, int]:
83+
"""Here we deduce registry host/port based upon input parameters."""
84+
parser = argparse.ArgumentParser(add_help=False)
85+
parser.add_argument('route', nargs='?', default=None)
86+
parser.add_argument('name', nargs='?', default=None)
87+
args, _remaining_args = parser.parse_known_args()
88+
grout_tld = default_grout_tld
89+
if args.name is not None and '.' in args.name:
90+
grout_tld = args.name.split('.', maxsplit=1)[1]
91+
grout_tld_parts = grout_tld.split(':')
92+
tld_host = grout_tld_parts[0]
93+
tld_port = 443
94+
if len(grout_tld_parts) > 1:
95+
tld_port = int(grout_tld_parts[1])
96+
return tld_host, tld_port
97+
98+
tld_host, tld_port = _parse()
99+
env = None
100+
attempts = 0
101+
try:
102+
while True:
103+
env = _env(scheme=HTTPS_PROTO, host=tld_host.encode(), port=int(tld_port))
104+
attempts += 1
105+
if env is not None:
106+
print('\rStarting ...' + ' ' * 30 + '\r', end='', flush=True)
107+
break
108+
time.sleep(1)
109+
_clear_line()
110+
print(
111+
'\rWaiting for connection {0}'.format('.' * (attempts % 4)),
112+
end='',
113+
flush=True,
114+
)
115+
time.sleep(1)
116+
except KeyboardInterrupt:
117+
sys.exit(1)
118+
119+
assert env is not None
120+
print('\r' + ' ' * 70 + '\r', end='', flush=True)
121+
Plugins.from_bytes(env['m'].encode(), name='client').grout(env=env['e']) # type: ignore[attr-defined]

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ install_requires =
111111
[options.entry_points]
112112
console_scripts =
113113
proxy = proxy:entry_point
114+
grout = proxy:grout
114115

115116
[options.package_data]
116117
proxy =

0 commit comments

Comments
 (0)