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.
- riptide_transfer-1.3.6/PKG-INFO +176 -0
- riptide_transfer-1.3.6/README.md +164 -0
- riptide_transfer-1.3.6/pyproject.toml +26 -0
- riptide_transfer-1.3.6/setup.cfg +4 -0
- riptide_transfer-1.3.6/src/riptide/__init__.py +28 -0
- riptide_transfer-1.3.6/src/riptide/__main__.py +4 -0
- riptide_transfer-1.3.6/src/riptide/app.py +45 -0
- riptide_transfer-1.3.6/src/riptide/core/__init__.py +1 -0
- riptide_transfer-1.3.6/src/riptide/core/config.py +176 -0
- riptide_transfer-1.3.6/src/riptide/core/file_select.py +89 -0
- riptide_transfer-1.3.6/src/riptide/core/firewall.py +552 -0
- riptide_transfer-1.3.6/src/riptide/core/focus.py +82 -0
- riptide_transfer-1.3.6/src/riptide/core/identity.py +243 -0
- riptide_transfer-1.3.6/src/riptide/core/nat.py +102 -0
- riptide_transfer-1.3.6/src/riptide/core/session.py +887 -0
- riptide_transfer-1.3.6/src/riptide/core/transfer.py +89 -0
- riptide_transfer-1.3.6/src/riptide/screens/__init__.py +1 -0
- riptide_transfer-1.3.6/src/riptide/screens/create.py +529 -0
- riptide_transfer-1.3.6/src/riptide/screens/join.py +479 -0
- riptide_transfer-1.3.6/src/riptide/screens/menu.py +94 -0
- riptide_transfer-1.3.6/src/riptide/screens/session_screen.py +1059 -0
- riptide_transfer-1.3.6/src/riptide/theme.py +1049 -0
- riptide_transfer-1.3.6/src/riptide_transfer.egg-info/PKG-INFO +176 -0
- riptide_transfer-1.3.6/src/riptide_transfer.egg-info/SOURCES.txt +57 -0
- riptide_transfer-1.3.6/src/riptide_transfer.egg-info/dependency_links.txt +1 -0
- riptide_transfer-1.3.6/src/riptide_transfer.egg-info/entry_points.txt +2 -0
- riptide_transfer-1.3.6/src/riptide_transfer.egg-info/requires.txt +3 -0
- riptide_transfer-1.3.6/src/riptide_transfer.egg-info/top_level.txt +1 -0
- riptide_transfer-1.3.6/tests/test_bye_disconnect.py +102 -0
- riptide_transfer-1.3.6/tests/test_create_banner_logo_hidden.py +88 -0
- riptide_transfer-1.3.6/tests/test_create_code.py +55 -0
- riptide_transfer-1.3.6/tests/test_create_join_keys.py +216 -0
- riptide_transfer-1.3.6/tests/test_create_rejoin.py +99 -0
- riptide_transfer-1.3.6/tests/test_create_scroll_download_hide.py +100 -0
- riptide_transfer-1.3.6/tests/test_dial_error_hint.py +28 -0
- riptide_transfer-1.3.6/tests/test_file_select.py +80 -0
- riptide_transfer-1.3.6/tests/test_fingerprint_code.py +59 -0
- riptide_transfer-1.3.6/tests/test_firewall_and_menu_redesign.py +504 -0
- riptide_transfer-1.3.6/tests/test_firewall_uac.py +316 -0
- riptide_transfer-1.3.6/tests/test_focus.py +64 -0
- riptide_transfer-1.3.6/tests/test_host_fingerprint.py +200 -0
- riptide_transfer-1.3.6/tests/test_identity_race.py +91 -0
- riptide_transfer-1.3.6/tests/test_ipv6_listener.py +175 -0
- riptide_transfer-1.3.6/tests/test_menu.py +61 -0
- riptide_transfer-1.3.6/tests/test_menu_page_titles.py +102 -0
- riptide_transfer-1.3.6/tests/test_popup_blank_overlay.py +63 -0
- riptide_transfer-1.3.6/tests/test_port50000_public_ip.py +96 -0
- riptide_transfer-1.3.6/tests/test_progress.py +82 -0
- riptide_transfer-1.3.6/tests/test_push_rename.py +125 -0
- riptide_transfer-1.3.6/tests/test_screens_smoke.py +29 -0
- riptide_transfer-1.3.6/tests/test_session.py +158 -0
- riptide_transfer-1.3.6/tests/test_session_screen.py +378 -0
- riptide_transfer-1.3.6/tests/test_theme.py +22 -0
- riptide_transfer-1.3.6/tests/test_transfer_complete_download_hide.py +114 -0
- riptide_transfer-1.3.6/tests/test_ui_colors_modal.py +105 -0
- riptide_transfer-1.3.6/tests/test_v11_messages_popups.py +143 -0
- riptide_transfer-1.3.6/tests/test_v12_truncate_quotes_popups.py +139 -0
- riptide_transfer-1.3.6/tests/test_v13_folder_truncate.py +86 -0
- 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
|
+

|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+

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

|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+

|
|
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,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,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
|