Path: blob/main/components/supervisor/pkg/ports/tunnel_test.go
2500 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package ports56import (7"context"8"fmt"9"io"10"io/ioutil"11"net"12"net/http"13"strconv"14"strings"15"sync"16"testing"1718"github.com/google/go-cmp/cmp"19"golang.org/x/sync/errgroup"2021"github.com/gitpod-io/gitpod/supervisor/api"22)2324// TODO(ak) add reverse test.25func TestLocalPortTunneling(t *testing.T) {26updates := make(chan []PortTunnelState, 4)27assertUpdate := func(expectation []PortTunnelState) {28update := <-updates29if diff := cmp.Diff(expectation, update); diff != "" {30t.Errorf("unexpected exposures (-want +got):\n%s", diff)31}32}3334doneCtx, done := context.WithCancel(context.Background())35eg, ctx := errgroup.WithContext(context.Background())36service := NewTunneledPortsService(false)37tunneled, errors := service.Observe(ctx)38eg.Go(func() error {39for {40select {41case <-doneCtx.Done():42return nil43case ports := <-tunneled:44if ports == nil {45close(updates)46return nil47}48updates <- ports49case err := <-errors:50return err51}52}53})54assertUpdate([]PortTunnelState{})5556localPort, err := availablePort()57if err != nil {58t.Fatal(err)59}60localListener, err := net.Listen("tcp", "127.0.0.1:"+strconv.FormatInt(int64(localPort), 10))61if err != nil {62t.Fatal(err)63}64fmt.Printf("local service is listening on %d\n", localPort)65eg.Go(func() error {66go func() {67<-doneCtx.Done()68localListener.Close()69}()70localServer := http.Server{71Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {72b, _ := ioutil.ReadAll(r.Body)73_, _ = w.Write(append(b, '!'))74}),75}76_ = localServer.Serve(localListener)77return nil78})7980targetPort, err := availablePort()81if err != nil {82t.Fatal(err)83}84desc := PortTunnelDescription{85LocalPort: localPort,86TargetPort: targetPort,87Visibility: api.TunnelVisiblity_host,88}89_, err = service.Tunnel(ctx, &TunnelOptions{90SkipIfExists: false,91}, &PortTunnelDescription{92LocalPort: localPort,93TargetPort: targetPort,94Visibility: api.TunnelVisiblity_host,95})96if err != nil {97t.Fatal(err)98}99fmt.Printf("%d:%d tunnel has been created\n", localPort, targetPort)100assertUpdate([]PortTunnelState{{Desc: desc, Clients: map[string]uint32{}}})101102targetAddr := "127.0.0.1:" + strconv.FormatInt(int64(targetPort), 10)103proxyAddr, err := net.ResolveTCPAddr("tcp", targetAddr)104if err != nil {105t.Fatal(err)106}107proxyListener, err := net.ListenTCP("tcp", proxyAddr)108if err != nil {109t.Fatal(err)110}111fmt.Printf("target proxy is listening on %d\n", targetPort)112eg.Go(func() error {113defer proxyListener.Close()114115src, err := proxyListener.Accept()116if err != nil {117return err118}119defer src.Close()120121dst, err := service.EstablishTunnel(ctx, "test", localPort, targetPort)122if err != nil {123return err124}125defer dst.Close()126127done := make(chan struct{})128var once sync.Once129go func() {130_, _ = io.Copy(src, dst)131once.Do(func() { close(done) })132}()133go func() {134_, _ = io.Copy(dst, src)135once.Do(func() { close(done) })136}()137<-done138return nil139})140141// actually open ssh channel142resp, err := http.Post("http://"+targetAddr, "text/plain", strings.NewReader("Hello World"))143if err != nil {144t.Fatal(err)145}146body, err := ioutil.ReadAll(resp.Body)147if err != nil {148t.Fatal(err)149}150if string(body) != ("Hello World!") {151t.Fatal("wrong resp")152}153assertUpdate([]PortTunnelState{{Desc: desc, Clients: map[string]uint32{"test": targetPort}}})154155_, err = service.CloseTunnel(ctx, localPort)156if err != nil {157t.Fatal(err)158}159assertUpdate([]PortTunnelState{})160161done()162163err = eg.Wait()164if err != nil && err != context.Canceled {165t.Error(err)166}167}168169func availablePort() (uint32, error) {170l, err := net.Listen("tcp", "127.0.0.1:0")171if err != nil {172return 0, err173}174l.Close()175_, parsed, err := net.SplitHostPort(l.Addr().String())176if err != nil {177return 0, err178}179port, err := strconv.Atoi(parsed)180if err != nil {181return 0, err182}183return uint32(port), nil184}185186187