github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/userAgent_test.go (about)

     1  /*
     2   * Copyright (c) 2017, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package psiphon
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"net/http"
    27  	"os"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    33  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values"
    34  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server"
    35  	"github.com/elazarl/goproxy"
    36  )
    37  
    38  // TODO: test that server receives and records correct user_agent value
    39  
    40  func TestOSSHUserAgent(t *testing.T) {
    41  	attemptConnectionsWithUserAgent(t, "OSSH", true)
    42  }
    43  
    44  func TestUnfrontedMeekUserAgent(t *testing.T) {
    45  	attemptConnectionsWithUserAgent(t, "UNFRONTED-MEEK-OSSH", false)
    46  }
    47  
    48  func TestUnfrontedMeekHTTPSUserAgent(t *testing.T) {
    49  	attemptConnectionsWithUserAgent(t, "UNFRONTED-MEEK-HTTPS-OSSH", true)
    50  }
    51  
    52  var mockUserAgents = []string{"UserAgentA", "UserAgentB"}
    53  var userAgentCountsMutex sync.Mutex
    54  var userAgentCounts map[string]int
    55  var initUserAgentCounter sync.Once
    56  
    57  func initMockUserAgents() {
    58  	values.SetUserAgentsSpec(values.NewPickOneSpec(mockUserAgents))
    59  }
    60  
    61  func resetUserAgentCounts() {
    62  	userAgentCountsMutex.Lock()
    63  	defer userAgentCountsMutex.Unlock()
    64  	userAgentCounts = make(map[string]int)
    65  }
    66  
    67  func countHTTPUserAgent(headers http.Header, isCONNECT bool) {
    68  	userAgentCountsMutex.Lock()
    69  	defer userAgentCountsMutex.Unlock()
    70  	if _, ok := headers["User-Agent"]; !ok {
    71  		userAgentCounts["BLANK"]++
    72  	} else if isCONNECT {
    73  		userAgentCounts["CONNECT-"+headers.Get("User-Agent")]++
    74  	} else {
    75  		userAgentCounts[headers.Get("User-Agent")]++
    76  	}
    77  }
    78  
    79  func countNoticeUserAgent(userAgent string) {
    80  	userAgentCountsMutex.Lock()
    81  	defer userAgentCountsMutex.Unlock()
    82  	userAgentCounts["NOTICE-"+userAgent]++
    83  }
    84  
    85  func checkUserAgentCounts(t *testing.T, isCONNECT bool) {
    86  	userAgentCountsMutex.Lock()
    87  	defer userAgentCountsMutex.Unlock()
    88  
    89  	for _, userAgent := range mockUserAgents {
    90  
    91  		if isCONNECT {
    92  			if userAgentCounts["CONNECT-"+userAgent] == 0 {
    93  				t.Fatalf("unexpected CONNECT user agent count of 0: %+v", userAgentCounts)
    94  				return
    95  			}
    96  		} else {
    97  
    98  			if userAgentCounts[userAgent] == 0 {
    99  				t.Fatalf("unexpected non-CONNECT user agent count of 0: %+v", userAgentCounts)
   100  				return
   101  			}
   102  		}
   103  
   104  		if userAgentCounts["NOTICE-"+userAgent] == 0 {
   105  			t.Fatalf("unexpected NOTICE user agent count of 0: %+v", userAgentCounts)
   106  			return
   107  		}
   108  	}
   109  
   110  	if userAgentCounts["BLANK"] == 0 {
   111  		t.Fatalf("unexpected BLANK user agent count of 0: %+v", userAgentCounts)
   112  		return
   113  	}
   114  
   115  	// TODO: check proportions
   116  	t.Logf("%+v", userAgentCounts)
   117  }
   118  
   119  func initUserAgentCounterUpstreamProxy() {
   120  	initUserAgentCounter.Do(func() {
   121  		go func() {
   122  			proxy := goproxy.NewProxyHttpServer()
   123  
   124  			proxy.OnRequest().DoFunc(
   125  				func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
   126  					countHTTPUserAgent(r.Header, false)
   127  					return nil, goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusUnauthorized, "")
   128  				})
   129  
   130  			proxy.OnRequest().HandleConnectFunc(
   131  				func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
   132  					countHTTPUserAgent(ctx.Req.Header, true)
   133  					return goproxy.RejectConnect, host
   134  				})
   135  
   136  			err := http.ListenAndServe("127.0.0.1:2163", proxy)
   137  			if err != nil {
   138  				fmt.Printf("upstream proxy failed: %s\n", err)
   139  			}
   140  		}()
   141  
   142  		// TODO: more robust wait-until-listening
   143  		time.Sleep(1 * time.Second)
   144  	})
   145  }
   146  
   147  func attemptConnectionsWithUserAgent(
   148  	t *testing.T, tunnelProtocol string, isCONNECT bool) {
   149  
   150  	testDataDirName, err := ioutil.TempDir("", "psiphon-user-agent-test")
   151  	if err != nil {
   152  		t.Fatalf("TempDir failed: %s\n", err)
   153  	}
   154  	defer os.RemoveAll(testDataDirName)
   155  
   156  	initMockUserAgents()
   157  	initUserAgentCounterUpstreamProxy()
   158  	resetUserAgentCounts()
   159  
   160  	// create a server entry
   161  
   162  	serverIPv4Address, serverIPv6Address, err := common.GetRoutableInterfaceIPAddresses()
   163  	if err != nil {
   164  		t.Fatalf("error getting server IP address: %s", err)
   165  	}
   166  	serverIPAddress := ""
   167  	if serverIPv4Address != nil {
   168  		serverIPAddress = serverIPv4Address.String()
   169  	} else {
   170  		serverIPAddress = serverIPv6Address.String()
   171  	}
   172  
   173  	_, _, _, _, encodedServerEntry, err := server.GenerateConfig(
   174  		&server.GenerateConfigParams{
   175  			ServerIPAddress:      serverIPAddress,
   176  			EnableSSHAPIRequests: true,
   177  			WebServerPort:        8000,
   178  			TunnelProtocolPorts:  map[string]int{tunnelProtocol: 4000},
   179  		})
   180  	if err != nil {
   181  		t.Fatalf("error generating server config: %s", err)
   182  	}
   183  
   184  	// attempt connections with client
   185  
   186  	// Connections are made through a mock upstream proxy that
   187  	// counts user agents. No server is running, and the upstream
   188  	// proxy rejects connections after counting the user agent.
   189  
   190  	// Note: calling LoadConfig ensures all *int config fields are initialized
   191  	clientConfigJSON := `
   192      {
   193          "ClientPlatform" : "Windows",
   194          "ClientVersion" : "0",
   195          "SponsorId" : "0",
   196          "PropagationChannelId" : "0",
   197          "ConnectionWorkerPoolSize" : 1,
   198          "EstablishTunnelPausePeriodSeconds" : 1,
   199          "DisableRemoteServerListFetcher" : true,
   200          "TransformHostNameProbability" : 0.0,
   201          "UpstreamProxyUrl" : "http://127.0.0.1:2163",
   202          "UpstreamProxyAllowAllServerEntrySources" : true
   203      }`
   204  	clientConfig, err := LoadConfig([]byte(clientConfigJSON))
   205  	if err != nil {
   206  		t.Fatalf("error processing configuration file: %s", err)
   207  	}
   208  
   209  	clientConfig.TargetServerEntry = string(encodedServerEntry)
   210  	clientConfig.TunnelProtocol = tunnelProtocol
   211  	clientConfig.DataRootDirectory = testDataDirName
   212  
   213  	err = clientConfig.Commit(false)
   214  	if err != nil {
   215  		t.Fatalf("error committing configuration file: %s", err)
   216  	}
   217  
   218  	err = OpenDataStore(clientConfig)
   219  	if err != nil {
   220  		t.Fatalf("error initializing client datastore: %s", err)
   221  	}
   222  	defer CloseDataStore()
   223  
   224  	SetNoticeWriter(NewNoticeReceiver(
   225  		func(notice []byte) {
   226  			noticeType, payload, err := GetNotice(notice)
   227  			if err != nil {
   228  				return
   229  			}
   230  			if noticeType == "ConnectingServer" {
   231  				userAgent, ok := payload["userAgent"]
   232  				if ok {
   233  					countNoticeUserAgent(userAgent.(string))
   234  				}
   235  			}
   236  		}))
   237  
   238  	controller, err := NewController(clientConfig)
   239  	if err != nil {
   240  		t.Fatalf("error creating client controller: %s", err)
   241  	}
   242  
   243  	ctx, cancelFunc := context.WithCancel(context.Background())
   244  
   245  	controllerWaitGroup := new(sync.WaitGroup)
   246  
   247  	controllerWaitGroup.Add(1)
   248  	go func() {
   249  		defer controllerWaitGroup.Done()
   250  		controller.Run(ctx)
   251  	}()
   252  
   253  	// repeat attempts for long enough to select each user agent
   254  
   255  	time.Sleep(30 * time.Second)
   256  
   257  	cancelFunc()
   258  
   259  	controllerWaitGroup.Wait()
   260  
   261  	checkUserAgentCounts(t, isCONNECT)
   262  }