Skip to content

Commit b300f2e

Browse files
Merge commit from fork
* Validate Jupyter Server URLs * Validate Jupyter Server URLs: address reviews * Validate Jupyter Server URLs: address test reviews * Validate Jupyter Server URLs: address test reviews
1 parent 74e7791 commit b300f2e

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

internal/codespaces/rpc/invoker.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99
"net"
10+
"net/url"
1011
"os"
1112
"regexp"
1213
"strconv"
@@ -184,7 +185,11 @@ func (i *invoker) StartJupyterServer(ctx context.Context) (port int, serverUrl s
184185
return 0, "", fmt.Errorf("failed to parse JupyterLab port: %w", err)
185186
}
186187

187-
return port, response.ServerUrl, err
188+
if !isJupyterServerURLValid(response.ServerUrl) {
189+
return 0, "", fmt.Errorf("invalid JupyterLab server URL: %q", response.ServerUrl)
190+
}
191+
192+
return port, response.ServerUrl, nil
188193
}
189194

190195
// Rebuilds the container using cached layers by default or from scratch if full is true
@@ -311,3 +316,20 @@ func isUsernameValid(username string) bool {
311316
re := regexp.MustCompile(validUsernamePattern)
312317
return re.MatchString(username)
313318
}
319+
320+
// Ensures that the Jupyter server URL is valid and points to a loopback http(s) URL
321+
func isJupyterServerURLValid(serverURL string) bool {
322+
u, err := url.Parse(serverURL)
323+
if err != nil {
324+
return false
325+
}
326+
if u.Scheme != "http" && u.Scheme != "https" {
327+
return false
328+
}
329+
host := u.Hostname()
330+
if strings.ToLower(host) == "localhost" {
331+
return true
332+
}
333+
ip := net.ParseIP(host)
334+
return ip != nil && ip.IsLoopback()
335+
}

internal/codespaces/rpc/invoker_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/cli/cli/v2/internal/codespaces/rpc/jupyter"
1212
"github.com/cli/cli/v2/internal/codespaces/rpc/ssh"
1313
rpctest "github.com/cli/cli/v2/internal/codespaces/rpc/test"
14+
"github.com/stretchr/testify/require"
1415
"google.golang.org/grpc"
1516
)
1617

@@ -311,3 +312,58 @@ func TestStartSSHServerFailure(t *testing.T) {
311312
t.Fatalf("expected %s, got %s", "", user)
312313
}
313314
}
315+
316+
func TestIsJupyterServerURLValid(t *testing.T) {
317+
tests := []struct {
318+
name string
319+
serverURL string
320+
want bool
321+
}{
322+
{
323+
name: "http loopback IPv4 with token",
324+
serverURL: "http://127.0.0.1:1234/lab?token=abc",
325+
want: true,
326+
},
327+
{
328+
name: "https localhost",
329+
serverURL: "https://localhost:8888/",
330+
want: true,
331+
},
332+
{
333+
name: "http loopback IPv6",
334+
serverURL: "http://[::1]:9000/lab",
335+
want: true,
336+
},
337+
{
338+
name: "vscode-insiders scheme",
339+
serverURL: "vscode-insiders://ms-vsliveshare.vsliveshare/join?foo=bar",
340+
want: false,
341+
},
342+
{
343+
name: "vscode scheme",
344+
serverURL: "vscode://vscode.git/clone?url=https://example.com",
345+
want: false,
346+
},
347+
{
348+
name: "non-loopback host",
349+
serverURL: "http://cli.github.com/lab",
350+
want: false,
351+
},
352+
{
353+
name: "file scheme",
354+
serverURL: "file:///mona-home/document",
355+
want: false,
356+
},
357+
{
358+
name: "empty string",
359+
serverURL: "",
360+
want: false,
361+
},
362+
}
363+
364+
for _, tt := range tests {
365+
t.Run(tt.name, func(t *testing.T) {
366+
require.Equal(t, tt.want, isJupyterServerURLValid(tt.serverURL))
367+
})
368+
}
369+
}

0 commit comments

Comments
 (0)