Merge pull request #20 from jehiah/config_support_20

add option/flag to specify config file in place of commandline options
This commit is contained in:
Jehiah Czebotar 2014-11-10 02:34:37 +01:00
commit 01969eebdc
8 changed files with 243 additions and 118 deletions

View File

@ -2,6 +2,8 @@ language: go
install: install:
- go get github.com/bmizerany/assert - go get github.com/bmizerany/assert
- go get github.com/bitly/go-simplejson - go get github.com/bitly/go-simplejson
- go get github.com/mreiferson/go-options
- go get github.com/BurntSushi/toml
notifications: notifications:
email: false email: false

View File

@ -42,28 +42,40 @@ intend to run `google_auth_proxy` on.
5. Take note of the **Client ID** and **Client Secret** 5. Take note of the **Client ID** and **Client Secret**
## Command Line Options ## Configuration
`google_auth_proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).
### Config File
An example [google_auth_proxy.cfg](contrib/google_auth_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/google_auth_proxy.cfg`
### Command Line Options
``` ```
Usage of ./google_auth_proxy: Usage of google_auth_proxy:
-authenticated-emails-file="": authenticate against emails via file (one per line) -authenticated-emails-file="": authenticate against emails via file (one per line)
-client-id="": the Google OAuth Client ID: ie: "123456.apps.googleusercontent.com" -client-id="": the Google OAuth Client ID: ie: "123456.apps.googleusercontent.com"
-client-secret="": the OAuth Client Secret -client-secret="": the OAuth Client Secret
-cookie-domain="": an optional cookie domain to force cookies to -config="": path to config file
-cookie-expire=168h: expire timeframe for cookie -cookie-domain="": an optional cookie domain to force cookies to (ie: .yourcompany.com)
-cookie-expire=168h0m0s: expire timeframe for cookie
-cookie-https-only=false: set HTTPS only cookie -cookie-https-only=false: set HTTPS only cookie
-cookie-secret="": the seed string for secure cookies -cookie-secret="": the seed string for secure cookies
-google-apps-domain=[]: authenticate against the given google apps domain (may be given multiple times) -google-apps-domain=: authenticate against the given Google apps domain (may be given multiple times)
-htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption -htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
-http-address="127.0.0.1:4180": <addr>:<port> to listen on for HTTP clients -http-address="127.0.0.1:4180": <addr>:<port> to listen on for HTTP clients
-pass-basic-auth=true: pass HTTP Basic Auth information to upstream -pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
-redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
-upstream=[]: the http url(s) of the upstream endpoint. If multiple, routing is based on path -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path
-version=false: print version string -version=false: print version string
``` ```
### Environment variables
## Example Configuration The environment variables `google_auth_client_id`, `google_auth_secret` and `google_auth_cookie_secret` can be used in place of the corresponding command-line arguments.
### Example Nginx Configuration
This example has a [Nginx](http://nginx.org/) SSL endpoint proxying to `google_auth_proxy` on port `4180`. This example has a [Nginx](http://nginx.org/) SSL endpoint proxying to `google_auth_proxy` on port `4180`.
`google_auth_proxy` then authenticates requests for an upstream application running on port `8080`. The external `google_auth_proxy` then authenticates requests for an upstream application running on port `8080`. The external
@ -105,13 +117,10 @@ The command line to run `google_auth_proxy` would look like this:
--client-secret=... --client-secret=...
``` ```
## Environment variables
The environment variables `google_auth_client_id`, `google_auth_secret` and `google_auth_cookie_secret` can be used in place of the corresponding command-line arguments.
## Endpoint Documentation ## Endpoint Documentation
Google Auth Proxy responds directly to the following endpoints. All other endpoints will be authenticated. Google Auth Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated.
* /ping - returns an 200 OK response * /ping - returns an 200 OK response
* /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies) * /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)

View File

@ -0,0 +1,44 @@
## Google Auth Proxy Config File
## https://github.com/bitly/google_auth_proxy
## <addr>:<port> to listen on for HTTP clients
# http_address = "127.0.0.1:4180"
## the OAuth Redirect URL.
# redirect_url = "https://internalapp.yourcompany.com/oauth2/callback"
## the http url(s) of the upstream endpoint. If multiple, routing is based on path
# upstreams = [
# "http://127.0.0.1:8080/"
# ]
## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
# pass_basic_auth = true
## Google Apps Domains to allow authentication for
# google_apps_domains = [
# "yourcompany.com"
# ]
## The Google OAuth Client ID, Secret
# client_id = "123456.apps.googleusercontent.com"
# client_secret = ""
## Authenticated Email Addresses File (one email per line)
# authenticated_emails_file = ""
## Htpasswd File (optional)
## Additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
## enabling exposes a username/login signin form
# htpasswd_file = ""
## Cookie Settings
## Secret - the seed string for secure cookies
## Domain - optional cookie domain to force cookies to (ie: .yourcompany.com)
## Expire - expire timeframe for cookie
# cookie_secret = ""
# cookie_domain = ""
# cookie_expire = "168h"
# cookie_https_only = false

136
main.go
View File

@ -6,49 +6,63 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/BurntSushi/toml"
"github.com/mreiferson/go-options"
) )
const VERSION = "0.1.0"
var (
showVersion = flag.Bool("version", false, "print version string")
httpAddr = flag.String("http-address", "127.0.0.1:4180", "<addr>:<port> to listen on for HTTP clients")
redirectUrl = flag.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
clientID = flag.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
clientSecret = flag.String("client-secret", "", "the OAuth Client Secret")
passBasicAuth = flag.Bool("pass-basic-auth", true, "pass HTTP Basic Auth information to upstream")
htpasswdFile = flag.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption")
cookieSecret = flag.String("cookie-secret", "", "the seed string for secure cookies")
cookieDomain = flag.String("cookie-domain", "", "an optional cookie domain to force cookies to")
cookieExpire = flag.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
cookieHttpsOnly = flag.Bool("cookie-https-only", false, "set HTTPS only cookie")
authenticatedEmailsFile = flag.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
googleAppsDomains = StringArray{}
upstreams = StringArray{}
)
func init() {
flag.Var(&googleAppsDomains, "google-apps-domain", "authenticate against the given google apps domain (may be given multiple times)")
flag.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint. If multiple, routing is based on path")
}
func main() { func main() {
flagSet := flag.NewFlagSet("google_auth_proxy", flag.ExitOnError)
flag.Parse() googleAppsDomains := StringArray{}
upstreams := StringArray{}
config := flagSet.String("config", "", "path to config file")
showVersion := flagSet.Bool("version", false, "print version string")
flagSet.String("http-address", "127.0.0.1:4180", "<addr>:<port> to listen on for HTTP clients")
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint. If multiple, routing is based on path")
flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream")
flagSet.Var(&googleAppsDomains, "google-apps-domain", "authenticate against the given Google apps domain (may be given multiple times)")
flagSet.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
flagSet.String("client-secret", "", "the OAuth Client Secret")
flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption")
flagSet.String("cookie-secret", "", "the seed string for secure cookies")
flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)")
flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
flagSet.Bool("cookie-https-only", false, "set HTTPS only cookie")
flagSet.Parse(os.Args[1:])
opts := NewOptions()
var cfg map[string]interface{}
if *config != "" {
_, err := toml.DecodeFile(*config, &cfg)
if err != nil {
log.Fatalf("ERROR: failed to load config file %s - %s", *config, err)
}
}
options.Resolve(opts, flagSet, cfg)
// Try to use env for secrets if no flag is set // Try to use env for secrets if no flag is set
if *clientID == "" { // TODO: better parsing of these values
*clientID = os.Getenv("google_auth_client_id") if opts.ClientID == "" {
opts.ClientID = os.Getenv("google_auth_client_id")
} }
if *clientSecret == "" { if opts.ClientSecret == "" {
*clientSecret = os.Getenv("google_auth_secret") opts.ClientSecret = os.Getenv("google_auth_secret")
} }
if *cookieSecret == "" { if opts.CookieSecret == "" {
*cookieSecret = os.Getenv("google_auth_cookie_secret") opts.CookieSecret = os.Getenv("google_auth_cookie_secret")
} }
if *showVersion { if *showVersion {
@ -56,59 +70,41 @@ func main() {
return return
} }
if len(upstreams) < 1 { err := opts.Validate()
log.Fatal("missing --upstream")
}
if *cookieSecret == "" {
log.Fatal("missing --cookie-secret")
}
if *clientID == "" {
log.Fatal("missing --client-id")
}
if *clientSecret == "" {
log.Fatal("missing --client-secret")
}
var upstreamUrls []*url.URL
for _, u := range upstreams {
upstreamUrl, err := url.Parse(u)
if err != nil {
log.Fatalf("error parsing --upstream %s", err.Error())
}
upstreamUrls = append(upstreamUrls, upstreamUrl)
}
redirectUrl, err := url.Parse(*redirectUrl)
if err != nil { if err != nil {
log.Fatalf("error parsing --redirect-url %s", err.Error()) log.Printf("%s", err)
os.Exit(1)
} }
validator := NewValidator(googleAppsDomains, *authenticatedEmailsFile) validator := NewValidator(opts.GoogleAppsDomains, opts.AuthenticatedEmailsFile)
oauthproxy := NewOauthProxy(upstreamUrls, *clientID, *clientSecret, validator) oauthproxy := NewOauthProxy(opts, validator)
oauthproxy.SetRedirectUrl(redirectUrl)
if len(googleAppsDomains) != 0 && *authenticatedEmailsFile == "" { if len(opts.GoogleAppsDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
if len(googleAppsDomains) > 1 { if len(opts.GoogleAppsDomains) > 1 {
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(googleAppsDomains, ", ")) oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.GoogleAppsDomains, ", "))
} else { } else {
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using %v", googleAppsDomains[0]) oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using %v", opts.GoogleAppsDomains[0])
} }
} }
if *htpasswdFile != "" {
oauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(*htpasswdFile) if opts.HtpasswdFile != "" {
oauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(opts.HtpasswdFile)
if err != nil { if err != nil {
log.Fatalf("FATAL: unable to open %s %s", *htpasswdFile, err.Error()) log.Fatalf("FATAL: unable to open %s %s", opts.HtpasswdFile, err)
} }
} }
listener, err := net.Listen("tcp", *httpAddr)
listener, err := net.Listen("tcp", opts.HttpAddress)
if err != nil { if err != nil {
log.Fatalf("FATAL: listen (%s) failed - %s", *httpAddr, err.Error()) log.Fatalf("FATAL: listen (%s) failed - %s", opts.HttpAddress, err)
} }
log.Printf("listening on %s", *httpAddr) log.Printf("listening on %s", opts.HttpAddress)
server := &http.Server{Handler: oauthproxy} server := &http.Server{Handler: oauthproxy}
err = server.Serve(listener) err = server.Serve(listener)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
log.Printf("ERROR: http.Serve() - %s", err.Error()) log.Printf("ERROR: http.Serve() - %s", err)
} }
log.Printf("HTTP: closing %s", listener.Addr().String()) log.Printf("HTTP: closing %s", listener.Addr())
} }

View File

@ -22,9 +22,12 @@ const oauthStartPath = "/oauth2/start"
const oauthCallbackPath = "/oauth2/callback" const oauthCallbackPath = "/oauth2/callback"
type OauthProxy struct { type OauthProxy struct {
CookieSeed string CookieSeed string
CookieKey string CookieKey string
Validator func(string) bool CookieDomain string
CookieHttpsOnly bool
CookieExpire time.Duration
Validator func(string) bool
redirectUrl *url.URL // the url to receive requests at redirectUrl *url.URL // the url to receive requests at
oauthRedemptionUrl *url.URL // endpoint to redeem the code oauthRedemptionUrl *url.URL // endpoint to redeem the code
@ -35,40 +38,41 @@ type OauthProxy struct {
SignInMessage string SignInMessage string
HtpasswdFile *HtpasswdFile HtpasswdFile *HtpasswdFile
serveMux *http.ServeMux serveMux *http.ServeMux
PassBasicAuth bool
} }
func NewOauthProxy(proxyUrls []*url.URL, clientID string, clientSecret string, validator func(string) bool) *OauthProxy { func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth") login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth")
redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token") redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token")
serveMux := http.NewServeMux() serveMux := http.NewServeMux()
for _, u := range proxyUrls { for _, u := range opts.proxyUrls {
path := u.Path path := u.Path
if len(path) == 0 {
path = "/"
}
u.Path = "" u.Path = ""
log.Printf("mapping %s => %s", path, u) log.Printf("mapping path %q => upstream %q", path, u)
serveMux.Handle(path, httputil.NewSingleHostReverseProxy(u)) serveMux.Handle(path, httputil.NewSingleHostReverseProxy(u))
} }
return &OauthProxy{ redirectUrl := opts.redirectUrl
CookieKey: "_oauthproxy", redirectUrl.Path = oauthCallbackPath
CookieSeed: *cookieSecret,
Validator: validator,
clientID: clientID, return &OauthProxy{
clientSecret: clientSecret, CookieKey: "_oauthproxy",
CookieSeed: opts.CookieSecret,
CookieDomain: opts.CookieDomain,
CookieHttpsOnly: opts.CookieHttpsOnly,
CookieExpire: opts.CookieExpire,
Validator: validator,
clientID: opts.ClientID,
clientSecret: opts.ClientSecret,
oauthScope: "profile email", oauthScope: "profile email",
oauthRedemptionUrl: redeem, oauthRedemptionUrl: redeem,
oauthLoginUrl: login, oauthLoginUrl: login,
serveMux: serveMux, serveMux: serveMux,
redirectUrl: redirectUrl,
PassBasicAuth: opts.PassBasicAuth,
} }
} }
func (p *OauthProxy) SetRedirectUrl(redirectUrl *url.URL) {
redirectUrl.Path = oauthCallbackPath
p.redirectUrl = redirectUrl
}
func (p *OauthProxy) GetLoginURL(redirectUrl string) string { func (p *OauthProxy) GetLoginURL(redirectUrl string) string {
params := url.Values{} params := url.Values{}
params.Add("redirect_uri", p.redirectUrl.String()) params.Add("redirect_uri", p.redirectUrl.String())
@ -164,8 +168,8 @@ func jwtDecodeSegment(seg string) ([]byte, error) {
func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
domain := strings.Split(req.Host, ":")[0] domain := strings.Split(req.Host, ":")[0]
if *cookieDomain != "" && strings.HasSuffix(domain, *cookieDomain) { if p.CookieDomain != "" && strings.HasSuffix(domain, p.CookieDomain) {
domain = *cookieDomain domain = p.CookieDomain
} }
cookie := &http.Cookie{ cookie := &http.Cookie{
Name: p.CookieKey, Name: p.CookieKey,
@ -181,8 +185,8 @@ func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) { func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
domain := strings.Split(req.Host, ":")[0] // strip the port (if any) domain := strings.Split(req.Host, ":")[0] // strip the port (if any)
if *cookieDomain != "" && strings.HasSuffix(domain, *cookieDomain) { if p.CookieDomain != "" && strings.HasSuffix(domain, p.CookieDomain) {
domain = *cookieDomain domain = p.CookieDomain
} }
cookie := &http.Cookie{ cookie := &http.Cookie{
Name: p.CookieKey, Name: p.CookieKey,
@ -190,8 +194,8 @@ func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val st
Path: "/", Path: "/",
Domain: domain, Domain: domain,
HttpOnly: true, HttpOnly: true,
Secure: *cookieHttpsOnly, Secure: p.CookieHttpsOnly,
Expires: time.Now().Add(*cookieExpire), Expires: time.Now().Add(p.CookieExpire),
} }
http.SetCookie(rw, cookie) http.SetCookie(rw, cookie)
} }
@ -267,11 +271,11 @@ func (p *OauthProxy) GetRedirect(req *http.Request) (string, error) {
func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// check if this is a redirect back at the end of oauth // check if this is a redirect back at the end of oauth
remoteIP := req.Header.Get("X-Real-IP") remoteAddr := req.RemoteAddr
if remoteIP == "" { if req.Header.Get("X-Real-IP") != "" {
remoteIP = req.RemoteAddr remoteAddr += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
} }
log.Printf("%s %s %s", remoteIP, req.Method, req.URL.Path) log.Printf("%s %s %s", remoteAddr, req.Method, req.URL.RequestURI())
var ok bool var ok bool
var user string var user string
@ -322,7 +326,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
_, email, err := p.redeemCode(req.Form.Get("code")) _, email, err := p.redeemCode(req.Form.Get("code"))
if err != nil { if err != nil {
log.Printf("error redeeming code %s", err) log.Printf("%s error redeeming code %s", remoteAddr, err)
p.ErrorPage(rw, 500, "Internal Error", err.Error()) p.ErrorPage(rw, 500, "Internal Error", err.Error())
return return
} }
@ -334,7 +338,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// set cookie, or deny // set cookie, or deny
if p.Validator(email) { if p.Validator(email) {
log.Printf("authenticating %s completed", email) log.Printf("%s authenticating %s completed", remoteAddr, email)
p.SetCookie(rw, req, email) p.SetCookie(rw, req, email)
http.Redirect(rw, req, redirect, 302) http.Redirect(rw, req, redirect, 302)
return return
@ -362,13 +366,13 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
} }
if !ok { if !ok {
log.Printf("invalid cookie") log.Printf("%s - invalid cookie session", remoteAddr)
p.SignInPage(rw, req, 403) p.SignInPage(rw, req, 403)
return return
} }
// At this point, the user is authenticated. proxy normally // At this point, the user is authenticated. proxy normally
if *passBasicAuth { if p.PassBasicAuth {
req.SetBasicAuth(user, "") req.SetBasicAuth(user, "")
req.Header["X-Forwarded-User"] = []string{user} req.Header["X-Forwarded-User"] = []string{user}
req.Header["X-Forwarded-Email"] = []string{email} req.Header["X-Forwarded-Email"] = []string{email}

67
options.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"errors"
"fmt"
"net/url"
"time"
)
// Configuration Options that can be set by Command Line Flag, or Config File
type Options struct {
HttpAddress string `flag:"http-address" cfg:"http_address"`
RedirectUrl string `flag:"redirect-url" cfg:"redirect_url"`
ClientID string `flag:"client-id" cfg:"client_id"`
ClientSecret string `flag:"client-secret" cfg:"client_secret"`
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret"`
CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain"`
CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire"`
CookieHttpsOnly bool `flag:"cookie-https-only" cfg:"cookie_https_only"`
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
GoogleAppsDomains []string `flag:"google-apps-domain" cfg:"google_apps_domains"`
Upstreams []string `flag:"upstream" cfg:"upstreams"`
// internal values that are set after config validation
redirectUrl *url.URL
proxyUrls []*url.URL
}
func NewOptions() *Options {
return &Options{}
}
func (o *Options) Validate() error {
if len(o.Upstreams) < 1 {
return errors.New("missing -upstream")
}
if o.CookieSecret == "" {
errors.New("missing -cookie-secret")
}
if o.ClientID == "" {
return errors.New("missing -client-id")
}
if o.ClientSecret == "" {
return errors.New("missing -client-secret")
}
redirectUrl, err := url.Parse(o.RedirectUrl)
if err != nil {
return fmt.Errorf("error parsing -redirect-url=%q %s", o.RedirectUrl, err)
}
o.redirectUrl = redirectUrl
for _, u := range o.Upstreams {
upstreamUrl, err := url.Parse(u)
if err != nil {
return fmt.Errorf("error parsing -upstream=%q %s", upstreamUrl, err)
}
if upstreamUrl.Path == "" {
upstreamUrl.Path = "/"
}
o.proxyUrls = append(o.proxyUrls, upstreamUrl)
}
return nil
}

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"fmt" "strings"
) )
type StringArray []string type StringArray []string
@ -12,5 +12,5 @@ func (a *StringArray) Set(s string) error {
} }
func (a *StringArray) String() string { func (a *StringArray) String() string {
return fmt.Sprint(*a) return strings.Join(*a, ",")
} }

3
version.go Normal file
View File

@ -0,0 +1,3 @@
package main
const VERSION = "0.1.0"