Repository: cli/cli
File: internal/codespaces/portforwarder/port_forwarder.go (Go)

Code before:
		}
		return nil
	case <-ctx.Done():
		return nil
	}
}

// ForwardPort informs the host that we would like to forward the given port.
func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts ForwardPortOpts) error {
	// Convert the port number to a uint16
	port, err := convertIntToUint16(opts.Port)
	if err != nil {
		return fmt.Errorf("error converting port: %w", err)
	}

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

	// If no visibility is provided, Dev Tunnels will use the default (private)
	if opts.Visibility != "" {
		// Check if the requested visibility is allowed
		allowed := false
		for _, allowedVisibility := range fwd.connection.AllowedPortPrivacySettings {
			if allowedVisibility == opts.Visibility {
				allowed = true
				break
			}
		}

		// If the requested visibility is not allowed, return an error
		if !allowed {
			return fmt.Errorf("visibility %s is not allowed", opts.Visibility)
		}

		accessControlEntries := visibilityToAccessControlEntries(opts.Visibility)
		if len(accessControlEntries) > 0 {
			tunnelPort.AccessControl = &tunnels.TunnelAccessControl{
				Entries: accessControlEntries,
			}
		}
	}

	// 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}
	} else {
		tunnelPort.Tags = []string{UserForwardedPortTag}
	}

	// Create the tunnel port
	_, err = fwd.connection.TunnelManager.CreateTunnelPort(ctx, fwd.connection.Tunnel, tunnelPort, fwd.connection.Options)
	if err != nil && !strings.Contains(err.Error(), "409") {

Review comment (performance):
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?

Code after:
		}
		return nil
	case <-ctx.Done():
		return nil
	}
}

// ForwardPort informs the host that we would like to forward the given port.
func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts ForwardPortOpts) error {
	// Convert the port number to a uint16
	port, err := convertIntToUint16(opts.Port)
	if err != nil {
		return fmt.Errorf("error converting port: %w", err)
	}

	// 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)
	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 != "" {
		// Check if the requested visibility is allowed
		allowed := false
		for _, allowedVisibility := range fwd.connection.AllowedPortPrivacySettings {
			if allowedVisibility == opts.Visibility {
				allowed = true
				break
			}
		}

		// If the requested visibility is not allowed, return an error
		if !allowed {
			return fmt.Errorf("visibility %s is not allowed", opts.Visibility)
		}