Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ require (
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-isatty v0.0.20
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/microsoft/dev-tunnels v0.0.25
github.com/microsoft/dev-tunnels v0.1.13
github.com/muhammadmuzzammil1998/jsonc v1.0.0
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/microsoft/dev-tunnels v0.0.25 h1:UlMKUI+2O8cSu4RlB52ioSyn1LthYSVkJA+CSTsdKoA=
github.com/microsoft/dev-tunnels v0.0.25/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ=
github.com/microsoft/dev-tunnels v0.1.13 h1:bp1qqCvP/5iLol1Vz0c/lM2sexG7Gd8fRGcGv58vZdE=
github.com/microsoft/dev-tunnels v0.1.13/go.mod h1:Jvr6RlyjUXomM6KsDmIQbq+hhKd5mWrBcv3MEsa78dc=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
Expand Down
4 changes: 3 additions & 1 deletion internal/codespaces/connection/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ func getTunnelManager(tunnelProperties api.TunnelProperties, httpClient *http.Cl
}

// Create the tunnel manager
tunnelManager, err = tunnels.NewManager(userAgent, nil, url, httpClient)
// This api version seems to be the only acceptable api version: https://github.com/microsoft/dev-tunnels/blob/bf96ae5a128041d1a23f81d53a47e9e6c26fdc8d/go/tunnels/manager.go#L66
apiVersion := "2023-09-27-preview"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consequence of now requiring api version as per description

tunnelManager, err = tunnels.NewManager(userAgent, nil, url, httpClient, apiVersion)
if err != nil {
return nil, fmt.Errorf("error creating tunnel manager: %w", err)
}
Expand Down
159 changes: 134 additions & 25 deletions internal/codespaces/connection/tunnels_api_server_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"net/http/httptest"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -25,7 +26,28 @@ import (
"golang.org/x/crypto/ssh"
)

func NewMockHttpClient() (*http.Client, error) {
type mockClientOpts struct {
ports map[int]tunnels.TunnelPort // Port number to protocol
}

type mockClientOpt func(*mockClientOpts)

// WithSpecificPorts allows you to specify a map of ports to TunnelPorts that will be returned by the mock HTTP client.
// Note that this does not take a copy of the map, so you should not modify the map after passing it to this function.
func WithSpecificPorts(ports map[int]tunnels.TunnelPort) mockClientOpt {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The majority of changes in this file relate to this. Previous behaviour was maintained when this is not provided to avoid a lot of test churn. If you provide these ports, they will be used when working with the http server tunnel port endpoints.

return func(opts *mockClientOpts) {
opts.ports = ports
}
}

func NewMockHttpClient(opts ...mockClientOpt) (*http.Client, error) {
mockClientOpts := &mockClientOpts{}
for _, opt := range opts {
opt(mockClientOpts)
}

specifiedPorts := mockClientOpts.ports

accessToken := "tunnel access-token"
relayServer, err := newMockrelayServer(withAccessToken(accessToken))
if err != nil {
Expand All @@ -35,7 +57,7 @@ func NewMockHttpClient() (*http.Client, error) {
hostURL := strings.Replace(relayServer.URL(), "http://", "ws://", 1)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var response []byte
if r.URL.Path == "/api/v1/tunnels/tunnel-id" {
if r.URL.Path == "/tunnels/tunnel-id" {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consequence of dropping of /api/v1 as per description

tunnel := &tunnels.Tunnel{
AccessTokens: map[tunnels.TunnelAccessScope]string{
tunnels.TunnelAccessScopeConnect: accessToken,
Expand All @@ -54,54 +76,141 @@ func NewMockHttpClient() (*http.Client, error) {
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
} else if strings.HasPrefix(r.URL.Path, "/api/v1/tunnels/tunnel-id/ports") {
// Use regex to check if the path ends with a number
match, err := regexp.MatchString(`\/\d+$`, r.URL.Path)
if err != nil {
log.Fatalf("regexp.MatchString returned an error: %v", err)
}

// If the path ends with a number, it's a request for a specific port
if match || r.Method == http.MethodPost {
_, _ = w.Write(response)
return
} else if strings.HasPrefix(r.URL.Path, "/tunnels/tunnel-id/ports") {
// Use regex to capture the port number from the end of the path
re := regexp.MustCompile(`\/(\d+)$`)
matches := re.FindStringSubmatch(r.URL.Path)
targetingSpecificPort := len(matches) > 0

if targetingSpecificPort {
if r.Method == http.MethodDelete {
w.WriteHeader(http.StatusOK)
return
}

tunnelPort := &tunnels.TunnelPort{
if r.Method == http.MethodGet {
// If no ports were configured, then we assume that every request for a port is valid.
if specifiedPorts == nil {
response, err := json.Marshal(tunnels.TunnelPort{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
})

if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}

_, _ = w.Write(response)
return
} else {
// Otherwise we'll fetch the port from our configured ports and include the protocol in the response.
port, err := strconv.Atoi(matches[1])
if err != nil {
log.Fatalf("strconv.Atoi returned an error: %v", err)
}

tunnelPort, ok := specifiedPorts[port]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}

response, err := json.Marshal(tunnelPort)

if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}

_, _ = w.Write(response)
return
}
}

// Else this is an unexpected request, fall through to 404 at the bottom
}

// If it's a PUT request, we assume it's for creating a new port so we'll do some validation
// and then return a stub.
if r.Method == http.MethodPut {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// If a port was already configured with this number, and the protocol has changed, return a 400 Bad Request.
if specifiedPorts != nil {
port, err := strconv.Atoi(matches[1])
if err != nil {
log.Fatalf("strconv.Atoi returned an error: %v", err)
}

var portRequest tunnels.TunnelPort
if err := json.NewDecoder(r.Body).Decode(&portRequest); err != nil {
log.Fatalf("json.NewDecoder returned an error: %v", err)
}

tunnelPort, ok := specifiedPorts[port]
if ok {
if tunnelPort.Protocol != portRequest.Protocol {
w.WriteHeader(http.StatusBadRequest)
return
}
}

// Create or update the new port entry.
specifiedPorts[port] = portRequest
}

response, err := json.Marshal(tunnels.TunnelPort{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
}
})

// Convert the tunnel to JSON and write it to the response
response, err = json.Marshal(*tunnelPort)
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
} else {
// If the path doesn't end with a number and we aren't making a POST request, return an array of ports
tunnelPorts := []tunnels.TunnelPort{
{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},

_, _ = w.Write(response)
return
}

// Finally, if it's not targeting a specific port or a POST request, we return a list of ports, either
// totally stubbed, or whatever was configured in the mock client options.
if specifiedPorts == nil {
response, err := json.Marshal(tunnels.TunnelPortListResponse{
Value: []tunnels.TunnelPort{
{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
},
},
})
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}

response, err = json.Marshal(tunnelPorts)
_, _ = w.Write(response)
return
} else {
var ports []tunnels.TunnelPort
for _, tunnelPort := range specifiedPorts {
ports = append(ports, tunnelPort)
}
response, err := json.Marshal(tunnels.TunnelPortListResponse{
Value: ports,
})
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
}

_, _ = w.Write(response)
return
}
} else {
w.WriteHeader(http.StatusNotFound)
return
}

// Write the response
_, _ = w.Write(response)
}))

url, err := url.Parse(mockServer.URL)
Expand Down
35 changes: 27 additions & 8 deletions internal/codespaces/portforwarder/port_forwarder.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
)

const (
githubSubjectId = "1"
InternalPortTag = "InternalPort"
UserForwardedPortTag = "UserForwardedPort"
githubSubjectId = "1"
InternalPortLabel = "InternalPort"
UserForwardedPortLabel = "UserForwardedPort"
)

const (
Expand Down Expand Up @@ -108,7 +108,26 @@ func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts Forwar
return fmt.Errorf("error converting port: %w", err)
}

tunnelPort := tunnels.NewTunnelPort(port, "", "", tunnels.TunnelProtocolHttp)
// In v0.0.25 of dev-tunnels, the dev-tunnel manager `CreateTunnelPort` would "accept" requests that
// change the port protocol but they would not result in any actual change. This has changed, resulting in
// an error `Invalid arguments. The tunnel port protocol cannot be changed.`. It's not clear why the previous
// behaviour existed, whether it was truly the API version, or whether the `If-Not-Match` header being set inside
// `CreateTunnelPort` avoided the server accepting the request to change the protocol and that has since regressed.
//
// In any case, now we check whether a port exists with the given port number, if it does, we use the existing protocol.
// If it doesn't exist, we default to HTTP, which was the previous behaviour for all ports.
protocol := tunnels.TunnelProtocolHttp

existingPort, err := fwd.connection.TunnelManager.GetTunnelPort(ctx, fwd.connection.Tunnel, opts.Port, fwd.connection.Options)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably could do a cached ListTunnelPorts once so that we don't need to do this every time. For cs ssh it's 2 ports (16634 and 2222) but for gh cs ports visibility it's unbounded, and done sequentially. Maybe wait for a complaint?

if err != nil && !strings.Contains(err.Error(), "404") {
return fmt.Errorf("error checking whether tunnel port already exists: %v", err)
}

if existingPort != nil {
protocol = tunnels.TunnelProtocol(existingPort.Protocol)
}

tunnelPort := tunnels.NewTunnelPort(port, "", "", protocol)

// If no visibility is provided, Dev Tunnels will use the default (private)
if opts.Visibility != "" {
Expand Down Expand Up @@ -136,9 +155,9 @@ func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts Forwar

// Tag the port as internal or user forwarded so we know if it needs to be shown in the UI
if opts.Internal {
tunnelPort.Tags = []string{InternalPortTag}
tunnelPort.Labels = []string{InternalPortLabel}
} else {
tunnelPort.Tags = []string{UserForwardedPortTag}
tunnelPort.Labels = []string{UserForwardedPortLabel}
}

// Create the tunnel port
Expand Down Expand Up @@ -362,8 +381,8 @@ func visibilityToAccessControlEntries(visibility string) []tunnels.TunnelAccessC

// IsInternalPort returns true if the port is internal.
func IsInternalPort(port *tunnels.TunnelPort) bool {
for _, tag := range port.Tags {
if strings.EqualFold(tag, InternalPortTag) {
for _, label := range port.Labels {
if strings.EqualFold(label, InternalPortLabel) {
return true
}
}
Expand Down
Loading