Skip to content

Commit 8cac304

Browse files
committed
Verify hostname before acting on remote file URL
Fixes #2971
1 parent ad3ead1 commit 8cac304

File tree

1 file changed

+46
-7
lines changed

1 file changed

+46
-7
lines changed

kittens/remote_file/main.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,19 @@ def help_text(x: str) -> str:
9797
return {'e': 'edit', 'o': 'open', 's': 'save'}.get(response, 'cancel')
9898

9999

100+
def hostname_matches(from_hyperlink: str, actual: str) -> bool:
101+
if from_hyperlink == actual:
102+
return True
103+
if from_hyperlink.partition('.')[0] == actual.partition('.')[0]:
104+
return True
105+
return False
106+
107+
100108
class ControlMaster:
101109

102-
def __init__(self, conn_data: SSHConnectionData, remote_path: str, dest: str = ''):
110+
def __init__(self, conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFileCLIOptions, dest: str = ''):
103111
self.conn_data = conn_data
112+
self.cli_opts = cli_opts
104113
self.remote_path = remote_path
105114
self.dest = dest
106115
self.tdir = ''
@@ -138,6 +147,33 @@ def is_alive(self) -> bool:
138147
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
139148
).wait() == 0
140149

150+
def check_hostname_matches(self) -> None:
151+
cp = subprocess.run(self.batch_cmd_prefix + [self.conn_data.hostname, 'hostname', '-f'], stdout=subprocess.PIPE,
152+
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
153+
if cp.returncode == 0:
154+
q = cp.stdout.decode('utf-8').strip()
155+
if not hostname_matches(self.cli_opts.hostname or '', q):
156+
print(reset_terminal(), end='')
157+
print(f'The remote hostname {styled(q, fg="green")} does not match the')
158+
print(f'hostname in the hyperlink {styled(self.cli_opts.hostname or "", fg="red")}')
159+
print('This indicates that kitty has not connected to the correct remote machine.')
160+
print('This can happen, for example, when using nested SSH sessions.')
161+
print(f'The hostname kitty used to connect was: {styled(self.conn_data.hostname, fg="yellow")}', end='')
162+
if self.conn_data.port is not None:
163+
print(f' with port: {self.conn_data.port}')
164+
print()
165+
print()
166+
print('Do you want to continue anyway?')
167+
print(
168+
f'{styled("Y", fg="green")}es',
169+
f'{styled("N", fg="red")}o', sep='\t'
170+
)
171+
sys.stdout.flush()
172+
response = get_key_press('yn', 'n')
173+
if response != 'y':
174+
raise SystemExit(1)
175+
print(reset_terminal(), end='')
176+
141177
def download(self) -> bool:
142178
with open(self.dest, 'wb') as f:
143179
return subprocess.run(
@@ -181,7 +217,7 @@ def main(args: List[str]) -> Result:
181217
show_error('Failed with unhandled exception')
182218

183219

184-
def save_as(conn_data: SSHConnectionData, remote_path: str) -> None:
220+
def save_as(conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFileCLIOptions) -> None:
185221
ddir = cache_dir()
186222
os.makedirs(ddir, exist_ok=True)
187223
last_used_store_path = os.path.join(ddir, 'remote-file-last-used.txt')
@@ -219,7 +255,7 @@ def save_as(conn_data: SSHConnectionData, remote_path: str) -> None:
219255
return
220256
if response == 'n':
221257
print(reset_terminal(), end='')
222-
return save_as(conn_data, remote_path)
258+
return save_as(conn_data, remote_path, cli_opts)
223259

224260
if response == 'r':
225261
q = dest
@@ -231,7 +267,8 @@ def save_as(conn_data: SSHConnectionData, remote_path: str) -> None:
231267
dest = q
232268
if os.path.dirname(dest):
233269
os.makedirs(os.path.dirname(dest), exist_ok=True)
234-
with ControlMaster(conn_data, remote_path, dest=dest) as master:
270+
with ControlMaster(conn_data, remote_path, cli_opts, dest=dest) as master:
271+
master.check_hostname_matches()
235272
if not master.download():
236273
show_error('Failed to copy file from remote machine')
237274

@@ -242,13 +279,15 @@ def handle_action(action: str, cli_opts: RemoteFileCLIOptions) -> Result:
242279
if action == 'open':
243280
print('Opening', cli_opts.path, 'from', cli_opts.hostname)
244281
dest = os.path.join(tempfile.mkdtemp(), os.path.basename(remote_path))
245-
with ControlMaster(conn_data, remote_path, dest=dest) as master:
282+
with ControlMaster(conn_data, remote_path, cli_opts, dest=dest) as master:
283+
master.check_hostname_matches()
246284
if master.download():
247285
return dest
248286
show_error('Failed to copy file from remote machine')
249287
elif action == 'edit':
250288
print('Editing', cli_opts.path, 'from', cli_opts.hostname)
251-
with ControlMaster(conn_data, remote_path) as master:
289+
with ControlMaster(conn_data, remote_path, cli_opts) as master:
290+
master.check_hostname_matches()
252291
if not master.download():
253292
show_error(f'Failed to download {remote_path}')
254293
return None
@@ -271,7 +310,7 @@ def handle_action(action: str, cli_opts: RemoteFileCLIOptions) -> Result:
271310
show_error(f'Failed to upload {remote_path}, SSH master process died')
272311
elif action == 'save':
273312
print('Saving', cli_opts.path, 'from', cli_opts.hostname)
274-
save_as(conn_data, remote_path)
313+
save_as(conn_data, remote_path, cli_opts)
275314

276315

277316
@result_handler()

0 commit comments

Comments
 (0)