github.com/greenpau/go-authcrunch@v1.1.4/cmd/authdbctl/connect.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"fmt"
    19  	"github.com/urfave/cli/v2"
    20  	"go.uber.org/zap"
    21  	"golang.org/x/net/html"
    22  	"log"
    23  	"net/http"
    24  	"net/url"
    25  	"strings"
    26  )
    27  
    28  func connect(c *cli.Context) error {
    29  	wr := new(wrapper)
    30  	if err := wr.configure(c); err != nil {
    31  		return err
    32  	}
    33  
    34  	var formKind, formURI string
    35  	var formData url.Values
    36  	var counter int
    37  	for {
    38  		counter++
    39  		if counter > 25 {
    40  			return fmt.Errorf("reached max attempts threshold")
    41  		}
    42  
    43  		if formKind == "" {
    44  			formKind = "login"
    45  			formURI = "/login"
    46  			formData = url.Values{}
    47  			formData.Set("username", wr.config.Username)
    48  			formData.Set("realm", wr.config.Realm)
    49  
    50  			req, _ := http.NewRequest(http.MethodGet, wr.config.BaseURL+"/", nil)
    51  			wr.browser.Do(req)
    52  		}
    53  
    54  		req, _ := http.NewRequest(http.MethodPost, wr.config.BaseURL+formURI, strings.NewReader(formData.Encode()))
    55  		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    56  		respBody, resp, err := wr.browser.Do(req)
    57  		if err != nil {
    58  			return fmt.Errorf("failed connecting to auth portal sandbox: %v", err)
    59  		}
    60  
    61  		redirectURL := resp.Header.Get("Location")
    62  
    63  		if redirectURL != "" {
    64  			wr.logger.Debug("request redirected", zap.String("redirect_url", redirectURL))
    65  			req, _ := http.NewRequest(http.MethodGet, redirectURL, nil)
    66  			respBody, resp, err = wr.browser.Do(req)
    67  			for _, cookie := range resp.Cookies() {
    68  				if cookie.Name == wr.config.CookieName {
    69  					wr.config.token = cookie.Value
    70  				}
    71  			}
    72  
    73  		}
    74  
    75  		respData, err := parseResponse(respBody)
    76  		if err != nil {
    77  			return fmt.Errorf("failed parsing auth portal sandbox response: %v", err)
    78  		}
    79  
    80  		if len(respData) > 0 {
    81  			wr.logger.Debug("received response data", zap.Any("data", respData))
    82  		}
    83  
    84  		formKind = respData["form_kind"]
    85  		var terminateLoop, continueLoop bool
    86  		switch formKind {
    87  		case "login", "password-auth":
    88  			formURI = respData["form_action"]
    89  		case "":
    90  			if redirectURL != "" && !strings.Contains(redirectURL, "/sandbox/") {
    91  				continueLoop = true
    92  			} else {
    93  				// Inspect headers and cookies for the presence of auth token.
    94  				terminateLoop = true
    95  			}
    96  		default:
    97  			return fmt.Errorf("the %q form is unsupported", formKind)
    98  		}
    99  
   100  		if terminateLoop {
   101  			wr.logger.Debug("logged in successfully")
   102  			break
   103  		}
   104  
   105  		if continueLoop {
   106  			continue
   107  		}
   108  
   109  		formData = url.Values{}
   110  		for k, v := range respData {
   111  			if !strings.HasPrefix(k, "input_") {
   112  				continue
   113  			}
   114  			k = strings.TrimPrefix(k, "input_")
   115  			if v != "" {
   116  				formData.Set(k, v)
   117  			} else {
   118  				switch {
   119  				case (k == "secret") && (formKind == "password-auth") && (wr.config.Password == ""):
   120  					input, err := wr.readUserInput("password")
   121  					if err != nil {
   122  						return err
   123  					}
   124  					formData.Set(k, string(input))
   125  				case (k == "secret") && (formKind == "password-auth"):
   126  					formData.Set(k, wr.config.Password)
   127  				default:
   128  					return fmt.Errorf("the %q input in %q form is unsupported: %v", k, formKind, respData)
   129  				}
   130  			}
   131  		}
   132  
   133  		wr.logger.Debug(
   134  			"prepared form inputs",
   135  			zap.String("form_kind", formKind),
   136  			zap.String("form_uri", formURI),
   137  			zap.Any("data", formData),
   138  		)
   139  	}
   140  
   141  	if wr.config.token != "" {
   142  		wr.logger.Debug(
   143  			"auth token acquired",
   144  			zap.String("token", wr.config.token),
   145  		)
   146  		if err := wr.commitToken(); err != nil {
   147  			return err
   148  		}
   149  		log.Printf("auth token acquired: %s", wr.config.TokenPath)
   150  		return nil
   151  	}
   152  	return fmt.Errorf("failed to obtain auth token")
   153  }
   154  
   155  func parseResponse(s string) (map[string]string, error) {
   156  	m := make(map[string]string)
   157  	doc, err := html.Parse(strings.NewReader(s))
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	var f func(*html.Node) error
   163  	f = func(n *html.Node) error {
   164  		if n.Type == html.ElementNode && n.Data == "form" {
   165  			if err := parseResponseForm(m, n); err != nil {
   166  				return err
   167  			}
   168  		}
   169  		for c := n.FirstChild; c != nil; c = c.NextSibling {
   170  			if err := f(c); err != nil {
   171  				return err
   172  			}
   173  		}
   174  		return nil
   175  	}
   176  	if err := f(doc); err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	return m, nil
   181  }
   182  
   183  func parseResponseForm(m map[string]string, doc *html.Node) error {
   184  	var formKind string
   185  	for _, a := range doc.Attr {
   186  		switch a.Key {
   187  		case "class", "action":
   188  			m["form_"+a.Key] = a.Val
   189  		}
   190  		if a.Key == "action" {
   191  			switch {
   192  			case strings.HasSuffix(a.Val, "/password-auth"):
   193  				formKind = "password-auth"
   194  			default:
   195  				return fmt.Errorf("detected unsupported form: %s", a.Val)
   196  			}
   197  			m["form_kind"] = formKind
   198  		}
   199  	}
   200  
   201  	if _, exists := m["form_kind"]; !exists {
   202  		return fmt.Errorf("failed to identify form kind")
   203  	}
   204  
   205  	var f func(*html.Node) error
   206  	f = func(n *html.Node) error {
   207  		if n.Data == "input" {
   208  			var elemKey, elemVal string
   209  			elem := make(map[string]string)
   210  			for _, a := range n.Attr {
   211  				switch a.Key {
   212  				case "id", "name", "value":
   213  					elem[a.Key] = a.Val
   214  				}
   215  			}
   216  			if _, exists := elem["name"]; !exists {
   217  				return fmt.Errorf("input has no name field: %v", elem)
   218  			}
   219  			elemKey = elem["name"]
   220  
   221  			if v, exists := elem["value"]; exists {
   222  				elemVal = v
   223  			}
   224  			m["input_"+elemKey] = elemVal
   225  		}
   226  
   227  		for c := n.FirstChild; c != nil; c = c.NextSibling {
   228  			if err := f(c); err != nil {
   229  				return err
   230  			}
   231  		}
   232  		return nil
   233  	}
   234  	if err := f(doc); err != nil {
   235  		return err
   236  	}
   237  	return nil
   238  }