├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api ├── api.go └── helpers.go ├── cmd ├── root_cmd.go ├── serve_cmd.go └── version_cmd.go ├── conf ├── configuration.go ├── logger.go └── reflect.go ├── config.example.json ├── glide.lock ├── glide.yaml └── main.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 21 | 22 | **- Do you want to request a *feature* or report a *bug*?** 23 | 24 | **- What is the current behavior?** 25 | 26 | **- If the current behavior is a bug, please provide the steps to reproduce.** 27 | 28 | **- What is the expected behavior?** 29 | 30 | **- Please mention your Go version, and operating system version.** 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | **- Summary** 15 | 16 | 20 | 21 | **- Test plan** 22 | 23 | 27 | 28 | **- Description for the changelog** 29 | 30 | 34 | 35 | **- A picture of a cute animal (not mandatory but encouraged)** 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | config.json 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | 4 | go: 5 | - 1.8 6 | 7 | install: make deps 8 | script: make all 9 | notifications: 10 | email: false 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at david@netlify.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Contributions are always welcome, no matter how large or small. Before contributing, 4 | please read the [code of conduct](CODE_OF_CONDUCT.md). 5 | 6 | ## Setup 7 | 8 | > Install Go and Glide https://github.com/Masterminds/glide 9 | 10 | ```sh 11 | $ git clone https://github.com/netlify/gotiator 12 | $ cd gotiator 13 | $ make deps 14 | ``` 15 | 16 | ## Building 17 | 18 | ```sh 19 | $ make build 20 | ``` 21 | 22 | ## Testing 23 | 24 | ```sh 25 | $ make test 26 | ``` 27 | 28 | ## Pull Requests 29 | 30 | We actively welcome your pull requests. 31 | 32 | 1. Fork the repo and create your branch from `master`. 33 | 2. If you've added code that should be tested, add tests. 34 | 3. If you've changed APIs, update the documentation. 35 | 4. Ensure the test suite passes. 36 | 5. Make sure your code lints. 37 | 38 | ## License 39 | 40 | By contributing to Netlify CMS, you agree that your contributions will be licensed 41 | under its [MIT license](LICENSE). 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM calavera/go-glide:v0.12.2 2 | 3 | ADD . /go/src/github.com/netlify/gotiator 4 | 5 | RUN useradd -m netlify && cd /go/src/github.com/netlify/gotiator && make deps build && mv gotiator /usr/local/bin/ 6 | 7 | USER netlify 8 | CMD ["gotiator", "serve"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Netlify 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PONY: all build deps image lint test 2 | 3 | help: ## Show this help. 4 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 5 | 6 | all: test build ## Run the tests and build the binary. 7 | 8 | build: ## Build the binary. 9 | go build -ldflags "-X github.com/netlify/gotiator/cmd.Version=`git rev-parse HEAD`" 10 | 11 | deps: ## Install dependencies. 12 | @go get -u github.com/golang/lint/golint 13 | @go get -u github.com/Masterminds/glide && glide install 14 | 15 | image: ## Build the Docker image. 16 | docker build . 17 | 18 | lint: ## Lint the code 19 | golint `go list ./... | grep -v /vendor/` 20 | 21 | test: ## Run tests. 22 | go test -v `go list ./... | grep -v /vendor/` 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gotiator 2 | 3 | A tiny API Gateway based on [JWTs](https://jwt.io/). 4 | 5 | Gotiator can handle simple API proxying with signing for single page apps that already use JWTs for authentication. 6 | 7 | Gotiator Proxy is released under the [MIT License](LICENSE). 8 | Please make sure you understand its [implications and guarantees](https://writing.kemitchell.com/2016/09/21/MIT-License-Line-by-Line.html). 9 | 10 | ## Installing 11 | 12 | ``` 13 | go get github.com/netlify/gotiator 14 | gotiator serve 15 | ``` 16 | 17 | ## Configuration 18 | 19 | Settings can be set either by creating a `config.json` or setting `NETLIFY_` prefixed environment 20 | variables. IE.: 21 | 22 | ```json 23 | { 24 | "jwt": { 25 | "secret": "2134" 26 | } 27 | } 28 | ``` 29 | 30 | Is the same as: 31 | 32 | ``` 33 | GOTIATOR_JWT_SECRET=2134 gotiator serve 34 | ``` 35 | 36 | You must set your JWT secret (and we strongly recommend doing this with an environment variable) 37 | to match the JWT issuer (like [Auth0](https://auth0.com)) or [netlify-auth](https://github.com/netlify/netlify-auth). 38 | 39 | You configure API proxying from the config.json: 40 | 41 | ``` 42 | { 43 | "apis": [ 44 | {"name": "github", "url": "https://api.github.com/repos/netlify/gotiator", "roles": ["contributor"]} 45 | ] 46 | } 47 | ``` 48 | 49 | To sign outgoing requests with a Bearer token, you must set an environment variable with the token, 50 | based on the name of the API. If the API is called `github`, you must set: 51 | 52 | ``` 53 | NETLIFY_API_GITHUB=1234 54 | ``` 55 | 56 | The `roles` property specifies which roles should have access to the API. Roles should be encoded in the 57 | JWT claims under `app_metadata.roles`. Any request with a correctly signed JWT that includes one of the 58 | roles in it's `app_metadata` will be allowed to make requests to the API signed with your token via 59 | `/:api_name`. 60 | 61 | With the above example, a user with a JWT proving the claim that she has the role "contributor", can 62 | send signed requests to GitHub's API scoped to this repo, via: 63 | 64 | ``` 65 | GET|POST|DELETE|PATCH /github 66 | ``` 67 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | "os" 9 | "regexp" 10 | "strings" 11 | 12 | jwt "github.com/dgrijalva/jwt-go" 13 | "github.com/netlify/gotiator/conf" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type API struct { 18 | version string 19 | jwtSecret string 20 | apis []*apiProxy 21 | } 22 | 23 | type apiProxy struct { 24 | matcher *regexp.Regexp 25 | handler http.Handler 26 | token string 27 | roles []string 28 | } 29 | 30 | type JWTClaims struct { 31 | Email string `json:"email"` 32 | AppMetaData map[string]interface{} `json:"app_metadata"` 33 | UserMetaData map[string]interface{} `json:"user_metadata"` 34 | *jwt.StandardClaims 35 | } 36 | 37 | var bearerRegexp = regexp.MustCompile(`^(?i)Bearer (\S+$)`) 38 | 39 | func (a *API) Version(w http.ResponseWriter, r *http.Request) { 40 | sendJSON(w, 200, map[string]string{ 41 | "version": a.version, 42 | "name": "Gotiator", 43 | "description": "Gotiator is a dead simple API gateway", 44 | }) 45 | } 46 | 47 | func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) { 48 | if r.URL.Path == "/" { 49 | a.Version(w, r) 50 | } else { 51 | for _, proxy := range a.apis { 52 | if proxy.matcher.MatchString(r.URL.Path) { 53 | if a.authenticateProxy(w, r, proxy) { 54 | proxy.handler.ServeHTTP(w, r) 55 | } 56 | return 57 | } 58 | } 59 | 60 | http.Error(w, "Not Found", 404) 61 | } 62 | } 63 | 64 | func NewAPIWithVersion(config *conf.Configuration, version string) *API { 65 | api := &API{version: version, jwtSecret: config.JWT.Secret} 66 | 67 | for _, apiSettings := range config.APIs { 68 | proxy := &apiProxy{} 69 | proxy.matcher = regexp.MustCompile("^/" + apiSettings.Name + "/?") 70 | proxy.token = os.Getenv("NETLIFY_API_" + strings.ToUpper(apiSettings.Name)) 71 | proxy.roles = apiSettings.Roles 72 | 73 | target, err := url.Parse(apiSettings.URL) 74 | if err != nil { 75 | logrus.WithError(err).Fatalf("Error parsing URL for %v: %v", apiSettings.Name, apiSettings.URL) 76 | } 77 | targetQuery := target.RawQuery 78 | director := func(req *http.Request) { 79 | req.Host = target.Host 80 | req.URL.Scheme = target.Scheme 81 | req.URL.Host = target.Host 82 | req.URL.Path = singleJoiningSlash(target.Path, proxy.matcher.ReplaceAllString(req.URL.Path, "/")) 83 | if targetQuery == "" || req.URL.RawQuery == "" { 84 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 85 | } else { 86 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 87 | } 88 | if _, ok := req.Header["User-Agent"]; !ok { 89 | // explicitly disable User-Agent so it's not set to default value 90 | req.Header.Set("User-Agent", "") 91 | } 92 | if req.Method != http.MethodOptions { 93 | if proxy.token != "" { 94 | req.Header.Set("Authorization", "Bearer "+proxy.token) 95 | } else { 96 | req.Header.Del("Authorization") 97 | } 98 | } 99 | // Make sure we don't end up with double cors headers 100 | logrus.Infof("Proxying to: %v", req.URL) 101 | } 102 | 103 | proxy.handler = &httputil.ReverseProxy{Director: director} 104 | api.apis = append(api.apis, proxy) 105 | } 106 | 107 | return api 108 | } 109 | 110 | // ListenAndServe starts the REST API 111 | func (a *API) ListenAndServe(hostAndPort string) error { 112 | return http.ListenAndServe(hostAndPort, a) 113 | } 114 | 115 | func (a *API) authenticateProxy(w http.ResponseWriter, r *http.Request, proxy *apiProxy) bool { 116 | if r.Method == http.MethodOptions { 117 | return true 118 | } 119 | 120 | authHeader := r.Header.Get("Authorization") 121 | if authHeader == "" { 122 | UnauthorizedError(w, "This endpoint requires a Bearer token") 123 | return false 124 | } 125 | 126 | matches := bearerRegexp.FindStringSubmatch(authHeader) 127 | if len(matches) != 2 { 128 | UnauthorizedError(w, "This endpoint requires a Bearer token") 129 | return false 130 | } 131 | 132 | claims := JWTClaims{} 133 | p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} 134 | _, err := p.ParseWithClaims(matches[1], &claims, func(token *jwt.Token) (interface{}, error) { 135 | return []byte(a.jwtSecret), nil 136 | }) 137 | if err != nil { 138 | UnauthorizedError(w, fmt.Sprintf("Invalid token: %v", err)) 139 | return false 140 | } 141 | 142 | roles, ok := claims.AppMetaData["roles"] 143 | if ok { 144 | roleStrings, _ := roles.([]interface{}) 145 | for _, data := range roleStrings { 146 | role, _ := data.(string) 147 | for _, proxyRole := range proxy.roles { 148 | if role == proxyRole { 149 | return true 150 | } 151 | } 152 | } 153 | } 154 | 155 | UnauthorizedError(w, "Required role not found in JWT") 156 | return false 157 | } 158 | -------------------------------------------------------------------------------- /api/helpers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Error is an error with a message 10 | type Error struct { 11 | Code int `json:"code"` 12 | Message string `json:"msg"` 13 | } 14 | 15 | func sendJSON(w http.ResponseWriter, status int, obj interface{}) { 16 | w.Header().Set("Content-Type", "application/json") 17 | w.WriteHeader(status) 18 | encoder := json.NewEncoder(w) 19 | encoder.Encode(obj) 20 | } 21 | 22 | // UnauthorizedError is simple Error Wrapper 23 | func UnauthorizedError(w http.ResponseWriter, message string) { 24 | sendJSON(w, 401, &Error{Code: 401, Message: message}) 25 | } 26 | 27 | // From https://golang.org/src/net/http/httputil/reverseproxy.go?s=2298:2359#L72 28 | func singleJoiningSlash(a, b string) string { 29 | aslash := strings.HasSuffix(a, "/") 30 | bslash := strings.HasPrefix(b, "/") 31 | switch { 32 | case aslash && bslash: 33 | return a + b[1:] 34 | case !aslash && !bslash: 35 | return a + "/" + b 36 | } 37 | return a + b 38 | } 39 | -------------------------------------------------------------------------------- /cmd/root_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/netlify/gotiator/conf" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Short: "gotiator", 12 | Long: "gotiator", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | execWithConfig(cmd, serve) 15 | }, 16 | } 17 | 18 | func RootCmd() *cobra.Command { 19 | rootCmd.PersistentFlags().StringP("config", "c", "", "a config file to use") 20 | rootCmd.AddCommand(versionCmd) 21 | rootCmd.AddCommand(serveCmd) 22 | 23 | return rootCmd 24 | } 25 | 26 | func execWithConfig(cmd *cobra.Command, fn func(config *conf.Configuration)) { 27 | configFile, err := cmd.Flags().GetString("config") 28 | if err != nil { 29 | logrus.Fatalf("%+v", err) 30 | } 31 | 32 | config, err := conf.Load(configFile) 33 | if err != nil { 34 | logrus.Fatalf("Failed to load configration: %+v", err) 35 | } 36 | 37 | fn(config) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/serve_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/netlify/gotiator/api" 8 | "github.com/netlify/gotiator/conf" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var serveCmd = &cobra.Command{ 13 | Use: "serve", 14 | Long: "Start API server", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | execWithConfig(cmd, serve) 17 | }, 18 | } 19 | 20 | func serve(config *conf.Configuration) { 21 | api := api.NewAPIWithVersion(config, Version) 22 | 23 | l := fmt.Sprintf("%v:%v", config.API.Host, config.API.Port) 24 | logrus.Infof("Netlify Auth API started on: %s", l) 25 | api.ListenAndServe(l) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/version_cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var Version string 10 | 11 | var versionCmd = &cobra.Command{ 12 | Run: showVersion, 13 | Use: "version", 14 | } 15 | 16 | func showVersion(cmd *cobra.Command, args []string) { 17 | fmt.Println(Version) 18 | } 19 | -------------------------------------------------------------------------------- /conf/configuration.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/pkg/errors" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // Config is the main configuration for Gotiator 15 | type Configuration struct { 16 | LogConf LoggingConfig `mapstructure:"log_conf"` 17 | JWT struct { 18 | Secret string `mapstructure:"secret"` 19 | } `mapstructure:"jwt"` 20 | APIs []APISettings `mapstructure:"apis"` 21 | API struct { 22 | Host string `mapstructure:"host"` 23 | Port int `mapstructure:"port"` 24 | } `mapstructure:"api"` 25 | } 26 | 27 | /* APISettings holds the settings for the APIs to proxy 28 | The Name determines both the path prefix (ie. /github) and the 29 | environment variable for the access token (NETLIFY_API_GITHUB) 30 | The URL is the API URL (ie. https://api.github.com/repos/netlify/netlify-www) 31 | 32 | Only requests signed with a JWT with a matching JWT secret and a claim: 33 | {"app_metadata": {"roles": ["api-role"]}} 34 | 35 | Will be accepted (where the api-role matches one of the roles defined in the API settings) */ 36 | type APISettings struct { 37 | Name string `mapstructure:"name"` 38 | URL string `mapstructure:"url"` 39 | Roles []string `mapstructure:"roles"` 40 | 41 | Token string `mapstructure:"-"` 42 | } 43 | 44 | // Load will construct the config from the file `config.json` 45 | func Load(configFile string) (*Configuration, error) { 46 | viper.SetConfigType("json") 47 | 48 | if configFile != "" { 49 | viper.SetConfigFile(configFile) 50 | } else { 51 | viper.SetConfigName("config") 52 | viper.AddConfigPath("./") // ./config.[json | toml] 53 | viper.AddConfigPath("$HOME/.gotiator/") // ~/.netlify-commerce/config.[json | toml] 54 | } 55 | 56 | viper.SetEnvPrefix("gotiator") 57 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 58 | viper.AutomaticEnv() 59 | 60 | if err := viper.ReadInConfig(); err != nil && !os.IsNotExist(err) { 61 | return nil, errors.Wrap(err, "reading configuration from files") 62 | } 63 | 64 | config := new(Configuration) 65 | if err := viper.Unmarshal(config); err != nil { 66 | return nil, errors.Wrap(err, "unmarshaling configuration") 67 | } 68 | 69 | config, err := populateConfig(config) 70 | if err != nil { 71 | return nil, errors.Wrap(err, "populate config") 72 | } 73 | 74 | if err := configureLogging(config); err != nil { 75 | return nil, errors.Wrap(err, "configure logging") 76 | } 77 | 78 | return validateConfig(config) 79 | } 80 | 81 | func configureLogging(config *Configuration) error { 82 | // always use the full timestamp 83 | logrus.SetFormatter(&logrus.TextFormatter{ 84 | FullTimestamp: true, 85 | DisableTimestamp: false, 86 | }) 87 | 88 | // use a file if you want 89 | if config.LogConf.File != "" { 90 | f, errOpen := os.OpenFile(config.LogConf.File, os.O_RDWR|os.O_APPEND, 0660) 91 | if errOpen != nil { 92 | return errOpen 93 | } 94 | logrus.SetOutput(bufio.NewWriter(f)) 95 | logrus.Infof("Set output file to %s", config.LogConf.File) 96 | } 97 | 98 | if config.LogConf.Level != "" { 99 | level, err := logrus.ParseLevel(strings.ToUpper(config.LogConf.Level)) 100 | if err != nil { 101 | return err 102 | } 103 | logrus.SetLevel(level) 104 | logrus.Debug("Set log level to: " + logrus.GetLevel().String()) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func validateConfig(config *Configuration) (*Configuration, error) { 111 | if config.API.Port == 0 && os.Getenv("PORT") != "" { 112 | port, err := strconv.Atoi(os.Getenv("PORT")) 113 | if err != nil { 114 | return nil, errors.Wrap(err, "formatting PORT into int") 115 | } 116 | 117 | config.API.Port = port 118 | } 119 | 120 | if config.API.Port == 0 && config.API.Host == "" { 121 | config.API.Port = 8080 122 | } 123 | 124 | return config, nil 125 | } 126 | -------------------------------------------------------------------------------- /conf/logger.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type LoggingConfig struct { 11 | Level string `mapstructure:"log_level" json:"log_level"` 12 | File string `mapstructure:"log_file" json:"log_file"` 13 | } 14 | 15 | // ConfigureLogging will take the logging configuration and also adds 16 | // a few default parameters 17 | func ConfigureLogging(config *LoggingConfig) (*logrus.Entry, error) { 18 | hostname, err := os.Hostname() 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | // always use the full timestamp 24 | logrus.SetFormatter(&logrus.TextFormatter{ 25 | FullTimestamp: true, 26 | DisableTimestamp: false, 27 | }) 28 | 29 | // use a file if you want 30 | if config.File != "" { 31 | f, errOpen := os.OpenFile(config.File, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660) 32 | if errOpen != nil { 33 | return nil, errOpen 34 | } 35 | logrus.SetOutput(f) 36 | logrus.Infof("Set output file to %s", config.File) 37 | } 38 | 39 | if config.Level != "" { 40 | level, err := logrus.ParseLevel(strings.ToUpper(config.Level)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | logrus.SetLevel(level) 45 | logrus.Debug("Set log level to: " + logrus.GetLevel().String()) 46 | } 47 | 48 | return logrus.StandardLogger().WithField("hostname", hostname), nil 49 | } 50 | -------------------------------------------------------------------------------- /conf/reflect.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | const tagPrefix = "viper" 11 | 12 | func populateConfig(config *Configuration) (*Configuration, error) { 13 | err := recursivelySet(reflect.ValueOf(config), "") 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return config, nil 19 | } 20 | 21 | func recursivelySet(val reflect.Value, prefix string) error { 22 | if val.Kind() != reflect.Ptr { 23 | return errors.New("Must pass a pointer") 24 | } 25 | 26 | // dereference 27 | val = reflect.Indirect(val) 28 | if val.Kind() != reflect.Struct { 29 | return errors.New("must be a reference to a struct") 30 | } 31 | 32 | // grab the type for this instance 33 | vType := reflect.TypeOf(val.Interface()) 34 | 35 | // go through child fields 36 | for i := 0; i < val.NumField(); i++ { 37 | thisField := val.Field(i) 38 | thisType := vType.Field(i) 39 | tag := prefix + getTag(thisType) 40 | 41 | switch thisField.Kind() { 42 | case reflect.Struct: 43 | err := recursivelySet(thisField.Addr(), tag+".") 44 | if err != nil { 45 | return err 46 | } 47 | case reflect.Int: 48 | fallthrough 49 | case reflect.Int32: 50 | fallthrough 51 | case reflect.Int64: 52 | // you can only set with an int64 -> int 53 | configVal := int64(viper.GetInt(tag)) 54 | thisField.SetInt(configVal) 55 | case reflect.Bool: 56 | configVal := viper.GetBool(tag) 57 | thisField.SetBool(configVal) 58 | case reflect.String: 59 | configVal := viper.GetString(tag) 60 | thisField.SetString(configVal) 61 | default: 62 | return nil 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func getTag(field reflect.StructField) string { 70 | // check if maybe we have a special magic tag 71 | tag := field.Tag 72 | if tag != "" { 73 | for _, prefix := range []string{tagPrefix, "mapstructure", "json"} { 74 | if v := tag.Get(prefix); v != "" { 75 | return v 76 | } 77 | } 78 | } 79 | 80 | return field.Name 81 | } 82 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "jwt": { 3 | "secret": "IMPORTANT - YOU MUST CHANGE ME!" 4 | }, 5 | "apis": [ 6 | { 7 | "name": "github", 8 | "url": "https://api.github.com", 9 | "roles": ["github"] 10 | } 11 | ], 12 | "api": { 13 | "port": 9191, 14 | "host": "localhost" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: c3b4859cbfb4d11ef83d8a29975100c1873a120a5682b5e754cf52ee8723ecdf 2 | updated: 2016-12-20T19:32:42.715914305-08:00 3 | imports: 4 | - name: github.com/dgrijalva/jwt-go 5 | version: d2709f9f1f31ebcda9651b03077758c1f3a0018c 6 | - name: github.com/fsnotify/fsnotify 7 | version: f12c6236fe7b5cf6bcf30e5935d08cb079d78334 8 | - name: github.com/hashicorp/hcl 9 | version: 99df0eb941dd8ddbc83d3f3605a34f6a686ac85e 10 | subpackages: 11 | - hcl/ast 12 | - hcl/parser 13 | - hcl/scanner 14 | - hcl/strconv 15 | - hcl/token 16 | - json/parser 17 | - json/scanner 18 | - json/token 19 | - name: github.com/inconshreveable/mousetrap 20 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 21 | - name: github.com/kr/fs 22 | version: 2788f0dbd16903de03cb8186e5c7d97b69ad387b 23 | - name: github.com/magiconair/properties 24 | version: 0723e352fa358f9322c938cc2dadda874e9151a9 25 | - name: github.com/mitchellh/mapstructure 26 | version: ca63d7c062ee3c9f34db231e352b60012b4fd0c1 27 | - name: github.com/pelletier/go-buffruneio 28 | version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d 29 | - name: github.com/pelletier/go-toml 30 | version: 31055c2ff0bb0c7f9095aec0d220aed21108121e 31 | - name: github.com/pkg/errors 32 | version: 17b591df37844cde689f4d5813e5cea0927d8dd2 33 | - name: github.com/pkg/sftp 34 | version: 8197a2e580736b78d704be0fc47b2324c0591a32 35 | - name: github.com/rs/cors 36 | version: a62a804a8a009876ca59105f7899938a1349f4b3 37 | - name: github.com/rs/xhandler 38 | version: ed27b6fd65218132ee50cd95f38474a3d8a2cd12 39 | - name: github.com/sirupsen/logrus 40 | version: d26492970760ca5d33129d2d799e34be5c4782eb 41 | - name: github.com/spf13/afero 42 | version: 20500e2abd0d1f4564a499e83d11d6c73cd58c27 43 | subpackages: 44 | - mem 45 | - sftp 46 | - name: github.com/spf13/cast 47 | version: e31f36ffc91a2ba9ddb72a4b6a607ff9b3d3cb63 48 | - name: github.com/spf13/cobra 49 | version: 9c28e4bbd74e5c3ed7aacbc552b2cab7cfdfe744 50 | - name: github.com/spf13/jwalterweatherman 51 | version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 52 | - name: github.com/spf13/pflag 53 | version: c7e63cf4530bcd3ba943729cee0efeff2ebea63f 54 | - name: github.com/spf13/viper 55 | version: 16990631d4aa7e38f73dbbbf37fa13e67c648531 56 | - name: golang.org/x/crypto 57 | version: 81372b2fc2f10bef2a7f338da115c315a56b2726 58 | subpackages: 59 | - curve25519 60 | - ed25519 61 | - ed25519/internal/edwards25519 62 | - ssh 63 | - name: golang.org/x/net 64 | version: de35ec43e7a9aabd6a9c54d2898220ea7e44de7d 65 | subpackages: 66 | - context 67 | - name: golang.org/x/sys 68 | version: 30de6d19a3bd89a5f38ae4028e23aaa5582648af 69 | subpackages: 70 | - unix 71 | - name: golang.org/x/text 72 | version: 04b8648d973c126ae60143b3e1473bc1576c7597 73 | subpackages: 74 | - transform 75 | - unicode/norm 76 | - name: gopkg.in/yaml.v2 77 | version: 31c299268d302dd0aa9a0dcf765a3d58971ac83f 78 | testImports: [] 79 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/netlify/gotiator 2 | import: 3 | - package: github.com/sirupsen/logrus 4 | version: ~0.11.0 5 | - package: github.com/dgrijalva/jwt-go 6 | version: ~3.0.0 7 | - package: github.com/rs/cors 8 | version: ~1.0.0 9 | - package: github.com/spf13/cobra 10 | - package: github.com/spf13/viper 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/netlify/gotiator/cmd" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func main() { 9 | if err := cmd.RootCmd().Execute(); err != nil { 10 | logrus.WithError(err).Fatal("Failed to run root cmd") 11 | } 12 | } 13 | --------------------------------------------------------------------------------