Skip to content

Commit 3672058

Browse files
authored
Grout: ngrok Alternative (#1407)
* Grout: An Ngrok Alternative * Consume `grout` entry point within `proxy.py` * Revert `check.py`
1 parent e713752 commit 3672058

File tree

4 files changed

+230
-47
lines changed

4 files changed

+230
-47
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

proxy/__init__.py

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

1414

1515
__all__ = [
16+
# Grout entry point. See
17+
# https://jaxl.io/
18+
'grout',
1619
# PyPi package entry_point. See
1720
# https://github.com/abhinavsingh/proxy.py#from-command-line-when-installed-using-pip
1821
'entry_point',

proxy/proxy.py

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,35 @@
1010
"""
1111
import os
1212
import sys
13+
import gzip
14+
import json
1315
import time
1416
import pprint
1517
import signal
18+
import socket
19+
import getpass
1620
import logging
1721
import argparse
1822
import threading
19-
from typing import TYPE_CHECKING, Any, List, Type, Optional, cast
23+
from typing import TYPE_CHECKING, Any, Dict, List, Type, Tuple, Optional, cast
2024

2125
from .core.ssh import SshTunnelListener, SshHttpProtocolHandler
2226
from .core.work import ThreadlessPool
2327
from .core.event import EventManager
28+
from .http.codes import httpStatusCodes
2429
from .common.flag import FlagParser, flags
30+
from .http.client import client
2531
from .common.utils import bytes_
2632
from .core.work.fd import RemoteFdExecutor
33+
from .http.methods import httpMethods
2734
from .core.acceptor import AcceptorPool
2835
from .core.listener import ListenerPool
2936
from .core.ssh.base import BaseSshTunnelListener
37+
from .common.plugins import Plugins
38+
from .common.version import __version__
3039
from .common.constants import (
31-
IS_WINDOWS, DEFAULT_PLUGINS, DEFAULT_VERSION, DEFAULT_LOG_FILE,
32-
DEFAULT_PID_FILE, DEFAULT_LOG_LEVEL, DEFAULT_BASIC_AUTH,
40+
IS_WINDOWS, HTTPS_PROTO, DEFAULT_PLUGINS, DEFAULT_VERSION,
41+
DEFAULT_LOG_FILE, DEFAULT_PID_FILE, DEFAULT_LOG_LEVEL, DEFAULT_BASIC_AUTH,
3342
DEFAULT_LOG_FORMAT, DEFAULT_WORK_KLASS, DEFAULT_OPEN_FILE_LIMIT,
3443
DEFAULT_ENABLE_DASHBOARD, DEFAULT_ENABLE_SSH_TUNNEL,
3544
DEFAULT_SSH_LISTENER_KLASS,
@@ -384,3 +393,98 @@ def main(**opts: Any) -> None:
384393

385394
def entry_point() -> None:
386395
main()
396+
397+
398+
def grout() -> None: # noqa: C901
399+
default_grout_tld = os.environ.get('JAXL_DEFAULT_GROUT_TLD', 'jaxl.io')
400+
401+
def _clear_line() -> None:
402+
print('\r' + ' ' * 60, end='', flush=True)
403+
404+
def _env(scheme: bytes, host: bytes, port: int) -> Optional[Dict[str, Any]]:
405+
response = client(
406+
scheme=scheme,
407+
host=host,
408+
port=port,
409+
path=b'/env/',
410+
method=httpMethods.BIND,
411+
body='v={0}&u={1}&h={2}'.format(
412+
__version__,
413+
os.environ.get('USER', getpass.getuser()),
414+
socket.gethostname(),
415+
).encode(),
416+
)
417+
if response:
418+
if (
419+
response.code is not None
420+
and int(response.code) == httpStatusCodes.OK
421+
and response.body is not None
422+
):
423+
return cast(
424+
Dict[str, Any],
425+
json.loads(
426+
(
427+
gzip.decompress(response.body).decode()
428+
if response.has_header(b'content-encoding')
429+
and response.header(b'content-encoding') == b'gzip'
430+
else response.body.decode()
431+
),
432+
),
433+
)
434+
if response.code is None:
435+
_clear_line()
436+
print('\r\033[91mUnable to fetch\033[0m', end='', flush=True)
437+
else:
438+
_clear_line()
439+
print(
440+
'\r\033[91mError code {0}\033[0m'.format(
441+
response.code.decode(),
442+
),
443+
end='',
444+
flush=True,
445+
)
446+
else:
447+
_clear_line()
448+
print('\r\033[91mUnable to connect\033[0m')
449+
return None
450+
451+
def _parse() -> Tuple[str, int]:
452+
"""Here we deduce registry host/port based upon input parameters."""
453+
parser = argparse.ArgumentParser(add_help=False)
454+
parser.add_argument('route', nargs='?', default=None)
455+
parser.add_argument('name', nargs='?', default=None)
456+
args, _remaining_args = parser.parse_known_args()
457+
grout_tld = default_grout_tld
458+
if args.name is not None and '.' in args.name:
459+
grout_tld = args.name.split('.', maxsplit=1)[1]
460+
grout_tld_parts = grout_tld.split(':')
461+
tld_host = grout_tld_parts[0]
462+
tld_port = 443
463+
if len(grout_tld_parts) > 1:
464+
tld_port = int(grout_tld_parts[1])
465+
return tld_host, tld_port
466+
467+
tld_host, tld_port = _parse()
468+
env = None
469+
attempts = 0
470+
try:
471+
while True:
472+
env = _env(scheme=HTTPS_PROTO, host=tld_host.encode(), port=int(tld_port))
473+
attempts += 1
474+
if env is not None:
475+
print('\rStarting ...' + ' ' * 30 + '\r', end='', flush=True)
476+
break
477+
time.sleep(1)
478+
_clear_line()
479+
print(
480+
'\rWaiting for connection {0}'.format('.' * (attempts % 4)),
481+
end='',
482+
flush=True,
483+
)
484+
time.sleep(1)
485+
except KeyboardInterrupt:
486+
sys.exit(1)
487+
488+
assert env is not None
489+
print('\r' + ' ' * 70 + '\r', end='', flush=True)
490+
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)