Skip to content

Commit 767abcf

Browse files
committed
Cloning from new host via ssh causes spurious error rather than prompting for confirmation and succeeding
1 parent 597149c commit 767abcf

File tree

5 files changed

+177
-6
lines changed

5 files changed

+177
-6
lines changed

jupyterlab_git/handlers.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,21 @@
2424
from .git import DEFAULT_REMOTE_NAME, Git, RebaseAction
2525
from .log import get_logger
2626

27+
from .ssh import SSH
28+
2729
# Git configuration options exposed through the REST API
2830
ALLOWED_OPTIONS = ["user.name", "user.email"]
2931
# REST API namespace
3032
NAMESPACE = "/git"
3133

3234

35+
class SSHHandler(APIHandler):
36+
37+
@property
38+
def ssh(self) -> SSH:
39+
return SSH()
40+
41+
3342
class GitHandler(APIHandler):
3443
"""
3544
Top-level parent class.
@@ -1087,6 +1096,28 @@ async def get(self, path: str = ""):
10871096
self.finish(json.dumps(result))
10881097

10891098

1099+
class SshHostHandler(SSHHandler):
1100+
"""
1101+
Handler for checking if a host is known by SSH
1102+
"""
1103+
1104+
@tornado.web.authenticated
1105+
async def get(self):
1106+
"""
1107+
GET request handler, check if the host is known by SSH
1108+
"""
1109+
hostname = self.get_query_argument("hostname")
1110+
is_known_host = self.ssh.is_known_host(hostname)
1111+
self.set_status(200)
1112+
self.finish(json.dumps(is_known_host))
1113+
1114+
@tornado.web.authenticated
1115+
async def post(self):
1116+
data = self.get_json_body()
1117+
hostname = data["hostname"]
1118+
self.ssh.add_host(hostname)
1119+
1120+
10901121
def setup_handlers(web_app):
10911122
"""
10921123
Setups all of the git command handlers.
@@ -1137,6 +1168,7 @@ def setup_handlers(web_app):
11371168
handlers = [
11381169
("/diffnotebook", GitDiffNotebookHandler),
11391170
("/settings", GitSettingsHandler),
1171+
("/known_hosts", SshHostHandler),
11401172
]
11411173

11421174
# add the baseurl to our paths

jupyterlab_git/ssh.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
Module for executing SSH commands
3+
"""
4+
5+
import re
6+
import subprocess
7+
import shutil
8+
from .log import get_logger
9+
from pathlib import Path
10+
11+
GIT_SSH_HOST = re.compile(r"git@(.+):.+")
12+
13+
14+
class SSH:
15+
"""
16+
A class to perform ssh actions
17+
"""
18+
19+
def is_known_host(self, hostname):
20+
"""
21+
Check if the given git clone URL contains a known host
22+
"""
23+
cmd = ["ssh-keygen", "-F", hostname.replace(" ", "")]
24+
try:
25+
code = subprocess.call(
26+
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
27+
)
28+
return code == 0
29+
except subprocess.CalledProcessError as e:
30+
get_logger().debug("Error verifying host using keygen")
31+
raise e
32+
33+
def add_host(self, hostname):
34+
"""
35+
Add the host to the known_hosts file
36+
"""
37+
get_logger().debug(f"adding host to the known hosts file {hostname}")
38+
try:
39+
result = subprocess.run(
40+
["ssh-keyscan", hostname], capture_output=True, text=True, check=True
41+
)
42+
known_hosts_file = f"{Path.home()}/.ssh/known_hosts"
43+
with open(known_hosts_file, "a") as f:
44+
f.write(result.stdout)
45+
get_logger().debug(f"Added {hostname} to known hosts.")
46+
except Exception as e:
47+
get_logger().error(f"Failed to add host: {e}.")
48+
raise e

src/cloneCommand.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ export const gitCloneCommandPlugin: JupyterFrontEndPlugin<void> = {
6363
const id = Notification.emit(trans.__('Cloning…'), 'in-progress', {
6464
autoClose: false
6565
});
66+
const url = decodeURIComponent(result.value.url);
67+
const hostnameMatch = url.match(/git@(.+):.+/);
68+
69+
if (hostnameMatch && hostnameMatch.length > 1) {
70+
const hostname = hostnameMatch[1];
71+
const isKnownHost = await gitModel.checkKnownHost(hostname);
72+
if (!isKnownHost) {
73+
const result = await showDialog({
74+
title: trans.__('Unknown Host'),
75+
body: trans.__('The host is unknown, would you like to add it to the list of known hosts?'),
76+
buttons: [
77+
Dialog.cancelButton({ label: trans.__('Cancel') }),
78+
Dialog.okButton({ label: trans.__('OK') })
79+
]
80+
});
81+
if (result.button.accept) {
82+
await gitModel.addHostToKnownList(hostname);
83+
}
84+
}
85+
}
86+
87+
6688
try {
6789
const details = await showGitOperationDialog<IGitCloneArgs>(
6890
gitModel as GitExtension,

src/model.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ export class GitExtension implements IGitExtension {
368368
*/
369369
protected get _currentMarker(): BranchMarker {
370370
if (this.pathRepository === null) {
371-
return new BranchMarker(() => {});
371+
return new BranchMarker(() => { });
372372
}
373373

374374
if (!this.__currentMarker) {
@@ -419,8 +419,8 @@ export class GitExtension implements IGitExtension {
419419
}
420420
const fileStatus = this._status?.files
421421
? this._status.files.find(status => {
422-
return this.getRelativeFilePath(status.to) === path;
423-
})
422+
return this.getRelativeFilePath(status.to) === path;
423+
})
424424
: null;
425425

426426
if (!fileStatus) {
@@ -2012,6 +2012,58 @@ export class GitExtension implements IGitExtension {
20122012
}
20132013
}
20142014

2015+
/**
2016+
* Checks if the hostname is a known host
2017+
*
2018+
* @param hostname - the host name to be checked
2019+
* @returns A boolean indicating that the host is a known one
2020+
*
2021+
* @throws {ServerConnection.NetworkError} If the request cannot be made
2022+
*/
2023+
async checkKnownHost(hostname: string): Promise<Boolean> {
2024+
try {
2025+
return await this._taskHandler.execute<Boolean>(
2026+
'git:checkHost',
2027+
async () => {
2028+
return await requestAPI<Boolean>(
2029+
`known_hosts?hostname=${hostname}`,
2030+
'GET'
2031+
);
2032+
}
2033+
);
2034+
2035+
} catch (error) {
2036+
console.error('Failed to check host');
2037+
// just ignore the host check
2038+
return true;
2039+
}
2040+
}
2041+
2042+
/**
2043+
* Adds a hostname to the list of known host files
2044+
* @param hostname - the hostname to be added
2045+
* @throws {ServerConnection.NetworkError} If the request cannot be made
2046+
*/
2047+
async addHostToKnownList(hostname: string): Promise<void> {
2048+
try {
2049+
await this._taskHandler.execute<Boolean>(
2050+
'git:addHost',
2051+
async () => {
2052+
return await requestAPI<Boolean>(
2053+
`known_hosts`,
2054+
'POST',
2055+
{
2056+
hostname: hostname
2057+
}
2058+
);
2059+
}
2060+
);
2061+
2062+
} catch (error) {
2063+
console.error('Failed to add hostname to the list of known hosts');
2064+
}
2065+
}
2066+
20152067
/**
20162068
* Make request for a list of all git branches in the repository
20172069
* Retrieve a list of repository branches.
@@ -2281,7 +2333,7 @@ export class GitExtension implements IGitExtension {
22812333
private _fetchPoll: Poll;
22822334
private _isDisposed = false;
22832335
private _markerCache = new Markers(() => this._markChanged.emit());
2284-
private __currentMarker: BranchMarker = new BranchMarker(() => {});
2336+
private __currentMarker: BranchMarker = new BranchMarker(() => { });
22852337
private _readyPromise: Promise<void> = Promise.resolve();
22862338
private _pendingReadyPromise = 0;
22872339
private _settings: ISettingRegistry.ISettings | null;
@@ -2326,7 +2378,7 @@ export class GitExtension implements IGitExtension {
23262378
}
23272379

23282380
export class BranchMarker implements Git.IBranchMarker {
2329-
constructor(private _refresh: () => void) {}
2381+
constructor(private _refresh: () => void) { }
23302382

23312383
add(fname: string, mark = true): void {
23322384
if (!(fname in this._marks)) {
@@ -2361,7 +2413,7 @@ export class BranchMarker implements Git.IBranchMarker {
23612413
}
23622414

23632415
export class Markers {
2364-
constructor(private _refresh: () => void) {}
2416+
constructor(private _refresh: () => void) { }
23652417

23662418
get(path: string, branch: string): BranchMarker {
23672419
const key = Markers.markerKey(path, branch);

src/tokens.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,23 @@ export interface IGitExtension extends IDisposable {
628628
*/
629629
revertCommit(message: string, hash: string): Promise<void>;
630630

631+
/**
632+
* Checks if the hostname is a known host
633+
*
634+
* @param hostname - the host name to be checked
635+
* @returns A boolean indicating that the host is a known one
636+
*
637+
* @throws {ServerConnection.NetworkError} If the request cannot be made
638+
*/
639+
checkKnownHost(hostname: string): Promise<Boolean>;
640+
641+
/**
642+
* Adds a hostname to the list of known host files
643+
* @param hostname - the hostname to be added
644+
* @throws {ServerConnection.NetworkError} If the request cannot be made
645+
*/
646+
addHostToKnownList(hostname: string): Promise<void>;
647+
631648
/**
632649
* Get the prefix path of a directory 'path',
633650
* with respect to the root directory of repository

0 commit comments

Comments
 (0)