diff --git a/Gopkg.lock b/Gopkg.lock index a2c53b8..76a447b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -73,6 +73,12 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" +[[projects]] + name = "github.com/rs/cors" + packages = ["."] + revision = "7af7a1e09ba336d2ea14b1ce73bf693c6837dbf6" + version = "v1.2" + [[projects]] name = "github.com/sirupsen/logrus" packages = ["."] @@ -118,6 +124,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "5dfb21b3ae89d0e1870a9330ead3dca39fa45ee2aa72eb3eedb39976bfbf3256" + inputs-digest = "dc429d4e2df1adc2ae9c6cb75a9df60fda34804b69610addb1bd964ece3f4150" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 58cf5d2..cfe7a7f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -60,3 +60,7 @@ [[constraint]] name = "github.com/gorilla/sessions" version = "1.1.0" + +[[constraint]] + name = "github.com/rs/cors" + version = "1.2.0" diff --git a/Makefile b/Makefile index ff5ddd5..713530a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Package configuration PROJECT = code-annotation -COMMANDS = server/cmd/code-annotation +COMMANDS = cli/server DEPENDENCIES = github.com/golang/dep/cmd/dep # Including ci Makefile @@ -12,8 +12,9 @@ CI_FOLDER = .ci YARN = yarn REMOVE = rm -rf -SERVER_URL ?= /api -API_PORT ?= 8080 +HOST ?= 127.0.0.1 +PORT ?= 8080 +SERVER_URL ?= //$(HOST):$(PORT) $(MAKEFILE): @git clone --quiet $(CI_REPOSITORY) $(CI_FOLDER); \ @@ -42,7 +43,7 @@ build: dependencies-frontend ## Compiles the dashboard assets, and serve the dashboard through its API serve: build - go run server/cmd/code-annotation/* + go run cli/server/server.go gorun: - go run server/cmd/code-annotation/* + go run cli/server/server.go diff --git a/cli/examples/import/example.sql b/cli/import/examples/example.sql similarity index 100% rename from cli/examples/import/example.sql rename to cli/import/examples/example.sql diff --git a/cli/server/server.go b/cli/server/server.go new file mode 100644 index 0000000..a5defd6 --- /dev/null +++ b/cli/server/server.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/kelseyhightower/envconfig" + + "github.com/src-d/code-annotation/server" + "github.com/src-d/code-annotation/server/repository" + "github.com/src-d/code-annotation/server/service" +) + +type appConfig struct { + Host string `envconfig:"HOST"` + Port int `envconfig:"PORT" default:"8080"` + UIDomain string `envconfig:"UI_DOMAIN" default:"http://127.0.0.1:8080"` +} + +func main() { + // main configuration + var conf appConfig + envconfig.MustProcess("", &conf) + + // create repos + userRepo := &repository.Users{} + + // create services + var oauthConfig service.OAuthConfig + envconfig.MustProcess("oauth", &oauthConfig) + oauth := service.NewOAuth(oauthConfig.ClientID, oauthConfig.ClientSecret) + + var jwtConfig service.JWTConfig + envconfig.MustProcess("jwt", &jwtConfig) + jwt := service.NewJWT(jwtConfig.SigningKey) + + // loger + logger := service.NewLogger() + + // start the router + router := server.Router(logger, jwt, oauth, conf.UIDomain, userRepo, "build") + logger.Info("running...") + err := http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Host, conf.Port), router) + logger.Fatal(err) +} diff --git a/server/cmd/code-annotation/main.go b/server/cmd/code-annotation/main.go deleted file mode 100644 index 2208b91..0000000 --- a/server/cmd/code-annotation/main.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "os" - - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" - "github.com/kelseyhightower/envconfig" - "github.com/sirupsen/logrus" - - "github.com/src-d/code-annotation/server/handler" - "github.com/src-d/code-annotation/server/repository" - "github.com/src-d/code-annotation/server/service" -) - -type appConfig struct { - Host string `envconfig:"HOST"` - Port int `envconfig:"PORT" default:"8080"` - UIDomain string `envconfig:"UI_DOMAIN" default:"http://127.0.0.1:8080"` -} - -func main() { - // main configuration - var conf appConfig - envconfig.MustProcess("", &conf) - - // create repos - userRepo := &repository.Users{} - - // create services - var oauthConfig service.OAuthConfig - envconfig.MustProcess("oauth", &oauthConfig) - oauth := service.NewOAuth(oauthConfig.ClientID, oauthConfig.ClientSecret) - - var jwtConfig service.JWTConfig - envconfig.MustProcess("jwt", &jwtConfig) - jwt := service.NewJWT(jwtConfig.SigningKey) - - // routing - r := chi.NewRouter() - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - r.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - headers := w.Header() - headers.Set("Access-Control-Allow-Origin", "*") - headers.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - headers.Set("Access-Control-Allow-Headers", "Location, Authorization") - if r.Method == "OPTIONS" { - return - } - h.ServeHTTP(w, r) - }) - }) - - r.Get("/login", handler.Login(oauth)) - r.Get("/oauth-callback", handler.OAuthCallback(oauth, jwt, userRepo, conf.UIDomain)) - - // protected handlers - r.Route("/api", func(r chi.Router) { - r.Use(jwt.Middleware) - - r.Get("/me", handler.Me(userRepo)) - }) - - // frontend - r.Get("/*", func(w http.ResponseWriter, r *http.Request) { - filepath := "build" + r.URL.Path - if _, err := os.Stat(filepath); err == nil { - http.ServeFile(w, r, filepath) - return - } - http.ServeFile(w, r, "build/index.html") - }) - - logrus.Info("running...") - if err := http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Host, conf.Port), r); err != nil { - panic(err) - } -} diff --git a/server/handler/assignments.go b/server/handler/assignments.go new file mode 100644 index 0000000..ca6075e --- /dev/null +++ b/server/handler/assignments.go @@ -0,0 +1,24 @@ +package handler + +import ( + "net/http" + + "github.com/src-d/code-annotation/server/serializer" +) + +func GetAssignmentsForUserExperiment() RequestProcessFunc { + return func(r *http.Request) (*serializer.Response, error) { + return nil, serializer.NewHTTPError(http.StatusNotImplemented) + } +} + +type assignmentRequest struct { + Answer string `json:"answer"` + Duration int `json:"duration"` +} + +func SaveAssignment() RequestProcessFunc { + return func(r *http.Request) (*serializer.Response, error) { + return nil, serializer.NewHTTPError(http.StatusNotImplemented) + } +} diff --git a/server/handler/auth.go b/server/handler/auth.go index 8794364..7c84d48 100644 --- a/server/handler/auth.go +++ b/server/handler/auth.go @@ -6,6 +6,7 @@ import ( "github.com/sirupsen/logrus" "github.com/src-d/code-annotation/server/repository" + "github.com/src-d/code-annotation/server/serializer" "github.com/src-d/code-annotation/server/service" ) @@ -18,33 +19,39 @@ func Login(oAuth *service.OAuth) http.HandlerFunc { } // OAuthCallback makes exchange with oauth provider, gets&creates user and redirects to index page with JWT token -func OAuthCallback(oAuth *service.OAuth, jwt *service.JWT, userRepo *repository.Users, uiDomain string) http.HandlerFunc { +func OAuthCallback( + oAuth *service.OAuth, + jwt *service.JWT, + userRepo *repository.Users, + uiDomain string, + logger logrus.FieldLogger, +) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := oAuth.ValidateState(r, r.FormValue("state")); err != nil { - writeResponse(w, respErr(http.StatusBadRequest, err.Error())) + write(w, r, serializer.NewEmptyResponse(), serializer.NewHTTPError(http.StatusBadRequest)) return } code := r.FormValue("code") user, err := oAuth.GetUser(r.Context(), code) if err != nil { - logrus.Errorf("oauth get user error: %s", err) + logger.Errorf("oauth get user error: %s", err) // FIXME can it be not server error? for wrong code - writeResponse(w, respInternalErr()) + write(w, r, serializer.NewEmptyResponse(), err) return } // FIXME with real repo we need to check does user exists already or not if err := userRepo.Create(user); err != nil { - logrus.Errorf("can't create user: %s", err) - writeResponse(w, respInternalErr()) + logger.Errorf("can't create user: %s", err) + write(w, r, serializer.NewEmptyResponse(), err) return } token, err := jwt.MakeToken(user) if err != nil { - logrus.Errorf("make jwt token error: %s", err) - writeResponse(w, respInternalErr()) + logger.Errorf("make jwt token error: %s", err) + write(w, r, serializer.NewEmptyResponse(), err) return } url := fmt.Sprintf("%s/?token=%s", uiDomain, token) diff --git a/server/handler/experiments.go b/server/handler/experiments.go new file mode 100644 index 0000000..53bdce6 --- /dev/null +++ b/server/handler/experiments.go @@ -0,0 +1,13 @@ +package handler + +import ( + "net/http" + + "github.com/src-d/code-annotation/server/serializer" +) + +func GetExperimentDetails() RequestProcessFunc { + return func(r *http.Request) (*serializer.Response, error) { + return nil, serializer.NewHTTPError(http.StatusNotImplemented) + } +} diff --git a/server/handler/file_pairs.go b/server/handler/file_pairs.go new file mode 100644 index 0000000..7113ddc --- /dev/null +++ b/server/handler/file_pairs.go @@ -0,0 +1,13 @@ +package handler + +import ( + "net/http" + + "github.com/src-d/code-annotation/server/serializer" +) + +func GetFilePairDetails() RequestProcessFunc { + return func(r *http.Request) (*serializer.Response, error) { + return nil, serializer.NewHTTPError(http.StatusNotImplemented) + } +} diff --git a/server/handler/render.go b/server/handler/render.go index 7421a1f..890885c 100644 --- a/server/handler/render.go +++ b/server/handler/render.go @@ -2,49 +2,63 @@ package handler import ( "encoding/json" + "fmt" "net/http" + + "github.com/src-d/code-annotation/server/serializer" + "github.com/src-d/code-annotation/server/service" ) -func respOK(d interface{}) response { - return response{ - statusCode: http.StatusOK, - Data: d, - } -} +func Get(rp RequestProcessFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + response, err := rp(r) + if response == nil { + response = serializer.NewEmptyResponse() + } -func respErr(statusCode int, msg string) response { - return response{ - statusCode: statusCode, - Errors: []errObj{errObj{Title: msg}}, + write(w, r, response, err) } } -func respInternalErr() response { - return respErr(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) -} - -type errObj struct { - Title string `json:"title"` -} +// write is the responsible of writing the response with the data from the passed *serializer.Response and error +// If the passed error has StatusCode, the http.Response will be returned with the StatusCode of the passed error +// If the passed error has not StatusCode, the http.Response will be returned as a http.StatusInternalServerError +func write(w http.ResponseWriter, r *http.Request, response *serializer.Response, err error) { + var statusCode int -type response struct { - statusCode int - Data interface{} `json:"data,omitempty"` - Errors []errObj `json:"errors,omitempty"` -} + // TODO: There should be no ppl calling write from the outside + if response == nil { + response = serializer.NewEmptyResponse() + } -type renderFunc func(r *http.Request) response + if err == nil { + statusCode = http.StatusOK + } else if httpError, ok := err.(serializer.HTTPError); ok { + statusCode = httpError.StatusCode() + response.Status = statusCode + response.Errors = []serializer.HTTPError{httpError} + } else { + statusCode = http.StatusInternalServerError + response.Status = statusCode + response.Errors = []serializer.HTTPError{serializer.NewHTTPError(statusCode, err.Error())} + } -func render(fn renderFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - writeResponse(w, fn(r)) + if statusCode >= http.StatusBadRequest { + service.NewLogger().Error(err.Error()) } -} -func writeResponse(w http.ResponseWriter, resp response) { - w.WriteHeader(resp.statusCode) - if err := json.NewEncoder(w).Encode(&resp); err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + content, err := json.Marshal(response) + if err != nil { + err = fmt.Errorf("response could not be marshalled; %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + service.NewLogger().Error(err.Error()) return } + + w.Header().Add("content-type", "application/json") + w.WriteHeader(statusCode) + w.Write(content) } + +// RequestProcessFunc is the function that takes a http.Request, and returns a serializer.Response and an error +type RequestProcessFunc func(*http.Request) (*serializer.Response, error) diff --git a/server/handler/static_files.go b/server/handler/static_files.go new file mode 100644 index 0000000..8e71b0e --- /dev/null +++ b/server/handler/static_files.go @@ -0,0 +1,21 @@ +package handler + +import ( + "net/http" + "os" +) + +// FrontendStatics handles the static-files requests, serving the files under the given staticsPath +// if useIndexFallback is set to true and the requested file does not exist, the staticsPath/index.html will be served +func FrontendStatics(staticsPath string, useIndexFallback bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + filepath := staticsPath + r.URL.Path + if useIndexFallback { + if _, err := os.Stat(filepath); err != nil { + filepath = staticsPath + "/index.html" + } + } + + http.ServeFile(w, r, filepath) + } +} diff --git a/server/handler/user.go b/server/handler/user.go index 8fd009b..1de5dff 100644 --- a/server/handler/user.go +++ b/server/handler/user.go @@ -1,23 +1,27 @@ package handler import ( + "fmt" "net/http" "github.com/src-d/code-annotation/server/repository" + "github.com/src-d/code-annotation/server/serializer" "github.com/src-d/code-annotation/server/service" ) // Me handler returns information about current user -func Me(usersRepo *repository.Users) http.HandlerFunc { - return render(func(r *http.Request) response { +func Me(usersRepo *repository.Users) RequestProcessFunc { + return func(r *http.Request) (*serializer.Response, error) { uID := service.GetUserID(r.Context()) if uID == 0 { - return respErr(http.StatusInternalServerError, "no user id in context") + return nil, fmt.Errorf("no user id in context") } + u, err := usersRepo.Get(uID) if err != nil { - return respErr(http.StatusNotFound, "user not found") + return nil, serializer.NewHTTPError(http.StatusNotFound, "user not found") } - return respOK(u) - }) + + return serializer.NewUserResponse(u), nil + } } diff --git a/server/model/models.go b/server/model/models.go new file mode 100644 index 0000000..6f14bcb --- /dev/null +++ b/server/model/models.go @@ -0,0 +1,53 @@ +package model + +type User struct { + ID int + Login string + Username string + AvatarURL string + Role Role +} + +type Experiment struct { + ID int + Name string + Description string +} + +type Assignment struct { + ID int + UserID int + PairID int + ExperimentID int + Answer string + Duration int +} + +type FilePairs struct { + ID int + ExperimentID int + Diff string + Left File + Right File +} + +type File struct { + Name string + Hash string + Content string +} + +type Role string + +const ( + Requester Role = "requester" + Worker Role = "worker" +) + +var Answers = map[string]string{ + "yes": "yes", + "maybe": "maybe", + "no": "no", + "skip": "skip", + "": "", +} diff --git a/server/model/user.go b/server/model/user.go deleted file mode 100644 index 0e38405..0000000 --- a/server/model/user.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type User struct { - ID int `json:"id"` - Login string `json:"login"` - Username string `json:"username"` - AvatarURL string `json:"avatarURL"` -} diff --git a/server/repository/repositories.go b/server/repository/repositories.go new file mode 100644 index 0000000..9424379 --- /dev/null +++ b/server/repository/repositories.go @@ -0,0 +1,68 @@ +//TODO: use "user.go" as a example, not this one ;) +package repository + +import ( + "fmt" + + "github.com/src-d/code-annotation/server/model" +) + +var assignments []*model.Assignment + +var ErrNoAssignmentsInitialized = fmt.Errorf("No assignments initialized") + +func GetExperimentByID(id int) (*model.Experiment, error) { + return &model.Experiment{ + ID: id, + Name: fmt.Sprintf("Experiment#%d", id), + Description: fmt.Sprintf("Description about experiment#%d", id), + }, nil +} + +func GetAssignmentsFor(userID int, experimentID int) ([]*model.Assignment, error) { + if len(assignments) == 0 { + return []*model.Assignment{}, ErrNoAssignmentsInitialized + } + + return assignments, nil +} + +func CreateAssignmentsFor(userID int, experimentID int) ([]*model.Assignment, error) { + assignments = []*model.Assignment{ + &model.Assignment{ID: 1, UserID: userID, PairID: 1, ExperimentID: experimentID}, + &model.Assignment{ID: 2, UserID: userID, PairID: 2, ExperimentID: experimentID}, + &model.Assignment{ID: 3, UserID: userID, PairID: 1, ExperimentID: experimentID}, + &model.Assignment{ID: 4, UserID: userID, PairID: 2, ExperimentID: experimentID}, + &model.Assignment{ID: 5, UserID: userID, PairID: 1, ExperimentID: experimentID}, + &model.Assignment{ID: 6, UserID: userID, PairID: 2, ExperimentID: experimentID}, + &model.Assignment{ID: 7, UserID: userID, PairID: 1, ExperimentID: experimentID}, + &model.Assignment{ID: 8, UserID: userID, PairID: 2, ExperimentID: experimentID}, + } + + return assignments, nil +} + +func UpdateAssignment(assignmentID int, answer string, duration int) error { + if _, ok := model.Answers[answer]; !ok { + return fmt.Errorf("Wrong answer provided: '%s'", answer) + } + + if assignmentID > len(assignments) { + return fmt.Errorf("Assignment #%d does not exist", assignmentID) + } + + assignments[assignmentID-1].Answer = answer + assignments[assignmentID-1].Duration = duration + return nil +} + +func GetFilePairFor(pairID int) (*model.FilePairs, error) { + name := fmt.Sprintf("filePair-%d", pairID) + return &model.FilePairs{ + ID: pairID, + ExperimentID: 1, + Diff: fmt.Sprintf("Diff of %s", name), + Left: model.File{Name: "left", Hash: "ABC", Content: fmt.Sprintf("%s left", name)}, + Right: model.File{Name: "right", Hash: "CBA", Content: fmt.Sprintf("%s right", name)}, + }, nil +} diff --git a/server/router.go b/server/router.go new file mode 100644 index 0000000..781f1e1 --- /dev/null +++ b/server/router.go @@ -0,0 +1,64 @@ +package server + +import ( + "net/http" + + "github.com/src-d/code-annotation/server/handler" + "github.com/src-d/code-annotation/server/repository" + "github.com/src-d/code-annotation/server/service" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/rs/cors" + "github.com/sirupsen/logrus" +) + +func Router( + logger logrus.FieldLogger, + jwt *service.JWT, + oauth *service.OAuth, + uiDomain string, + userRepo *repository.Users, + staticsPath string, +) http.Handler { + + // cors options + corsOptions := cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "OPTIONS"}, + AllowedHeaders: []string{"Location", "Authorization", "Content-Type"}, + } + + r := chi.NewRouter() + + r.Use(middleware.Recoverer) + r.Use(cors.New(corsOptions).Handler) + r.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger})) + + r.Get("/login", handler.Login(oauth)) + r.Get("/oauth-callback", handler.OAuthCallback(oauth, jwt, userRepo, uiDomain, logger)) + + r.Route("/api", func(r chi.Router) { + r.Use(jwt.Middleware) + + r.Get("/me", handler.Get(handler.Me(userRepo))) + + r.Route("/experiments/{experimentId}", func(r chi.Router) { + + r.Get("/", handler.Get(handler.GetExperimentDetails())) + + r.Route("/assignments", func(r chi.Router) { + + r.Get("/", handler.Get(handler.GetAssignmentsForUserExperiment())) + r.Put("/{assignmentId}", handler.Get(handler.SaveAssignment())) + }) + + r.Get("/file-pairs/{pairId}", handler.Get(handler.GetFilePairDetails())) + }) + }) + + r.Get("/static/*", handler.FrontendStatics(staticsPath, false)) + r.Get("/*", handler.FrontendStatics(staticsPath, true)) + + return r +} diff --git a/server/serializer/serializers.go b/server/serializer/serializers.go new file mode 100644 index 0000000..d29090a --- /dev/null +++ b/server/serializer/serializers.go @@ -0,0 +1,126 @@ +package serializer + +import ( + "net/http" + "strings" + + "github.com/src-d/code-annotation/server/model" +) + +// HTTPError defines an Error message as it will be written in the http.Response +type HTTPError interface { + error + StatusCode() int +} + +// Response encapsulate the content of an http.Response +type Response struct { + Status int `json:"status"` + Data interface{} `json:"data,omitempty"` + Errors []HTTPError `json:"errors,omitempty"` +} + +type httpError struct { + Status int `json:"status"` + Title string `json:"title"` + Details string `json:"details,omitempty"` +} + +// StatusCode returns the Status of the httpError +func (e httpError) StatusCode() int { + return e.Status +} + +// Error returns the string content of the httpError +func (e httpError) Error() string { + if msg := e.Title; msg != "" { + return msg + } + + if msg := http.StatusText(e.Status); msg != "" { + return msg + } + + return http.StatusText(http.StatusInternalServerError) +} + +// NewHTTPError returns an Error +func NewHTTPError(statusCode int, msg ...string) HTTPError { + return httpError{Status: statusCode, Title: strings.Join(msg, " ")} +} + +func newResponse(c interface{}) *Response { + if c == nil { + return &Response{ + Status: http.StatusNoContent, + } + } + + return &Response{ + Status: http.StatusOK, + Data: c, + } +} + +// NewEmptyResponse returns an empty Response +func NewEmptyResponse() *Response { + return &Response{} +} + +type experimentResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +func NewExperimentResponse(e *model.Experiment) *Response { + return newResponse(experimentResponse{ + ID: e.ID, + Name: e.Name, + Description: e.Description, + }) +} + +type assignmentResponse struct { + ID int `json:"id"` + PairID int `json:"pairId"` + Answer string `json:"answer"` + Duration int `json:"duration"` +} + +func NewAssignmentsResponse(as []*model.Assignment) *Response { + assignments := make([]assignmentResponse, len(as)) + for i, a := range as { + assignments[i] = assignmentResponse{a.ID, a.PairID, string(a.Answer), a.Duration} + } + + return newResponse(assignments) +} + +type filePairsResponse struct { + ID int `json:"id"` + Diff string `json:"diff"` +} + +func NewFilePairsResponse(fp *model.FilePairs) *Response { + return newResponse(filePairsResponse{fp.ID, fp.Diff}) +} + +type userResponse struct { + ID int `json:"id"` + Login string `json:"login"` + Username string `json:"username"` + AvatarURL string `json:"avatarURL"` +} + +func NewUserResponse(u *model.User) *Response { + return newResponse(userResponse{u.ID, u.Login, u.Username, u.AvatarURL}) +} + +type countResponse struct { + Count int `json:"count"` +} + +func NewCountResponse(c int) *Response { + return newResponse(countResponse{c}) +} diff --git a/server/service/logger.go b/server/service/logger.go new file mode 100644 index 0000000..342fc80 --- /dev/null +++ b/server/service/logger.go @@ -0,0 +1,15 @@ +package service + +import ( + "github.com/sirupsen/logrus" +) + +func NewLogger() logrus.FieldLogger { + logger := logrus.New() + logger.Formatter = &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + } + + return logger +} diff --git a/vendor/github.com/rs/cors/.travis.yml b/vendor/github.com/rs/cors/.travis.yml new file mode 100644 index 0000000..3baa364 --- /dev/null +++ b/vendor/github.com/rs/cors/.travis.yml @@ -0,0 +1,7 @@ +language: go +go: +- 1.8 +- tip +matrix: + allow_failures: + - go: tip diff --git a/vendor/github.com/rs/cors/LICENSE b/vendor/github.com/rs/cors/LICENSE new file mode 100644 index 0000000..d8e2df5 --- /dev/null +++ b/vendor/github.com/rs/cors/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Olivier Poitrey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/rs/cors/README.md b/vendor/github.com/rs/cors/README.md new file mode 100644 index 0000000..9e74902 --- /dev/null +++ b/vendor/github.com/rs/cors/README.md @@ -0,0 +1,100 @@ +# Go CORS handler [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/cors) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/cors/master/LICENSE) [![build](https://img.shields.io/travis/rs/cors.svg?style=flat)](https://travis-ci.org/rs/cors) [![Coverage](http://gocover.io/_badge/github.com/rs/cors)](http://gocover.io/github.com/rs/cors) + +CORS is a `net/http` handler implementing [Cross Origin Resource Sharing W3 specification](http://www.w3.org/TR/cors/) in Golang. + +## Getting Started + +After installing Go and setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH), create your first `.go` file. We'll call it `server.go`. + +```go +package main + +import ( + "net/http" + + "github.com/rs/cors" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + // cors.Default() setup the middleware with default options being + // all origins accepted with simple methods (GET, POST). See + // documentation below for more options. + handler := cors.Default().Handler(mux) + http.ListenAndServe(":8080", handler) +} +``` + +Install `cors`: + + go get github.com/rs/cors + +Then run your server: + + go run server.go + +The server now runs on `localhost:8080`: + + $ curl -D - -H 'Origin: http://foo.com' http://localhost:8080/ + HTTP/1.1 200 OK + Access-Control-Allow-Origin: foo.com + Content-Type: application/json + Date: Sat, 25 Oct 2014 03:43:57 GMT + Content-Length: 18 + + {"hello": "world"} + +### More Examples + +* `net/http`: [examples/nethttp/server.go](https://github.com/rs/cors/blob/master/examples/nethttp/server.go) +* [Goji](https://goji.io): [examples/goji/server.go](https://github.com/rs/cors/blob/master/examples/goji/server.go) +* [Martini](http://martini.codegangsta.io): [examples/martini/server.go](https://github.com/rs/cors/blob/master/examples/martini/server.go) +* [Negroni](https://github.com/codegangsta/negroni): [examples/negroni/server.go](https://github.com/rs/cors/blob/master/examples/negroni/server.go) +* [Alice](https://github.com/justinas/alice): [examples/alice/server.go](https://github.com/rs/cors/blob/master/examples/alice/server.go) +* [HttpRouter](https://github.com/julienschmidt/httprouter): [examples/httprouter/server.go](https://github.com/rs/cors/blob/master/examples/httprouter/server.go) + +## Parameters + +Parameters are passed to the middleware thru the `cors.New` method as follow: + +```go +c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + AllowCredentials: true, +}) + +// Insert the middleware +handler = c.Handler(handler) +``` + +* **AllowedOrigins** `[]string`: A list of origins a cross-domain request can be executed from. If the special `*` value is present in the list, all origins will be allowed. An origin may contain a wildcard (`*`) to replace 0 or more characters (i.e.: `http://*.domain.com`). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin. The default value is `*`. +* **AllowOriginFunc** `func (origin string) bool`: A custom function to validate the origin. It take the origin as argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins` is ignored +* **AllowedMethods** `[]string`: A list of methods the client is allowed to use with cross-domain requests. Default value is simple methods (`GET` and `POST`). +* **AllowedHeaders** `[]string`: A list of non simple headers the client is allowed to use with cross-domain requests. +* **ExposedHeaders** `[]string`: Indicates which headers are safe to expose to the API of a CORS API specification +* **AllowCredentials** `bool`: Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates. The default is `false`. +* **MaxAge** `int`: Indicates how long (in seconds) the results of a preflight request can be cached. The default is `0` which stands for no max age. +* **OptionsPassthrough** `bool`: Instructs preflight to let other potential next handlers to process the `OPTIONS` method. Turn this on if your application handles `OPTIONS`. +* **Debug** `bool`: Debugging flag adds additional output to debug server side CORS issues. + +See [API documentation](http://godoc.org/github.com/rs/cors) for more info. + +## Benchmarks + + BenchmarkWithout 20000000 64.6 ns/op 8 B/op 1 allocs/op + BenchmarkDefault 3000000 469 ns/op 114 B/op 2 allocs/op + BenchmarkAllowedOrigin 3000000 608 ns/op 114 B/op 2 allocs/op + BenchmarkPreflight 20000000 73.2 ns/op 0 B/op 0 allocs/op + BenchmarkPreflightHeader 20000000 73.6 ns/op 0 B/op 0 allocs/op + BenchmarkParseHeaderList 2000000 847 ns/op 184 B/op 6 allocs/op + BenchmarkParse…Single 5000000 290 ns/op 32 B/op 3 allocs/op + BenchmarkParse…Normalized 2000000 776 ns/op 160 B/op 6 allocs/op + +## Licenses + +All source code is licensed under the [MIT License](https://raw.github.com/rs/cors/master/LICENSE). diff --git a/vendor/github.com/rs/cors/bench_test.go b/vendor/github.com/rs/cors/bench_test.go new file mode 100644 index 0000000..b6e3721 --- /dev/null +++ b/vendor/github.com/rs/cors/bench_test.go @@ -0,0 +1,88 @@ +package cors + +import ( + "net/http" + "testing" +) + +type FakeResponse struct { + header http.Header +} + +func (r FakeResponse) Header() http.Header { + return r.header +} + +func (r FakeResponse) WriteHeader(n int) { +} + +func (r FakeResponse) Write(b []byte) (n int, err error) { + return len(b), nil +} + +func BenchmarkWithout(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + testHandler.ServeHTTP(res, req) + } +} + +func BenchmarkDefault(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "somedomain.com") + handler := Default().Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} + +func BenchmarkAllowedOrigin(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "somedomain.com") + c := New(Options{ + AllowedOrigins: []string{"somedomain.com"}, + }) + handler := c.Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} + +func BenchmarkPreflight(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Access-Control-Request-Method", "GET") + handler := Default().Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} + +func BenchmarkPreflightHeader(b *testing.B) { + res := FakeResponse{http.Header{}} + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Access-Control-Request-Method", "GET") + req.Header.Add("Access-Control-Request-Headers", "Accept") + handler := Default().Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(res, req) + } +} diff --git a/vendor/github.com/rs/cors/cors.go b/vendor/github.com/rs/cors/cors.go new file mode 100644 index 0000000..8179c61 --- /dev/null +++ b/vendor/github.com/rs/cors/cors.go @@ -0,0 +1,407 @@ +/* +Package cors is net/http handler to handle CORS related requests +as defined by http://www.w3.org/TR/cors/ + +You can configure it by passing an option struct to cors.New: + + c := cors.New(cors.Options{ + AllowedOrigins: []string{"foo.com"}, + AllowedMethods: []string{"GET", "POST", "DELETE"}, + AllowCredentials: true, + }) + +Then insert the handler in the chain: + + handler = c.Handler(handler) + +See Options documentation for more options. + +The resulting handler is a standard net/http handler. +*/ +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + // AllowOriginFunc is a custom function to validate the origin. It take the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(origin string) bool + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (GET and POST) + AllowedMethods []string + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Cors http handler +type Cors struct { + // Debug logger + Log *log.Logger + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + // Normalized list of plain allowed origins + allowedOrigins []string + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + // Optional origin validator function + allowOriginFunc func(origin string) bool + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + // Normalized list of allowed headers + allowedHeaders []string + // Normalized list of allowed methods + allowedMethods []string + // Normalized list of exposed headers + exposedHeaders []string + allowCredentials bool + maxAge int + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1 : len(origin)]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{"GET", "POST"} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Default creates a new Cors handler with default options. +func Default() *Cors { + return New(Options{}) +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"*"}, + AllowCredentials: true, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + h.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + h.ServeHTTP(w, r) + } + }) +} + +// HandlerFunc provides Martini compatible handler +func (c *Cors) HandlerFunc(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("HandlerFunc: Preflight request") + c.handlePreflight(w, r) + } else { + c.logf("HandlerFunc: Actual request") + c.handleActualRequest(w, r) + } +} + +// Negroni compatible interface +func (c *Cors) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("ServeHTTP: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("ServeHTTP: Actual request") + c.handleActualRequest(w, r) + next(w, r) + } +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != "OPTIONS" { + c.logf(" Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf(" Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(origin) { + c.logf(" Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf(" Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll && !c.allowCredentials { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf(" Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method == "OPTIONS" { + c.logf(" Actual request no headers added: method == %s", r.Method) + return + } + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf(" Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(origin) { + c.logf(" Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf(" Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll && !c.allowCredentials { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf(" Actual response added headers: %v", headers) +} + +// convenience method. checks if debugging is turned on before printing +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoing +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == "OPTIONS" { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + } + } + if !found { + return false + } + } + return true +} diff --git a/vendor/github.com/rs/cors/cors_test.go b/vendor/github.com/rs/cors/cors_test.go new file mode 100644 index 0000000..916c101 --- /dev/null +++ b/vendor/github.com/rs/cors/cors_test.go @@ -0,0 +1,502 @@ +package cors + +import ( + "net/http" + "net/http/httptest" + "regexp" + "strings" + "testing" +) + +var testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("bar")) +}) + +var allHeaders = []string{ + "Vary", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Credentials", + "Access-Control-Max-Age", + "Access-Control-Expose-Headers", +} + +func assertHeaders(t *testing.T, resHeaders http.Header, expHeaders map[string]string) { + for _, name := range allHeaders { + got := strings.Join(resHeaders[name], ", ") + want := expHeaders[name] + if got != want { + t.Errorf("Response header %q = %q, want %q", name, got, want) + } + } +} + +func assertResponse(t *testing.T, res *httptest.ResponseRecorder, responseCode int) { + if responseCode != res.Code { + t.Errorf("assertResponse: expected response code to be %d but got %d. ", responseCode, res.Code) + } +} + +func TestSpec(t *testing.T) { + cases := []struct { + name string + options Options + method string + reqHeaders map[string]string + resHeaders map[string]string + }{ + { + "NoConfig", + Options{ + // Intentionally left blank. + }, + "GET", + map[string]string{}, + map[string]string{ + "Vary": "Origin", + }, + }, + { + "MatchAllOrigin", + Options{ + AllowedOrigins: []string{"*"}, + }, + "GET", + map[string]string{ + "Origin": "http://foobar.com", + }, + map[string]string{ + "Vary": "Origin", + "Access-Control-Allow-Origin": "*", + }, + }, + { + "MatchAllOriginWithCredentials", + Options{ + AllowedOrigins: []string{"*"}, + AllowCredentials: true, + }, + "GET", + map[string]string{ + "Origin": "http://foobar.com", + }, + map[string]string{ + "Vary": "Origin", + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Credentials": "true", + }, + }, + { + "AllowedOrigin", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + }, + "GET", + map[string]string{ + "Origin": "http://foobar.com", + }, + map[string]string{ + "Vary": "Origin", + "Access-Control-Allow-Origin": "http://foobar.com", + }, + }, + { + "WildcardOrigin", + Options{ + AllowedOrigins: []string{"http://*.bar.com"}, + }, + "GET", + map[string]string{ + "Origin": "http://foo.bar.com", + }, + map[string]string{ + "Vary": "Origin", + "Access-Control-Allow-Origin": "http://foo.bar.com", + }, + }, + { + "DisallowedOrigin", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + }, + "GET", + map[string]string{ + "Origin": "http://barbaz.com", + }, + map[string]string{ + "Vary": "Origin", + }, + }, + { + "DisallowedWildcardOrigin", + Options{ + AllowedOrigins: []string{"http://*.bar.com"}, + }, + "GET", + map[string]string{ + "Origin": "http://foo.baz.com", + }, + map[string]string{ + "Vary": "Origin", + }, + }, + { + "AllowedOriginFuncMatch", + Options{ + AllowOriginFunc: func(o string) bool { + return regexp.MustCompile("^http://foo").MatchString(o) + }, + }, + "GET", + map[string]string{ + "Origin": "http://foobar.com", + }, + map[string]string{ + "Vary": "Origin", + "Access-Control-Allow-Origin": "http://foobar.com", + }, + }, + { + "AllowedOriginFuncNotMatch", + Options{ + AllowOriginFunc: func(o string) bool { + return regexp.MustCompile("^http://foo").MatchString(o) + }, + }, + "GET", + map[string]string{ + "Origin": "http://barfoo.com", + }, + map[string]string{ + "Vary": "Origin", + }, + }, + { + "MaxAge", + Options{ + AllowedOrigins: []string{"http://example.com/"}, + AllowedMethods: []string{"GET"}, + MaxAge: 10, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://example.com/", + "Access-Control-Request-Method": "GET", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + "Access-Control-Allow-Origin": "http://example.com/", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Max-Age": "10", + }, + }, + { + "AllowedMethod", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedMethods: []string{"PUT", "DELETE"}, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "PUT", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "PUT", + }, + }, + { + "DisallowedMethod", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedMethods: []string{"PUT", "DELETE"}, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "PATCH", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + }, + }, + { + "AllowedHeaders", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedHeaders: []string{"X-Header-1", "x-header-2"}, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "X-Header-2, X-HEADER-1", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "X-Header-2, X-Header-1", + }, + }, + { + "AllowedWildcardHeader", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedHeaders: []string{"*"}, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "X-Header-2, X-HEADER-1", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "X-Header-2, X-Header-1", + }, + }, + { + "DisallowedHeader", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowedHeaders: []string{"X-Header-1", "x-header-2"}, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "X-Header-3, X-Header-1", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + }, + }, + { + "OriginHeader", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "origin", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "Origin", + }, + }, + { + "ExposedHeader", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + ExposedHeaders: []string{"X-Header-1", "x-header-2"}, + }, + "GET", + map[string]string{ + "Origin": "http://foobar.com", + }, + map[string]string{ + "Vary": "Origin", + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Expose-Headers": "X-Header-1, X-Header-2", + }, + }, + { + "AllowedCredentials", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + AllowCredentials: true, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "GET", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + "Access-Control-Allow-Origin": "http://foobar.com", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Credentials": "true", + }, + }, + { + "OptionPassthrough", + Options{ + OptionsPassthrough: true, + }, + "OPTIONS", + map[string]string{ + "Origin": "http://foobar.com", + "Access-Control-Request-Method": "GET", + }, + map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET", + }, + }, + { + "NonPreflightOptions", + Options{ + AllowedOrigins: []string{"http://foobar.com"}, + }, + "OPTIONS", + map[string]string{}, + map[string]string{}, + }, + } + for i := range cases { + tc := cases[i] + t.Run(tc.name, func(t *testing.T) { + s := New(tc.options) + + req, _ := http.NewRequest(tc.method, "http://example.com/foo", nil) + for name, value := range tc.reqHeaders { + req.Header.Add(name, value) + } + + t.Run("Handler", func(t *testing.T) { + res := httptest.NewRecorder() + s.Handler(testHandler).ServeHTTP(res, req) + assertHeaders(t, res.Header(), tc.resHeaders) + }) + t.Run("HandlerFunc", func(t *testing.T) { + res := httptest.NewRecorder() + s.HandlerFunc(res, req) + assertHeaders(t, res.Header(), tc.resHeaders) + }) + t.Run("Negroni", func(t *testing.T) { + res := httptest.NewRecorder() + s.ServeHTTP(res, req, testHandler) + assertHeaders(t, res.Header(), tc.resHeaders) + }) + + }) + } +} + +func TestDebug(t *testing.T) { + s := New(Options{ + Debug: true, + }) + + if s.Log == nil { + t.Error("Logger not created when debug=true") + } +} + +func TestDefault(t *testing.T) { + s := Default() + if s.Log != nil { + t.Error("c.log should be nil when Default") + } + if !s.allowedOriginsAll { + t.Error("c.allowedOriginsAll should be true when Default") + } + if s.allowedHeaders == nil { + t.Error("c.allowedHeaders should be nil when Default") + } + if s.allowedMethods == nil { + t.Error("c.allowedMethods should be nil when Default") + } +} + +func TestHandlePreflightInvlaidOriginAbortion(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://example.com/") + + s.handlePreflight(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", + }) +} + +func TestHandlePreflightNoOptionsAbortion(t *testing.T) { + s := New(Options{ + // Intentionally left blank. + }) + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + + s.handlePreflight(res, req) + + assertHeaders(t, res.Header(), map[string]string{}) +} + +func TestHandleActualRequestAbortsOptionsMethod(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + res := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://example.com/") + + s.handleActualRequest(res, req) + + assertHeaders(t, res.Header(), map[string]string{}) +} + +func TestHandleActualRequestInvlaidOriginAbortion(t *testing.T) { + s := New(Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://example.com/") + + s.handleActualRequest(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Vary": "Origin", + }) +} + +func TestHandleActualRequestInvlaidMethodAbortion(t *testing.T) { + s := New(Options{ + AllowedMethods: []string{"POST"}, + AllowCredentials: true, + }) + res := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://example.com/foo", nil) + req.Header.Add("Origin", "http://example.com/") + + s.handleActualRequest(res, req) + + assertHeaders(t, res.Header(), map[string]string{ + "Vary": "Origin", + }) +} + +func TestIsMethodAllowedReturnsFalseWithNoMethods(t *testing.T) { + s := New(Options{ + // Intentionally left blank. + }) + s.allowedMethods = []string{} + if s.isMethodAllowed("") { + t.Error("IsMethodAllowed should return false when c.allowedMethods is nil.") + } +} + +func TestIsMethodAllowedReturnsTrueWithOptions(t *testing.T) { + s := New(Options{ + // Intentionally left blank. + }) + if !s.isMethodAllowed("OPTIONS") { + t.Error("IsMethodAllowed should return true when c.allowedMethods is nil.") + } +} diff --git a/vendor/github.com/rs/cors/examples/alice/server.go b/vendor/github.com/rs/cors/examples/alice/server.go new file mode 100644 index 0000000..0a3e15c --- /dev/null +++ b/vendor/github.com/rs/cors/examples/alice/server.go @@ -0,0 +1,24 @@ +package main + +import ( + "net/http" + + "github.com/justinas/alice" + "github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + chain := alice.New(c.Handler).Then(mux) + http.ListenAndServe(":8080", chain) +} diff --git a/vendor/github.com/rs/cors/examples/default/server.go b/vendor/github.com/rs/cors/examples/default/server.go new file mode 100644 index 0000000..ccb5e1b --- /dev/null +++ b/vendor/github.com/rs/cors/examples/default/server.go @@ -0,0 +1,19 @@ +package main + +import ( + "net/http" + + "github.com/rs/cors" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + // Use default options + handler := cors.Default().Handler(mux) + http.ListenAndServe(":8080", handler) +} diff --git a/vendor/github.com/rs/cors/examples/goji/server.go b/vendor/github.com/rs/cors/examples/goji/server.go new file mode 100644 index 0000000..1fb4073 --- /dev/null +++ b/vendor/github.com/rs/cors/examples/goji/server.go @@ -0,0 +1,22 @@ +package main + +import ( + "net/http" + + "github.com/rs/cors" + "github.com/zenazn/goji" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + goji.Use(c.Handler) + + goji.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + goji.Serve() +} diff --git a/vendor/github.com/rs/cors/examples/gorilla/server.go b/vendor/github.com/rs/cors/examples/gorilla/server.go new file mode 100644 index 0000000..c24fb74 --- /dev/null +++ b/vendor/github.com/rs/cors/examples/gorilla/server.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/cors" +) + +func main() { + r := mux.NewRouter() + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + // Use default options + handler := cors.Default().Handler(r) + http.ListenAndServe(":8080", handler) +} diff --git a/vendor/github.com/rs/cors/examples/httprouter/server.go b/vendor/github.com/rs/cors/examples/httprouter/server.go new file mode 100644 index 0000000..fc123e3 --- /dev/null +++ b/vendor/github.com/rs/cors/examples/httprouter/server.go @@ -0,0 +1,22 @@ +package main + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/rs/cors" +) + +func Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) +} + +func main() { + router := httprouter.New() + router.GET("/", Hello) + + handler := cors.Default().Handler(router) + + http.ListenAndServe(":8080", handler) +} diff --git a/vendor/github.com/rs/cors/examples/martini/server.go b/vendor/github.com/rs/cors/examples/martini/server.go new file mode 100644 index 0000000..081af32 --- /dev/null +++ b/vendor/github.com/rs/cors/examples/martini/server.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/go-martini/martini" + "github.com/martini-contrib/render" + "github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + m := martini.Classic() + m.Use(render.Renderer()) + m.Use(c.HandlerFunc) + + m.Get("/", func(r render.Render) { + r.JSON(200, map[string]interface{}{"hello": "world"}) + }) + + m.Run() +} diff --git a/vendor/github.com/rs/cors/examples/negroni/server.go b/vendor/github.com/rs/cors/examples/negroni/server.go new file mode 100644 index 0000000..3cb33bf --- /dev/null +++ b/vendor/github.com/rs/cors/examples/negroni/server.go @@ -0,0 +1,26 @@ +package main + +import ( + "net/http" + + "github.com/codegangsta/negroni" + "github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + n := negroni.Classic() + n.Use(c) + n.UseHandler(mux) + n.Run(":3000") +} diff --git a/vendor/github.com/rs/cors/examples/nethttp/server.go b/vendor/github.com/rs/cors/examples/nethttp/server.go new file mode 100644 index 0000000..eaa775e --- /dev/null +++ b/vendor/github.com/rs/cors/examples/nethttp/server.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + + "github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://foo.com"}, + }) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + http.ListenAndServe(":8080", c.Handler(handler)) +} diff --git a/vendor/github.com/rs/cors/examples/openbar/server.go b/vendor/github.com/rs/cors/examples/openbar/server.go new file mode 100644 index 0000000..0940423 --- /dev/null +++ b/vendor/github.com/rs/cors/examples/openbar/server.go @@ -0,0 +1,22 @@ +package main + +import ( + "net/http" + + "github.com/rs/cors" +) + +func main() { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowCredentials: true, + }) + + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"hello\": \"world\"}")) + }) + + http.ListenAndServe(":8080", c.Handler(h)) +} diff --git a/vendor/github.com/rs/cors/utils.go b/vendor/github.com/rs/cors/utils.go new file mode 100644 index 0000000..c7a0aa0 --- /dev/null +++ b/vendor/github.com/rs/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' || b == '_' + } + } + return headers +} diff --git a/vendor/github.com/rs/cors/utils_test.go b/vendor/github.com/rs/cors/utils_test.go new file mode 100644 index 0000000..83053b3 --- /dev/null +++ b/vendor/github.com/rs/cors/utils_test.go @@ -0,0 +1,70 @@ +package cors + +import ( + "strings" + "testing" +) + +func TestWildcard(t *testing.T) { + w := wildcard{"foo", "bar"} + if !w.match("foobar") { + t.Error("foo*bar should match foobar") + } + if !w.match("foobazbar") { + t.Error("foo*bar should match foobazbar") + } + if w.match("foobaz") { + t.Error("foo*bar should not match foobaz") + } + + w = wildcard{"foo", "oof"} + if w.match("foof") { + t.Error("foo*oof should not match foof") + } +} + +func TestConvert(t *testing.T) { + s := convert([]string{"A", "b", "C"}, strings.ToLower) + e := []string{"a", "b", "c"} + if s[0] != e[0] || s[1] != e[1] || s[2] != e[2] { + t.Errorf("%v != %v", s, e) + } +} + +func TestParseHeaderList(t *testing.T) { + h := parseHeaderList("header, second-header, THIRD-HEADER, Numb3r3d-H34d3r") + e := []string{"Header", "Second-Header", "Third-Header", "Numb3r3d-H34d3r"} + if h[0] != e[0] || h[1] != e[1] || h[2] != e[2] { + t.Errorf("%v != %v", h, e) + } +} + +func TestParseHeaderListEmpty(t *testing.T) { + if len(parseHeaderList("")) != 0 { + t.Error("should be empty sclice") + } + if len(parseHeaderList(" , ")) != 0 { + t.Error("should be empty sclice") + } +} + +func BenchmarkParseHeaderList(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + parseHeaderList("header, second-header, THIRD-HEADER") + } +} + +func BenchmarkParseHeaderListSingle(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + parseHeaderList("header") + } +} + +func BenchmarkParseHeaderListNormalized(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + parseHeaderList("Header1, Header2, Third-Header") + } +}