diff --git a/bots-common/consts.go b/bots-common/consts.go index 3c77afd..ff7b2b3 100644 --- a/bots-common/consts.go +++ b/bots-common/consts.go @@ -19,9 +19,11 @@ package common */ const ( - GiteaTokenEnv = "GITEA_TOKEN" - ObsUserEnv = "OBS_USER" - ObsPasswordEnv = "OBS_PASSWORD" + GiteaTokenEnv = "GITEA_TOKEN" + ObsUserEnv = "OBS_USER" + ObsPasswordEnv = "OBS_PASSWORD" + ObsSshkeyEnv = "OBS_SSHKEY" + ObsSshkeyFileEnv = "OBS_SSHKEYFILE" DefaultGitPrj = "_ObsPrj" PrjLinksFile = "links.json" diff --git a/bots-common/obs_utils.go b/bots-common/obs_utils.go index b54dc26..9b59eb9 100644 --- a/bots-common/obs_utils.go +++ b/bots-common/obs_utils.go @@ -20,13 +20,20 @@ package common import ( "bytes" + "encoding/base64" "encoding/xml" + "errors" "fmt" "io" "log" + "os/exec" + "regexp" "net/http" "net/url" "slices" + "strings" + "syscall" + "time" ) type ObsClient struct { @@ -34,6 +41,8 @@ type ObsClient struct { client *http.Client user, password string cookie string + sshkey string + sshkeyfile string HomeProject string } @@ -45,10 +54,12 @@ func NewObsClient(host string) (*ObsClient, error) { } return &ObsClient{ - baseUrl: baseUrl, - client: &http.Client{}, - user: obsUser, - password: obsPassword, + baseUrl: baseUrl, + client: &http.Client{}, + user: obsUser, + password: obsPassword, + sshkey: obsSshkey, + sshkeyfile: obsSshkeyFile, HomeProject: fmt.Sprintf("home:%s", obsUser), }, nil @@ -139,13 +150,7 @@ func parseProjectMeta(data []byte) (*ProjectMeta, error) { } func (c *ObsClient) GetGroupMeta(gid string) (*GroupMeta, error) { - req, err := http.NewRequest("GET", c.baseUrl.JoinPath("group", gid).String(), nil) - if err != nil { - return nil, err - } - req.SetBasicAuth(c.user, c.password) - res, err := c.client.Do(req) - + res, err := c.ObsRequest("GET", c.baseUrl.JoinPath("group", gid).String(), nil) if err != nil { return nil, err } @@ -174,13 +179,7 @@ func (c *ObsClient) GetGroupMeta(gid string) (*GroupMeta, error) { } func (c *ObsClient) GetUserMeta(uid string) (*UserMeta, error) { - req, err := http.NewRequest("GET", c.baseUrl.JoinPath("person", uid).String(), nil) - if err != nil { - return nil, err - } - req.SetBasicAuth(c.user, c.password) - res, err := c.client.Do(req) - + res, err := c.ObsRequest("GET", c.baseUrl.JoinPath("person", uid).String(), nil) if err != nil { return nil, err } @@ -208,13 +207,100 @@ func (c *ObsClient) GetUserMeta(uid string) (*UserMeta, error) { return &meta, nil } -func (c *ObsClient) GetProjectMeta(project string) (*ProjectMeta, error) { - req, err := http.NewRequest("GET", c.baseUrl.JoinPath("source", project, "_meta").String(), nil) +func (c *ObsClient) ObsRequest(method string, url string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, url, body) + if err != nil { return nil, err } - req.SetBasicAuth(c.user, c.password) + if body != nil { + req.Body = io.NopCloser(body) + } + if c.cookie != "" { + req.Header.Add("cookie", c.cookie) + } res, err := c.client.Do(req) + if err == nil && res.StatusCode == 200 { + auth_cookie := res.Header.Get("set-cookie") + if auth_cookie != "" { + c.cookie = auth_cookie + } + return res, nil + } + + if res.StatusCode == 401 { +// log.Printf("new authentication is needed ...") + if c.sshkey == "" { + req.SetBasicAuth(c.user, c.password) + } else { + www := res.Header.Get("www-authenticate") +// log.Printf("www-authenticate %s", www) + re := regexp.MustCompile(`Signature realm="(.*?)",headers="\(created\)`) + match := re.FindStringSubmatch(www) + if len(match) < 1 { + return nil, errors.New("No realm found") + } + realm := string(match[1]) + sshKeygenPath, err := exec.LookPath("ssh-keygen") + if err != nil { + fmt.Println("ssh-keygen not found") + } + + // SSH Sign + cmd := exec.Command(sshKeygenPath, "-Y", "sign", "-f", c.sshkeyfile, "-n", realm, "-q") + now := time.Now().Unix() + sigdata := fmt.Sprintf("(created): %d", now) + cmd.Stdin = strings.NewReader(sigdata) + stdout, err := cmd.Output() + if err != nil { + log.Panic("SSH sign error:%s", cmd.Stderr) + if exitError, ok := err.(*exec.ExitError); ok { + waitStatus := exitError.Sys().(syscall.WaitStatus) + exitCode := waitStatus.ExitStatus() + return nil, errors.New(fmt.Sprintf("ssh-keygen signature creation failed: %d", exitCode)) + } + return nil, errors.New("ssh-keygen signature creation failed") + } + reg := regexp.MustCompile("(?s)-----BEGIN SSH SIGNATURE-----\n(.*?)\n-----END SSH SIGNATURE-----") + match = reg.FindStringSubmatch(string(stdout)) + if len(match) < 2 { + return nil, errors.New("could not extract ssh signature") + } + + signature, err := base64.StdEncoding.DecodeString(string(match[1])) + if err != nil { + return nil, err + } + + signatureBase64 := base64.StdEncoding.EncodeToString(signature) + authorization := fmt.Sprintf(`keyId="%s",algorithm="ssh",headers="(created)",created=%d,signature="%s"`, + c.user, now, signatureBase64) + +// log.Printf("Add Authorization Signature ", authorization) + req.Header.Add("Authorization", "Signature " + authorization) + } + } + + // Another time with authentification header +// log.Printf("Trying again with authorization: %s", req.Header.Get("Authorization")) + + res, err = c.client.Do(req) + if err != nil { + log.Panic("Authentification failed: %d", res.StatusCode) + return nil, err + } + + // Store the cookie for next call + auth_cookie := res.Header.Get("set-cookie") + if auth_cookie != "" { + c.cookie = auth_cookie + } + + return res, err +} + +func (c *ObsClient) GetProjectMeta(project string) (*ProjectMeta, error) { + res, err := c.ObsRequest("GET", c.baseUrl.JoinPath("source", project, "_meta").String(), nil) if err != nil { return nil, err @@ -238,12 +324,7 @@ func (c *ObsClient) GetProjectMeta(project string) (*ProjectMeta, error) { } func (c *ObsClient) GetPackageMeta(project, pkg string) (*PackageMeta, error) { - req, err := http.NewRequest("GET", c.baseUrl.JoinPath("source", project, pkg, "_meta").String(), nil) - if err != nil { - return nil, err - } - req.SetBasicAuth(c.user, c.password) - res, err := c.client.Do(req) + res, err := c.ObsRequest("GET", c.baseUrl.JoinPath("source", project, pkg, "_meta").String(), nil) if err != nil { return nil, err @@ -311,19 +392,12 @@ func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error { } } - req, err := http.NewRequest("PUT", c.baseUrl.JoinPath("source", meta.Name, "_meta").String(), nil) - if err != nil { - return err - } - req.SetBasicAuth(c.user, c.password) xml, err := xml.Marshal(meta) if err != nil { return err } - req.Body = io.NopCloser(bytes.NewReader(xml)) - log.Printf("xml: %s", xml) - res, err := c.client.Do(req) + res, err := c.ObsRequest("PUT", c.baseUrl.JoinPath("source", meta.Name, "_meta").String(), io.NopCloser(bytes.NewReader(xml))) if err != nil { return err } @@ -339,14 +413,7 @@ func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error { } func (c *ObsClient) DeleteProject(project string) error { - req, err := http.NewRequest("DELETE", c.baseUrl.JoinPath("source", project).String(), nil) - if err != nil { - return err - } - - req.SetBasicAuth(c.user, c.password) - res, err := c.client.Do(req) - + res, err := c.ObsRequest("DELETE", c.baseUrl.JoinPath("source", project).String(), nil) if err != nil { return err } @@ -597,10 +664,7 @@ func (obs ObsProjectNotFound) Error() string { func (c *ObsClient) ProjectConfig(project string) (string, error) { u := c.baseUrl.JoinPath("source", project, "_config") - req, err := http.NewRequest("GET", u.String(), nil) - req.SetBasicAuth(c.user, c.password) - res, err := c.client.Do(req) - + res, err := c.ObsRequest("GET", u.String(), nil) if err != nil { return "", err } @@ -625,15 +689,7 @@ func (c *ObsClient) BuildStatus(project string, packages ...string) (*BuildResul } } u.RawQuery = query.Encode() - req, err := http.NewRequest("GET", u.String(), nil) - log.Print(u.String()) - if err != nil { - return nil, err - } - - req.SetBasicAuth(c.user, c.password) - res, err := c.client.Do(req) - + res, err := c.ObsRequest("GET", u.String(), nil) if err != nil { return nil, err } diff --git a/bots-common/tokens.go b/bots-common/tokens.go index 2df535f..1efa85c 100644 --- a/bots-common/tokens.go +++ b/bots-common/tokens.go @@ -30,7 +30,7 @@ const ( ) var giteaToken string -var obsUser, obsPassword string +var obsUser, obsPassword, obsSshkey, obsSshkeyFile string var rabbitUser, rabbitPassword string func RequireGiteaSecretToken() error { @@ -54,9 +54,14 @@ func GetGiteaToken() string { func RequireObsSecretToken() error { obsUser = os.Getenv(ObsUserEnv) obsPassword = os.Getenv(ObsPasswordEnv) + obsSshkey = os.Getenv(ObsSshkeyEnv) + obsSshkeyFile = os.Getenv(ObsSshkeyFileEnv) - if len(obsPassword) < 10 || len(obsUser) < 2 { - return fmt.Errorf("Missing OBS authentication: %s %s", ObsUserEnv, ObsPasswordEnv) + if len(obsUser) < 2 { + return fmt.Errorf("Missing OBS authentication user") + } + if len(obsPassword) < 10 && len(obsSshkeyFile) < 2 { + return fmt.Errorf("Missing OBS authentication, either password or sshkey file need to be set") } err := os.Setenv(ObsUserEnv, "")