diff --git a/datastore/bolt/repo.go b/datastore/bolt/repo.go
index 771750f5b..c35b2f478 100644
--- a/datastore/bolt/repo.go
+++ b/datastore/bolt/repo.go
@@ -83,3 +83,26 @@ func (db *DB) DeleteRepo(repo *common.Repo) error {
// TODO(bradrydzewski) delete all tasks
return t.Commit()
}
+
+// GetSubscriber gets the subscriber by login for the
+// named repository.
+func (db *DB) GetSubscriber(repo string, login string) (*common.Subscriber, error) {
+ sub := &common.Subscriber{}
+ key := []byte(login + "/" + repo)
+ err := get(db, bucketUserRepos, key, sub)
+ return sub, err
+}
+
+// InsertSubscriber inserts a subscriber for the named
+// repository.
+func (db *DB) InsertSubscriber(repo string, sub *common.Subscriber) error {
+ key := []byte(sub.Login + "/" + repo)
+ return insert(db, bucketUserRepos, key, sub)
+}
+
+// DeleteSubscriber removes the subscriber by login for the
+// named repository.
+func (db *DB) DeleteSubscriber(repo string, sub *common.Subscriber) error {
+ key := []byte(sub.Login + "/" + repo)
+ return delete(db, bucketUserRepos, key)
+}
diff --git a/datastore/datastore.go b/datastore/datastore.go
index 863c8ebf6..abe574c9d 100644
--- a/datastore/datastore.go
+++ b/datastore/datastore.go
@@ -42,6 +42,18 @@ type Datastore interface {
// DeleteUser deletes the token.
DeleteToken(*common.Token) error
+ // GetSubscriber gets the subscriber by login for the
+ // named repository.
+ GetSubscriber(string, string) (*common.Subscriber, error)
+
+ // InsertSubscriber inserts a subscriber for the named
+ // repository.
+ InsertSubscriber(string, *common.Subscriber) error
+
+ // DeleteSubscriber removes the subscriber by login for the
+ // named repository.
+ DeleteSubscriber(string, *common.Subscriber) error
+
// GetRepo gets the repository by name.
GetRepo(string) (*common.Repo, error)
@@ -125,7 +137,3 @@ type Datastore interface {
// named repository and build number.
UpsertTaskLogs(string, int, int, []byte) error
}
-
-// GetSubscriber(string, string) (*common.Subscriber, error)
-// InsertSubscriber(string, *common.Subscriber) error
-// DeleteSubscriber(string, string) error
diff --git a/server/badge.go b/server/badge.go
new file mode 100644
index 000000000..163a4740b
--- /dev/null
+++ b/server/badge.go
@@ -0,0 +1,75 @@
+package server
+
+import (
+ "github.com/gin-gonic/gin"
+
+ "github.com/drone/drone/common"
+ "github.com/drone/drone/common/ccmenu"
+ "github.com/drone/drone/common/httputil"
+)
+
+var (
+ badgeSuccess = []byte(`build build success success `)
+ badgeFailure = []byte(`build build failure failure `)
+ badgeStarted = []byte(`build build started started `)
+ badgeError = []byte(`build build error error `)
+ badgeNone = []byte(`build build none none `)
+)
+
+// GetBadge accepts a request to retrieve the named
+// repo and branhes latest build details from the datastore
+// and return an SVG badges representing the build results.
+//
+// GET /api/badge/:owner/:name/status.svg
+//
+func GetBadge(c *gin.Context) {
+ var repo = ToRepo(c)
+
+ // an SVG response is always served, even when error, so
+ // we can go ahead and set the content type appropriately.
+ c.Writer.Header().Set("Content-Type", "image/svg+xml")
+
+ // if no commit was found then display
+ // the 'none' badge, instead of throwing
+ // an error response
+ if repo.Last == nil {
+ c.Writer.Write(badgeNone)
+ return
+ }
+
+ switch repo.Last.State {
+ case common.StateSuccess:
+ c.Writer.Write(badgeSuccess)
+ case common.StateFailure:
+ c.Writer.Write(badgeFailure)
+ case common.StateError, common.StateKilled:
+ c.Writer.Write(badgeError)
+ case common.StatePending, common.StateRunning:
+ c.Writer.Write(badgeStarted)
+ default:
+ c.Writer.Write(badgeNone)
+ }
+}
+
+// GetCC accepts a request to retrieve the latest build
+// status for the given repository from the datastore and
+// in CCTray XML format.
+//
+// GET /api/badge/:host/:owner/:name/cc.xml
+//
+// TODO(bradrydzewski) this will not return in-progress builds, which it should
+func GetCC(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ last, err := ds.GetBuildLast(repo.FullName)
+ if err != nil {
+ c.Fail(404, err)
+ return
+ }
+
+ link := httputil.GetURL(c.Request) + "/" + repo.FullName
+ cc := ccmenu.NewCC(repo, last, link)
+
+ c.Writer.Header().Set("Content-Type", "application/xml")
+ c.XML(200, cc)
+}
diff --git a/server/builds.go b/server/builds.go
new file mode 100644
index 000000000..69f360037
--- /dev/null
+++ b/server/builds.go
@@ -0,0 +1,45 @@
+package server
+
+import (
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetBuild accepts a request to retrieve a build
+// from the datastore for the given repository and
+// build number.
+//
+// GET /api/builds/:owner/:name/:number
+//
+func GetBuild(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ num, err := strconv.Atoi(c.Params.ByName("number"))
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+ build, err := ds.GetBuild(repo.FullName, num)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, build)
+ }
+}
+
+// GetBuild accepts a request to retrieve a list
+// of builds from the datastore for the given repository.
+//
+// GET /api/builds/:owner/:name
+//
+func GetBuilds(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ builds, err := ds.GetBuildList(repo.FullName)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, builds)
+ }
+}
diff --git a/server/hooks.go b/server/hooks.go
new file mode 100644
index 000000000..6b27bad84
--- /dev/null
+++ b/server/hooks.go
@@ -0,0 +1,90 @@
+package server
+
+import (
+ "strings"
+
+ "github.com/drone/drone/common"
+ // "github.com/bradrydzewski/drone/worker"
+ "github.com/gin-gonic/gin"
+)
+
+// PostHook accepts a post-commit hook and parses the payload
+// in order to trigger a build.
+//
+// GET /api/hook
+//
+func PostHook(c *gin.Context) {
+ remote := ToRemote(c)
+ store := ToDatastore(c)
+
+ hook, err := remote.Hook(c.Request)
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+ if hook == nil {
+ c.Writer.WriteHeader(200)
+ return
+ }
+ if hook.Repo == nil {
+ c.Writer.WriteHeader(400)
+ return
+ }
+
+ // a build may be skipped if the text [CI SKIP]
+ // is found inside the commit message
+ if hook.Commit != nil && strings.Contains(hook.Commit.Message, "[CI SKIP]") {
+ c.Writer.WriteHeader(204)
+ return
+ }
+
+ repo, err := store.GetRepo(hook.Repo.FullName)
+ if err != nil {
+ c.Fail(404, err)
+ return
+ }
+
+ if repo.Disabled || repo.User == nil || (repo.DisablePR && hook.PullRequest != nil) {
+ c.Writer.WriteHeader(204)
+ return
+ }
+
+ user, err := store.GetUser(repo.User.Login)
+ if err != nil {
+ c.Fail(500, err)
+ return
+ }
+
+ build := &common.Build{}
+ build.State = common.StatePending
+ build.Commit = hook.Commit
+ build.PullRequest = hook.PullRequest
+
+ // featch the .drone.yml file from the database
+ _, err = remote.Script(user, repo, build)
+ if err != nil {
+ c.Fail(404, err)
+ return
+ }
+
+ err = store.InsertBuild(repo.FullName, build)
+ if err != nil {
+ c.Fail(500, err)
+ return
+ }
+
+ // w := worker.Work{
+ // User: user,
+ // Repo: repo,
+ // Build: build,
+ // }
+
+ // verify the branches can be built vs skipped
+ // s, _ := script.ParseBuild(string(yml))
+ // if len(hook.PullRequest) == 0 && !s.MatchBranch(hook.Branch) {
+ // w.WriteHeader(http.StatusOK)
+ // return
+ // }
+
+ c.JSON(200, build)
+}
diff --git a/server/login.go b/server/login.go
new file mode 100644
index 000000000..f572c9d75
--- /dev/null
+++ b/server/login.go
@@ -0,0 +1,181 @@
+package server
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+
+ "github.com/drone/drone/common"
+ "github.com/drone/drone/common/gravatar"
+ "github.com/drone/drone/common/httputil"
+ "github.com/drone/drone/common/oauth2"
+)
+
+// GetLogin accepts a request to authorize the user and to
+// return a valid OAuth2 access token. The access token is
+// returned as url segment #access_token
+//
+// GET /authorize
+//
+func GetLogin(c *gin.Context) {
+ settings := ToSettings(c)
+ session := ToSession(c)
+ store := ToDatastore(c)
+
+ // when dealing with redirects we may need
+ // to adjust the content type. I cannot, however,
+ // rememver why, so need to revisit this line.
+ c.Writer.Header().Del("Content-Type")
+
+ // depending on the configuration a user may
+ // authenticate with OAuth1, OAuth2 or Basic
+ // Auth (username and password). This will delegate
+ // authorization accordingly.
+ switch {
+ case settings.Service.OAuth == nil:
+ getLoginBasic(c)
+ case settings.Service.OAuth.RequestToken != "":
+ getLoginOauth1(c)
+ default:
+ getLoginOauth2(c)
+ }
+
+ // exit if authorization fails
+ // TODO(bradrydzewski) return an error message instead
+ if c.Writer.Status() != 200 {
+ return
+ }
+
+ // get the user from the database
+ login := ToUser(c)
+ u, err := store.GetUser(login.Login)
+ if err != nil {
+ // if self-registration is disabled we should
+ // return a notAuthorized error. the only exception
+ // is if no users exist yet in the system we'll proceed.
+ if !settings.Service.Open {
+ count, err := store.GetUserCount()
+ if err != nil || count != 0 {
+ c.String(400, "Unable to create account. Registration is closed")
+ return
+ }
+ }
+
+ // create the user account
+ u = &common.User{}
+ u.Login = login.Login
+ u.Token = login.Token
+ u.Secret = login.Secret
+ u.Name = login.Name
+ u.Email = login.Email
+ u.Gravatar = gravatar.Generate(u.Email)
+
+ // insert the user into the database
+ if err := store.InsertUser(u); err != nil {
+ log.Println(err)
+ c.Fail(400, err)
+ return
+ }
+
+ // // if this is the first user, they
+ // // should be an admin.
+ //if u.ID == 1 {
+ if u.Login == "bradrydzewski" {
+ u.Admin = true
+ }
+ }
+
+ // update the user meta data and authorization
+ // data and cache in the datastore.
+ u.Token = login.Token
+ u.Secret = login.Secret
+ u.Name = login.Name
+ u.Email = login.Email
+ u.Gravatar = gravatar.Generate(u.Email)
+
+ if err := store.UpdateUser(u); err != nil {
+ log.Println(err)
+ c.Fail(400, err)
+ return
+ }
+
+ token, err := session.GenerateToken(c.Request, u)
+ if err != nil {
+ log.Println(err)
+ c.Fail(400, err)
+ return
+ }
+ c.Redirect(303, "/#access_token="+token)
+}
+
+// getLoginOauth2 is the default authorization implementation
+// using the oauth2 protocol.
+func getLoginOauth2(c *gin.Context) {
+ var settings = ToSettings(c)
+ var remote = ToRemote(c)
+
+ var config = &oauth2.Config{
+ ClientId: settings.Service.OAuth.Client,
+ ClientSecret: settings.Service.OAuth.Secret,
+ Scope: strings.Join(settings.Service.OAuth.Scope, ","),
+ AuthURL: settings.Service.OAuth.Authorize,
+ TokenURL: settings.Service.OAuth.AccessToken,
+ RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(c.Request)),
+ //settings.Server.Scheme, settings.Server.Hostname),
+ }
+
+ // get the OAuth code
+ var code = c.Request.FormValue("code")
+ //var state = c.Request.FormValue("state")
+ if len(code) == 0 {
+ c.Redirect(303, config.AuthCodeURL("random"))
+ return
+ }
+
+ // exhange for a token
+ var trans = &oauth2.Transport{Config: config}
+ var token, err = trans.Exchange(code)
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+
+ // get user account
+ user, err := remote.Login(token.AccessToken, token.RefreshToken)
+ if err != nil {
+ c.Fail(404, err)
+ return
+ }
+
+ // add the user to the request
+ c.Set("user", user)
+}
+
+// getLoginOauth1 is able to authorize a user with the oauth1
+// authentication protocol. This is used primarily with Bitbucket
+// and Stash only, and one day I hope can be removed.
+func getLoginOauth1(c *gin.Context) {
+
+}
+
+// getLoginBasic is able to authorize a user with a username and
+// password. This can be used for systems that do not support oauth.
+func getLoginBasic(c *gin.Context) {
+ var (
+ remote = ToRemote(c)
+ username = c.Request.FormValue("username")
+ password = c.Request.FormValue("username")
+ )
+
+ // get user account
+ user, err := remote.Login(username, password)
+ if err != nil {
+ c.Fail(404, err)
+ return
+ }
+
+ // add the user to the request
+ c.Set("user", user)
+}
diff --git a/server/logs.go b/server/logs.go
new file mode 100644
index 000000000..459182d70
--- /dev/null
+++ b/server/logs.go
@@ -0,0 +1,27 @@
+package server
+
+import (
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetLogs accepts a request to retrieve logs from the
+// datastore for the given repository, build and task
+// number.
+//
+// GET /api/logs/:owner/:name/:number/:task
+//
+func GetLogs(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ build, _ := strconv.Atoi(c.Params.ByName("number"))
+ task, _ := strconv.Atoi(c.Params.ByName("task"))
+
+ logs, err := ds.GetTaskLogs(repo.FullName, build, task)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.Writer.Write(logs)
+ }
+}
diff --git a/server/repos.go b/server/repos.go
new file mode 100644
index 000000000..e0f1f60f5
--- /dev/null
+++ b/server/repos.go
@@ -0,0 +1,242 @@
+package server
+
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+
+ "github.com/drone/drone/common"
+ "github.com/drone/drone/common/httputil"
+ "github.com/drone/drone/common/sshutil"
+ "github.com/drone/drone/remote"
+)
+
+// repoResp is a data structure used for sending
+// repository data to the client, augmented with
+// additional repository meta-data.
+type repoResp struct {
+ *common.Repo
+ Perms *common.Perm `json:"permissions,omitempty"`
+ Watch *common.Subscriber `json:"subscription,omitempty"`
+ Params map[string]string `json:"params,omitempty"`
+}
+
+// repoReq is a data structure used for receiving
+// repository data from the client to modify the
+// attributes of an existing repository.
+//
+// note that attributes are pointers so that we can
+// accept null values, effectively patching an existing
+// repository object with only the supplied fields.
+type repoReq struct {
+ Disabled *bool `json:"disabled"`
+ DisablePR *bool `json:"disable_prs"`
+ DisableTag *bool `json:"disable_tags"`
+ Trusted *bool `json:"privileged"`
+ Timeout *int64 `json:"timeout"`
+
+ // optional private parameters can only be
+ // supplied by the repository admin.
+ Params *map[string]string `json:"params"`
+}
+
+// GetRepo accepts a request to retrieve a commit
+// from the datastore for the given repository, branch and
+// commit hash.
+//
+// GET /api/repos/:owner/:name
+//
+func GetRepo(c *gin.Context) {
+ store := ToDatastore(c)
+ repo := ToRepo(c)
+ user := ToUser(c)
+ perm := ToPerm(c)
+ data := repoResp{repo, perm, nil, nil}
+ // if the user is an administrator of the project
+ // we should display the private parameter data.
+ if perm.Admin {
+ data.Params, _ = store.GetRepoParams(repo.FullName)
+ }
+ // if the user is authenticated, we should display
+ // if she is watching the current repository.
+ if user != nil {
+ data.Watch, _ = store.GetSubscriber(repo.FullName, user.Login)
+ }
+ c.JSON(200, data)
+}
+
+// PutRepo accapets a request to update the named repository
+// in the datastore. It expects a JSON input and returns the
+// updated repository in JSON format if successful.
+//
+// PUT /api/repos/:owner/:name
+//
+func PutRepo(c *gin.Context) {
+ store := ToDatastore(c)
+ perm := ToPerm(c)
+ u := ToUser(c)
+ r := ToRepo(c)
+
+ in := &repoReq{}
+ if !c.BindWith(in, binding.JSON) {
+ return
+ }
+
+ if in.Params != nil {
+ err := store.UpsertRepoParams(r.FullName, *in.Params)
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+ }
+ if in.Disabled != nil {
+ r.Disabled = *in.Disabled
+ }
+ if in.DisablePR != nil {
+ r.DisablePR = *in.DisablePR
+ }
+ if in.DisableTag != nil {
+ r.DisableTag = *in.DisableTag
+ }
+ if in.Trusted != nil && u.Admin {
+ r.Trusted = *in.Trusted
+ }
+ if in.Timeout != nil && u.Admin {
+ r.Timeout = *in.Timeout
+ }
+
+ err := store.UpdateRepo(r)
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+
+ data := repoResp{r, perm, nil, nil}
+ data.Params, _ = store.GetRepoParams(r.FullName)
+ data.Watch, _ = store.GetSubscriber(r.FullName, u.Login)
+ c.JSON(200, data)
+}
+
+// DeleteRepo accepts a request to delete the named
+// repository.
+//
+// DEL /api/repos/:owner/:name
+//
+func DeleteRepo(c *gin.Context) {
+ ds := ToDatastore(c)
+ u := ToUser(c)
+ r := ToRepo(c)
+
+ link := fmt.Sprintf(
+ "%s/api/hook",
+ httputil.GetURL(c.Request),
+ )
+
+ remote := ToRemote(c)
+ err := remote.Deactivate(u, r, link)
+ if err != nil {
+ c.Fail(400, err)
+ }
+
+ err = ds.DeleteRepo(r)
+ if err != nil {
+ c.Fail(400, err)
+ }
+ c.Writer.WriteHeader(200)
+}
+
+// PostRepo accapets a request to activate the named repository
+// in the datastore. It returns a 201 status created if successful
+//
+// POST /api/repos/:owner/:name
+//
+func PostRepo(c *gin.Context) {
+ user := ToUser(c)
+ store := ToDatastore(c)
+ owner := c.Params.ByName("owner")
+ name := c.Params.ByName("name")
+
+ link := fmt.Sprintf(
+ "%s/api/hook",
+ httputil.GetURL(c.Request),
+ )
+
+ // TODO(bradrydzewski) verify repo not exists
+
+ // get the repository and user permissions
+ // from the remote system.
+ remote := ToRemote(c)
+ r, err := remote.Repo(user, owner, name)
+ if err != nil {
+ c.Fail(400, err)
+ }
+ m, err := remote.Perm(user, owner, name)
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+ if !m.Admin {
+ c.Fail(403, fmt.Errorf("must be repository admin"))
+ return
+ }
+
+ // set the repository owner to the
+ // currently authenticated user.
+ r.User = user
+
+ // generate an RSA key and add to the repo
+ key, err := sshutil.GeneratePrivateKey()
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+ keypair := &common.Keypair{}
+ keypair.Public = sshutil.MarshalPublicKey(&key.PublicKey)
+ keypair.Private = sshutil.MarshalPrivateKey(key)
+ err = store.UpsertRepoKeys(r.FullName, keypair)
+ if err != nil {
+ c.Fail(500, err)
+ return
+ }
+
+ // store the repository and the users' permissions
+ // in the datastore.
+ err = store.InsertRepo(user, r)
+ if err != nil {
+ c.Fail(500, err)
+ return
+ }
+ err = store.InsertSubscriber(r.FullName, &common.Subscriber{Subscribed: true})
+ if err != nil {
+ c.Fail(500, err)
+ return
+ }
+
+ err = remote.Activate(user, r, keypair, link)
+ if err != nil {
+ c.Fail(500, err)
+ return
+ }
+
+ c.JSON(200, r)
+}
+
+// perms is a helper function that returns user permissions
+// for a particular repository.
+func perms(remote remote.Remote, u *common.User, r *common.Repo) *common.Perm {
+ switch {
+ case u == nil && r.Private:
+ return &common.Perm{}
+ case u == nil && r.Private == false:
+ return &common.Perm{Pull: true}
+ case u.Admin:
+ return &common.Perm{Pull: true, Push: true, Admin: true}
+ }
+
+ p, err := remote.Perm(u, r.Owner, r.Name)
+ if err != nil {
+ return &common.Perm{}
+ }
+ return p
+}
diff --git a/server/server.go b/server/server.go
new file mode 100644
index 000000000..a16767b6a
--- /dev/null
+++ b/server/server.go
@@ -0,0 +1,241 @@
+package server
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "github.com/drone/drone/common"
+ "github.com/drone/drone/datastore"
+ "github.com/drone/drone/eventbus"
+ "github.com/drone/drone/remote"
+ "github.com/drone/drone/server/session"
+ "github.com/drone/drone/settings"
+)
+
+func SetBus(r eventbus.Bus) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Set("eventbus", r)
+ c.Next()
+ }
+}
+
+func ToBus(c *gin.Context) eventbus.Bus {
+ v, err := c.Get("eventbus")
+ if err != nil {
+ return nil
+ }
+ return v.(eventbus.Bus)
+}
+
+func ToRemote(c *gin.Context) remote.Remote {
+ v, err := c.Get("remote")
+ if err != nil {
+ return nil
+ }
+ return v.(remote.Remote)
+}
+
+func SetRemote(r remote.Remote) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Set("remote", r)
+ c.Next()
+ }
+}
+
+func ToSettings(c *gin.Context) *settings.Settings {
+ v, err := c.Get("settings")
+ if err != nil {
+ return nil
+ }
+ return v.(*settings.Settings)
+}
+
+func SetSettings(s *settings.Settings) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Set("settings", s)
+ c.Next()
+ }
+}
+
+func ToPerm(c *gin.Context) *common.Perm {
+ v, err := c.Get("perm")
+ if err != nil {
+ return nil
+ }
+ return v.(*common.Perm)
+}
+
+func ToUser(c *gin.Context) *common.User {
+ v, err := c.Get("user")
+ if err != nil {
+ return nil
+ }
+ return v.(*common.User)
+}
+
+func ToRepo(c *gin.Context) *common.Repo {
+ v, err := c.Get("repo")
+ if err != nil {
+ return nil
+ }
+ return v.(*common.Repo)
+}
+
+func ToDatastore(c *gin.Context) datastore.Datastore {
+ return c.MustGet("datastore").(datastore.Datastore)
+}
+
+func ToSession(c *gin.Context) session.Session {
+ return c.MustGet("session").(session.Session)
+}
+
+func SetDatastore(ds datastore.Datastore) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Set("datastore", ds)
+ c.Next()
+ }
+}
+
+func SetSession(s session.Session) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Set("session", s)
+ c.Next()
+ }
+}
+
+func SetUser(s session.Session) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ ds := ToDatastore(c)
+ login := s.GetLogin(c.Request)
+ if len(login) == 0 {
+ c.Next()
+ return
+ }
+
+ u, err := ds.GetUser(login)
+ if err == nil {
+ c.Set("user", u)
+ }
+ }
+}
+
+func SetRepo() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ ds := ToDatastore(c)
+ u := ToUser(c)
+ owner := c.Params.ByName("owner")
+ name := c.Params.ByName("name")
+ r, err := ds.GetRepo(owner + "/" + name)
+ switch {
+ case err != nil && u != nil:
+ c.Fail(401, err)
+ return
+ case err != nil && u == nil:
+ c.Fail(404, err)
+ return
+ }
+ c.Set("repo", r)
+ c.Next()
+ }
+}
+
+func SetPerm() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ remote := ToRemote(c)
+ user := ToUser(c)
+ repo := ToRepo(c)
+ perm := perms(remote, user, repo)
+ c.Set("perm", perm)
+ c.Next()
+ }
+}
+
+func MustUser() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ u := ToUser(c)
+ if u == nil {
+ c.AbortWithStatus(401)
+ } else {
+ c.Set("user", u)
+ c.Next()
+ }
+ }
+}
+
+func MustAdmin() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ u := ToUser(c)
+ if u == nil {
+ c.AbortWithStatus(401)
+ } else if !u.Admin {
+ c.AbortWithStatus(403)
+ } else {
+ c.Set("user", u)
+ c.Next()
+ }
+ }
+}
+
+func CheckPull() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ u := ToUser(c)
+ m := ToPerm(c)
+
+ switch {
+ case u == nil && m == nil:
+ c.AbortWithStatus(401)
+ case u == nil && m.Pull == false:
+ c.AbortWithStatus(401)
+ case u != nil && m.Pull == false:
+ c.AbortWithStatus(404)
+ default:
+ c.Next()
+ }
+ }
+}
+
+func CheckPush() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ switch c.Request.Method {
+ case "GET", "OPTIONS":
+ c.Next()
+ return
+ }
+
+ u := ToUser(c)
+ m := ToPerm(c)
+
+ switch {
+ case u == nil && m.Push == false:
+ c.AbortWithStatus(401)
+ case u != nil && m.Push == false:
+ c.AbortWithStatus(404)
+ default:
+ c.Next()
+ }
+ }
+}
+
+func SetHeaders() gin.HandlerFunc {
+ return func(c *gin.Context) {
+
+ c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
+ c.Writer.Header().Add("X-Frame-Options", "DENY")
+ c.Writer.Header().Add("X-Content-Type-Options", "nosniff")
+ c.Writer.Header().Add("X-XSS-Protection", "1; mode=block")
+ c.Writer.Header().Add("Cache-Control", "no-cache")
+ c.Writer.Header().Add("Cache-Control", "no-store")
+ c.Writer.Header().Add("Cache-Control", "max-age=0")
+ c.Writer.Header().Add("Cache-Control", "must-revalidate")
+ c.Writer.Header().Add("Cache-Control", "value")
+ c.Writer.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
+ c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
+ if c.Request.TLS != nil {
+ c.Writer.Header().Add("Strict-Transport-Security", "max-age=31536000")
+ }
+
+ c.Next()
+ }
+}
diff --git a/server/session/session.go b/server/session/session.go
new file mode 100644
index 000000000..88a5164b8
--- /dev/null
+++ b/server/session/session.go
@@ -0,0 +1,109 @@
+package session
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/dgrijalva/jwt-go"
+ "github.com/drone/drone/common"
+ "github.com/drone/drone/common/httputil"
+ "github.com/drone/drone/settings"
+ "github.com/gorilla/securecookie"
+)
+
+type Session interface {
+ GenerateToken(*http.Request, *common.User) (string, error)
+ GetLogin(*http.Request) string
+}
+
+type session struct {
+ secret []byte
+ expire time.Duration
+}
+
+func New(s *settings.Session) Session {
+ secret := securecookie.GenerateRandomKey(32)
+ expire := time.Hour * 72
+ return &session{
+ secret: secret,
+ expire: expire,
+ }
+}
+
+// GenerateToken generates a JWT token for the user session
+// that can be appended to the #access_token segment to
+// facilitate client-based OAuth2.
+func (s *session) GenerateToken(r *http.Request, user *common.User) (string, error) {
+ token := jwt.New(jwt.GetSigningMethod("HS256"))
+ token.Claims["user_id"] = user.Login
+ token.Claims["audience"] = httputil.GetURL(r)
+ token.Claims["expires"] = time.Now().UTC().Add(s.expire).Unix()
+ return token.SignedString(s.secret)
+}
+
+// GetLogin gets the currently authenticated user for the
+// http.Request. The user details will be stored as either
+// a simple API token or JWT bearer token.
+func (s *session) GetLogin(r *http.Request) (_ string) {
+ token := getToken(r)
+ if len(token) == 0 {
+ return
+ }
+
+ claims := getClaims(token, s.secret)
+ if claims == nil || claims["user_id"] == nil {
+ return
+ }
+
+ userid, ok := claims["user_id"].(string)
+ if !ok {
+ return
+ }
+
+ // tokenid, ok := claims["token_id"].(string)
+ // if ok {
+ // _, err := datastore.GetToken(c, int64(tokenid))
+ // if err != nil {
+ // return nil
+ // }
+ // }
+
+ return userid
+}
+
+// getToken is a helper function that extracts the token
+// from the http.Request.
+func getToken(r *http.Request) string {
+ token := getTokenHeader(r)
+ if len(token) == 0 {
+ token = getTokenParam(r)
+ }
+ return token
+}
+
+// getTokenHeader parses the JWT token value from
+// the http Authorization header.
+func getTokenHeader(r *http.Request) string {
+ var tokenstr = r.Header.Get("Authorization")
+ fmt.Sscanf(tokenstr, "Bearer %s", &tokenstr)
+ return tokenstr
+}
+
+// getTokenParam parses the JWT token value from
+// the http Request's query parameter.
+func getTokenParam(r *http.Request) string {
+ return r.FormValue("access_token")
+}
+
+// getClaims is a helper function that extracts the token
+// claims from the JWT token string.
+func getClaims(token string, secret []byte) map[string]interface{} {
+ t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
+ return secret, nil
+ })
+ if err != nil || !t.Valid {
+ return nil
+ }
+ return t.Claims
+}
diff --git a/server/static/index.html b/server/static/index.html
new file mode 100644
index 000000000..6aad5f817
--- /dev/null
+++ b/server/static/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/server/static/scripts/controllers/builds.js b/server/static/scripts/controllers/builds.js
new file mode 100644
index 000000000..64b0d5207
--- /dev/null
+++ b/server/static/scripts/controllers/builds.js
@@ -0,0 +1,101 @@
+(function () {
+
+ /**
+ * BuildsCtrl responsible for rendering the repo's
+ * recent build history.
+ */
+ function BuildsCtrl($scope, $routeParams, builds, repos, users) {
+
+ var owner = $routeParams.owner;
+ var name = $routeParams.name;
+ var fullName = owner+'/'+name;
+
+ // Gets the currently authenticated user
+ users.getCached().then(function(payload){
+ $scope.user = payload.data;
+ });
+
+ // Gets a repository
+ repos.get(fullName).then(function(payload){
+ $scope.repo = payload.data;
+ }).catch(function(err){
+ $scope.error = err;
+ });
+
+ // Gets a list of builds
+ builds.list(fullName).then(function(payload){
+ $scope.builds = angular.isArray(payload.data) ? payload.data : [];
+ }).catch(function(err){
+ $scope.error = err;
+ });
+
+ $scope.watch = function(repo) {
+ repos.watch(repo.full_name).then(function(payload) {
+ $scope.repo.subscription = payload.data;
+ });
+ }
+
+ $scope.unwatch = function(repo) {
+ repos.unwatch(repo.full_name).then(function() {
+ delete $scope.repo.subscription;
+ });
+ }
+ }
+
+ /**
+ * BuildCtrl responsible for rendering a build.
+ */
+ function BuildCtrl($scope, $routeParams, logs, tasks, builds, repos, users) {
+
+ var step = parseInt($routeParams.step) || 1;
+ var number = $routeParams.number;
+ var owner = $routeParams.owner;
+ var name = $routeParams.name;
+ var fullName = owner+'/'+name;
+
+ // Gets the currently authenticated user
+ users.getCached().then(function(payload){
+ $scope.user = payload.data;
+ });
+
+ // Gets a repository
+ repos.get(fullName).then(function(payload){
+ $scope.repo = payload.data;
+ }).catch(function(err){
+ $scope.error = err;
+ });
+
+ // Gets the build
+ builds.get(fullName, number).then(function(payload){
+ $scope.build = payload.data;
+ }).catch(function(err){
+ $scope.error = err;
+ });
+
+ // Gets a list of build steps
+ tasks.list(fullName, number).then(function(payload){
+ $scope.tasks = payload.data || [];
+ $scope.tasks.forEach(function(task) {
+ if (task.number === step) {
+ $scope.task = task;
+ }
+ });
+ }).catch(function(err){
+ $scope.error = err;
+ });
+
+ if (step) {
+ // Gets a list of build steps
+ logs.get(fullName, number, step).then(function(payload){
+ $scope.logs = payload.data;
+ }).catch(function(err){
+ $scope.error = err;
+ });
+ }
+ }
+
+ angular
+ .module('drone')
+ .controller('BuildCtrl', BuildCtrl)
+ .controller('BuildsCtrl', BuildsCtrl);
+})();
\ No newline at end of file
diff --git a/server/static/scripts/controllers/repos.js b/server/static/scripts/controllers/repos.js
new file mode 100644
index 000000000..cb86a7ae4
--- /dev/null
+++ b/server/static/scripts/controllers/repos.js
@@ -0,0 +1,92 @@
+(function () {
+
+ /**
+ * ReposCtrl responsible for rendering the user's
+ * repository home screen.
+ */
+ function ReposCtrl($scope, $routeParams, repos, users) {
+
+ // Gets the currently authenticated user
+ users.getCached().then(function(payload){
+ $scope.user = payload.data;
+ });
+
+ // Gets a list of repos to display in the
+ // dropdown.
+ repos.list().then(function(payload){
+ $scope.repos = angular.isArray(payload.data) ? payload.data : [];
+ }).catch(function(err){
+ $scope.error = err;
+ });
+ }
+
+ /**
+ * RepoAddCtrl responsible for activaing a new
+ * repository.
+ */
+ function RepoAddCtrl($scope, $location, repos, users) {
+ $scope.add = function(slug) {
+ repos.post(slug).then(function(payload) {
+ $location.path('/'+slug);
+ }).catch(function(err){
+ $scope.error = err;
+ });
+ }
+ }
+
+ /**
+ * RepoEditCtrl responsible for editing a repository.
+ */
+ function RepoEditCtrl($scope, $location, $routeParams, repos, users) {
+ var owner = $routeParams.owner;
+ var name = $routeParams.name;
+ var fullName = owner+'/'+name;
+
+ // Gets the currently authenticated user
+ users.getCached().then(function(payload){
+ $scope.user = payload.data;
+ });
+
+ // Gets a repository
+ repos.get(fullName).then(function(payload){
+ $scope.repo = payload.data;
+ }).catch(function(err){
+ $scope.error = err;
+ });
+
+ $scope.save = function(repo) {
+ repos.update(repo).then(function(payload) {
+ $scope.repo = payload.data;
+ }).catch(function(err){
+ $scope.error = err;
+ });
+ }
+
+ $scope.delete = function(repo) {
+ repos.delete(repo).then(function(payload) {
+ $location.path('/');
+ }).catch(function(err){
+ $scope.error = err;
+ });
+ }
+
+ $scope.param={}
+ $scope.addParam = function(param) {
+ if (!$scope.repo.params) {
+ $scope.repo.params = {}
+ }
+ $scope.repo.params[param.key]=param.value;
+ $scope.param={}
+ }
+
+ $scope.deleteParam = function(key) {
+ delete $scope.repo.params[key];
+ }
+ }
+
+ angular
+ .module('drone')
+ .controller('ReposCtrl', ReposCtrl)
+ .controller('RepoAddCtrl', RepoAddCtrl)
+ .controller('RepoEditCtrl', RepoEditCtrl);
+})();
\ No newline at end of file
diff --git a/server/static/scripts/controllers/users.js b/server/static/scripts/controllers/users.js
new file mode 100644
index 000000000..0b5ba8c4f
--- /dev/null
+++ b/server/static/scripts/controllers/users.js
@@ -0,0 +1,54 @@
+(function () {
+
+ /**
+ * UserCtrl is responsible for managing user settings.
+ */
+ function UserCtrl($scope, users) {
+
+ // Gets the currently authenticated user
+ users.getCurrent().then(function(payload){
+ $scope.user = payload.data;
+ });
+ }
+
+ /**
+ * UsersCtrl is responsible for managing user accounts.
+ * This part of the site is for administrators only.
+ */
+ function UsersCtrl($scope, users) {
+ // Gets the currently authenticated user
+ users.getCached().then(function(payload){
+ $scope.user = payload.data;
+ });
+
+ users.list().then(function(payload){
+ $scope.users = payload.data;
+ });
+
+ $scope.login="";
+ $scope.add = function(login) {
+ users.post(login).then(function(payload){
+ $scope.users.push(payload.data);
+ $scope.login="";
+ });
+ }
+
+ $scope.toggle = function(user) {
+ user.admin = !user.admin;
+ users.put(user);
+ }
+
+ $scope.remove = function(user) {
+ users.delete(user).then(function(){
+ users.list().then(function(payload){
+ $scope.users = payload.data;
+ });
+ });
+ }
+ }
+
+ angular
+ .module('drone')
+ .controller('UserCtrl', UserCtrl)
+ .controller('UsersCtrl', UsersCtrl);
+})();
\ No newline at end of file
diff --git a/server/static/scripts/drone.js b/server/static/scripts/drone.js
new file mode 100644
index 000000000..3f9db519e
--- /dev/null
+++ b/server/static/scripts/drone.js
@@ -0,0 +1,140 @@
+'use strict';
+
+(function () {
+
+ /**
+ * Creates the angular application.
+ */
+ angular.module('drone', [
+ 'ngRoute',
+ 'ui.filters'
+ ]);
+
+ /**
+ * Bootstraps the application and retrieves the
+ * token from the
+ */
+ function Authorize() {
+ // First, parse the query string
+ var params = {}, queryString = location.hash.substring(1),
+ regex = /([^&=]+)=([^&]*)/g, m;
+
+ // Loop through and retrieve the token
+ while (m = regex.exec(queryString)) {
+ params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
+ }
+
+ // if the user has just received an auth token we
+ // should extract from the URL, save to local storage
+ // and then remove from the URL for good measure.
+ if (params.access_token) {
+ localStorage.setItem("access_token", params.access_token);
+ history.replaceState({}, document.title, location.pathname);
+ }
+ }
+
+ /**
+ * Defines the route configuration for the
+ * main application.
+ */
+ function Config ($routeProvider, $httpProvider, $locationProvider) {
+
+ // Resolver that will attempt to load the currently
+ // authenticated user prior to loading the page.
+ var resolveUser = {
+ user: function(users) {
+ return users.getCached();
+ }
+ }
+
+ $routeProvider
+ .when('/', {
+ templateUrl: '/static/scripts/views/repos.html',
+ controller: 'ReposCtrl',
+ resolve: resolveUser
+ })
+ .when('/login', {
+ templateUrl: '/static/scripts/views/login.html',
+ })
+ .when('/profile', {
+ templateUrl: '/static/scripts/views/user.html',
+ controller: 'UserCtrl',
+ resolve: resolveUser
+ })
+ .when('/users', {
+ templateUrl: '/static/scripts/views/users.html',
+ controller: 'UsersCtrl',
+ resolve: resolveUser
+ })
+ .when('/new', {
+ templateUrl: '/static/scripts/views/repos_add.html',
+ controller: 'RepoAddCtrl',
+ resolve: resolveUser
+ })
+ .when('/:owner/:name', {
+ templateUrl: '/static/scripts/views/builds.html',
+ controller: 'BuildsCtrl',
+ resolve: resolveUser
+ })
+ .when('/:owner/:name/edit', {
+ templateUrl: '/static/scripts/views/repos_edit.html',
+ controller: 'RepoEditCtrl',
+ resolve: resolveUser
+ })
+ .when('/:owner/:name/:number', {
+ templateUrl: '/static/scripts/views/build.html',
+ controller: 'BuildCtrl',
+ resolve: resolveUser
+ })
+ .when('/:owner/:name/:number/:step', {
+ templateUrl: '/static/scripts/views/build.html',
+ controller: 'BuildCtrl',
+ resolve: resolveUser
+ });
+
+ // Enables html5 mode
+ $locationProvider.html5Mode(true)
+
+ // Appends the Bearer token to authorize every
+ // outbound http request.
+ $httpProvider.defaults.headers.common.Authorization = 'Bearer '+localStorage.getItem('access_token');
+
+ // Intercepts every oubput http response and redirects
+ // the user to the logic screen if the request was rejected.
+ $httpProvider.interceptors.push(function($q, $location) {
+ return {
+ 'responseError': function(rejection) {
+ if (rejection.status === 401) {// && rejection.config.url != "/api/user") {
+ $location.path('/login');
+ }
+ if (rejection.status === 0) {
+ // this happens when the app is down or
+ // the browser loses internet connectivity.
+ }
+ return $q.reject(rejection);
+ }
+ };
+ });
+ }
+
+ // /**
+ // *
+ // */
+ // function RouteChange($rootScope, stdout, projs) {
+ // $rootScope.$on('$routeChangeStart', function (event, next) {
+ // projs.unsubscribe();
+ // stdout.unsubscribe();
+ // });
+
+ // //$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
+ // // document.title = current.$$route.title + ' ยท drone.io';
+ // //});
+ // }
+
+ angular
+ .module('drone')
+ .config(Authorize)
+ .config(Config);
+ // .run(RouteChange);
+
+})();
\ No newline at end of file
diff --git a/server/static/scripts/filters/filter.js b/server/static/scripts/filters/filter.js
new file mode 100644
index 000000000..b9cf15fce
--- /dev/null
+++ b/server/static/scripts/filters/filter.js
@@ -0,0 +1,61 @@
+'use strict';
+
+(function () {
+
+ /**
+ * author is a helper function that return the builds
+ * commit or pull request author.
+ */
+ function author() {
+ return function(build) {
+ if (!build) { return ""; }
+ if (!build.head_commit && !build.pull_request) { return ""; }
+ if (build.head_commit) { return build.head_commit.author.login || ""; }
+ return build.pull_request.source.author.login;
+ }
+ }
+
+ /**
+ * sha is a helper function that return the builds sha.
+ */
+ function sha() {
+ return function(build) {
+ if (!build) { return ""; }
+ if (!build.head_commit && !build.pull_request) { return ""; }
+ if (build.head_commit) { return build.head_commit.sha || ""; }
+ return build.pull_request.source.sha;
+ }
+ }
+
+ /**
+ * ref is a helper function that return the builds sha.
+ */
+ function ref() {
+ return function(build) {
+ if (!build) { return ""; }
+ if (!build.head_commit && !build.pull_request) { return ""; }
+ if (build.head_commit) { return build.head_commit.ref || ""; }
+ return build.pull_request.source.ref;
+ }
+ }
+
+ /**
+ * message is a helper function that return the builds message.
+ */
+ function message() {
+ return function(build) {
+ if (!build) { return ""; }
+ if (!build.head_commit && !build.pull_request) { return ""; }
+ if (build.head_commit) { return build.head_commit.message || ""; }
+ return build.pull_request.title || "";
+ }
+ }
+
+ angular
+ .module('drone')
+ .filter('author', author)
+ .filter('message', message)
+ .filter('sha', sha)
+ .filter('ref', ref);
+
+})();
diff --git a/server/static/scripts/filters/gravatar.js b/server/static/scripts/filters/gravatar.js
new file mode 100644
index 000000000..2fbe71f86
--- /dev/null
+++ b/server/static/scripts/filters/gravatar.js
@@ -0,0 +1,32 @@
+'use strict';
+
+(function () {
+
+ /**
+ * gravatar is a helper function that return the user's gravatar
+ * image URL given an email hash.
+ */
+ function gravatar() {
+ return function(hash) {
+ if (hash === undefined) { return ""; }
+ return "https://secure.gravatar.com/avatar/"+hash+"?s=48&d=mm";
+ }
+ }
+
+ /**
+ * gravatarLarge is a helper function that return the user's gravatar
+ * image URL given an email hash.
+ */
+ function gravatarLarge() {
+ return function(hash) {
+ if (hash === undefined) { return ""; }
+ return "https://secure.gravatar.com/avatar/"+hash+"?s=128&d=mm";
+ }
+ }
+
+ angular
+ .module('drone')
+ .filter('gravatar', gravatar)
+ .filter('gravatarLarge', gravatarLarge)
+
+})();
diff --git a/server/static/scripts/filters/time.js b/server/static/scripts/filters/time.js
new file mode 100644
index 000000000..5404e497e
--- /dev/null
+++ b/server/static/scripts/filters/time.js
@@ -0,0 +1,45 @@
+'use strict';
+
+(function () {
+
+ /**
+ * fromNow is a helper function that returns a human readable
+ * string for the elapsed time between the given unix date and the
+ * current time (ex. 10 minutes ago).
+ */
+ function fromNow() {
+ return function(date) {
+ if (!date) {
+ return;
+ }
+ return moment(new Date(date*1000)).fromNow();
+ }
+ }
+
+ /**
+ * toDuration is a helper function that returns a human readable
+ * string for the given duration in seconds (ex. 1 hour and 20 minutes).
+ */
+ function toDuration() {
+ return function(seconds) {
+ return moment.duration(seconds, "seconds").humanize();
+ }
+ }
+
+ /**
+ * toDate is a helper function that returns a human readable
+ * string gor the given unix date.
+ */
+ function toDate() {
+ return function(date) {
+ return moment(new Date(date*1000)).format('ll');
+ }
+ }
+
+ angular
+ .module('drone')
+ .filter('fromNow', fromNow)
+ .filter('toDate', toDate)
+ .filter('toDuration', toDuration)
+
+})();
diff --git a/server/static/scripts/services/builds.js b/server/static/scripts/services/builds.js
new file mode 100644
index 000000000..e59ccad7c
--- /dev/null
+++ b/server/static/scripts/services/builds.js
@@ -0,0 +1,34 @@
+'use strict';
+
+(function () {
+
+ /**
+ * The BuildsService provides access to build
+ * data using REST API calls.
+ */
+ function BuildService($http, $window) {
+
+ /**
+ * Gets a list of builds.
+ *
+ * @param {string} Name of the repository.
+ */
+ this.list = function(repoName) {
+ return $http.get('/api/builds/'+repoName);
+ };
+
+ /**
+ * Gets a build.
+ *
+ * @param {string} Name of the repository.
+ * @param {number} Number of the build.
+ */
+ this.get = function(repoName, buildNumber) {
+ return $http.get('/api/builds/'+repoName+'/'+buildNumber);
+ };
+ }
+
+ angular
+ .module('drone')
+ .service('builds', BuildService);
+})();
\ No newline at end of file
diff --git a/server/static/scripts/services/logs.js b/server/static/scripts/services/logs.js
new file mode 100644
index 000000000..144972fe0
--- /dev/null
+++ b/server/static/scripts/services/logs.js
@@ -0,0 +1,26 @@
+'use strict';
+
+(function () {
+
+ /**
+ * The LogService provides access to build
+ * log data using REST API calls.
+ */
+ function LogService($http, $window) {
+
+ /**
+ * Gets a task logs.
+ *
+ * @param {string} Name of the repository.
+ * @param {number} Number of the build.
+ * @param {number} Number of the task.
+ */
+ this.get = function(repoName, number, step) {
+ return $http.get('/api/logs/'+repoName+'/'+number+'/'+step);
+ };
+ }
+
+ angular
+ .module('drone')
+ .service('logs', LogService);
+})();
\ No newline at end of file
diff --git a/server/static/scripts/services/repos.js b/server/static/scripts/services/repos.js
new file mode 100644
index 000000000..4e13337c2
--- /dev/null
+++ b/server/static/scripts/services/repos.js
@@ -0,0 +1,80 @@
+'use strict';
+
+(function () {
+
+ /**
+ * The RepoService provides access to repository
+ * data using REST API calls.
+ */
+ function RepoService($http, $window) {
+
+ var callback,
+ websocket,
+ token = localStorage.getItem('access_token');
+
+ /**
+ * Gets a list of all repositories.
+ */
+ this.list = function() {
+ return $http.get('/api/user/repos');
+ };
+
+ /**
+ * Gets a repository by name.
+ *
+ * @param {string} Name of the repository.
+ */
+ this.get = function(repoName) {
+ return $http.get('/api/repos/'+repoName);
+ };
+
+ /**
+ * Creates a new repository.
+ *
+ * @param {object} JSON representation of a repository.
+ */
+ this.post = function(repoName) {
+ return $http.post('/api/repos/' + repoName);
+ };
+
+ /**
+ * Updates an existing repository.
+ *
+ * @param {object} JSON representation of a repository.
+ */
+ this.update = function(repo) {
+ return $http.put('/api/repos/'+repo.full_name, repo);
+ };
+
+ /**
+ * Deletes a repository.
+ *
+ * @param {string} Name of the repository.
+ */
+ this.delete = function(repoName) {
+ return $http.delete('/api/repos/'+repoName);
+ };
+
+ /**
+ * Watch a repository.
+ *
+ * @param {string} Name of the repository.
+ */
+ this.watch = function(repoName) {
+ return $http.post('/api/subscribers/'+repoName);
+ };
+
+ /**
+ * Unwatch a repository.
+ *
+ * @param {string} Name of the repository.
+ */
+ this.unwatch = function(repoName) {
+ return $http.delete('/api/subscribers/'+repoName);
+ };
+ }
+
+ angular
+ .module('drone')
+ .service('repos', RepoService);
+})();
\ No newline at end of file
diff --git a/server/static/scripts/services/tasks.js b/server/static/scripts/services/tasks.js
new file mode 100644
index 000000000..11bd59aa9
--- /dev/null
+++ b/server/static/scripts/services/tasks.js
@@ -0,0 +1,47 @@
+'use strict';
+
+(function () {
+
+ /**
+ * The TaskService provides access to build
+ * task data using REST API calls.
+ */
+ function TaskService($http, $window) {
+
+ /**
+ * Gets a list of builds.
+ *
+ * @param {string} Name of the repository.
+ * @param {number} Number of the build.
+ */
+ this.list = function(repoName, number) {
+ return $http.get('/api/tasks/'+repoName+'/'+number);
+ };
+
+ /**
+ * Gets a task.
+ *
+ * @param {string} Name of the repository.
+ * @param {number} Number of the build.
+ * @param {number} Number of the task.
+ */
+ this.get = function(repoName, number, step) {
+ return $http.get('/api/tasks/'+repoName+'/'+name+'/'+step);
+ };
+
+ /**
+ * Gets a task.
+ *
+ * @param {string} Name of the repository.
+ * @param {number} Number of the build.
+ * @param {number} Number of the task.
+ */
+ this.get = function(repoName, number, step) {
+ return $http.get('/api/tasks/'+repoName+'/'+name+'/'+step);
+ };
+ }
+
+ angular
+ .module('drone')
+ .service('tasks', TaskService);
+})();
\ No newline at end of file
diff --git a/server/static/scripts/services/users.js b/server/static/scripts/services/users.js
new file mode 100644
index 000000000..4d4d1f2c1
--- /dev/null
+++ b/server/static/scripts/services/users.js
@@ -0,0 +1,86 @@
+'use strict';
+
+(function () {
+
+ /**
+ * Cached user object.
+ */
+ var _user;
+
+ /**
+ * The UserService provides access to useer
+ * data using REST API calls.
+ */
+ function UserService($http, $q) {
+
+ /**
+ * Gets a list of all users.
+ */
+ this.list = function() {
+ return $http.get('/api/users');
+ };
+
+ /**
+ * Gets a user by login.
+ */
+ this.get = function(login) {
+ return $http.get('/api/users/'+login);
+ };
+
+ /**
+ * Gets the currently authenticated user.
+ */
+ this.getCurrent = function() {
+ return $http.get('/api/user');
+ };
+
+ /**
+ * Updates an existing user
+ */
+ this.post = function(user) {
+ return $http.post('/api/users/'+user);
+ };
+
+ /**
+ * Updates an existing user
+ */
+ this.put = function(user) {
+ return $http.put('/api/users/'+user.login, user);
+ };
+
+ /**
+ * Deletes a user.
+ */
+ this.delete = function(user) {
+ return $http.delete('/api/users/'+user.login);
+ };
+
+ /**
+ * Gets the currently authenticated user from
+ * the local cache. If not exists, it will fetch
+ * from the server.
+ */
+ this.getCached = function() {
+ var defer = $q.defer();
+
+ // if the user is already authenticated
+ if (_user) {
+ defer.resolve(_user);
+ return defer.promise;
+ }
+
+ // else fetch the currently authenticated
+ // user using the REST API.
+ this.getCurrent().then(function(payload){
+ _user=payload;
+ defer.resolve(_user);
+ });
+
+ return defer.promise;
+ }
+ }
+
+ angular
+ .module('drone')
+ .service('users', UserService);
+})();
diff --git a/server/static/scripts/views/build.html b/server/static/scripts/views/build.html
new file mode 100644
index 000000000..173896ff7
--- /dev/null
+++ b/server/static/scripts/views/build.html
@@ -0,0 +1,80 @@
+{{ repo.full_name }}/{{ build.number }}
+
+Back
+
+
+ Build State
+ {{ build.state }}
+
+ Started
+ {{ build.started_at | fromNow }}
+
+ Duration
+ {{ build.duration | toDuration }}
+
+ Type
+ {{ build.head_commit ? "push" : "pull request" }}
+
+ Ref
+ {{ build | ref }}
+
+ Sha
+ {{ build | sha }}
+
+ Author
+ {{ build | author }}
+
+ Message
+ {{ build | message }}
+
+
+
+
+
+ Task State
+ {{ task.state }}
+
+ Started
+ {{ task.started_at | fromNow }}
+
+ Finished
+ {{ task.finished_at | fromNow }}
+
+ Duration
+ {{ task.duration | toDuration }}
+
+ Exit Code
+ {{ task.exit_code }}
+
+ Matrix
+ {{ task.environment }}
+
+
+
+
+{{ logs }}
+
+
+
+
+ Number
+ Status
+ Started
+ Finished
+ Duration
+ Exit Code
+ Matrix
+
+
+
+
+ {{ task.number }}
+ {{ task.state }}
+ {{ task.started_at | fromNow }}
+ {{ task.finished_at | fromNow }}
+ {{ task.duration | toDuration }}
+ {{ task.exit_code }}
+ {{ task.environment }}
+
+
+
diff --git a/server/static/scripts/views/builds.html b/server/static/scripts/views/builds.html
new file mode 100644
index 000000000..7fb5e6d98
--- /dev/null
+++ b/server/static/scripts/views/builds.html
@@ -0,0 +1,36 @@
+{{ repo.full_name }}
+
+Back
+Settings
+
+Watch
+Unwatch
+
+
+
+
+ Number
+ Status
+ Started
+ Duration
+ Type
+ Ref
+ Commit
+ Author
+ Message
+
+
+
+
+ {{ build.number }}
+ {{ build.state }}
+ {{ build.started_at | fromNow }}
+ {{ build.duration | toDuration }}
+ {{ build.head_commit ? "push" : "pull request" }}
+ {{ build | ref }}
+ {{ build | sha }}
+ {{ build | author }}
+ {{ build | message }}
+
+
+
\ No newline at end of file
diff --git a/server/static/scripts/views/login.html b/server/static/scripts/views/login.html
new file mode 100644
index 000000000..a4ae37938
--- /dev/null
+++ b/server/static/scripts/views/login.html
@@ -0,0 +1,3 @@
+Login
+
+Login
\ No newline at end of file
diff --git a/server/static/scripts/views/repos.html b/server/static/scripts/views/repos.html
new file mode 100644
index 000000000..701c26e43
--- /dev/null
+++ b/server/static/scripts/views/repos.html
@@ -0,0 +1,30 @@
+Dashboard
+
+New
+Settings
+User Management
+
+
+
+
+ Repo
+ Status
+ Number
+ Started
+ Duration
+ Branch
+ Commit
+
+
+
+
+ {{ repo.full_name }}
+ {{ repo.last_build.state }}
+ {{ repo.last_build.number }}
+ {{ repo.last_build.started_at | fromNow }}
+ {{ repo.last_build.duration | toDuration }}
+ {{ repo.last_build.head_commit.ref || repo.last_build.pull_request.source.ref }}
+ {{ repo.last_build.head_commit.sha || repo.last_build.pull_request.source.ref }}
+
+
+
\ No newline at end of file
diff --git a/server/static/scripts/views/repos_add.html b/server/static/scripts/views/repos_add.html
new file mode 100644
index 000000000..187e898c9
--- /dev/null
+++ b/server/static/scripts/views/repos_add.html
@@ -0,0 +1,8 @@
+Add Repository
+
+Back
+
+
\ No newline at end of file
diff --git a/server/static/scripts/views/repos_edit.html b/server/static/scripts/views/repos_edit.html
new file mode 100644
index 000000000..c59fe9a73
--- /dev/null
+++ b/server/static/scripts/views/repos_edit.html
@@ -0,0 +1,42 @@
+{{ repo.full_name }} / Edit
+
+Back
+
+
\ No newline at end of file
diff --git a/server/static/scripts/views/user.html b/server/static/scripts/views/user.html
new file mode 100644
index 000000000..2123b25a6
--- /dev/null
+++ b/server/static/scripts/views/user.html
@@ -0,0 +1,26 @@
+{{ user.login }}
+
+Back
+
+
+ Login
+ {{ user.login }}
+
+ Full Name
+ {{ user.name }}
+
+ Created
+ {{ user.created_at | fromNow }}
+
+ Updated
+ {{ user.update_at | fromNow }}
+
+ Email
+ {{ user.email }}
+
+ Site Admin
+ {{ user.admin }}
+
+ Gravatar
+
+
\ No newline at end of file
diff --git a/server/static/scripts/views/users.html b/server/static/scripts/views/users.html
new file mode 100644
index 000000000..ab8fc7361
--- /dev/null
+++ b/server/static/scripts/views/users.html
@@ -0,0 +1,37 @@
+Users
+
+Back
+
+
+
+
+ Login
+ Full Name
+ Created
+ Updated
+ Email
+ Site Admin
+ Gravatar
+
+
+
+
+
+
+ {{ user.login }}
+ {{ user.name }}
+ {{ user.created_at | fromNow }}
+ {{ user.updated_at | fromNow }}
+ {{ user.email }}
+ {{ !!user.admin }}
+
+ toggle admin
+ delete
+
+
+
+
+
\ No newline at end of file
diff --git a/server/status.go b/server/status.go
new file mode 100644
index 000000000..0d3b9fec8
--- /dev/null
+++ b/server/status.go
@@ -0,0 +1,75 @@
+package server
+
+import (
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+
+ "github.com/drone/drone/common"
+)
+
+// GetStatus accepts a request to retrieve a build status
+// from the datastore for the given repository and
+// build number.
+//
+// GET /api/status/:owner/:name/:number/:context
+//
+func GetStatus(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ num, _ := strconv.Atoi(c.Params.ByName("number"))
+ ctx := c.Params.ByName("context")
+
+ status, err := ds.GetBuildStatus(repo.FullName, num, ctx)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, status)
+ }
+}
+
+// PostStatus accepts a request to create a new build
+// status. The created user status is returned in JSON
+// format if successful.
+//
+// POST /api/status/:owner/:name/:number
+//
+func PostStatus(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ num, err := strconv.Atoi(c.Params.ByName("number"))
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+ in := &common.Status{}
+ if !c.BindWith(in, binding.JSON) {
+ c.AbortWithStatus(400)
+ return
+ }
+ if err := ds.InsertBuildStatus(repo.Name, num, in); err != nil {
+ c.Fail(400, err)
+ } else {
+ c.JSON(201, in)
+ }
+}
+
+// GetStatusList accepts a request to retrieve a list of
+// all build status from the datastore for the given repository
+// and build number.
+//
+// GET /api/status/:owner/:name/:number/:context
+//
+func GetStatusList(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ num, _ := strconv.Atoi(c.Params.ByName("number"))
+
+ list, err := ds.GetBuildStatusList(repo.FullName, num)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, list)
+ }
+}
diff --git a/server/subscribe.go b/server/subscribe.go
new file mode 100644
index 000000000..a783a3eb3
--- /dev/null
+++ b/server/subscribe.go
@@ -0,0 +1,84 @@
+package server
+
+import (
+ "github.com/gin-gonic/gin"
+
+ "github.com/drone/drone/common"
+)
+
+// GetSubscriber accepts a request to retrieve a repository
+// subscriber from the datastore for the given repository by
+// user Login.
+//
+// GET /api/subscribers/:owner/:name/:login
+//
+func GetSubscriber(c *gin.Context) {
+ store := ToDatastore(c)
+ repo := ToRepo(c)
+ login := c.Params.ByName("login")
+ subsc, err := store.GetSubscriber(repo.FullName, login)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, subsc)
+ }
+}
+
+// GetSubscribers accepts a request to retrieve a repository
+// watchers from the datastore for the given repository.
+//
+// GET /api/subscribers/:owner/:name
+//
+func GetSubscribers(c *gin.Context) {
+ // store := ToDatastore(c)
+ // repo := ToRepo(c)
+ // subs, err := store.GetSubscribers(repo.FullName)
+ // if err != nil {
+ // c.Fail(404, err)
+ // } else {
+ // c.JSON(200, subs)
+ // }
+ c.Writer.WriteHeader(501)
+}
+
+// Unubscribe accapets a request to unsubscribe the
+// currently authenticated user to the repository.
+//
+// DEL /api/subscribers/:owner/:name
+//
+func Unsubscribe(c *gin.Context) {
+ store := ToDatastore(c)
+ repo := ToRepo(c)
+ user := ToUser(c)
+ sub, err := store.GetSubscriber(repo.FullName, user.Login)
+ if err != nil {
+ c.Fail(404, err)
+ }
+ err = store.DeleteSubscriber(repo.FullName, sub)
+ if err != nil {
+ c.Fail(400, err)
+ } else {
+ c.Writer.WriteHeader(200)
+ }
+}
+
+// Subscribe accapets a request to subscribe the
+// currently authenticated user to the repository.
+//
+// POST /api/subscriber/:owner/:name
+//
+func Subscribe(c *gin.Context) {
+ store := ToDatastore(c)
+ repo := ToRepo(c)
+ user := ToUser(c)
+ subscriber := &common.Subscriber{
+ Login: user.Login,
+ Subscribed: true,
+ }
+ err := store.InsertSubscriber(repo.FullName, subscriber)
+ if err != nil {
+ c.Fail(400, err)
+ } else {
+ c.JSON(200, subscriber)
+ }
+}
diff --git a/server/tasks.go b/server/tasks.go
new file mode 100644
index 000000000..3491ec0ea
--- /dev/null
+++ b/server/tasks.go
@@ -0,0 +1,46 @@
+package server
+
+import (
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetTask accepts a request to retrieve a build task
+// from the datastore for the given repository and
+// build number.
+//
+// GET /api/tasks/:owner/:name/:number/:task
+//
+func GetTask(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ b, _ := strconv.Atoi(c.Params.ByName("number"))
+ t, _ := strconv.Atoi(c.Params.ByName("task"))
+
+ task, err := ds.GetTask(repo.FullName, b, t)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, task)
+ }
+}
+
+// GetTasks accepts a request to retrieve a list of
+// build tasks from the datastore for the given repository
+// and build number.
+//
+// GET /api/tasks/:owner/:name/:number
+//
+func GetTasks(c *gin.Context) {
+ ds := ToDatastore(c)
+ repo := ToRepo(c)
+ num, _ := strconv.Atoi(c.Params.ByName("number"))
+
+ tasks, err := ds.GetTaskList(repo.FullName, num)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, tasks)
+ }
+}
diff --git a/server/user.go b/server/user.go
new file mode 100644
index 000000000..5a0fbda5d
--- /dev/null
+++ b/server/user.go
@@ -0,0 +1,59 @@
+package server
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+
+ "github.com/drone/drone/common"
+ "github.com/drone/drone/common/gravatar"
+)
+
+// GetUserCurr accepts a request to retrieve the
+// currently authenticated user from the datastore
+// and return in JSON format.
+//
+// GET /api/user
+//
+func GetUserCurr(c *gin.Context) {
+ c.JSON(200, ToUser(c))
+}
+
+// PutUserCurr accepts a request to update the currently
+// authenticated User profile.
+//
+// PUT /api/user
+//
+func PutUserCurr(c *gin.Context) {
+ ds := ToDatastore(c)
+ me := ToUser(c)
+
+ in := &common.User{}
+ if !c.BindWith(in, binding.JSON) {
+ return
+ }
+ me.Email = in.Email
+ me.Gravatar = gravatar.Generate(in.Email)
+ err := ds.UpdateUser(me)
+ if err != nil {
+ c.Fail(400, err)
+ } else {
+ c.JSON(200, me)
+ }
+}
+
+// GetUserRepos accepts a request to get the currently
+// authenticated user's repository list from the datastore,
+// encoded and returned in JSON format.
+//
+// GET /api/user/repos
+//
+func GetUserRepos(c *gin.Context) {
+ ds := ToDatastore(c)
+ me := ToUser(c)
+ repos, err := ds.GetUserRepos(me.Login)
+ if err != nil {
+ c.Fail(400, err)
+ } else {
+ c.JSON(200, &repos)
+ }
+}
diff --git a/server/users.go b/server/users.go
new file mode 100644
index 000000000..cc8f1086a
--- /dev/null
+++ b/server/users.go
@@ -0,0 +1,125 @@
+package server
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+
+ "github.com/drone/drone/common"
+ "github.com/drone/drone/common/gravatar"
+)
+
+// GetUsers accepts a request to retrieve all users
+// from the datastore and return encoded in JSON format.
+//
+// GET /api/users
+//
+func GetUsers(c *gin.Context) {
+ ds := ToDatastore(c)
+ users, err := ds.GetUserList()
+ if err != nil {
+ c.Fail(400, err)
+ } else {
+ c.JSON(200, users)
+ }
+}
+
+// PostUser accepts a request to create a new user in the
+// system. The created user account is returned in JSON
+// format if successful.
+//
+// POST /api/users
+//
+func PostUser(c *gin.Context) {
+ ds := ToDatastore(c)
+ name := c.Params.ByName("name")
+ user := &common.User{Login: name, Name: name}
+ if err := ds.InsertUser(user); err != nil {
+ c.Fail(400, err)
+ } else {
+ c.JSON(201, user)
+ }
+}
+
+// GetUser accepts a request to retrieve a user by hostname
+// and login from the datastore and return encoded in JSON
+// format.
+//
+// GET /api/users/:name
+//
+func GetUser(c *gin.Context) {
+ ds := ToDatastore(c)
+ name := c.Params.ByName("name")
+ user, err := ds.GetUser(name)
+ if err != nil {
+ c.Fail(404, err)
+ } else {
+ c.JSON(200, user)
+ }
+}
+
+// PutUser accepts a request to update an existing user in
+// the system. The modified user account is returned in JSON
+// format if successful.
+//
+// PUT /api/users/:name
+//
+func PutUser(c *gin.Context) {
+ ds := ToDatastore(c)
+ me := ToUser(c)
+ name := c.Params.ByName("name")
+ user, err := ds.GetUser(name)
+ if err != nil {
+ c.Fail(404, err)
+ return
+ }
+
+ in := &common.User{}
+ if !c.BindWith(in, binding.JSON) {
+ return
+ }
+ user.Email = in.Email
+ user.Gravatar = gravatar.Generate(user.Email)
+
+ // an administrator must not be able to
+ // downgrade her own account.
+ if me.Login != user.Login {
+ user.Admin = in.Admin
+ }
+
+ err = ds.UpdateUser(user)
+ if err != nil {
+ c.Fail(400, err)
+ } else {
+ c.JSON(200, user)
+ }
+}
+
+// DeleteUser accepts a request to delete the specified
+// user account from the system. A successful request will
+// respond with an OK 200 status.
+//
+// DELETE /api/users/:name
+//
+func DeleteUser(c *gin.Context) {
+ ds := ToDatastore(c)
+ me := ToUser(c)
+ name := c.Params.ByName("name")
+ user, err := ds.GetUser(name)
+ if err != nil {
+ c.Fail(404, err)
+ return
+ }
+
+ // an administrator must not be able to
+ // delete her own account.
+ if user.Login == me.Login {
+ c.Writer.WriteHeader(403)
+ return
+ }
+
+ if err := ds.DeleteUser(user); err != nil {
+ c.Fail(400, err)
+ } else {
+ c.Writer.WriteHeader(204)
+ }
+}
diff --git a/server/ws.go b/server/ws.go
new file mode 100644
index 000000000..670b5afcd
--- /dev/null
+++ b/server/ws.go
@@ -0,0 +1,92 @@
+package server
+
+import (
+ "time"
+
+ "github.com/drone/drone/eventbus"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+)
+
+const (
+ // Time allowed to write the message to the client.
+ writeWait = 10 * time.Second
+
+ // Time allowed to read the next pong message from the client.
+ pongWait = 60 * time.Second
+
+ // Send pings to client with this period. Must be less than pongWait.
+ pingPeriod = (pongWait * 9) / 10
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+// GetEvents will upgrade the connection to a Websocket and will stream
+// event updates to the browser.
+func GetEvents(c *gin.Context) {
+ bus := ToBus(c)
+ user := ToUser(c)
+ remote := ToRemote(c)
+
+ // upgrade the websocket
+ ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ c.Fail(400, err)
+ return
+ }
+
+ ticker := time.NewTicker(pingPeriod)
+ eventc := make(chan *eventbus.Event, 1)
+ bus.Subscribe(eventc)
+ defer func() {
+ bus.Unsubscribe(eventc)
+ ticker.Stop()
+ ws.Close()
+ close(eventc)
+ }()
+
+ go func() {
+ for {
+ select {
+ case event := <-eventc:
+ if event == nil {
+ return // why would this ever happen?
+ }
+ perms := perms(remote, user, event.Repo)
+ if perms != nil && perms.Pull {
+ ws.WriteJSON(event)
+ }
+ case <-ticker.C:
+ ws.SetWriteDeadline(time.Now().Add(writeWait))
+ err := ws.WriteMessage(websocket.PingMessage, []byte{})
+ if err != nil {
+ ws.Close()
+ return
+ }
+ }
+ }
+ }()
+
+ readWebsocket(ws)
+}
+
+// readWebsocket will block while reading the websocket data
+func readWebsocket(ws *websocket.Conn) {
+ defer ws.Close()
+ ws.SetReadLimit(512)
+ ws.SetReadDeadline(time.Now().Add(pongWait))
+ ws.SetPongHandler(func(string) error {
+ ws.SetReadDeadline(time.Now().Add(pongWait))
+ return nil
+ })
+ for {
+ _, _, err := ws.ReadMessage()
+ if err != nil {
+ break
+ }
+ }
+}