riptide-transfer 1.3.6__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.
Files changed (59) hide show
  1. riptide_transfer-1.3.6/PKG-INFO +176 -0
  2. riptide_transfer-1.3.6/README.md +164 -0
  3. riptide_transfer-1.3.6/pyproject.toml +26 -0
  4. riptide_transfer-1.3.6/setup.cfg +4 -0
  5. riptide_transfer-1.3.6/src/riptide/__init__.py +28 -0
  6. riptide_transfer-1.3.6/src/riptide/__main__.py +4 -0
  7. riptide_transfer-1.3.6/src/riptide/app.py +45 -0
  8. riptide_transfer-1.3.6/src/riptide/core/__init__.py +1 -0
  9. riptide_transfer-1.3.6/src/riptide/core/config.py +176 -0
  10. riptide_transfer-1.3.6/src/riptide/core/file_select.py +89 -0
  11. riptide_transfer-1.3.6/src/riptide/core/firewall.py +552 -0
  12. riptide_transfer-1.3.6/src/riptide/core/focus.py +82 -0
  13. riptide_transfer-1.3.6/src/riptide/core/identity.py +243 -0
  14. riptide_transfer-1.3.6/src/riptide/core/nat.py +102 -0
  15. riptide_transfer-1.3.6/src/riptide/core/session.py +887 -0
  16. riptide_transfer-1.3.6/src/riptide/core/transfer.py +89 -0
  17. riptide_transfer-1.3.6/src/riptide/screens/__init__.py +1 -0
  18. riptide_transfer-1.3.6/src/riptide/screens/create.py +529 -0
  19. riptide_transfer-1.3.6/src/riptide/screens/join.py +479 -0
  20. riptide_transfer-1.3.6/src/riptide/screens/menu.py +94 -0
  21. riptide_transfer-1.3.6/src/riptide/screens/session_screen.py +1059 -0
  22. riptide_transfer-1.3.6/src/riptide/theme.py +1049 -0
  23. riptide_transfer-1.3.6/src/riptide_transfer.egg-info/PKG-INFO +176 -0
  24. riptide_transfer-1.3.6/src/riptide_transfer.egg-info/SOURCES.txt +57 -0
  25. riptide_transfer-1.3.6/src/riptide_transfer.egg-info/dependency_links.txt +1 -0
  26. riptide_transfer-1.3.6/src/riptide_transfer.egg-info/entry_points.txt +2 -0
  27. riptide_transfer-1.3.6/src/riptide_transfer.egg-info/requires.txt +3 -0
  28. riptide_transfer-1.3.6/src/riptide_transfer.egg-info/top_level.txt +1 -0
  29. riptide_transfer-1.3.6/tests/test_bye_disconnect.py +102 -0
  30. riptide_transfer-1.3.6/tests/test_create_banner_logo_hidden.py +88 -0
  31. riptide_transfer-1.3.6/tests/test_create_code.py +55 -0
  32. riptide_transfer-1.3.6/tests/test_create_join_keys.py +216 -0
  33. riptide_transfer-1.3.6/tests/test_create_rejoin.py +99 -0
  34. riptide_transfer-1.3.6/tests/test_create_scroll_download_hide.py +100 -0
  35. riptide_transfer-1.3.6/tests/test_dial_error_hint.py +28 -0
  36. riptide_transfer-1.3.6/tests/test_file_select.py +80 -0
  37. riptide_transfer-1.3.6/tests/test_fingerprint_code.py +59 -0
  38. riptide_transfer-1.3.6/tests/test_firewall_and_menu_redesign.py +504 -0
  39. riptide_transfer-1.3.6/tests/test_firewall_uac.py +316 -0
  40. riptide_transfer-1.3.6/tests/test_focus.py +64 -0
  41. riptide_transfer-1.3.6/tests/test_host_fingerprint.py +200 -0
  42. riptide_transfer-1.3.6/tests/test_identity_race.py +91 -0
  43. riptide_transfer-1.3.6/tests/test_ipv6_listener.py +175 -0
  44. riptide_transfer-1.3.6/tests/test_menu.py +61 -0
  45. riptide_transfer-1.3.6/tests/test_menu_page_titles.py +102 -0
  46. riptide_transfer-1.3.6/tests/test_popup_blank_overlay.py +63 -0
  47. riptide_transfer-1.3.6/tests/test_port50000_public_ip.py +96 -0
  48. riptide_transfer-1.3.6/tests/test_progress.py +82 -0
  49. riptide_transfer-1.3.6/tests/test_push_rename.py +125 -0
  50. riptide_transfer-1.3.6/tests/test_screens_smoke.py +29 -0
  51. riptide_transfer-1.3.6/tests/test_session.py +158 -0
  52. riptide_transfer-1.3.6/tests/test_session_screen.py +378 -0
  53. riptide_transfer-1.3.6/tests/test_theme.py +22 -0
  54. riptide_transfer-1.3.6/tests/test_transfer_complete_download_hide.py +114 -0
  55. riptide_transfer-1.3.6/tests/test_ui_colors_modal.py +105 -0
  56. riptide_transfer-1.3.6/tests/test_v11_messages_popups.py +143 -0
  57. riptide_transfer-1.3.6/tests/test_v12_truncate_quotes_popups.py +139 -0
  58. riptide_transfer-1.3.6/tests/test_v13_folder_truncate.py +86 -0
  59. riptide_transfer-1.3.6/tests/test_v14_join_button_hidden.py +33 -0
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: riptide-transfer
3
+ Version: 1.3.6
4
+ Summary: Direct, encrypted PC-to-PC file transfer in the terminal. No cloud upload, no website in the data path.
5
+ Author: riptide
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: textual>=0.86
10
+ Requires-Dist: cryptography>=42
11
+ Requires-Dist: paramiko>=3.4
12
+
13
+ # Riptide
14
+
15
+ Riptide is a terminal program for direct, encrypted, PC to PC file transfer. Run `riptide` in a folder, connect two computers, and send files from one to the other over one encrypted TCP connection. Files go straight from sender to receiver. No server stores a copy in between.
16
+
17
+ The name comes from Minecraft. Riptide is the trident enchantment that launches the player forward at high speed. With an Elytra in rain, Riptide plus Elytra is the fastest travel method in Minecraft. Riptide the program aims for the same idea for files: the shortest route, from one PC to the other.
18
+
19
+ ## Screenshots
20
+
21
+ ![Homepage](docs/screenshots/homepage.png)
22
+
23
+ ![Create Server page](docs/screenshots/create-server.png)
24
+
25
+ ![Join Server page](docs/screenshots/join-server.png)
26
+
27
+ ![File transfer page](docs/screenshots/transfer.png)
28
+
29
+ ## Features
30
+
31
+ - Direct session between two PCs. One side listens, the other dials in. Both sides must be online at the same time.
32
+ - Either side can send files after the session opens. File direction does not depend on connection direction.
33
+ - Multiple file selection. Select files with Space, send the batch with Enter.
34
+ - File manager with folders, live search in `Find:`, auto-highlight of the first file or folder, and `0` to deselect all.
35
+ - File requests with approval. Type a peer path, press Enter, and nothing is sent until the peer presses Allow.
36
+ - Saved connections. Peers, hostnames, and certificate fingerprints are saved in `~/.riptide/config.toml`. Saved devices appear on the Join Server page.
37
+ - Automatically add firewall rule for inbound connections.
38
+ - Live progress with speed, transferred total, remaining size, and ETA, on sender and receiver.
39
+ - Hash verification. Each file is SHA-256 hashed before sending, written as `.part` on receipt, and renamed only if the hash matches.
40
+ - TLS encryption for headers and file bytes, with per-machine self-signed certificates.
41
+ - Fingerprint approval. First connection to a peer shows its certificate fingerprint for approval. A changed fingerprint later aborts the session.
42
+ - Public address display on Create Server.
43
+
44
+ Security, in brief: TLS protects files in transit. Fingerprints authenticate peers, and first connect shows a 6-digit code to compare out of band. Download requests need explicit approval. Private keys stay in `~/.riptide/` at 0600 and are never stored in `config.toml`. Files on disk are not encrypted. A push after session approval does not ask per file, but it never overwrites, duplicates are saved as (1), (2), etc., so only approve sessions for peers you trust. Full detail is in [Docs/Security](docs/security.md).
45
+
46
+ ## Installation
47
+
48
+ Published on PyPI as `riptide-transfer`. The command stays `riptide`.
49
+
50
+ Requirements: Python 3.11 or newer, Windows or Linux. For the GitHub install you also need `git`.
51
+
52
+ ```sh
53
+ pip install riptide-transfer
54
+ ```
55
+
56
+ Or install from GitHub:
57
+
58
+ ```sh
59
+ pip install git+https://github.com/mohakps/riptide
60
+ ```
61
+
62
+ Run it from the folder you want to send from or receive into:
63
+
64
+ ```sh
65
+ riptide
66
+ ```
67
+
68
+ Update:
69
+
70
+ ```sh
71
+ pip install --upgrade riptide-transfer
72
+ ```
73
+
74
+ Uninstall:
75
+
76
+ ```sh
77
+ pip uninstall riptide-transfer
78
+ ```
79
+
80
+ If upgrade fails, uninstall and install again.
81
+
82
+ ## Usage
83
+
84
+ Create Server listens. Join Server dials. Create stays on top in the menu.
85
+
86
+ 1. Create side: run `riptide`, choose Create Server, and share the public address shown on that page or your DDNS hostname with the other person. Keep the page open.
87
+ 2. Join side: run `riptide`, choose Join Server, enter the host hostname or IP, or pick a saved device, and press Enter.
88
+ 3. Approve the connection when asked, on either side.
89
+ 4. Send: select files with Space in the file manager, press Enter. Request: type a peer path in Download From Peer, press Enter, and wait for the peer to Allow it.
90
+ 5. Files arrive in the folder where the receiving side ran `riptide`.
91
+
92
+ If you are behind CGNAT, swap roles. Let the other side Create, and you Join. Full usage, shortcuts, and setup steps are in [Docs/Usage](docs/usage.md). Learn more about CGNAT or who should create server in [Docs/Architecture](docs/architecture.md).
93
+
94
+ ### Shortcuts
95
+
96
+ | Key | Action |
97
+ | --- | --- |
98
+ | Up / Down | Move in a list |
99
+ | Space | Open a folder, or select a file |
100
+ | Enter | Send selected files, or Allow a pending connection |
101
+ | Backspace | Erase Find text, go up a folder when Find is empty, or Deny a pending connection |
102
+ | 0 | Deselect all files |
103
+ | Tab | Cycle between file list and download input |
104
+ | Esc | Back, or leave the session |
105
+
106
+ ## Advantages
107
+
108
+ - One direct transfer, at bottleneck speed. No server in the middle to slow, throttle, or queue it.
109
+ - About half the time of a cloud transfer for large files. Cloud transfer is upload plus download. Direct transfer is one transfer.
110
+ - No account, storage quota, or file size limit from Riptide. Disk space is the limit.
111
+ - No third party holds a copy, so privacy does not depend on a provider policy.
112
+ - LAN speed when both PCs are on the same LAN, using a private LAN address.
113
+
114
+ Full details in [Docs/Advantages](docs/advantages.md).
115
+
116
+ ## Limitations
117
+
118
+ - First setup on the Create side: port forward 50000 on the router, once, and DDNS if the public IP changes often. The PC firewall rule for port 50000 is checked and added automatically when you click Create Server, on Windows and Linux. If Riptide is not run as admin, or sudo needs a password, it asks once on that click: UAC on Windows, pkexec on Linux. If you cancel, or it cannot add the rule, the Create page shows the exact manual command. Only rules that explicitly mention port 50000 are checked and opened. Any-port rules are ignored.
119
+ - No direct connection between two CGNAT devices on IPv4, unless both have global IPv6. There is no relay fallback.
120
+ - Folders cannot be sent yet. Zip a folder first, or select its files one by one.
121
+ - No resume for an interrupted file. It restarts from the start.
122
+ - One file at a time, in batch order. No parallel streams.
123
+ - Both sides must be online together. No offline queue.
124
+ - You must exchange the host address outside Riptide, by message or DDNS hostname.
125
+ - Terminal only, Windows and Linux primarily. macOS is not specifically tested.
126
+
127
+ Full detail, with workarounds where they exist, is in [Docs/Limitations](docs/limitations.md).
128
+
129
+ ## Project structure
130
+
131
+ ```
132
+ riptide/
133
+ ├── src/riptide/
134
+ │ ├── app.py
135
+ │ ├── theme.py
136
+ │ ├── screens/
137
+ │ │ ├── menu.py
138
+ │ │ ├── create.py
139
+ │ │ ├── join.py
140
+ │ │ └── session_screen.py
141
+ │ └── core/
142
+ │ ├── session.py
143
+ │ ├── identity.py
144
+ │ ├── nat.py
145
+ │ ├── config.py
146
+ │ ├── file_select.py
147
+ │ ├── firewall.py
148
+ │ ├── focus.py
149
+ │ └── transfer.py
150
+ ├── tests/
151
+ ├── REMOVED.md
152
+ └── README.md
153
+ ```
154
+
155
+ For detail structure, read [Docs/Project structure](docs/project-structure.md).
156
+
157
+ ## How this was built
158
+
159
+ - Meta Muse: primary assistant for panning, implementation, testing, and documentation.
160
+ - Gemini 3.8 Flash: TUI Design
161
+
162
+ Human decisions, testing on real machines, and final choices about names, ports, and behaviour are the author's. AI-generated code was tested, not trusted blindly. The test suite and headless Textual pilots are part of the workflow, and live two-machine testing is what proves a transfer.
163
+
164
+ ## Future Scope
165
+
166
+ - Support for transferring folders
167
+ - Support for MacOS
168
+
169
+ ## Docs
170
+
171
+ - [Architecture](docs/architecture.md)
172
+ - [Security](docs/security.md)
173
+ - [Advantages](docs/advantages.md)
174
+ - [Limitations](docs/limitations.md)
175
+ - [Troubleshooting](docs/troubleshooting.md)
176
+ - [Sources and references](docs/sources-references.md)
@@ -0,0 +1,164 @@
1
+ # Riptide
2
+
3
+ Riptide is a terminal program for direct, encrypted, PC to PC file transfer. Run `riptide` in a folder, connect two computers, and send files from one to the other over one encrypted TCP connection. Files go straight from sender to receiver. No server stores a copy in between.
4
+
5
+ The name comes from Minecraft. Riptide is the trident enchantment that launches the player forward at high speed. With an Elytra in rain, Riptide plus Elytra is the fastest travel method in Minecraft. Riptide the program aims for the same idea for files: the shortest route, from one PC to the other.
6
+
7
+ ## Screenshots
8
+
9
+ ![Homepage](docs/screenshots/homepage.png)
10
+
11
+ ![Create Server page](docs/screenshots/create-server.png)
12
+
13
+ ![Join Server page](docs/screenshots/join-server.png)
14
+
15
+ ![File transfer page](docs/screenshots/transfer.png)
16
+
17
+ ## Features
18
+
19
+ - Direct session between two PCs. One side listens, the other dials in. Both sides must be online at the same time.
20
+ - Either side can send files after the session opens. File direction does not depend on connection direction.
21
+ - Multiple file selection. Select files with Space, send the batch with Enter.
22
+ - File manager with folders, live search in `Find:`, auto-highlight of the first file or folder, and `0` to deselect all.
23
+ - File requests with approval. Type a peer path, press Enter, and nothing is sent until the peer presses Allow.
24
+ - Saved connections. Peers, hostnames, and certificate fingerprints are saved in `~/.riptide/config.toml`. Saved devices appear on the Join Server page.
25
+ - Automatically add firewall rule for inbound connections.
26
+ - Live progress with speed, transferred total, remaining size, and ETA, on sender and receiver.
27
+ - Hash verification. Each file is SHA-256 hashed before sending, written as `.part` on receipt, and renamed only if the hash matches.
28
+ - TLS encryption for headers and file bytes, with per-machine self-signed certificates.
29
+ - Fingerprint approval. First connection to a peer shows its certificate fingerprint for approval. A changed fingerprint later aborts the session.
30
+ - Public address display on Create Server.
31
+
32
+ Security, in brief: TLS protects files in transit. Fingerprints authenticate peers, and first connect shows a 6-digit code to compare out of band. Download requests need explicit approval. Private keys stay in `~/.riptide/` at 0600 and are never stored in `config.toml`. Files on disk are not encrypted. A push after session approval does not ask per file, but it never overwrites, duplicates are saved as (1), (2), etc., so only approve sessions for peers you trust. Full detail is in [Docs/Security](docs/security.md).
33
+
34
+ ## Installation
35
+
36
+ Published on PyPI as `riptide-transfer`. The command stays `riptide`.
37
+
38
+ Requirements: Python 3.11 or newer, Windows or Linux. For the GitHub install you also need `git`.
39
+
40
+ ```sh
41
+ pip install riptide-transfer
42
+ ```
43
+
44
+ Or install from GitHub:
45
+
46
+ ```sh
47
+ pip install git+https://github.com/mohakps/riptide
48
+ ```
49
+
50
+ Run it from the folder you want to send from or receive into:
51
+
52
+ ```sh
53
+ riptide
54
+ ```
55
+
56
+ Update:
57
+
58
+ ```sh
59
+ pip install --upgrade riptide-transfer
60
+ ```
61
+
62
+ Uninstall:
63
+
64
+ ```sh
65
+ pip uninstall riptide-transfer
66
+ ```
67
+
68
+ If upgrade fails, uninstall and install again.
69
+
70
+ ## Usage
71
+
72
+ Create Server listens. Join Server dials. Create stays on top in the menu.
73
+
74
+ 1. Create side: run `riptide`, choose Create Server, and share the public address shown on that page or your DDNS hostname with the other person. Keep the page open.
75
+ 2. Join side: run `riptide`, choose Join Server, enter the host hostname or IP, or pick a saved device, and press Enter.
76
+ 3. Approve the connection when asked, on either side.
77
+ 4. Send: select files with Space in the file manager, press Enter. Request: type a peer path in Download From Peer, press Enter, and wait for the peer to Allow it.
78
+ 5. Files arrive in the folder where the receiving side ran `riptide`.
79
+
80
+ If you are behind CGNAT, swap roles. Let the other side Create, and you Join. Full usage, shortcuts, and setup steps are in [Docs/Usage](docs/usage.md). Learn more about CGNAT or who should create server in [Docs/Architecture](docs/architecture.md).
81
+
82
+ ### Shortcuts
83
+
84
+ | Key | Action |
85
+ | --- | --- |
86
+ | Up / Down | Move in a list |
87
+ | Space | Open a folder, or select a file |
88
+ | Enter | Send selected files, or Allow a pending connection |
89
+ | Backspace | Erase Find text, go up a folder when Find is empty, or Deny a pending connection |
90
+ | 0 | Deselect all files |
91
+ | Tab | Cycle between file list and download input |
92
+ | Esc | Back, or leave the session |
93
+
94
+ ## Advantages
95
+
96
+ - One direct transfer, at bottleneck speed. No server in the middle to slow, throttle, or queue it.
97
+ - About half the time of a cloud transfer for large files. Cloud transfer is upload plus download. Direct transfer is one transfer.
98
+ - No account, storage quota, or file size limit from Riptide. Disk space is the limit.
99
+ - No third party holds a copy, so privacy does not depend on a provider policy.
100
+ - LAN speed when both PCs are on the same LAN, using a private LAN address.
101
+
102
+ Full details in [Docs/Advantages](docs/advantages.md).
103
+
104
+ ## Limitations
105
+
106
+ - First setup on the Create side: port forward 50000 on the router, once, and DDNS if the public IP changes often. The PC firewall rule for port 50000 is checked and added automatically when you click Create Server, on Windows and Linux. If Riptide is not run as admin, or sudo needs a password, it asks once on that click: UAC on Windows, pkexec on Linux. If you cancel, or it cannot add the rule, the Create page shows the exact manual command. Only rules that explicitly mention port 50000 are checked and opened. Any-port rules are ignored.
107
+ - No direct connection between two CGNAT devices on IPv4, unless both have global IPv6. There is no relay fallback.
108
+ - Folders cannot be sent yet. Zip a folder first, or select its files one by one.
109
+ - No resume for an interrupted file. It restarts from the start.
110
+ - One file at a time, in batch order. No parallel streams.
111
+ - Both sides must be online together. No offline queue.
112
+ - You must exchange the host address outside Riptide, by message or DDNS hostname.
113
+ - Terminal only, Windows and Linux primarily. macOS is not specifically tested.
114
+
115
+ Full detail, with workarounds where they exist, is in [Docs/Limitations](docs/limitations.md).
116
+
117
+ ## Project structure
118
+
119
+ ```
120
+ riptide/
121
+ ├── src/riptide/
122
+ │ ├── app.py
123
+ │ ├── theme.py
124
+ │ ├── screens/
125
+ │ │ ├── menu.py
126
+ │ │ ├── create.py
127
+ │ │ ├── join.py
128
+ │ │ └── session_screen.py
129
+ │ └── core/
130
+ │ ├── session.py
131
+ │ ├── identity.py
132
+ │ ├── nat.py
133
+ │ ├── config.py
134
+ │ ├── file_select.py
135
+ │ ├── firewall.py
136
+ │ ├── focus.py
137
+ │ └── transfer.py
138
+ ├── tests/
139
+ ├── REMOVED.md
140
+ └── README.md
141
+ ```
142
+
143
+ For detail structure, read [Docs/Project structure](docs/project-structure.md).
144
+
145
+ ## How this was built
146
+
147
+ - Meta Muse: primary assistant for panning, implementation, testing, and documentation.
148
+ - Gemini 3.8 Flash: TUI Design
149
+
150
+ Human decisions, testing on real machines, and final choices about names, ports, and behaviour are the author's. AI-generated code was tested, not trusted blindly. The test suite and headless Textual pilots are part of the workflow, and live two-machine testing is what proves a transfer.
151
+
152
+ ## Future Scope
153
+
154
+ - Support for transferring folders
155
+ - Support for MacOS
156
+
157
+ ## Docs
158
+
159
+ - [Architecture](docs/architecture.md)
160
+ - [Security](docs/security.md)
161
+ - [Advantages](docs/advantages.md)
162
+ - [Limitations](docs/limitations.md)
163
+ - [Troubleshooting](docs/troubleshooting.md)
164
+ - [Sources and references](docs/sources-references.md)
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "riptide-transfer"
7
+ version = "1.3.6"
8
+ description = "Direct, encrypted PC-to-PC file transfer in the terminal. No cloud upload, no website in the data path."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "riptide"}]
13
+ dependencies = [
14
+ "textual>=0.86",
15
+ "cryptography>=42",
16
+ "paramiko>=3.4",
17
+ ]
18
+
19
+ [project.scripts]
20
+ riptide = "riptide.app:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ """Riptide: direct, encrypted PC-to-PC file transfer in the terminal."""
2
+
3
+ from pathlib import Path
4
+ import tomllib
5
+
6
+
7
+ def _load_version() -> str:
8
+ pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
9
+ if pyproject_path.exists():
10
+ try:
11
+ data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
12
+ return str(data.get("project", {}).get("version", "0.0.0"))
13
+ except Exception:
14
+ pass
15
+ try:
16
+ from importlib.metadata import version
17
+ for dist_name in ("riptide-transfer", "riptide"):
18
+ try:
19
+ return version(dist_name)
20
+ except Exception:
21
+ continue
22
+ except Exception:
23
+ pass
24
+ return "0.0.0"
25
+
26
+
27
+ __version__ = _load_version()
28
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from .app import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,45 @@
1
+ """Riptide Textual app. Entry point: riptide = riptide.app:main"""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+
7
+ from textual.app import App
8
+
9
+ from . import __version__
10
+ from .theme import CSS
11
+
12
+
13
+ class RiptideApp(App):
14
+ CSS = CSS
15
+ TITLE = "riptide"
16
+
17
+ def render(self):
18
+ from textual.renderables.blank import Blank
19
+ return Blank("transparent")
20
+
21
+ def on_mount(self) -> None:
22
+ self.styles.background = "transparent"
23
+ from .screens.menu import MenuScreen
24
+
25
+ self.push_screen(MenuScreen())
26
+
27
+ def build_parser() -> argparse.ArgumentParser:
28
+ parser = argparse.ArgumentParser(
29
+ prog="riptide",
30
+ description="Direct, encrypted PC-to-PC file transfer in the terminal.",
31
+ )
32
+ parser.add_argument("--version", action="version", version=f"riptide {__version__}")
33
+ return parser
34
+
35
+
36
+ def main(argv: list[str] | None = None) -> int:
37
+ parser = build_parser()
38
+ parser.parse_args(argv)
39
+ app = RiptideApp()
40
+ app.run()
41
+ return 0
42
+
43
+
44
+ if __name__ == "__main__":
45
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Core, non-UI logic for riptide."""
@@ -0,0 +1,176 @@
1
+ """Saved connections and settings.
2
+
3
+ Location is always Path.home() / ".riptide" / "config.toml", on every OS.
4
+ Private keys and certificates are never stored here. A peer is identified
5
+ by host + TLS certificate fingerprint, approved in the UI on first connect.
6
+
7
+ Since v1.1, the host side also keeps trusted clients, separate from
8
+ dial-out peers. A trusted client is identified by fingerprint, with
9
+ the device name as a display label only. The dial-out peer list is
10
+ keyed by host, for filling the Join address, while the trusted
11
+ client list is keyed by fingerprint, for host-side authentication.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+
18
+ try:
19
+ import tomllib
20
+ except ModuleNotFoundError: # Python < 3.11, not supported by pyproject
21
+ import tomli as tomllib # type: ignore
22
+
23
+ # Session port. Kept in code/config only, except for the
24
+ # Create/Join header and waiting text.
25
+ # 50000 is in the dynamic/private range (49152-65535), so it is not
26
+ # IANA-reserved, but it is the de facto default for IBM DB2, Jenkins
27
+ # agents, and SAP NetWeaver Java, so a machine running one of those
28
+ # will already have it in use and Riptide will fail to bind.
29
+ DEFAULT_SESSION_PORT = 50000
30
+
31
+
32
+ def config_dir(home: Path | None = None) -> Path:
33
+ return (home or Path.home()) / ".riptide"
34
+
35
+
36
+ def config_path(home: Path | None = None) -> Path:
37
+ return config_dir(home) / "config.toml"
38
+
39
+
40
+ @dataclass
41
+ class Peer:
42
+ name: str
43
+ host: str = ""
44
+ fingerprint: str = ""
45
+
46
+
47
+ @dataclass
48
+ class Config:
49
+ peers: list[Peer] = field(default_factory=list)
50
+ trusted_clients: list[Peer] = field(default_factory=list)
51
+ session_port: int = DEFAULT_SESSION_PORT
52
+ firewall_port_open: bool | None = None
53
+ def peer_for(self, host: str) -> Peer | None:
54
+ for peer in self.peers:
55
+ if peer.host == host:
56
+ return peer
57
+ return None
58
+ def trusted_client_for_fingerprint(self, fingerprint: str) -> Peer | None:
59
+ for peer in self.trusted_clients:
60
+ if peer.fingerprint and peer.fingerprint == fingerprint:
61
+ return peer
62
+ return None
63
+ def trusted_client_for_device(self, device: str) -> Peer | None:
64
+ for peer in self.trusted_clients:
65
+ if peer.name and peer.name == device:
66
+ return peer
67
+ return None
68
+
69
+
70
+ def _escape(value: str) -> str:
71
+ return value.replace("\\", "\\\\").replace('"', '\\"')
72
+
73
+
74
+ def _contains_private_key(text: str) -> bool:
75
+ return "PRIVATE KEY" in text or "-----BEGIN" in text
76
+
77
+
78
+ def load_config(home: Path | None = None) -> Config:
79
+ path = config_path(home)
80
+ if not path.exists():
81
+ return Config()
82
+ try:
83
+ data = tomllib.loads(path.read_text(encoding="utf-8"))
84
+ except Exception:
85
+ return Config()
86
+ peers = []
87
+ for item in data.get("peer", []) if isinstance(data.get("peer", []), list) else []:
88
+ if not isinstance(item, dict):
89
+ continue
90
+ peers.append(Peer(
91
+ name=str(item.get("name", "")),
92
+ host=str(item.get("host", "")),
93
+ fingerprint=str(item.get("fingerprint", "")),
94
+ ))
95
+ trusted_clients = []
96
+ for item in data.get("trusted_client", []) if isinstance(data.get("trusted_client", []), list) else []:
97
+ if not isinstance(item, dict):
98
+ continue
99
+ trusted_clients.append(Peer(
100
+ name=str(item.get("name", "")),
101
+ host=str(item.get("host", "")),
102
+ fingerprint=str(item.get("fingerprint", "")),
103
+ ))
104
+ session_port = data.get("session_port", DEFAULT_SESSION_PORT)
105
+ try:
106
+ session_port = int(session_port)
107
+ except Exception:
108
+ session_port = DEFAULT_SESSION_PORT
109
+ firewall_val = data.get("firewall_port_open")
110
+ firewall_port_open = bool(firewall_val) if firewall_val is not None else None
111
+ return Config(peers=peers, trusted_clients=trusted_clients, session_port=session_port, firewall_port_open=firewall_port_open)
112
+
113
+
114
+ def save_config(config: Config, home: Path | None = None) -> Path:
115
+ for peer in list(config.peers) + list(config.trusted_clients):
116
+ for value in (peer.name, peer.host, peer.fingerprint):
117
+ if _contains_private_key(value):
118
+ raise ValueError("refusing to store a private key in config")
119
+ directory = config_dir(home)
120
+ directory.mkdir(parents=True, exist_ok=True)
121
+ lines = [f"session_port = {int(config.session_port)}"]
122
+ if config.firewall_port_open is not None:
123
+ lines.append(f"firewall_port_open = {'true' if config.firewall_port_open else 'false'}")
124
+ lines.append("")
125
+ for peer in config.peers:
126
+ lines.append("[[peer]]")
127
+ lines.append(f'name = "{_escape(peer.name)}"')
128
+ lines.append(f'host = "{_escape(peer.host)}"')
129
+ lines.append(f'fingerprint = "{_escape(peer.fingerprint)}"')
130
+ lines.append("")
131
+ for peer in config.trusted_clients:
132
+ lines.append("[[trusted_client]]")
133
+ lines.append(f'name = "{_escape(peer.name)}"')
134
+ lines.append(f'host = "{_escape(peer.host)}"')
135
+ lines.append(f'fingerprint = "{_escape(peer.fingerprint)}"')
136
+ lines.append("")
137
+ path = config_path(home)
138
+ path.write_text("\n".join(lines), encoding="utf-8")
139
+ return path
140
+
141
+
142
+ def add_peer(config: Config, peer: Peer, home: Path | None = None) -> Config:
143
+ existing = config.peer_for(peer.host) if peer.host else None
144
+ if existing is not None:
145
+ existing.name = peer.name or existing.name
146
+ existing.fingerprint = peer.fingerprint or existing.fingerprint
147
+ else:
148
+ config.peers.append(peer)
149
+ save_config(config, home=home)
150
+ return config
151
+
152
+
153
+ def add_trusted_client(config: Config, peer: Peer, home: Path | None = None) -> Config:
154
+ """Save a host-side trusted client, keyed by fingerprint.
155
+
156
+ If the fingerprint is already trusted, update its label.
157
+ If the device label is already trusted with a different
158
+ fingerprint, replace that fingerprint, which is the explicit
159
+ Replace flow after a changed-fingerprint warning. Otherwise
160
+ append a new trusted client.
161
+ """
162
+ if peer.fingerprint:
163
+ existing = config.trusted_client_for_fingerprint(peer.fingerprint)
164
+ if existing is not None:
165
+ existing.name = peer.name or existing.name
166
+ save_config(config, home=home)
167
+ return config
168
+ if peer.name:
169
+ by_device = config.trusted_client_for_device(peer.name)
170
+ if by_device is not None:
171
+ by_device.fingerprint = peer.fingerprint
172
+ save_config(config, home=home)
173
+ return config
174
+ config.trusted_clients.append(peer)
175
+ save_config(config, home=home)
176
+ return config