2014-06-04 21:25:38 +00:00
|
|
|
package bitbucket
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2014-06-09 22:47:35 +00:00
|
|
|
"net/url"
|
2014-10-21 17:09:14 +00:00
|
|
|
"regexp"
|
2014-06-04 21:25:38 +00:00
|
|
|
"time"
|
|
|
|
|
2014-06-12 22:28:05 +00:00
|
|
|
"github.com/drone/drone/shared/httputil"
|
2014-09-02 07:18:17 +00:00
|
|
|
"github.com/drone/drone/shared/model"
|
2014-06-04 21:25:38 +00:00
|
|
|
"github.com/drone/go-bitbucket/bitbucket"
|
|
|
|
"github.com/drone/go-bitbucket/oauth1"
|
|
|
|
)
|
|
|
|
|
2014-09-02 07:18:17 +00:00
|
|
|
const (
|
|
|
|
DefaultAPI = "https://api.bitbucket.org/1.0"
|
|
|
|
DefaultURL = "https://bitbucket.org"
|
|
|
|
)
|
2014-06-04 21:25:38 +00:00
|
|
|
|
2014-10-22 08:02:14 +00:00
|
|
|
// parses an email address from string format
|
|
|
|
// `John Doe <john.doe@example.com>`
|
|
|
|
var emailRegexp = regexp.MustCompile("<(.*)>")
|
2014-10-22 07:37:04 +00:00
|
|
|
|
2014-09-02 07:18:17 +00:00
|
|
|
type Bitbucket struct {
|
|
|
|
URL string
|
|
|
|
API string
|
|
|
|
Client string
|
|
|
|
Secret string
|
2015-01-12 22:59:06 +00:00
|
|
|
Open bool
|
2014-06-04 21:25:38 +00:00
|
|
|
}
|
|
|
|
|
2015-01-12 22:59:06 +00:00
|
|
|
func New(url, api, client, secret string, open bool) *Bitbucket {
|
2014-09-02 07:18:17 +00:00
|
|
|
return &Bitbucket{
|
|
|
|
URL: url,
|
|
|
|
API: api,
|
|
|
|
Client: client,
|
|
|
|
Secret: secret,
|
2015-01-12 22:59:06 +00:00
|
|
|
Open: open,
|
2014-06-09 22:47:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-12 22:59:06 +00:00
|
|
|
func NewDefault(client, secret string, open bool) *Bitbucket {
|
|
|
|
return New(DefaultURL, DefaultAPI, client, secret, open)
|
2014-06-04 21:25:38 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 07:18:17 +00:00
|
|
|
// Authorize handles Bitbucket API Authorization
|
|
|
|
func (r *Bitbucket) Authorize(res http.ResponseWriter, req *http.Request) (*model.Login, error) {
|
2014-06-04 21:25:38 +00:00
|
|
|
consumer := oauth1.Consumer{
|
|
|
|
RequestTokenURL: "https://bitbucket.org/api/1.0/oauth/request_token/",
|
|
|
|
AuthorizationURL: "https://bitbucket.org/!api/1.0/oauth/authenticate",
|
|
|
|
AccessTokenURL: "https://bitbucket.org/api/1.0/oauth/access_token/",
|
2014-09-30 07:43:50 +00:00
|
|
|
CallbackURL: httputil.GetScheme(req) + "://" + httputil.GetHost(req) + "/api/auth/bitbucket.org",
|
2014-09-02 07:18:17 +00:00
|
|
|
ConsumerKey: r.Client,
|
|
|
|
ConsumerSecret: r.Secret,
|
2014-06-04 21:25:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// get the oauth verifier
|
2014-09-02 07:18:17 +00:00
|
|
|
verifier := req.FormValue("oauth_verifier")
|
2014-06-04 21:25:38 +00:00
|
|
|
if len(verifier) == 0 {
|
|
|
|
// Generate a Request Token
|
|
|
|
requestToken, err := consumer.RequestToken()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// add the request token as a signed cookie
|
2014-09-02 07:18:17 +00:00
|
|
|
httputil.SetCookie(res, req, "bitbucket_token", requestToken.Encode())
|
2014-06-04 21:25:38 +00:00
|
|
|
|
|
|
|
url, _ := consumer.AuthorizeRedirect(requestToken)
|
2014-09-02 07:18:17 +00:00
|
|
|
http.Redirect(res, req, url, http.StatusSeeOther)
|
2014-06-04 21:25:38 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove bitbucket token data once before redirecting
|
|
|
|
// back to the application.
|
2014-09-02 07:18:17 +00:00
|
|
|
defer httputil.DelCookie(res, req, "bitbucket_token")
|
2014-06-04 21:25:38 +00:00
|
|
|
|
|
|
|
// get the tokens from the request
|
2014-09-02 07:18:17 +00:00
|
|
|
requestTokenStr := httputil.GetCookie(req, "bitbucket_token")
|
2014-06-04 21:25:38 +00:00
|
|
|
requestToken, err := oauth1.ParseRequestTokenStr(requestTokenStr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// exchange for an access token
|
|
|
|
accessToken, err := consumer.AuthorizeToken(requestToken, verifier)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// create the Bitbucket client
|
|
|
|
client := bitbucket.New(
|
2014-09-02 07:18:17 +00:00
|
|
|
r.Client,
|
|
|
|
r.Secret,
|
2014-06-04 21:25:38 +00:00
|
|
|
accessToken.Token(),
|
|
|
|
accessToken.Secret(),
|
|
|
|
)
|
|
|
|
|
|
|
|
// get the currently authenticated Bitbucket User
|
|
|
|
user, err := client.Users.Current()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// put the user data in the common format
|
2014-09-02 07:18:17 +00:00
|
|
|
login := model.Login{
|
2014-06-04 21:25:38 +00:00
|
|
|
Login: user.User.Username,
|
|
|
|
Access: accessToken.Token(),
|
|
|
|
Secret: accessToken.Secret(),
|
|
|
|
Name: user.User.DisplayName,
|
|
|
|
}
|
|
|
|
|
2014-07-10 05:23:49 +00:00
|
|
|
email, _ := client.Emails.FindPrimary(user.User.Username)
|
|
|
|
if email != nil {
|
|
|
|
login.Email = email.Email
|
|
|
|
}
|
|
|
|
|
2014-06-04 21:25:38 +00:00
|
|
|
return &login, nil
|
|
|
|
}
|
|
|
|
|
2014-09-02 07:18:17 +00:00
|
|
|
// GetKind returns the internal identifier of this remote Bitbucket instane.
|
|
|
|
func (r *Bitbucket) GetKind() string {
|
|
|
|
return model.RemoteBitbucket
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetHost returns the hostname of this remote Bitbucket instance.
|
|
|
|
func (r *Bitbucket) GetHost() string {
|
|
|
|
uri, _ := url.Parse(r.URL)
|
|
|
|
return uri.Host
|
2014-06-04 21:25:38 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 07:18:17 +00:00
|
|
|
// GetRepos fetches all repositories that the specified
|
|
|
|
// user has access to in the remote system.
|
|
|
|
func (r *Bitbucket) GetRepos(user *model.User) ([]*model.Repo, error) {
|
|
|
|
var repos []*model.Repo
|
|
|
|
var client = bitbucket.New(
|
|
|
|
r.Client,
|
|
|
|
r.Secret,
|
|
|
|
user.Access,
|
|
|
|
user.Secret,
|
|
|
|
)
|
|
|
|
var list, err = client.Repos.List()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var remote = r.GetKind()
|
|
|
|
var hostname = r.GetHost()
|
|
|
|
|
|
|
|
for _, item := range list {
|
|
|
|
// for now we only support git repos
|
|
|
|
if item.Scm != "git" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// these are the urls required to clone the repository
|
|
|
|
// TODO use the bitbucketurl.Host and bitbucketurl.Scheme instead of hardcoding
|
|
|
|
// so that we can support Stash.
|
2015-01-16 16:36:56 +00:00
|
|
|
var html = fmt.Sprintf("https://bitbucket.org/%s/%s", item.Owner, item.Slug)
|
|
|
|
var clone = fmt.Sprintf("https://bitbucket.org/%s/%s.git", item.Owner, item.Slug)
|
|
|
|
var ssh = fmt.Sprintf("git@bitbucket.org:%s/%s.git", item.Owner, item.Slug)
|
2014-09-02 07:18:17 +00:00
|
|
|
|
|
|
|
var repo = model.Repo{
|
|
|
|
UserID: user.ID,
|
|
|
|
Remote: remote,
|
|
|
|
Host: hostname,
|
|
|
|
Owner: item.Owner,
|
2015-01-16 16:36:56 +00:00
|
|
|
Name: item.Slug,
|
2014-09-02 07:18:17 +00:00
|
|
|
Private: item.Private,
|
2015-01-11 01:13:12 +00:00
|
|
|
URL: html,
|
2014-09-02 07:18:17 +00:00
|
|
|
CloneURL: clone,
|
|
|
|
GitURL: clone,
|
|
|
|
SSHURL: ssh,
|
|
|
|
Role: &model.Perm{
|
|
|
|
Admin: true,
|
|
|
|
Write: true,
|
|
|
|
Read: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if repo.Private {
|
|
|
|
repo.CloneURL = repo.SSHURL
|
|
|
|
}
|
|
|
|
|
|
|
|
repos = append(repos, &repo)
|
|
|
|
}
|
|
|
|
|
|
|
|
return repos, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetScript fetches the build script (.drone.yml) from the remote
|
|
|
|
// repository and returns in string format.
|
|
|
|
func (r *Bitbucket) GetScript(user *model.User, repo *model.Repo, hook *model.Hook) ([]byte, error) {
|
|
|
|
var client = bitbucket.New(
|
|
|
|
r.Client,
|
|
|
|
r.Secret,
|
|
|
|
user.Access,
|
|
|
|
user.Secret,
|
|
|
|
)
|
|
|
|
|
|
|
|
// get the yaml from the database
|
|
|
|
var raw, err = client.Sources.Find(repo.Owner, repo.Name, hook.Sha, ".drone.yml")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return []byte(raw.Data), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Activate activates a repository by adding a Post-commit hook and
|
|
|
|
// a Public Deploy key, if applicable.
|
|
|
|
func (r *Bitbucket) Activate(user *model.User, repo *model.Repo, link string) error {
|
|
|
|
var client = bitbucket.New(
|
|
|
|
r.Client,
|
|
|
|
r.Secret,
|
|
|
|
user.Access,
|
|
|
|
user.Secret,
|
|
|
|
)
|
|
|
|
|
|
|
|
// parse the hostname from the hook, and use this
|
|
|
|
// to name the ssh key
|
|
|
|
var hookurl, err = url.Parse(link)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the repository is private we'll need
|
|
|
|
// to upload a github key to the repository
|
|
|
|
if repo.Private {
|
|
|
|
// name the key
|
|
|
|
var keyname = "drone@" + hookurl.Host
|
|
|
|
var _, err = client.RepoKeys.CreateUpdate(repo.Owner, repo.Name, repo.PublicKey, keyname)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add the hook
|
|
|
|
_, err = client.Brokers.CreateUpdate(repo.Owner, repo.Name, link, bitbucket.BrokerTypePost)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-02-04 13:42:24 +00:00
|
|
|
// Deactivate removes a repository by removing all the post-commit hooks
|
|
|
|
// which are equal to link and removing the SSH deploy key.
|
|
|
|
func (r *Bitbucket) Deactivate(user *model.User, repo *model.Repo, link string) error {
|
2015-02-06 10:36:31 +00:00
|
|
|
var client = bitbucket.New(
|
|
|
|
r.Client,
|
|
|
|
r.Secret,
|
|
|
|
user.Access,
|
|
|
|
user.Secret,
|
|
|
|
)
|
2015-02-06 11:02:02 +00:00
|
|
|
title, err := GetKeyTitle(link)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := client.RepoKeys.DeleteName(repo.Owner, repo.Name, title); err != nil {
|
|
|
|
return err
|
2015-02-06 10:36:31 +00:00
|
|
|
}
|
|
|
|
return client.Brokers.DeleteUrl(repo.Owner, repo.Name, link, bitbucket.BrokerTypePost)
|
2015-02-04 13:42:24 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 07:18:17 +00:00
|
|
|
// ParseHook parses the post-commit hook from the Request body
|
|
|
|
// and returns the required data in a standard format.
|
|
|
|
func (r *Bitbucket) ParseHook(req *http.Request) (*model.Hook, error) {
|
|
|
|
var payload = req.FormValue("payload")
|
|
|
|
var hook, err = bitbucket.ParseHook([]byte(payload))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// verify the payload has the minimum amount of required data.
|
|
|
|
if hook.Repo == nil || hook.Commits == nil || len(hook.Commits) == 0 {
|
|
|
|
return nil, fmt.Errorf("Invalid Bitbucket post-commit Hook. Missing Repo or Commit data.")
|
|
|
|
}
|
|
|
|
|
2014-10-22 08:02:14 +00:00
|
|
|
var author = hook.Commits[len(hook.Commits)-1].RawAuthor
|
|
|
|
var matches = emailRegexp.FindStringSubmatch(author)
|
|
|
|
if len(matches) == 2 {
|
|
|
|
author = matches[1]
|
2014-10-22 07:37:04 +00:00
|
|
|
}
|
2014-10-21 17:09:14 +00:00
|
|
|
|
2014-09-02 07:18:17 +00:00
|
|
|
return &model.Hook{
|
|
|
|
Owner: hook.Repo.Owner,
|
2015-01-16 16:36:56 +00:00
|
|
|
Repo: hook.Repo.Slug,
|
2014-09-02 07:18:17 +00:00
|
|
|
Sha: hook.Commits[len(hook.Commits)-1].Hash,
|
|
|
|
Branch: hook.Commits[len(hook.Commits)-1].Branch,
|
2014-10-22 08:02:14 +00:00
|
|
|
Author: author,
|
2014-09-02 07:18:17 +00:00
|
|
|
Timestamp: time.Now().UTC().String(),
|
|
|
|
Message: hook.Commits[len(hook.Commits)-1].Message,
|
|
|
|
}, nil
|
2014-06-04 21:25:38 +00:00
|
|
|
}
|
2015-01-12 22:59:06 +00:00
|
|
|
|
|
|
|
func (r *Bitbucket) OpenRegistration() bool {
|
|
|
|
return r.Open
|
|
|
|
}
|
2015-01-26 23:32:42 +00:00
|
|
|
|
|
|
|
func (r *Bitbucket) GetToken(user *model.User) (*model.Token, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2015-02-06 11:02:02 +00:00
|
|
|
|
|
|
|
// GetKeyTitle is a helper function that generates a title for the
|
|
|
|
// RSA public key based on the username and domain name.
|
|
|
|
func GetKeyTitle(rawurl string) (string, error) {
|
|
|
|
var uri, err = url.Parse(rawurl)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("drone@%s", uri.Host), nil
|
|
|
|
}
|