From f8e269cfb40b70df6a63af95a3c22851b6d622db Mon Sep 17 00:00:00 2001 From: Kasra Asadzadeh <36865270+kasadzadeh-r7@users.noreply.github.com> Date: Thu, 25 Oct 2018 13:58:02 -0400 Subject: [PATCH] The changes commited are intended to provide reading system proxies through scutil command. There are some unit tests added as well to test out the feature added (#3) --- proxy/provider.go | 28 ++++++ proxy/provider_darwin.go | 130 ++++++++++++++++++++++++- proxy/provider_darwin_test.go | 175 ++++++++++++++++++++++++++++++++++ 3 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 proxy/provider_darwin_test.go diff --git a/proxy/provider.go b/proxy/provider.go index 91cbe5e..f1e22e0 100644 --- a/proxy/provider.go +++ b/proxy/provider.go @@ -13,6 +13,7 @@ package proxy import ( + "context" "encoding/json" "errors" "fmt" @@ -20,6 +21,7 @@ import ( "log" "net/url" "os" + "os/exec" "path/filepath" "strings" ) @@ -56,14 +58,18 @@ type Provider interface { type getEnvAdapter func(string) (string) +type commandAdapter func (context.Context, string, ...string) *exec.Cmd + type provider struct { configFile string getEnv getEnvAdapter + proc commandAdapter } func (p *provider) init(configFile string) { p.configFile = configFile p.getEnv = os.Getenv + p.proc = exec.CommandContext } /* @@ -299,6 +305,12 @@ func (e notFoundError) Error() (string) { return "No proxy found" } +type timeoutError struct {} + +func (e timeoutError) Error() (string) { + return "Timed out" +} + /* Returns: true: The error represents a Proxy not being found @@ -314,3 +326,19 @@ func isNotFound(e error) (bool) { return false } } + +/* +Returns: + true: The error represents a Time out + false: Otherwise +s*/ +func isTimedOut(e error) (bool) { + switch e.(type) { + case *timeoutError: + return true + case timeoutError: + return true + default: + return false + } +} \ No newline at end of file diff --git a/proxy/provider_darwin.go b/proxy/provider_darwin.go index 913bd06..2868b0d 100644 --- a/proxy/provider_darwin.go +++ b/proxy/provider_darwin.go @@ -12,6 +12,16 @@ // without specific prior written permission. package proxy +import ( + "bufio" + "bytes" + "context" + "log" + "regexp" + "strings" + "time" +) + type providerDarwin struct { provider } @@ -42,7 +52,10 @@ Returns: nil: A proxy was not found, or an error occurred */ func (p *providerDarwin) Get(protocol string, targetUrlStr string) (Proxy) { - return p.provider.get(protocol, ParseTargetURL(targetUrlStr)) + if proxy := p.provider.get(protocol, ParseTargetURL(targetUrlStr)); proxy != nil { + return proxy + } + return p.readDarwinNetworkSettingProxy(protocol) } /* @@ -60,4 +73,119 @@ Returns: */ func (p *providerDarwin) GetHTTPS(targetUrl string) (Proxy) { return p.Get("https", targetUrl) +} + + +/* +Returns the Network Setting Proxy found. +If none is found, or an error occurs, nil is returned. +Params: + protocol: The protocol of interest +Returns: + Proxy: A proxy was found + nil: A proxy was not found, or an error occurred +*/ +func (p *providerDarwin) readDarwinNetworkSettingProxy(protocol string) (Proxy) { + proxy, err := p.parseScutildata(protocol, "scutil", "--proxy") + if err != nil { + if isNotFound(err){ + log.Printf("[proxy.Provider.readDarwinNetworkSettingProxy]: %s proxy is not enabled.\n", protocol) + }else if isTimedOut(err){ + log.Printf("[proxy.Provider.readDarwinNetworkSettingProxy]: Operation timed out. \n") + } else { + log.Printf("[proxy.Provider.readDarwinNetworkSettingProxy]: Failed to parse Scutil data, %s\n", err) + } + } + return proxy +} + +/* +Returns the Proxy found by parsing the Scutil output. +If none is found, or an error occurs, nil is returned. +Params: + protocol: The protocol of interest + name: The name of the program (scutil) + arg: The list of the arguments (--proxy) +Returns: + Proxy: A proxy was found + nil: A proxy was not found, or an error occurred +*/ +func (p *providerDarwin) parseScutildata(protocol string, name string, arg ...string) (Proxy, error) { + lookupProtocol := strings.ToUpper(protocol) // to cover search for http, HTTP, https, HTTPS + + ctx, cancel := context.WithTimeout(context.Background(), time.Second * 1) // Die after one second + defer cancel() + + cmd := p.proc(ctx, name, arg...) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, new(timeoutError) + } + + scanner := bufio.NewScanner(strings.NewReader(out.String())) + /* init values */ + var enable bool + var port string + var host string + + regexEnable, err := regexp.Compile(lookupProtocol + "Enable:1") + if err != nil { + return nil, err + } + regexDisable, err := regexp.Compile(lookupProtocol + "Enable:0") + if err != nil { + return nil, err + } + regexPort, err := regexp.Compile(lookupProtocol + "Port:") + if err != nil { + return nil, err + } + regexProxy, err := regexp.Compile(lookupProtocol + "Proxy:") + if err != nil { + return nil, err + } + + for scanner.Scan() { + str := strings.Replace(scanner.Text(), " ", "", -1) // removing spaces + if !enable { // don't search if already found + // if proxy is disabled, stop the search + protocolDisableFound := regexDisable.FindStringIndex(str) + if protocolDisableFound != nil { + break + } + protocolEnableFound := regexEnable.FindStringIndex(str) + if protocolEnableFound != nil { + enable = true + } + } + if port == "" { // don't search if already found + portFoundLoc := regexPort.FindStringIndex(str) + if portFoundLoc != nil { + port = str[portFoundLoc[1]:] + } + } + if host == "" { // don't search if already found + proxyFoundLoc := regexProxy.FindStringIndex(str) + if proxyFoundLoc != nil { + host = str[proxyFoundLoc[1]:] + } + } + } + if !enable { + return nil, new(notFoundError) + } + + proxyUrlStr := host + ":" + port + proxyUrl, err := ParseURL(proxyUrlStr, protocol) + if err != nil { + return nil, err + } + src := "State:/Network/Global/Proxies" + proxy, err := NewProxy(protocol, proxyUrl, src) + if err != nil { + return nil, err + } + return proxy, nil } \ No newline at end of file diff --git a/proxy/provider_darwin_test.go b/proxy/provider_darwin_test.go new file mode 100644 index 0000000..98c4511 --- /dev/null +++ b/proxy/provider_darwin_test.go @@ -0,0 +1,175 @@ +package proxy + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "os/exec" + "strings" + "testing" +) + +const ( + ScutilDataHttpsHttp = "ScutilDataHttpsHttp" + ScutilDataHttps = "ScutilDataHttps" + ScutilDataHttp = "ScutilDataHttp" +) + +var providerDarwinTestCases = []struct { + testType string + test string +}{ + {ScutilDataHttpsHttp, + fmt.Sprintf(" {\n HTTPSEnable : 1\n HTTPSPort : %s\n HTTPSProxy : %s\n" + + " HTTPEnable : 1\n HTTPPort : %s\n HTTPProxy : %s\n}", "1234", "1.2.3.4", "1234", "1.2.3.4")}, + {ScutilDataHttpsHttp, + fmt.Sprintf(" { \n HTTPSEnable:1 \nHTTPSPort : %s\n HTTPSProxy : %s\n " + + "HTTPEnable : 1 \n HTTPPort : %s\nHTTPProxy: %s \n }", "1234", "1.2.3.4", "1234", "1.2.3.4")}, + {ScutilDataHttps, + fmt.Sprintf(" {\n HTTPEnable : 0\n HTTPSEnable : 1\n HTTPSPort : %s\n " + + "HTTPSProxy : %s\n}", "1234", "1.2.3.4")}, + {ScutilDataHttps, + fmt.Sprintf(" {\n HTTPEnable: 0\n HTTPSEnable: 1\n " + + "HTTPSPort : %s\n HTTPSProxy: %s\n}", "1234", "1.2.3.4")}, + {ScutilDataHttp, + fmt.Sprintf(" {\n HTTPSEnable : 0\n HTTPEnable : 1\n HTTPPort : %s\n " + + "HTTPProxy : %s\n}", "1234", "1.2.3.4")}, + {ScutilDataHttp, fmt.Sprintf(" {\n HTTPSEnable: 0\n HTTPEnable: 1\n " + + "HTTPPort : %s\n HTTPProxy: %s\n}", "1234", "1.2.3.4")}, +} + +func getDarwinProviderTests(key string)([]string){ + var s []string + for _, v := range providerDarwinTestCases { + if v.testType == key { + s = append(s, v.test) + } + } + return s +} + +/* +Below tests cover cases when both https and http proxies are present. +following tests are being performed: +- Test https and http proxies are not nil, +- Test https and http proxies match expected, +- Test for lower case and upper case, i.e. https/HTTPS/..., +- Test no errors are returned +*/ +func TestParseScutildata_Read_HTTPS_HTTP(t *testing.T) { + a := assert.New(t) + + c := newDarwinTestProvider() + + commands := getDarwinProviderTests(ScutilDataHttpsHttp) + + protocols := [4]string{"http", "https", "HTTP", "HTTPS"} + for _, protocol := range protocols { + for _, command := range commands { + expectedProxy, err := c.parseScutildata(protocol, "echo", command) + // test error is nil + a.Nil(err) + // test expected https proxy matches hardcoded proxy, test lowercase + a.Equal(&proxy{src: "State:/Network/Global/Proxies", protocol: strings.ToLower(protocol), host: "1.2.3.4", port: 1234}, + expectedProxy) + // test https and https proxies are not nil + a.NotNil(c.parseScutildata(protocol, "echo", command)) + } + } +} + +/* +Below tests cover cases when only https proxy is present. +following tests are being performed: +- Test https proxy is not nil, +- Test http proxy is nil, +- Test https proxy match expected, +- Test for lower case and upper case, i.e. https/HTTPS/..., +- Test no errors are returned +*/ +func TestParseScutildata_Read_HTTPS(t *testing.T) { + a := assert.New(t) + + c := newDarwinTestProvider() + + commands := getDarwinProviderTests(ScutilDataHttps) + + protocols := [4]string{"http", "https", "HTTP", "HTTPS"} + for _, protocol := range protocols { + for _, command := range commands { + if strings.ToLower(protocol) == "https" { + expectedProxy, err := c.parseScutildata(protocol, "echo", command) + // test error is nil + a.Nil(err) + // test expected https proxy matches hardcoded proxy + a.Equal(&proxy{src: "State:/Network/Global/Proxies", protocol: strings.ToLower(protocol), host: "1.2.3.4", port: 1234}, + expectedProxy) + // test https proxy is not nil + a.NotNil(c.parseScutildata(protocol, "echo", command)) + }else{ + // test http proxy is nil + a.Nil(c.parseScutildata(protocol, "echo", command)) + } + + } + } +} + +/* +Below tests cover cases when only http proxy is present. +following tests are being performed: +- Test http proxy is not nil, +- Test https proxy is nil, +- Test http proxy match expected, +- Test for lower case and upper case, i.e. https/HTTPS/..., +- Test no errors are returned +*/ +func TestParseScutildata_Read_HTTP(t *testing.T) { + a := assert.New(t) + + c := newDarwinTestProvider() + + commands := getDarwinProviderTests(ScutilDataHttp) + + protocols := [4]string{"http", "https", "HTTP", "HTTPS"} + for _, protocol := range protocols { + for _, command := range commands { + if strings.ToLower(protocol) == "http" { + expectedProxy, err := c.parseScutildata(protocol, "echo", command) + // test error is nil + a.Nil(err) + // test expected http proxy matches hardcoded proxy + a.Equal(&proxy{src: "State:/Network/Global/Proxies", protocol: strings.ToLower(protocol), host: "1.2.3.4", port: 1234}, + expectedProxy) + // test http proxy is not nil + a.NotNil(c.parseScutildata(protocol, "echo", command)) + }else{ + // test https proxy is nil + a.Nil(c.parseScutildata(protocol, "echo", command)) + } + } + } +} + +/* +Tests whether the timeout property functions as expected +*/ +func TestExecCommandsHandledProperly(t *testing.T) { + a := assert.New(t) + + c := newDarwinTestProvider() + + expectedProxy, err := c.parseScutildata("", "exit", "") + + a.Equal(isTimedOut(err), true) + a.Equal(expectedProxy, nil) +} + +func newDarwinTestProvider() (*providerDarwin) { + Cmd := func (ctx context.Context, name string, args ...string) *exec.Cmd { + return exec.CommandContext(ctx, name, args...) + } + c := new(providerDarwin) + c.proc = Cmd + return c +} \ No newline at end of file