package github import ( "fmt" "net/http" "net/url" "strconv" "strings" "time" "github.com/drone/drone/plugin/remote/github/oauth" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/model" "github.com/drone/go-github/github" ) const ( DefaultAPI = "https://api.github.com/" DefaultURL = "https://github.com" DefaultScope = "repo,repo:status,user:email" ) type GitHub struct { URL string API string Client string Secret string Private bool } func New(url, api, client, secret string, private bool) *GitHub { var github = GitHub{ URL: url, API: api, Client: client, Secret: secret, Private: private, } // the API must have a trailing slash if !strings.HasSuffix(github.API, "/") { github.API += "/" } // the URL must NOT have a trailing slash if strings.HasSuffix(github.URL, "/") { github.URL = github.URL[:len(github.URL)-1] } return &github } func NewDefault(client, secret string) *GitHub { return New(DefaultURL, DefaultAPI, client, secret, false) } // Authorize handles GitHub API Authorization. func (r *GitHub) Authorize(res http.ResponseWriter, req *http.Request) (*model.Login, error) { var config = &oauth.Config{ ClientId: r.Client, ClientSecret: r.Secret, Scope: DefaultScope, AuthURL: fmt.Sprintf("%s/login/oauth/authorize", r.URL), TokenURL: fmt.Sprintf("%s/login/oauth/access_token", r.URL), RedirectURL: fmt.Sprintf("%s/api/auth/%s", httputil.GetURL(req), r.GetKind()), } // get the OAuth code var code = req.FormValue("code") var state = req.FormValue("state") if len(code) == 0 { var random = GetRandom() httputil.SetCookie(res, req, "github_state", random) http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther) return nil, nil } cookieState := httputil.GetCookie(req, "github_state") httputil.DelCookie(res, req, "github_state") if cookieState != state { return nil, fmt.Errorf("Error matching state in OAuth2 redirect") } var trans = &oauth.Transport{Config: config} var token, err = trans.Exchange(code) if err != nil { return nil, fmt.Errorf("Error exchanging token. %s", err) } var client = NewClient(r.API, token.AccessToken) var useremail, errr = GetUserEmail(client) if errr != nil { return nil, fmt.Errorf("Error retrieving user or verified email. %s", errr) } var login = new(model.Login) login.ID = int64(*useremail.ID) login.Access = token.AccessToken login.Login = *useremail.Login login.Email = *useremail.Email if useremail.Name != nil { login.Name = *useremail.Name } return login, nil } // GetKind returns the internal identifier of this remote GitHub instane. func (r *GitHub) GetKind() string { if r.IsEnterprise() { return model.RemoteGithubEnterprise } else { return model.RemoteGithub } } // GetHost returns the hostname of this remote GitHub instance. func (r *GitHub) GetHost() string { uri, _ := url.Parse(r.URL) return uri.Host } // IsEnterprise returns true if the remote system is an // instance of GitHub Enterprise Edition. func (r *GitHub) IsEnterprise() bool { return r.URL != DefaultURL } // GetRepos fetches all repositories that the specified // user has access to in the remote system. func (r *GitHub) GetRepos(user *model.User) ([]*model.Repo, error) { var repos []*model.Repo var client = NewClient(r.API, user.Access) var list, err = GetAllRepos(client) if err != nil { return nil, err } var remote = r.GetKind() var hostname = r.GetHost() for _, item := range list { var repo = model.Repo{ UserID: user.ID, Remote: remote, Host: hostname, Owner: *item.Owner.Login, Name: *item.Name, Private: *item.Private, URL: *item.HTMLURL, CloneURL: *item.GitURL, GitURL: *item.GitURL, SSHURL: *item.SSHURL, Role: &model.Perm{}, } if r.Private || repo.Private { repo.CloneURL = *item.SSHURL } // if no permissions we should skip the repository // entirely, since this should never happen if item.Permissions == nil { continue } repo.Role.Admin = (*item.Permissions)["admin"] repo.Role.Write = (*item.Permissions)["push"] repo.Role.Read = (*item.Permissions)["pull"] 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 *GitHub) GetScript(user *model.User, repo *model.Repo, hook *model.Hook) ([]byte, error) { var client = NewClient(r.API, user.Access) return GetFile(client, repo.Owner, repo.Name, ".drone.yml", hook.Sha) } // Activate activates a repository by adding a Post-commit hook and // a Public Deploy key, if applicable. func (r *GitHub) Activate(user *model.User, repo *model.Repo, link string) error { var client = NewClient(r.API, user.Access) var title, err = GetKeyTitle(link) if err != nil { return err } // if the CloneURL is using the SSHURL then we know that // we need to add an SSH key to GitHub. if repo.SSHURL == repo.CloneURL { _, err = CreateUpdateKey(client, repo.Owner, repo.Name, title, repo.PublicKey) if err != nil { return err } } _, err = CreateUpdateHook(client, repo.Owner, repo.Name, link) return err } // ParseHook parses the post-commit hook from the Request body // and returns the required data in a standard format. func (r *GitHub) ParseHook(req *http.Request) (*model.Hook, error) { // handle github ping if req.Header.Get("X-Github-Event") == "ping" { return nil, nil } // handle github pull request hook differently if req.Header.Get("X-Github-Event") == "pull_request" { return r.ParsePullRequestHook(req) } // parse the github Hook payload var payload = GetPayload(req) var data, err = github.ParseHook(payload) if err != nil { return nil, nil } // make sure this is being triggered because of a commit // and not something like a tag deletion or whatever if data.IsTag() || data.IsGithubPages() || data.IsHead() == false || data.IsDeleted() { return nil, nil } var hook = new(model.Hook) hook.Repo = data.Repo.Name hook.Owner = data.Repo.Owner.Login hook.Sha = data.Head.Id hook.Branch = data.Branch() if len(hook.Owner) == 0 { hook.Owner = data.Repo.Owner.Name } // extract the author and message from the commit // this is kind of experimental, since I don't know // what I'm doing here. if data.Head != nil && data.Head.Author != nil { hook.Message = data.Head.Message hook.Timestamp = data.Head.Timestamp hook.Author = data.Head.Author.Email } else if data.Commits != nil && len(data.Commits) > 0 && data.Commits[0].Author != nil { hook.Message = data.Commits[0].Message hook.Timestamp = data.Commits[0].Timestamp hook.Author = data.Commits[0].Author.Email } return hook, nil } // ParsePullRequestHook parses the pull request hook from the Request body // and returns the required data in a standard format. func (r *GitHub) ParsePullRequestHook(req *http.Request) (*model.Hook, error) { // parse the payload to retrieve the pull-request // hook meta-data. var payload = GetPayload(req) var data, err = github.ParsePullRequestHook(payload) if err != nil { return nil, err } // ignore these if data.Action != "opened" && data.Action != "synchronize" { return nil, nil } // TODO we should also store the pull request branch (ie from x to y) // we can find it here: data.PullRequest.Head.Ref var hook = model.Hook{ Owner: data.Repo.Owner.Login, Repo: data.Repo.Name, Sha: data.PullRequest.Head.Sha, Branch: data.PullRequest.Base.Ref, Author: data.PullRequest.User.Login, Gravatar: data.PullRequest.User.GravatarId, Timestamp: time.Now().UTC().String(), Message: data.PullRequest.Title, PullRequest: strconv.Itoa(data.Number), } if len(hook.Owner) == 0 { hook.Owner = data.Repo.Owner.Name } return &hook, nil }