diff --git a/Gopkg.toml b/Gopkg.toml index 931442876c..09d7a14f8b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -185,7 +185,6 @@ required = [ non-go = false go-tests = false - [[constraint]] name = "github.com/andygrunwald/go-gerrit" version = "0.5.2" diff --git a/pkg/jenkins/utils.go b/pkg/jenkins/utils.go index 4f16e726f2..3a1509e146 100644 --- a/pkg/jenkins/utils.go +++ b/pkg/jenkins/utils.go @@ -99,6 +99,10 @@ func JenkinsTokenURL(url string) string { return tokenUrl } +func JenkinsNewTokenURL(url string) string { + return util.UrlJoin(url, "/me/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken") +} + func EditUserAuth(url string, configService *jenkauth.AuthConfigService, config *jenkauth.AuthConfig, auth *jenkauth.UserAuth, tokenUrl string, batchMode bool, in terminal.FileReader, out terminal.FileWriter, outErr io.Writer) (jenkauth.UserAuth, error) { log.Infof("\nTo be able to connect to the Jenkins server we need a username and API Token\n\n") diff --git a/pkg/jx/cmd/create_jenkins_token.go b/pkg/jx/cmd/create_jenkins_token.go index ec4f1e6fc0..97e6f0adfb 100644 --- a/pkg/jx/cmd/create_jenkins_token.go +++ b/pkg/jx/cmd/create_jenkins_token.go @@ -3,25 +3,35 @@ package cmd import ( "bufio" "context" + "encoding/json" "fmt" "io" "io/ioutil" + golog "log" + "net/http" + "os" + "path/filepath" + "strings" "time" "github.com/chromedp/cdproto/cdp" "github.com/chromedp/chromedp" "github.com/chromedp/chromedp/runner" + "github.com/hpcloud/tail" "github.com/jenkins-x/jx/pkg/auth" "github.com/jenkins-x/jx/pkg/jenkins" "github.com/jenkins-x/jx/pkg/jx/cmd/templates" "github.com/jenkins-x/jx/pkg/kube" "github.com/jenkins-x/jx/pkg/log" "github.com/jenkins-x/jx/pkg/util" + "github.com/pkg/errors" "github.com/spf13/cobra" "gopkg.in/AlecAivazis/survey.v1/terminal" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const JenkinsCookieName = "JSESSIONID" + var ( create_jenkins_user_long = templates.LongDesc(` Creates a new user and API Token for the current Jenkins Server @@ -138,12 +148,14 @@ func (o *CreateJenkinsUserOptions) Run() error { tokenUrl := jenkins.JenkinsTokenURL(server.URL) if o.Verbose { - log.Infof("using url %s\n", tokenUrl) + log.Infof("Using url %s\n", tokenUrl) } if userAuth.IsInvalid() && o.Password != "" && o.UseBrowser { - err := o.tryFindAPITokenFromBrowser(tokenUrl, userAuth) + newTokenUrl := jenkins.JenkinsNewTokenURL(server.URL) + err = o.tryFindAPITokenFromBrowser(tokenUrl, newTokenUrl, userAuth) if err != nil { - log.Warnf("unable to automatically find API token with chromedp using URL %s\n", tokenUrl) + log.Warnf("Unable to automatically find API token with chromedp using URL %s\n", tokenUrl) + log.Warnf("Error: %v\n", err) } } @@ -187,36 +199,43 @@ func (o *CreateJenkinsUserOptions) Run() error { } // lets try use the users browser to find the API token -func (o *CreateJenkinsUserOptions) tryFindAPITokenFromBrowser(tokenUrl string, userAuth *auth.UserAuth) error { - var ctxt context.Context +func (o *CreateJenkinsUserOptions) tryFindAPITokenFromBrowser(tokenUrl string, newTokenUrl string, userAuth *auth.UserAuth) error { + var ctx context.Context var cancel context.CancelFunc if o.Timeout != "" { duration, err := time.ParseDuration(o.Timeout) if err != nil { - return err + return errors.Wrap(err, "parsing the timeout value") } - ctxt, cancel = context.WithTimeout(context.Background(), duration) + ctx, cancel = context.WithTimeout(context.Background(), duration) } else { - ctxt, cancel = context.WithCancel(context.Background()) + ctx, cancel = context.WithCancel(context.Background()) } defer cancel() - c, err := o.createChromeClient(ctxt) + userDataDir, err := ioutil.TempDir("/tmp", "jx-login-chrome-userdata-dir") if err != nil { - return err + return errors.Wrap(err, "creating the chrome user data dir") + } + defer os.RemoveAll(userDataDir) + netLogFile := filepath.Join(userDataDir, "net-logs.json") + + c, err := o.createChromeClientWithNetLog(ctx, userDataDir, netLogFile) + if err != nil { + return errors.Wrap(err, "creating the chrome client") } - err = c.Run(ctxt, chromedp.Tasks{ + err = c.Run(ctx, chromedp.Tasks{ chromedp.Navigate(tokenUrl), }) if err != nil { - return err + return errors.Wrapf(err, "navigating to token URL '%s'", tokenUrl) } nodeSlice := []*cdp.Node{} - err = c.Run(ctxt, chromedp.Nodes("//input", &nodeSlice)) + err = c.Run(ctx, chromedp.Nodes("//input", &nodeSlice)) if err != nil { - return err + return errors.Wrap(err, "serching the login form") } login := false @@ -230,57 +249,115 @@ func (o *CreateJenkinsUserOptions) tryFindAPITokenFromBrowser(tokenUrl string, u } if login { - // disable screenshots to try and reduce errors when running headless - //o.captureScreenshot(ctxt, c, "screenshot-jenkins-login.png", "main-panel", chromedp.ByID) - - log.Infoln("logging in") - err = c.Run(ctxt, chromedp.Tasks{ + log.Infoln("Logging in...") + err = c.Run(ctx, chromedp.Tasks{ chromedp.WaitVisible(userNameInputName, chromedp.ByID), chromedp.SendKeys(userNameInputName, userAuth.Username, chromedp.ByID), chromedp.SendKeys(passwordInputSelector, o.Password+"\n"), }) if err != nil { - return err + return errors.Wrap(err, "filling up the login form") } } - // disable screenshots to try and reduce errors when running headless - //o.captureScreenshot(ctxt, c, "screenshot-jenkins-api-token.png", "main-panel", chromedp.ByID) - - getAPITokenButtonSelector := "//button[normalize-space(text())='Show API Token...']" - nodeSlice = []*cdp.Node{} - - log.Infoln("Getting the API Token...") - err = c.Run(ctxt, chromedp.Tasks{ - chromedp.Sleep(2 * time.Second), - chromedp.WaitVisible(getAPITokenButtonSelector), - chromedp.Click(getAPITokenButtonSelector), - //chromedp.WaitVisible("apiToken", chromedp.ByID), - chromedp.Nodes("apiToken", &nodeSlice, chromedp.ByID), - }) + t, err := tail.TailFile(netLogFile, tail.Config{ + Follow: true, + Logger: golog.New(ioutil.Discard, "", golog.LstdFlags)}) if err != nil { - return err + return errors.Wrap(err, "reading the netlog file") } - token := "" - for _, node := range nodeSlice { - text := node.AttributeValue("value") - if text != "" && token == "" { - token = text + + cookie := "" + for line := range t.Lines { + if strings.Contains(line.Text, JenkinsCookieName) && + strings.Contains(line.Text, "GET /me/configure") { + cookie = o.extractJenkinsCookie(line.Text) break } } - log.Infoln("Found API Token") + + if cookie == "" { + return errors.New("No Jenkins cookie found") + } + + token, err := o.generateNewApiToken(newTokenUrl, cookie) + if err != nil { + return errors.Wrap(err, "generating the API token") + } + if token != "" { userAuth.ApiToken = token } - err = c.Shutdown(ctxt) + err = c.Shutdown(ctx) if err != nil { - return err + return errors.Wrap(err, "shutting down the chrome client") } + return nil } +func (o *CommonOptions) generateNewApiToken(newTokenUrl string, cookie string) (string, error) { + client := http.Client{} + req, err := http.NewRequest(http.MethodPost, newTokenUrl, nil) + if err != nil { + return "", errors.Wrap(err, "building request to generate the API token") + } + parts := strings.Split(cookie, "=") + if len(parts) != 2 { + return "", errors.Wrap(err, "building jenkins cookie") + } + jenkinsCookie := http.Cookie{Name: parts[0], Value: parts[1]} + req.AddCookie(&jenkinsCookie) + resp, err := client.Do(req) + if err != nil { + return "", errors.Wrap(err, "execute generate API token request") + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrap(err, "reading API token from response body") + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("generate API token status code: %d, error: %s", resp.StatusCode, string(body)) + } + + type TokenData struct { + TokenName string `json:"tokenName"` + TokenUuid string `json:"tokenUuid"` + TokenValue string `json:"tokenValue"` + } + + type TokenResponse struct { + Status string `json:"status"` + Data TokenData `json:"data"` + } + tokenResponse := &TokenResponse{} + if err := json.Unmarshal(body, tokenResponse); err != nil { + return "", errors.Wrap(err, "parsing the API token from response") + } + return tokenResponse.Data.TokenValue, nil +} + +func (o *CommonOptions) extractJenkinsCookie(text string) string { + start := strings.Index(text, JenkinsCookieName) + if start < 0 { + return "" + } + end := -1 + for i, ch := range text[start:] { + if ch == '"' { + end = start + i + break + } + } + if end < 0 { + return "" + } + return text[start:end] +} + // lets try use the users browser to find the API token func (o *CommonOptions) createChromeClient(ctxt context.Context) (*chromedp.CDP, error) { if o.Headless { @@ -296,6 +373,28 @@ func (o *CommonOptions) createChromeClient(ctxt context.Context) (*chromedp.CDP, return chromedp.New(ctxt) } +func (o *CommonOptions) createChromeClientWithNetLog(ctx context.Context, userDataDir string, netLogFile string) (*chromedp.CDP, error) { + options := func(m map[string]interface{}) error { + if o.Headless { + m["remote-debugging-port"] = 9222 + m["no-sandbox"] = true + m["headless"] = true + } + m["user-data-dir"] = userDataDir + m["log-net-log"] = netLogFile + m["net-log-capture-mode"] = "IncludeCookiesAndCredentials" + m["v"] = 1 + return nil + } + + logger := func(string, ...interface{}) { + return + } + return chromedp.New(ctx, + chromedp.WithRunnerOptions(runner.CommandLineOption(options)), + chromedp.WithLog(logger)) +} + func (o *CommonOptions) captureScreenshot(ctxt context.Context, c *chromedp.CDP, screenshotFile string, selector interface{}, options ...chromedp.QueryOption) error { log.Infoln("Creating a screenshot...") diff --git a/pkg/jx/cmd/install.go b/pkg/jx/cmd/install.go index 128d69069a..10a8d3a44d 100644 --- a/pkg/jx/cmd/install.go +++ b/pkg/jx/cmd/install.go @@ -875,7 +875,7 @@ func (options *InstallOptions) logAdminPassword() { ******************************************************** ` - log.Infof(astrix, fmt.Sprintf("Your admin password is: %s", util.ColorInfo(options.AdminSecretsService.Flags.DefaultAdminPassword))) + log.Infof(astrix+"\n", fmt.Sprintf("Your admin password is: %s", util.ColorInfo(options.AdminSecretsService.Flags.DefaultAdminPassword))) } // LoadVersionFromCloudEnvironmentsDir loads a version from the cloud environments directory