buglupa-connect 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.
- buglupa_connect-0.1.0/.github/workflows/workflow.yml +59 -0
- buglupa_connect-0.1.0/.gitignore +9 -0
- buglupa_connect-0.1.0/LICENSE +21 -0
- buglupa_connect-0.1.0/PKG-INFO +35 -0
- buglupa_connect-0.1.0/README.md +20 -0
- buglupa_connect-0.1.0/pyproject.toml +49 -0
- buglupa_connect-0.1.0/src/buglupa_connect/__init__.py +8 -0
- buglupa_connect-0.1.0/src/buglupa_connect/__main__.py +5 -0
- buglupa_connect-0.1.0/src/buglupa_connect/cli.py +200 -0
- buglupa_connect-0.1.0/src/buglupa_connect/client.py +376 -0
- buglupa_connect-0.1.0/src/buglupa_connect/forward.py +395 -0
- buglupa_connect-0.1.0/src/buglupa_connect/http1.py +387 -0
- buglupa_connect-0.1.0/src/buglupa_connect/protocol.py +306 -0
- buglupa_connect-0.1.0/src/buglupa_connect/ws.py +306 -0
- buglupa_connect-0.1.0/tests/test_client_rules.py +60 -0
- buglupa_connect-0.1.0/tests/test_http1.py +92 -0
- buglupa_connect-0.1.0/tests/test_protocol.py +115 -0
- buglupa_connect-0.1.0/tests/test_target.py +72 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Tests every push; publishes to PyPI only for a version tag (v0.1.0 and so on) whose version matches pyproject.toml.
|
|
2
|
+
# PyPI trusts this exact file (trusted publishing, no stored token), in the "pypi" environment.
|
|
3
|
+
name: Test and release
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
pull_request:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
python: ["3.10", "3.12", "3.14"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python }}
|
|
27
|
+
- run: uv run --group dev pytest -q
|
|
28
|
+
|
|
29
|
+
build:
|
|
30
|
+
needs: test
|
|
31
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
35
|
+
with:
|
|
36
|
+
persist-credentials: false
|
|
37
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
38
|
+
- name: The tag matches the package version
|
|
39
|
+
run: |
|
|
40
|
+
version=$(uv version --short)
|
|
41
|
+
test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME doesn't match version $version"; exit 1; }
|
|
42
|
+
- run: uv build
|
|
43
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
|
|
48
|
+
publish:
|
|
49
|
+
needs: build
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
environment: pypi
|
|
52
|
+
permissions:
|
|
53
|
+
id-token: write
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
56
|
+
with:
|
|
57
|
+
name: dist
|
|
58
|
+
path: dist/
|
|
59
|
+
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yarqat (SMC-Private) Limited (https://yarqat.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: buglupa-connect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: buglupa connect: let BugLupa Cloud's scanner test an app on your computer through a private, outbound-only tunnel.
|
|
5
|
+
Project-URL: Homepage, https://buglupa.com
|
|
6
|
+
Project-URL: Documentation, https://buglupa.com/docs
|
|
7
|
+
Author-email: "Yarqat (SMC-Private) Limited" <support@yarqat.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# buglupa-connect
|
|
17
|
+
|
|
18
|
+
Let BugLupa Cloud's scanner test an app running on your computer, through a private tunnel that only BugLupa's
|
|
19
|
+
scanner can use. Your app is never put on the internet.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
$ pipx install buglupa-connect # or: uvx buglupa-connect localhost:3000
|
|
23
|
+
$ export BUGLUPA_CONNECT_TOKEN=... # a connect token for your project, from BugLupa Cloud
|
|
24
|
+
$ buglupa connect localhost:3000
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- The client makes one outbound, encrypted connection to BugLupa and never listens for connections.
|
|
28
|
+
- It forwards requests only to the one address you name (local and private addresses only).
|
|
29
|
+
- Every connection needs a short-lived token from your BugLupa Cloud project; Ctrl+C ends access at once.
|
|
30
|
+
- Python 3.10 or newer, standard library only, so there is nothing else to audit.
|
|
31
|
+
|
|
32
|
+
BugLupa Cloud is in early access at [buglupa.com](https://buglupa.com). Documentation: https://buglupa.com/docs
|
|
33
|
+
|
|
34
|
+
This client is open source under the MIT licence so you can read exactly what runs on your computer. BugLupa itself
|
|
35
|
+
is a separate product by [Yarqat](https://yarqat.com). Security reports: support@yarqat.com.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# buglupa-connect
|
|
2
|
+
|
|
3
|
+
Let BugLupa Cloud's scanner test an app running on your computer, through a private tunnel that only BugLupa's
|
|
4
|
+
scanner can use. Your app is never put on the internet.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
$ pipx install buglupa-connect # or: uvx buglupa-connect localhost:3000
|
|
8
|
+
$ export BUGLUPA_CONNECT_TOKEN=... # a connect token for your project, from BugLupa Cloud
|
|
9
|
+
$ buglupa connect localhost:3000
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- The client makes one outbound, encrypted connection to BugLupa and never listens for connections.
|
|
13
|
+
- It forwards requests only to the one address you name (local and private addresses only).
|
|
14
|
+
- Every connection needs a short-lived token from your BugLupa Cloud project; Ctrl+C ends access at once.
|
|
15
|
+
- Python 3.10 or newer, standard library only, so there is nothing else to audit.
|
|
16
|
+
|
|
17
|
+
BugLupa Cloud is in early access at [buglupa.com](https://buglupa.com). Documentation: https://buglupa.com/docs
|
|
18
|
+
|
|
19
|
+
This client is open source under the MIT licence so you can read exactly what runs on your computer. BugLupa itself
|
|
20
|
+
is a separate product by [Yarqat](https://yarqat.com). Security reports: support@yarqat.com.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "buglupa-connect"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "buglupa connect: let BugLupa Cloud's scanner test an app on your computer through a private, outbound-only tunnel."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Yarqat (SMC-Private) Limited", email = "support@yarqat.com" }]
|
|
10
|
+
# Standard library only: nothing to audit but this package, and a single-file build stays simple.
|
|
11
|
+
dependencies = []
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://buglupa.com"
|
|
20
|
+
Documentation = "https://buglupa.com/docs"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
buglupa-connect = "buglupa_connect.cli:main"
|
|
24
|
+
buglupa = "buglupa_connect.cli:buglupa_main"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24"]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling>=1.27"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/buglupa_connect"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
asyncio_mode = "auto"
|
|
39
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 120
|
|
43
|
+
target-version = "py310"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "W", "I", "B", "S", "UP"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
"tests/**" = ["S101", "S105", "S106", "S107"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""`buglupa connect`: let BugLupa Cloud's scanner test an app on your computer through a private tunnel.
|
|
2
|
+
|
|
3
|
+
The client opens one outbound, encrypted WebSocket to BugLupa's tunnel service and never listens for connections.
|
|
4
|
+
Requests arrive over that connection, and the client forwards each one only to the address you named (for example
|
|
5
|
+
`localhost:3000`). Standard library only.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""`buglupa connect localhost:3000` (also `buglupa-connect localhost:3000`).
|
|
2
|
+
|
|
3
|
+
The connect token comes from the BUGLUPA_CONNECT_TOKEN environment variable, a file (`--token-file`) or a hidden
|
|
4
|
+
prompt; never from the command line, where other users of the computer could see it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextlib
|
|
12
|
+
import datetime as dt
|
|
13
|
+
import getpass
|
|
14
|
+
import ipaddress
|
|
15
|
+
import os
|
|
16
|
+
import signal
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TextIO
|
|
20
|
+
from urllib.parse import urlsplit
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from .client import ClientConfig, Event, TunnelClient
|
|
24
|
+
from .forward import TargetError, parse_target, resolve_target
|
|
25
|
+
from .ws import split_url
|
|
26
|
+
|
|
27
|
+
DEFAULT_SERVER = "wss://tunnel.buglupa.com/v1/connect"
|
|
28
|
+
TOKEN_ENV = "BUGLUPA_CONNECT_TOKEN" # noqa: S105 - the variable's name, not a secret
|
|
29
|
+
SERVER_ENV = "BUGLUPA_CONNECT_SERVER"
|
|
30
|
+
TOKEN_PREFIX = "blct_" # noqa: S105 - the token format's prefix
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Output:
|
|
34
|
+
"""The terminal view: a short status, then one line per request."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, stream: TextIO, color: bool, quiet: bool = False) -> None:
|
|
37
|
+
self.stream = stream
|
|
38
|
+
self.color = color
|
|
39
|
+
self.quiet = quiet
|
|
40
|
+
encoding = (getattr(stream, "encoding", None) or "ascii").lower()
|
|
41
|
+
fancy = encoding.replace("-", "") in ("utf8", "utf16", "utf32")
|
|
42
|
+
self.ok, self.bad, self.warn = ("✓", "✗", "!") if fancy else ("OK", "X", "!")
|
|
43
|
+
|
|
44
|
+
def _paint(self, text: str, code: str) -> str:
|
|
45
|
+
return f"\x1b[{code}m{text}\x1b[0m" if self.color else text
|
|
46
|
+
|
|
47
|
+
def line(self, text: str = "") -> None:
|
|
48
|
+
with contextlib.suppress(OSError, ValueError):
|
|
49
|
+
print(text, file=self.stream, flush=True)
|
|
50
|
+
|
|
51
|
+
def event(self, event: Event, target: str) -> None:
|
|
52
|
+
if event.kind in ("connected", "reconnected"):
|
|
53
|
+
data = event.data
|
|
54
|
+
project = data.get("project") or "your project"
|
|
55
|
+
if event.kind == "connected":
|
|
56
|
+
self.line(f'{self._paint(self.ok, "32")} Connected to BugLupa as "{project}" (project token)')
|
|
57
|
+
self.line(" Only BugLupa's scanner can reach this. Press Ctrl+C to stop.")
|
|
58
|
+
expires = data.get("expires_at")
|
|
59
|
+
if isinstance(expires, (int, float)):
|
|
60
|
+
until = dt.datetime.fromtimestamp(expires).strftime("%H:%M")
|
|
61
|
+
self.line(self._paint(f" Forwarding to {target}. The token is valid until {until}.", "2"))
|
|
62
|
+
else:
|
|
63
|
+
self.line(f"{self._paint(self.ok, '32')} Reconnected to BugLupa")
|
|
64
|
+
elif event.kind == "request":
|
|
65
|
+
if not self.quiet:
|
|
66
|
+
self.request(event.data)
|
|
67
|
+
elif event.kind in ("retry", "lost", "warning", "local_down"):
|
|
68
|
+
self.line(f"{self._paint(self.warn, '33')} {event.message}")
|
|
69
|
+
elif event.kind == "final":
|
|
70
|
+
self.line(f"{self._paint(self.bad, '31')} {event.message}")
|
|
71
|
+
elif event.kind == "stopped":
|
|
72
|
+
self.line(event.message)
|
|
73
|
+
|
|
74
|
+
def request(self, data: dict[str, object]) -> None:
|
|
75
|
+
method = str(data.get("method", ""))[:7]
|
|
76
|
+
path = str(data.get("path", ""))
|
|
77
|
+
status = data.get("status")
|
|
78
|
+
ms = data.get("ms", 0)
|
|
79
|
+
shown = f"{status}" if status else "---"
|
|
80
|
+
if self.color and isinstance(status, int):
|
|
81
|
+
code = "32" if status < 300 else "36" if status < 400 else "33" if status < 500 else "31"
|
|
82
|
+
shown = self._paint(shown, code)
|
|
83
|
+
note = f" {data['note']}" if data.get("note") else ""
|
|
84
|
+
self.line(f" {method:<5}{path:<12} {shown:>3} {ms:>4} ms{note}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def read_token(token_file: str | None, stdin_tty: bool) -> str:
|
|
88
|
+
if token_file:
|
|
89
|
+
path = Path(token_file).expanduser()
|
|
90
|
+
try:
|
|
91
|
+
token = path.read_text(encoding="utf-8").strip()
|
|
92
|
+
except OSError as exc:
|
|
93
|
+
raise SystemExit(f"buglupa connect: can't read the token file {path} ({exc.strerror})") from exc
|
|
94
|
+
elif os.environ.get(TOKEN_ENV, "").strip():
|
|
95
|
+
token = os.environ[TOKEN_ENV].strip()
|
|
96
|
+
elif stdin_tty:
|
|
97
|
+
token = getpass.getpass("Connect token (from BugLupa; hidden): ").strip()
|
|
98
|
+
else:
|
|
99
|
+
raise SystemExit(f"buglupa connect: set {TOKEN_ENV} or use --token-file (the token is never an argument)")
|
|
100
|
+
if not token.startswith(TOKEN_PREFIX) or len(token) < 40 or any(c.isspace() for c in token):
|
|
101
|
+
raise SystemExit("buglupa connect: that isn't a BugLupa connect token (it starts with blct_)")
|
|
102
|
+
return token
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_server(url: str) -> str:
|
|
106
|
+
try:
|
|
107
|
+
tls, host, _, _ = split_url(url)
|
|
108
|
+
except ValueError as exc:
|
|
109
|
+
raise SystemExit(f"buglupa connect: {exc}") from exc
|
|
110
|
+
if not tls:
|
|
111
|
+
try:
|
|
112
|
+
loopback = ipaddress.ip_address(host).is_loopback
|
|
113
|
+
except ValueError:
|
|
114
|
+
loopback = host == "localhost"
|
|
115
|
+
if not loopback:
|
|
116
|
+
raise SystemExit("buglupa connect: an unencrypted ws:// server is only allowed on this computer")
|
|
117
|
+
return url
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parser(prog: str) -> argparse.ArgumentParser:
|
|
121
|
+
p = argparse.ArgumentParser(
|
|
122
|
+
prog=prog,
|
|
123
|
+
description="Let BugLupa Cloud's scanner test an app on your computer through a private tunnel.",
|
|
124
|
+
epilog=f"The connect token comes from {TOKEN_ENV}, --token-file or a hidden prompt.",
|
|
125
|
+
)
|
|
126
|
+
p.add_argument("target", help="the app's address: 3000, localhost:3000, 127.0.0.1:8080 or https://localhost:8443")
|
|
127
|
+
p.add_argument("--token-file", help="read the connect token from this file")
|
|
128
|
+
p.add_argument("--server", help=f"tunnel server (default {DEFAULT_SERVER}; env {SERVER_ENV})")
|
|
129
|
+
p.add_argument(
|
|
130
|
+
"--insecure-local-tls",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="for an https:// local app with a self-signed certificate: don't verify its certificate",
|
|
133
|
+
)
|
|
134
|
+
p.add_argument("--max-streams", type=int, default=32, help="most requests forwarded at once (default 32)")
|
|
135
|
+
p.add_argument("--quiet", action="store_true", help="don't print a line per request")
|
|
136
|
+
p.add_argument("--no-color", action="store_true", help="plain output")
|
|
137
|
+
p.add_argument("--version", action="version", version=f"buglupa-connect {__version__}")
|
|
138
|
+
return p
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run(argv: list[str] | None, prog: str) -> int:
|
|
142
|
+
args = parser(prog).parse_args(argv)
|
|
143
|
+
color = sys.stdout.isatty() and not args.no_color and not os.environ.get("NO_COLOR")
|
|
144
|
+
out = Output(sys.stdout, color, args.quiet)
|
|
145
|
+
try:
|
|
146
|
+
target = resolve_target(parse_target(args.target, args.insecure_local_tls))
|
|
147
|
+
except TargetError as exc:
|
|
148
|
+
out.line(f"{out.bad} {exc}")
|
|
149
|
+
return 2
|
|
150
|
+
if not 1 <= args.max_streams <= 256:
|
|
151
|
+
out.line(f"{out.bad} --max-streams must be between 1 and 256")
|
|
152
|
+
return 2
|
|
153
|
+
server = check_server(args.server or os.environ.get(SERVER_ENV) or DEFAULT_SERVER)
|
|
154
|
+
if server != DEFAULT_SERVER:
|
|
155
|
+
out.line(f"{out.warn} Using the tunnel server {urlsplit(server).netloc} (not BugLupa's default).")
|
|
156
|
+
token = read_token(args.token_file, sys.stdin.isatty())
|
|
157
|
+
config = ClientConfig(server=server, token=token, target=target, max_streams=args.max_streams)
|
|
158
|
+
return asyncio.run(_main(config, out))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def _main(config: ClientConfig, out: Output) -> int:
|
|
162
|
+
client = TunnelClient(config, lambda event: out.event(event, config.target.display))
|
|
163
|
+
loop = asyncio.get_running_loop()
|
|
164
|
+
presses = 0
|
|
165
|
+
|
|
166
|
+
def interrupt() -> None:
|
|
167
|
+
nonlocal presses
|
|
168
|
+
presses += 1
|
|
169
|
+
if presses > 1:
|
|
170
|
+
os._exit(130)
|
|
171
|
+
client.stop()
|
|
172
|
+
|
|
173
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
174
|
+
with contextlib.suppress(NotImplementedError, RuntimeError, ValueError):
|
|
175
|
+
loop.add_signal_handler(sig, interrupt)
|
|
176
|
+
try:
|
|
177
|
+
return await client.run()
|
|
178
|
+
except asyncio.CancelledError: # Ctrl+C where signal handlers aren't available (Windows)
|
|
179
|
+
client.stop()
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main(argv: list[str] | None = None) -> int:
|
|
184
|
+
"""`buglupa-connect TARGET`."""
|
|
185
|
+
try:
|
|
186
|
+
return run(argv, "buglupa-connect")
|
|
187
|
+
except KeyboardInterrupt:
|
|
188
|
+
return 130
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def buglupa_main(argv: list[str] | None = None) -> int:
|
|
192
|
+
"""`buglupa connect TARGET`."""
|
|
193
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
194
|
+
if not args or args[0] != "connect":
|
|
195
|
+
print("usage: buglupa connect TARGET (this package only provides `buglupa connect`)", file=sys.stderr)
|
|
196
|
+
return 2
|
|
197
|
+
try:
|
|
198
|
+
return run(args[1:], "buglupa connect")
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
return 130
|