diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b444581 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 8b18c60..1cec1db 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -31,13 +31,17 @@ jobs: - name: Print builder available platforms run: echo ${{ steps.buildx.outputs.platforms }} - - name: Dockerhub login - run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Run Buildx + name: Dockerhub login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Run Buildx for amd64, armv7, arm64 architectures run: | docker buildx build \ - --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \ + --platform linux/amd64,linux/arm/v7,linux/arm64 \ --output "type=image,push=false" \ --push \ --tag "$DOCKERHUB_REPOSITORY:latest" \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7fca40..2d9b28d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,17 +18,20 @@ jobs: name: Set up Docker Buildx id: buildx uses: crazy-max/ghaction-docker-buildx@v1.1.0 - - name: Release version + - + name: Release version run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV - - name: Print release version + - + name: Print release version run: echo ${{ env.RELEASE_VERSION }} - - name: Dockerhub login + - + name: Dockerhub login run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Run Buildx + name: Run Buildx for amd64, armv7, arm64 architectures run: | docker buildx build \ - --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \ + --platform linux/amd64,linux/arm/v7,linux/arm64 \ --output "type=image,push=false" \ --push \ --tag "$DOCKERHUB_REPOSITORY:$RELEASE_VERSION" \ diff --git a/Dockerfile b/Dockerfile index 0dc298d..4696548 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG GOLANG_VERSION="1.16.5" +ARG GOLANG_VERSION="1.19.1" FROM golang:$GOLANG_VERSION-alpine as builder RUN apk --no-cache add tzdata @@ -6,8 +6,6 @@ WORKDIR /go/src/github.com/serjs/socks5 COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s' -o ./socks5 -FROM scratch -COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +FROM gcr.io/distroless/static:nonroot COPY --from=builder /go/src/github.com/serjs/socks5/socks5 / ENTRYPOINT ["/socks5"] diff --git a/Dockerfile.armv6 b/Dockerfile.armv6 new file mode 100644 index 0000000..4cf72a3 --- /dev/null +++ b/Dockerfile.armv6 @@ -0,0 +1,12 @@ +ARG GOLANG_VERSION="1.19.1" + +FROM golang:$GOLANG_VERSION-alpine as builder +RUN apk --no-cache add tzdata +WORKDIR /go/src/github.com/serjs/socks5 +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s' -o ./socks5 + +FROM scratch +COPY --from=builder /go/src/github.com/serjs/socks5/socks5 / +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +ENTRYPOINT ["/socks5"] diff --git a/README.md b/README.md index bcc009f..6fa7e57 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ Leave `PROXY_USER` and `PROXY_PASSWORD` empty for skip authentication options wh |PROXY_PORT|String|1080|Set listen port for application inside docker container| |ALLOWED_DEST_FQDN|String|EMPTY|Allowed destination address regular expression pattern. Default allows all.| |TZ|String|UTC|Set Timezone like in many common Operation Systems| +|ALLOWED_IPS|String|Empty|Set allowed IP's that can connect to proxy, separator `,`| + +`ALLOWED_IPS` parameter is not included in `serjs/go-socks5-proxy` image.\ +You can build your image with: +`docker-compose -f docker-compose.build.yml up -d`\ +Just don't forget to set parameters in the `.env` file. ## Test running service diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 0000000..6e87be8 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,11 @@ +version: '3' +services: + socks5-build: + build: + context: . + dockerfile: Dockerfile + container_name: socks5-build + env_file: .env + ports: + - "1080:1080" + restart: unless-stopped diff --git a/go.mod b/go.mod index e1dd651..3a29ddb 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/serjs/socks5-server -go 1.16 +go 1.19 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 - github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible - golang.org/x/net v0.0.0-20180406214816-61147c48b25b // indirect + github.com/caarlos0/env/v6 v6.10.1 ) + +require golang.org/x/net v0.1.0 // indirect diff --git a/go.sum b/go.sum index 1329c0c..95fdbdc 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible h1:FjzBjKb2W4ggJbyACel2jbTWSGqCpmu9YqXwcpUp+3M= -github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= -golang.org/x/net v0.0.0-20180406214816-61147c48b25b h1:7rskAFQwNXGW6AD8E/6y0LDHW5mT9rsLD7ViLVFfh5w= -golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= +github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= diff --git a/server.go b/server.go index efdd533..921f1ac 100644 --- a/server.go +++ b/server.go @@ -2,17 +2,19 @@ package main import ( "log" + "net" "os" "github.com/armon/go-socks5" - "github.com/caarlos0/env" + "github.com/caarlos0/env/v6" ) type params struct { - User string `env:"PROXY_USER" envDefault:""` - Password string `env:"PROXY_PASSWORD" envDefault:""` - Port string `env:"PROXY_PORT" envDefault:"1080"` - AllowedDestFqdn string `env:"ALLOWED_DEST_FQDN" envDefault:""` + User string `env:"PROXY_USER" envDefault:""` + Password string `env:"PROXY_PASSWORD" envDefault:""` + Port string `env:"PROXY_PORT" envDefault:"1080"` + AllowedDestFqdn string `env:"ALLOWED_DEST_FQDN" envDefault:""` + AllowedIPs []string `env:"ALLOWED_IPS" envSeparator:"," envDefault:""` } func main() { @@ -45,6 +47,15 @@ func main() { log.Fatal(err) } + // Set IP whitelist + if len(cfg.AllowedIPs) > 0 { + whitelist := make([]net.IP, len(cfg.AllowedIPs)) + for i, ip := range cfg.AllowedIPs { + whitelist[i] = net.ParseIP(ip) + } + server.SetIPWhitelist(whitelist) + } + log.Printf("Start listening proxy service on port %s\n", cfg.Port) if err := server.ListenAndServe("tcp", ":"+cfg.Port); err != nil { log.Fatal(err) diff --git a/vendor/github.com/armon/go-socks5/socks5.go b/vendor/github.com/armon/go-socks5/socks5.go index a17be68..2d630fb 100644 --- a/vendor/github.com/armon/go-socks5/socks5.go +++ b/vendor/github.com/armon/go-socks5/socks5.go @@ -55,6 +55,7 @@ type Config struct { type Server struct { config *Config authMethods map[uint8]Authenticator + isIPAllowed func(net.IP) bool } // New creates a new Server and potentially returns an error @@ -93,6 +94,11 @@ func New(conf *Config) (*Server, error) { server.authMethods[a.GetCode()] = a } + // Set default IP whitelist function + server.isIPAllowed = func(ip net.IP) bool { + return true // default allow all IPs + } + return server, nil } @@ -117,11 +123,37 @@ func (s *Server) Serve(l net.Listener) error { return nil } +// SetIPWhitelist sets the function to check if a given IP is allowed +func (s *Server) SetIPWhitelist(allowedIPs []net.IP) { + s.isIPAllowed = func(ip net.IP) bool { + for _, allowedIP := range allowedIPs { + if ip.Equal(allowedIP) { + return true + } + } + return false + } +} + // ServeConn is used to serve a single connection. func (s *Server) ServeConn(conn net.Conn) error { defer conn.Close() bufConn := bufio.NewReader(conn) + // Check client IP against whitelist + clientIP, _, err := net.SplitHostPort(conn.RemoteAddr().String()) + if err != nil { + s.config.Logger.Printf("[ERR] socks: Failed to get client IP address: %v", err) + return err + } + ip := net.ParseIP(clientIP) + if s.isIPAllowed(ip) { + s.config.Logger.Printf("[INFO] socks: Connection from allowed IP address: %s", clientIP) + } else { + s.config.Logger.Printf("[WARN] socks: Connection from not allowed IP address: %s", clientIP) + return fmt.Errorf("connection from not allowed IP address") + } + // Read the version byte version := []byte{0} if _, err := bufConn.Read(version); err != nil { diff --git a/vendor/github.com/caarlos0/env/.gitignore b/vendor/github.com/caarlos0/env/.gitignore deleted file mode 100644 index 2d83068..0000000 --- a/vendor/github.com/caarlos0/env/.gitignore +++ /dev/null @@ -1 +0,0 @@ -coverage.out diff --git a/vendor/github.com/caarlos0/env/.hound.yml b/vendor/github.com/caarlos0/env/.hound.yml deleted file mode 100644 index e5c719d..0000000 --- a/vendor/github.com/caarlos0/env/.hound.yml +++ /dev/null @@ -1,2 +0,0 @@ -go: - enabled: true diff --git a/vendor/github.com/caarlos0/env/.travis.yml b/vendor/github.com/caarlos0/env/.travis.yml deleted file mode 100644 index 4ad7e69..0000000 --- a/vendor/github.com/caarlos0/env/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: go -go: - - 1.5 - - 1.6 - - 1.7 - - 1.8 - - tip -before_install: - - go get github.com/axw/gocov/gocov - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover -script: - - go test -v -cover -race -coverprofile=coverage.out -after_script: - - go get github.com/mattn/goveralls - - goveralls -coverprofile=coverage.out -service=travis-ci -repotoken='eCcizKmTdSaJCz8Ih33WDppdqb9kioYwi' diff --git a/vendor/github.com/caarlos0/env/README.md b/vendor/github.com/caarlos0/env/README.md deleted file mode 100644 index cf46c54..0000000 --- a/vendor/github.com/caarlos0/env/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# env [![Build Status](https://travis-ci.org/caarlos0/env.svg?branch=master)](https://travis-ci.org/caarlos0/env) [![Coverage Status](https://coveralls.io/repos/caarlos0/env/badge.svg?branch=master&service=github)](https://coveralls.io/github/caarlos0/env?branch=master) [![](https://godoc.org/github.com/caarlos0/env?status.svg)](http://godoc.org/github.com/caarlos0/env) [![](http://goreportcard.com/badge/caarlos0/env)](http://goreportcard.com/report/caarlos0/env) [![SayThanks.io](https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg?style=flat-square)](https://saythanks.io/to/caarlos0) - -A KISS way to deal with environment variables in Go. - -## Why - -At first, it was boring for me to write down an entire function just to -get some `var` from the environment and default to another in case it's missing. - -For that manner, I wrote a `GetOr` function in the -[go-idioms](https://github.com/caarlos0/go-idioms) project. - -Then, I got pissed about writing `os.Getenv`, `os.Setenv`, `os.Unsetenv`... -it kind of make more sense to me write it as `env.Get`, `env.Set`, `env.Unset`. -So I did. - -Then I got a better idea: to use `struct` tags to do all that work for me. - -## Example - -A very basic example (check the `examples` folder): - -```go -package main - -import ( - "fmt" - "time" - - "github.com/caarlos0/env" -) - -type config struct { - Home string `env:"HOME"` - Port int `env:"PORT" envDefault:"3000"` - IsProduction bool `env:"PRODUCTION"` - Hosts []string `env:"HOSTS" envSeparator:":"` - Duration time.Duration `env:"DURATION"` -} - -func main() { - cfg := config{} - err := env.Parse(&cfg) - if err != nil { - fmt.Printf("%+v\n", err) - } - fmt.Printf("%+v\n", cfg) -} -``` - -You can run it like this: - -```sh -$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run examples/first.go -{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s} -``` - -## Supported types and defaults - -The library has built-in support for the following types: - -* `string` -* `int` -* `uint` -* `int64` -* `bool` -* `float32` -* `float64` -* `time.Duration` -* `[]string` -* `[]int` -* `[]bool` -* `[]float32` -* `[]float64` -* `[]time.Duration` -* .. or use/define a [custom parser func](#custom-parser-funcs) for any other type - -If you set the `envDefault` tag for something, this value will be used in the -case of absence of it in the environment. If you don't do that AND the -environment variable is also not set, the zero-value -of the type will be used: empty for `string`s, `false` for `bool`s -and `0` for `int`s. - -By default, slice types will split the environment value on `,`; you can change this behavior by setting the `envSeparator` tag. - -## Custom Parser Funcs - -If you have a type that is not supported out of the box by the lib, you are able -to use (or define) and pass custom parsers (and their associated `reflect.Type`) to the -`env.ParseWithFuncs()` function. - -In addition to accepting a struct pointer (same as `Parse()`), this function also -accepts a `env.CustomParsers` arg that under the covers is a `map[reflect.Type]env.ParserFunc`. - -To see what this looks like in practice, take a look at the [commented block in the example](https://github.com/caarlos0/env/blob/master/examples/first.go#L35-L39). - -`env` also ships with some pre-built custom parser funcs for common types. You -can check them out [here](parsers/). - -## Required fields - -The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added -to ensure that some environment variable is set. In the example above, -an error is returned if the `config` struct is changed to: - - -```go -type config struct { - Home string `env:"HOME"` - Port int `env:"PORT" envDefault:"3000"` - IsProduction bool `env:"PRODUCTION"` - Hosts []string `env:"HOSTS" envSeparator:":"` - SecretKey string `env:"SECRET_KEY,required"` -} -``` diff --git a/vendor/github.com/caarlos0/env/env.go b/vendor/github.com/caarlos0/env/env.go deleted file mode 100644 index e06fbd0..0000000 --- a/vendor/github.com/caarlos0/env/env.go +++ /dev/null @@ -1,398 +0,0 @@ -package env - -import ( - "errors" - "fmt" - "os" - "reflect" - "strconv" - "strings" - "time" -) - -var ( - // ErrNotAStructPtr is returned if you pass something that is not a pointer to a - // Struct to Parse - ErrNotAStructPtr = errors.New("Expected a pointer to a Struct") - // ErrUnsupportedType if the struct field type is not supported by env - ErrUnsupportedType = errors.New("Type is not supported") - // ErrUnsupportedSliceType if the slice element type is not supported by env - ErrUnsupportedSliceType = errors.New("Unsupported slice type") - // OnEnvVarSet is an optional convenience callback, such as for logging purposes. - // If not nil, it's called after successfully setting the given field from the given value. - OnEnvVarSet func(reflect.StructField, string) - // Friendly names for reflect types - sliceOfInts = reflect.TypeOf([]int(nil)) - sliceOfInt64s = reflect.TypeOf([]int64(nil)) - sliceOfUint64s = reflect.TypeOf([]uint64(nil)) - sliceOfStrings = reflect.TypeOf([]string(nil)) - sliceOfBools = reflect.TypeOf([]bool(nil)) - sliceOfFloat32s = reflect.TypeOf([]float32(nil)) - sliceOfFloat64s = reflect.TypeOf([]float64(nil)) - sliceOfDurations = reflect.TypeOf([]time.Duration(nil)) -) - -// CustomParsers is a friendly name for the type that `ParseWithFuncs()` accepts -type CustomParsers map[reflect.Type]ParserFunc - -// ParserFunc defines the signature of a function that can be used within `CustomParsers` -type ParserFunc func(v string) (interface{}, error) - -// Parse parses a struct containing `env` tags and loads its values from -// environment variables. -func Parse(v interface{}) error { - ptrRef := reflect.ValueOf(v) - if ptrRef.Kind() != reflect.Ptr { - return ErrNotAStructPtr - } - ref := ptrRef.Elem() - if ref.Kind() != reflect.Struct { - return ErrNotAStructPtr - } - return doParse(ref, make(map[reflect.Type]ParserFunc, 0)) -} - -// ParseWithFuncs is the same as `Parse` except it also allows the user to pass -// in custom parsers. -func ParseWithFuncs(v interface{}, funcMap CustomParsers) error { - ptrRef := reflect.ValueOf(v) - if ptrRef.Kind() != reflect.Ptr { - return ErrNotAStructPtr - } - ref := ptrRef.Elem() - if ref.Kind() != reflect.Struct { - return ErrNotAStructPtr - } - return doParse(ref, funcMap) -} - -func doParse(ref reflect.Value, funcMap CustomParsers) error { - refType := ref.Type() - var errorList []string - - for i := 0; i < refType.NumField(); i++ { - refField := ref.Field(i) - if reflect.Ptr == refField.Kind() && !refField.IsNil() && refField.CanSet() { - err := Parse(refField.Interface()) - if nil != err { - return err - } - continue - } - refTypeField := refType.Field(i) - value, err := get(refTypeField) - if err != nil { - errorList = append(errorList, err.Error()) - continue - } - if value == "" { - continue - } - if err := set(refField, refTypeField, value, funcMap); err != nil { - errorList = append(errorList, err.Error()) - continue - } - if OnEnvVarSet != nil { - OnEnvVarSet(refTypeField, value) - } - } - if len(errorList) == 0 { - return nil - } - return errors.New(strings.Join(errorList, ". ")) -} - -func get(field reflect.StructField) (string, error) { - var ( - val string - err error - ) - - key, opts := parseKeyForOption(field.Tag.Get("env")) - - defaultValue := field.Tag.Get("envDefault") - val = getOr(key, defaultValue) - - if len(opts) > 0 { - for _, opt := range opts { - // The only option supported is "required". - switch opt { - case "": - break - case "required": - val, err = getRequired(key) - default: - err = errors.New("Env tag option " + opt + " not supported.") - } - } - } - - return val, err -} - -// split the env tag's key into the expected key and desired option, if any. -func parseKeyForOption(key string) (string, []string) { - opts := strings.Split(key, ",") - return opts[0], opts[1:] -} - -func getRequired(key string) (string, error) { - if value, ok := os.LookupEnv(key); ok { - return value, nil - } - // We do not use fmt.Errorf to avoid another import. - return "", errors.New("Required environment variable " + key + " is not set") -} - -func getOr(key, defaultValue string) string { - value, ok := os.LookupEnv(key) - if ok { - return value - } - return defaultValue -} - -func set(field reflect.Value, refType reflect.StructField, value string, funcMap CustomParsers) error { - switch field.Kind() { - case reflect.Slice: - separator := refType.Tag.Get("envSeparator") - return handleSlice(field, value, separator) - case reflect.String: - field.SetString(value) - case reflect.Bool: - bvalue, err := strconv.ParseBool(value) - if err != nil { - return err - } - field.SetBool(bvalue) - case reflect.Int: - intValue, err := strconv.ParseInt(value, 10, 32) - if err != nil { - return err - } - field.SetInt(intValue) - case reflect.Uint: - uintValue, err := strconv.ParseUint(value, 10, 32) - if err != nil { - return err - } - field.SetUint(uintValue) - case reflect.Float32: - v, err := strconv.ParseFloat(value, 32) - if err != nil { - return err - } - field.SetFloat(v) - case reflect.Float64: - v, err := strconv.ParseFloat(value, 64) - if err != nil { - return err - } - field.Set(reflect.ValueOf(v)) - case reflect.Int64: - if refType.Type.String() == "time.Duration" { - dValue, err := time.ParseDuration(value) - if err != nil { - return err - } - field.Set(reflect.ValueOf(dValue)) - } else { - intValue, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return err - } - field.SetInt(intValue) - } - case reflect.Uint64: - uintValue, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return err - } - field.SetUint(uintValue) - case reflect.Struct: - return handleStruct(field, refType, value, funcMap) - default: - parserFunc, ok := funcMap[refType.Type] - if !ok { - return ErrUnsupportedType - } - val, err := parserFunc(value) - if err != nil { - return err - } - field.Set(reflect.ValueOf(val)) - } - return nil -} - -func handleStruct(field reflect.Value, refType reflect.StructField, value string, funcMap CustomParsers) error { - // Does the custom parser func map contain this type? - parserFunc, ok := funcMap[field.Type()] - if !ok { - // Map does not contain a custom parser for this type - return ErrUnsupportedType - } - - // Call on the custom parser func - data, err := parserFunc(value) - if err != nil { - return fmt.Errorf("Custom parser error: %v", err) - } - - // Set the field to the data returned by the customer parser func - rv := reflect.ValueOf(data) - field.Set(rv) - - return nil -} - -func handleSlice(field reflect.Value, value, separator string) error { - if separator == "" { - separator = "," - } - - splitData := strings.Split(value, separator) - - switch field.Type() { - case sliceOfStrings: - field.Set(reflect.ValueOf(splitData)) - case sliceOfInts: - intData, err := parseInts(splitData) - if err != nil { - return err - } - field.Set(reflect.ValueOf(intData)) - case sliceOfInt64s: - int64Data, err := parseInt64s(splitData) - if err != nil { - return err - } - field.Set(reflect.ValueOf(int64Data)) - case sliceOfUint64s: - uint64Data, err := parseUint64s(splitData) - if err != nil { - return err - } - field.Set(reflect.ValueOf(uint64Data)) - case sliceOfFloat32s: - data, err := parseFloat32s(splitData) - if err != nil { - return err - } - field.Set(reflect.ValueOf(data)) - case sliceOfFloat64s: - data, err := parseFloat64s(splitData) - if err != nil { - return err - } - field.Set(reflect.ValueOf(data)) - case sliceOfBools: - boolData, err := parseBools(splitData) - if err != nil { - return err - } - field.Set(reflect.ValueOf(boolData)) - case sliceOfDurations: - durationData, err := parseDurations(splitData) - if err != nil { - return err - } - field.Set(reflect.ValueOf(durationData)) - default: - return ErrUnsupportedSliceType - } - return nil -} - -func parseInts(data []string) ([]int, error) { - intSlice := make([]int, 0, len(data)) - - for _, v := range data { - intValue, err := strconv.ParseInt(v, 10, 32) - if err != nil { - return nil, err - } - intSlice = append(intSlice, int(intValue)) - } - return intSlice, nil -} - -func parseInt64s(data []string) ([]int64, error) { - intSlice := make([]int64, 0, len(data)) - - for _, v := range data { - intValue, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return nil, err - } - intSlice = append(intSlice, int64(intValue)) - } - return intSlice, nil -} - -func parseUint64s(data []string) ([]uint64, error) { - var uintSlice []uint64 - - for _, v := range data { - uintValue, err := strconv.ParseUint(v, 10, 64) - if err != nil { - return nil, err - } - uintSlice = append(uintSlice, uint64(uintValue)) - } - return uintSlice, nil -} - -func parseFloat32s(data []string) ([]float32, error) { - float32Slice := make([]float32, 0, len(data)) - - for _, v := range data { - data, err := strconv.ParseFloat(v, 32) - if err != nil { - return nil, err - } - float32Slice = append(float32Slice, float32(data)) - } - return float32Slice, nil -} - -func parseFloat64s(data []string) ([]float64, error) { - float64Slice := make([]float64, 0, len(data)) - - for _, v := range data { - data, err := strconv.ParseFloat(v, 64) - if err != nil { - return nil, err - } - float64Slice = append(float64Slice, float64(data)) - } - return float64Slice, nil -} - -func parseBools(data []string) ([]bool, error) { - boolSlice := make([]bool, 0, len(data)) - - for _, v := range data { - bvalue, err := strconv.ParseBool(v) - if err != nil { - return nil, err - } - - boolSlice = append(boolSlice, bvalue) - } - return boolSlice, nil -} - -func parseDurations(data []string) ([]time.Duration, error) { - durationSlice := make([]time.Duration, 0, len(data)) - - for _, v := range data { - dvalue, err := time.ParseDuration(v) - if err != nil { - return nil, err - } - - durationSlice = append(durationSlice, dvalue) - } - return durationSlice, nil -} diff --git a/vendor/github.com/caarlos0/env/v6/.gitignore b/vendor/github.com/caarlos0/env/v6/.gitignore new file mode 100644 index 0000000..ca6a0ff --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.gitignore @@ -0,0 +1,4 @@ +coverage.txt +bin +card.png +dist diff --git a/vendor/github.com/caarlos0/env/v6/.golangci.yml b/vendor/github.com/caarlos0/env/v6/.golangci.yml new file mode 100644 index 0000000..ff791f8 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.golangci.yml @@ -0,0 +1,8 @@ +linters: + enable: + - thelper + - gofumpt + - tparallel + - unconvert + - unparam + - wastedassign diff --git a/vendor/github.com/caarlos0/env/v6/.goreleaser.yml b/vendor/github.com/caarlos0/env/v6/.goreleaser.yml new file mode 100644 index 0000000..4688983 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.goreleaser.yml @@ -0,0 +1,3 @@ +includes: + - from_url: + url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml diff --git a/vendor/github.com/caarlos0/env/LICENSE.md b/vendor/github.com/caarlos0/env/v6/LICENSE.md similarity index 95% rename from vendor/github.com/caarlos0/env/LICENSE.md rename to vendor/github.com/caarlos0/env/v6/LICENSE.md index b739867..3a59b6b 100644 --- a/vendor/github.com/caarlos0/env/LICENSE.md +++ b/vendor/github.com/caarlos0/env/v6/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2016 Carlos Alexandro Becker +Copyright (c) 2015-2022 Carlos Alexandro Becker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/github.com/caarlos0/env/v6/Makefile b/vendor/github.com/caarlos0/env/v6/Makefile new file mode 100644 index 0000000..da8595f --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/Makefile @@ -0,0 +1,37 @@ +SOURCE_FILES?=./... +TEST_PATTERN?=. + +export GO111MODULE := on + +setup: + go mod tidy +.PHONY: setup + +build: + go build +.PHONY: build + +test: + go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m +.PHONY: test + +cover: test + go tool cover -html=coverage.txt +.PHONY: cover + +fmt: + gofumpt -w -l . +.PHONY: fmt + +lint: + golangci-lint run ./... +.PHONY: lint + +ci: build test +.PHONY: ci + +card: + wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" +.PHONY: card + +.DEFAULT_GOAL := ci diff --git a/vendor/github.com/caarlos0/env/v6/README.md b/vendor/github.com/caarlos0/env/v6/README.md new file mode 100644 index 0000000..e56399b --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/README.md @@ -0,0 +1,452 @@ +# env + +[![Build Status](https://img.shields.io/github/workflow/status/caarlos0/env/build?style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build) +[![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env) +[![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v6) + +A simple and zero-dependencies library to parse environment variables into structs. + +## Example + +Get the module with: + +```sh +go get github.com/caarlos0/env/v6 +``` + +The usage looks like this: + +```go +package main + +import ( + "fmt" + "time" + + "github.com/caarlos0/env/v6" +) + +type config struct { + Home string `env:"HOME"` + Port int `env:"PORT" envDefault:"3000"` + Password string `env:"PASSWORD,unset"` + IsProduction bool `env:"PRODUCTION"` + Hosts []string `env:"HOSTS" envSeparator:":"` + Duration time.Duration `env:"DURATION"` + TempFolder string `env:"TEMP_FOLDER" envDefault:"${HOME}/tmp" envExpand:"true"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + fmt.Printf("%+v\n", cfg) +} +``` + +You can run it like this: + +```sh +$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go +{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s} +``` + +⚠️⚠️⚠️ **Attention:** _unexported fields_ will be **ignored**. + +## Supported types and defaults + +Out of the box all built-in types are supported, plus a few others that +are commonly used. + +Complete list: + +- `string` +- `bool` +- `int` +- `int8` +- `int16` +- `int32` +- `int64` +- `uint` +- `uint8` +- `uint16` +- `uint32` +- `uint64` +- `float32` +- `float64` +- `time.Duration` +- `encoding.TextUnmarshaler` +- `url.URL` + +Pointers, slices and slices of pointers of those types are also supported. + +You can also use/define a [custom parser func](#custom-parser-funcs) for any +other type you want. + +If you set the `envDefault` tag for something, this value will be used in the +case of absence of it in the environment. + +By default, slice types will split the environment value on `,`; you can change +this behavior by setting the `envSeparator` tag. + +If you set the `envExpand` tag, environment variables (either in `${var}` or +`$var` format) in the string will be replaced according with the actual value +of the variable. + +## Custom Parser Funcs + +If you have a type that is not supported out of the box by the lib, you are able +to use (or define) and pass custom parsers (and their associated `reflect.Type`) +to the `env.ParseWithFuncs()` function. + +In addition to accepting a struct pointer (same as `Parse()`), this function +also accepts a `map[reflect.Type]env.ParserFunc`. + +If you add a custom parser for, say `Foo`, it will also be used to parse +`*Foo` and `[]Foo` types. + +Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v6) +for more info. + +### A note about `TextUnmarshaler` and `time.Time` + +Env supports by default anything that implements the `TextUnmarshaler` interface. +That includes things like `time.Time` for example. +The upside is that depending on the format you need, you don't need to change anything. +The downside is that if you do need time in another format, you'll need to create your own type. + +Its fairly straightforward: + +```go +type MyTime time.Time + +func (t *MyTime) UnmarshalText(text []byte) error { + tt, err := time.Parse("2006-01-02", string(text)) + *t = MyTime(tt) + return err +} + +type Config struct { + SomeTime MyTime `env:"SOME_TIME"` +} +``` + +And then you can parse `Config` with `env.Parse`. + +## Required fields + +The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added to ensure that some environment variable is set. +In the example above, an error is returned if the `config` struct is changed to: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,required"` +} +``` + +## Not Empty fields + +While `required` demands the environment variable to be check, it doesn't check its value. +If you want to make sure the environment is set and not empty, you need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`). + +Example: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,notEmpty"` +} +``` + +## Unset environment variable after reading it + +The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added +to ensure that some environment variable is unset after reading it. + +Example: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,unset"` +} +``` + +## From file + +The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added +to in order to indicate that the value of the variable shall be loaded from a file. The path of that file is given +by the environment variable associated with it +Example below + +```go +package main + +import ( + "fmt" + "time" + "github.com/caarlos0/env/v6" +) + +type config struct { + Secret string `env:"SECRET,file"` + Password string `env:"PASSWORD,file" envDefault:"/tmp/password"` + Certificate string `env:"CERTIFICATE,file" envDefault:"${CERTIFICATE_FILE}" envExpand:"true"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + fmt.Printf("%+v\n", cfg) +} +``` + +```sh +$ echo qwerty > /tmp/secret +$ echo dvorak > /tmp/password +$ echo coleman > /tmp/certificate + +$ SECRET=/tmp/secret \ + CERTIFICATE_FILE=/tmp/certificate \ + go run main.go +{Secret:qwerty Password:dvorak Certificate:coleman} +``` + +## Options + +### Environment + +By setting the `Options.Environment` map you can tell `Parse` to add those `keys` and `values` +as env vars before parsing is done. These envs are stored in the map and never actually set by `os.Setenv`. +This option effectively makes `env` ignore the OS environment variables: only the ones provided in the option are used. + +This can make your testing scenarios a bit more clean and easy to handle. + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{Environment: map[string]string{ + "PASSWORD": "MY_PASSWORD", + }} + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +### Changing default tag name + +You can change what tag name to use for setting the env vars by setting the `Options.TagName` +variable. + +For example +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Password string `json:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{TagName: "json"} + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +### Prefixes + +You can prefix sub-structs env tags, as well as a whole `env.Parse` call. + +Here's an example flexing it a bit: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Home string `env:"HOME"` +} + +type ComplexConfig struct { + Foo Config `envPrefix:"FOO_"` + Clean Config + Bar Config `envPrefix:"BAR_"` + Blah string `env:"BLAH"` +} + +func main() { + cfg := ComplexConfig{} + if err := Parse(&cfg, Options{ + Prefix: "T_", + Environment: map[string]string{ + "T_FOO_HOME": "/foo", + "T_BAR_HOME": "/bar", + "T_BLAH": "blahhh", + "T_HOME": "/clean", + }, + }); err != nil { + log.Fatal(err) + } + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +### On set hooks + +You might want to listen to value sets and, for example, log something or do some other kind of logic. +You can do this by passing a `OnSet` option: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{ + OnSet: func(tag string, value interface{}, isDefault bool) { + fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) + }, + } + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +## Making all fields to required + +You can make all fields that don't have a default value be required by setting the `RequiredIfNoDef: true` in the `Options`. + +For example + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{RequiredIfNoDef: true} + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +## Defaults from code + +You may define default value also in code, by initialising the config data before it's filled by `env.Parse`. +Default values defined as struct tags will overwrite existing values during Parse. + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + var cfg = Config{ + Username: "test", + Password: "123456", + } + + if err := env.Parse(&cfg); err != nil { + fmt.Println("failed:", err) + } + + fmt.Printf("%+v", cfg) // {Username:admin Password:123456} +} +``` + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) diff --git a/vendor/github.com/caarlos0/env/v6/env.go b/vendor/github.com/caarlos0/env/v6/env.go new file mode 100644 index 0000000..f0b09df --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env.go @@ -0,0 +1,504 @@ +package env + +import ( + "encoding" + "errors" + "fmt" + "net/url" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +// nolint: gochecknoglobals +var ( + // ErrNotAStructPtr is returned if you pass something that is not a pointer to a + // Struct to Parse. + ErrNotAStructPtr = errors.New("env: expected a pointer to a Struct") + + defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ + reflect.Bool: func(v string) (interface{}, error) { + return strconv.ParseBool(v) + }, + reflect.String: func(v string) (interface{}, error) { + return v, nil + }, + reflect.Int: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int(i), err + }, + reflect.Int16: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 16) + return int16(i), err + }, + reflect.Int32: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int32(i), err + }, + reflect.Int64: func(v string) (interface{}, error) { + return strconv.ParseInt(v, 10, 64) + }, + reflect.Int8: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 8) + return int8(i), err + }, + reflect.Uint: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint(i), err + }, + reflect.Uint16: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 16) + return uint16(i), err + }, + reflect.Uint32: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint32(i), err + }, + reflect.Uint64: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 64) + return i, err + }, + reflect.Uint8: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 8) + return uint8(i), err + }, + reflect.Float64: func(v string) (interface{}, error) { + return strconv.ParseFloat(v, 64) + }, + reflect.Float32: func(v string) (interface{}, error) { + f, err := strconv.ParseFloat(v, 32) + return float32(f), err + }, + } +) + +func defaultTypeParsers() map[reflect.Type]ParserFunc { + return map[reflect.Type]ParserFunc{ + reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) { + u, err := url.Parse(v) + if err != nil { + return nil, fmt.Errorf("unable to parse URL: %v", err) + } + return *u, nil + }, + reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) { + s, err := time.ParseDuration(v) + if err != nil { + return nil, fmt.Errorf("unable to parse duration: %v", err) + } + return s, err + }, + } +} + +// ParserFunc defines the signature of a function that can be used within `CustomParsers`. +type ParserFunc func(v string) (interface{}, error) + +// OnSetFn is a hook that can be run when a value is set. +type OnSetFn func(tag string, value interface{}, isDefault bool) + +// Options for the parser. +type Options struct { + // Environment keys and values that will be accessible for the service. + Environment map[string]string + + // TagName specifies another tagname to use rather than the default env. + TagName string + + // RequiredIfNoDef automatically sets all env as required if they do not declare 'envDefault' + RequiredIfNoDef bool + + // OnSet allows to run a function when a value is set + OnSet OnSetFn + + // Prefix define a prefix for each key + Prefix string + + // Sets to true if we have already configured once. + configured bool +} + +// configure will do the basic configurations and defaults. +func configure(opts []Options) []Options { + // If we have already configured the first item + // of options will have been configured set to true. + if len(opts) > 0 && opts[0].configured { + return opts + } + + // Created options with defaults. + opt := Options{ + TagName: "env", + Environment: toMap(os.Environ()), + configured: true, + } + + // Loop over all opts structs and set + // to opt if value is not default/empty. + for _, item := range opts { + if item.Environment != nil { + opt.Environment = item.Environment + } + if item.TagName != "" { + opt.TagName = item.TagName + } + if item.OnSet != nil { + opt.OnSet = item.OnSet + } + if item.Prefix != "" { + opt.Prefix = item.Prefix + } + opt.RequiredIfNoDef = item.RequiredIfNoDef + } + + return []Options{opt} +} + +func getOnSetFn(opts []Options) OnSetFn { + return opts[0].OnSet +} + +// getTagName returns the tag name. +func getTagName(opts []Options) string { + return opts[0].TagName +} + +// getEnvironment returns the environment map. +func getEnvironment(opts []Options) map[string]string { + return opts[0].Environment +} + +// Parse parses a struct containing `env` tags and loads its values from +// environment variables. +func Parse(v interface{}, opts ...Options) error { + return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, opts...) +} + +// ParseWithFuncs is the same as `Parse` except it also allows the user to pass +// in custom parsers. +func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error { + opts = configure(opts) + + ptrRef := reflect.ValueOf(v) + if ptrRef.Kind() != reflect.Ptr { + return ErrNotAStructPtr + } + ref := ptrRef.Elem() + if ref.Kind() != reflect.Struct { + return ErrNotAStructPtr + } + parsers := defaultTypeParsers() + for k, v := range funcMap { + parsers[k] = v + } + + return doParse(ref, parsers, opts) +} + +func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Options) error { + refType := ref.Type() + + var agrErr aggregateError + + for i := 0; i < refType.NumField(); i++ { + refField := ref.Field(i) + refTypeField := refType.Field(i) + + if err := doParseField(refField, refTypeField, funcMap, opts); err != nil { + if val, ok := err.(aggregateError); ok { + agrErr.errors = append(agrErr.errors, val.errors...) + } else { + agrErr.errors = append(agrErr.errors, err) + } + } + } + + if len(agrErr.errors) == 0 { + return nil + } + + return agrErr +} + +func doParseField(refField reflect.Value, refTypeField reflect.StructField, funcMap map[reflect.Type]ParserFunc, opts []Options) error { + if !refField.CanSet() { + return nil + } + if reflect.Ptr == refField.Kind() && refField.Elem().Kind() == reflect.Struct { + return ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refTypeField, opts)...) + } + if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { + return ParseWithFuncs(refField.Addr().Interface(), funcMap, optsWithPrefix(refTypeField, opts)...) + } + value, err := get(refTypeField, opts) + if err != nil { + return err + } + + if value != "" { + return set(refField, refTypeField, value, funcMap) + } + + if reflect.Struct == refField.Kind() { + return doParse(refField, funcMap, optsWithPrefix(refTypeField, opts)) + } + + return nil +} + +func get(field reflect.StructField, opts []Options) (val string, err error) { + var exists bool + var isDefault bool + var loadFile bool + var unset bool + var notEmpty bool + + required := opts[0].RequiredIfNoDef + prefix := opts[0].Prefix + ownKey, tags := parseKeyForOption(field.Tag.Get(getTagName(opts))) + key := prefix + ownKey + for _, tag := range tags { + switch tag { + case "": + continue + case "file": + loadFile = true + case "required": + required = true + case "unset": + unset = true + case "notEmpty": + notEmpty = true + default: + return "", fmt.Errorf("tag option %q not supported", tag) + } + } + expand := strings.EqualFold(field.Tag.Get("envExpand"), "true") + defaultValue, defExists := field.Tag.Lookup("envDefault") + val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts)) + + if expand { + val = os.ExpandEnv(val) + } + + if unset { + defer os.Unsetenv(key) + } + + if required && !exists && len(ownKey) > 0 { + return "", fmt.Errorf(`required environment variable %q is not set`, key) + } + + if notEmpty && val == "" { + return "", fmt.Errorf("environment variable %q should not be empty", key) + } + + if loadFile && val != "" { + filename := val + val, err = getFromFile(filename) + if err != nil { + return "", fmt.Errorf(`could not load content of file "%s" from variable %s: %v`, filename, key, err) + } + } + + if onSetFn := getOnSetFn(opts); onSetFn != nil { + onSetFn(key, val, isDefault) + } + return val, err +} + +// split the env tag's key into the expected key and desired option, if any. +func parseKeyForOption(key string) (string, []string) { + opts := strings.Split(key, ",") + return opts[0], opts[1:] +} + +func getFromFile(filename string) (value string, err error) { + b, err := os.ReadFile(filename) + return string(b), err +} + +func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) { + value, exists := envs[key] + switch { + case (!exists || key == "") && defExists: + return defaultValue, true, true + case !exists: + return "", false, false + } + + return value, true, false +} + +func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { + if tm := asTextUnmarshaler(field); tm != nil { + if err := tm.UnmarshalText([]byte(value)); err != nil { + return newParseError(sf, err) + } + return nil + } + + typee := sf.Type + fieldee := field + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + fieldee = field.Elem() + } + + parserFunc, ok := funcMap[typee] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val)) + return nil + } + + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val).Convert(typee)) + return nil + } + + if field.Kind() == reflect.Slice { + return handleSlice(field, value, sf, funcMap) + } + + return newNoParserError(sf) +} + +func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + parts := strings.Split(value, separator) + + typee := sf.Type.Elem() + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + } + + if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { + return parseTextUnmarshalers(field, parts, sf) + } + + parserFunc, ok := funcMap[typee] + if !ok { + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + result := reflect.MakeSlice(sf.Type, 0, len(parts)) + for _, part := range parts { + r, err := parserFunc(part) + if err != nil { + return newParseError(sf, err) + } + v := reflect.ValueOf(r).Convert(typee) + if sf.Type.Elem().Kind() == reflect.Ptr { + v = reflect.New(typee) + v.Elem().Set(reflect.ValueOf(r).Convert(typee)) + } + result = reflect.Append(result, v) + } + field.Set(result) + return nil +} + +func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { + if reflect.Ptr == field.Kind() { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + } else if field.CanAddr() { + field = field.Addr() + } + + tm, ok := field.Interface().(encoding.TextUnmarshaler) + if !ok { + return nil + } + return tm +} + +func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { + s := len(data) + elemType := field.Type().Elem() + slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) + for i, v := range data { + sv := slice.Index(i) + kind := sv.Kind() + if kind == reflect.Ptr { + sv = reflect.New(elemType.Elem()) + } else { + sv = sv.Addr() + } + tm := sv.Interface().(encoding.TextUnmarshaler) + if err := tm.UnmarshalText([]byte(v)); err != nil { + return newParseError(sf, err) + } + if kind == reflect.Ptr { + slice.Index(i).Set(sv) + } + } + + field.Set(slice) + + return nil +} + +func newParseError(sf reflect.StructField, err error) error { + return parseError{ + sf: sf, + err: err, + } +} + +type parseError struct { + sf reflect.StructField + err error +} + +func (e parseError) Error() string { + return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err) +} + +func newNoParserError(sf reflect.StructField) error { + return fmt.Errorf(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type) +} + +func optsWithPrefix(field reflect.StructField, opts []Options) []Options { + subOpts := make([]Options, len(opts)) + copy(subOpts, opts) + if prefix := field.Tag.Get("envPrefix"); prefix != "" { + subOpts[0].Prefix += prefix + } + return subOpts +} + +type aggregateError struct { + errors []error +} + +func (e aggregateError) Error() string { + var sb strings.Builder + sb.WriteString("env:") + + for _, err := range e.errors { + sb.WriteString(fmt.Sprintf(" %v;", err.Error())) + } + + return strings.TrimRight(sb.String(), ";") +} diff --git a/vendor/github.com/caarlos0/env/v6/env_unix.go b/vendor/github.com/caarlos0/env/v6/env_unix.go new file mode 100644 index 0000000..411d438 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env_unix.go @@ -0,0 +1,15 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + r[p[0]] = p[1] + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v6/env_windows.go b/vendor/github.com/caarlos0/env/v6/env_windows.go new file mode 100644 index 0000000..e12123c --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env_windows.go @@ -0,0 +1,25 @@ +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + + // On Windows, environment variables can start with '='. If so, Split at next character. + // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 + prefixEqualSign := false + if len(e) > 0 && e[0] == '=' { + e = e[1:] + prefixEqualSign = true + } + p = strings.SplitN(e, "=", 2) + if prefixEqualSign { + p[0] = "=" + p[0] + } + + r[p[0]] = p[1] + } + return r +} diff --git a/vendor/golang.org/x/net/AUTHORS b/vendor/golang.org/x/net/AUTHORS deleted file mode 100644 index 15167cd..0000000 --- a/vendor/golang.org/x/net/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code refers to The Go Authors for copyright purposes. -# The master list of authors is in the main Go distribution, -# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/golang.org/x/net/CONTRIBUTORS b/vendor/golang.org/x/net/CONTRIBUTORS deleted file mode 100644 index 1c4577e..0000000 --- a/vendor/golang.org/x/net/CONTRIBUTORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code was written by the Go contributors. -# The master list of contributors is in the main Go distribution, -# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/golang.org/x/net/context/context.go b/vendor/golang.org/x/net/context/context.go index a3c021d..cf66309 100644 --- a/vendor/golang.org/x/net/context/context.go +++ b/vendor/golang.org/x/net/context/context.go @@ -21,9 +21,9 @@ // explicitly to each function that needs it. The Context should be the first // parameter, typically named ctx: // -// func DoSomething(ctx context.Context, arg Arg) error { -// // ... use ctx ... -// } +// func DoSomething(ctx context.Context, arg Arg) error { +// // ... use ctx ... +// } // // Do not pass a nil Context, even if a function permits it. Pass context.TODO // if you are unsure about which Context to use. diff --git a/vendor/golang.org/x/net/context/go17.go b/vendor/golang.org/x/net/context/go17.go index d20f52b..2cb9c40 100644 --- a/vendor/golang.org/x/net/context/go17.go +++ b/vendor/golang.org/x/net/context/go17.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.7 // +build go1.7 package context @@ -31,7 +32,7 @@ var DeadlineExceeded = context.DeadlineExceeded // call cancel as soon as the operations running in this Context complete. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { ctx, f := context.WithCancel(parent) - return ctx, CancelFunc(f) + return ctx, f } // WithDeadline returns a copy of the parent context with the deadline adjusted @@ -45,7 +46,7 @@ func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { // call cancel as soon as the operations running in this Context complete. func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { ctx, f := context.WithDeadline(parent, deadline) - return ctx, CancelFunc(f) + return ctx, f } // WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). @@ -53,11 +54,11 @@ func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { // Canceling this context releases resources associated with it, so code should // call cancel as soon as the operations running in this Context complete: // -// func slowOperationWithTimeout(ctx context.Context) (Result, error) { -// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) -// defer cancel() // releases resources if slowOperation completes before timeout elapses -// return slowOperation(ctx) -// } +// func slowOperationWithTimeout(ctx context.Context) (Result, error) { +// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) +// defer cancel() // releases resources if slowOperation completes before timeout elapses +// return slowOperation(ctx) +// } func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } diff --git a/vendor/golang.org/x/net/context/go19.go b/vendor/golang.org/x/net/context/go19.go index d88bd1d..64d31ec 100644 --- a/vendor/golang.org/x/net/context/go19.go +++ b/vendor/golang.org/x/net/context/go19.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.9 // +build go1.9 package context diff --git a/vendor/golang.org/x/net/context/pre_go17.go b/vendor/golang.org/x/net/context/pre_go17.go index 0f35592..7b6b685 100644 --- a/vendor/golang.org/x/net/context/pre_go17.go +++ b/vendor/golang.org/x/net/context/pre_go17.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.7 // +build !go1.7 package context @@ -263,11 +264,11 @@ func (c *timerCtx) cancel(removeFromParent bool, err error) { // Canceling this context releases resources associated with it, so code should // call cancel as soon as the operations running in this Context complete: // -// func slowOperationWithTimeout(ctx context.Context) (Result, error) { -// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) -// defer cancel() // releases resources if slowOperation completes before timeout elapses -// return slowOperation(ctx) -// } +// func slowOperationWithTimeout(ctx context.Context) (Result, error) { +// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) +// defer cancel() // releases resources if slowOperation completes before timeout elapses +// return slowOperation(ctx) +// } func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } diff --git a/vendor/golang.org/x/net/context/pre_go19.go b/vendor/golang.org/x/net/context/pre_go19.go index b105f80..1f97153 100644 --- a/vendor/golang.org/x/net/context/pre_go19.go +++ b/vendor/golang.org/x/net/context/pre_go19.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.9 // +build !go1.9 package context diff --git a/vendor/modules.txt b/vendor/modules.txt index 11935a7..6129cc3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,9 +1,9 @@ # github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 ## explicit github.com/armon/go-socks5 -# github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible -## explicit -github.com/caarlos0/env -# golang.org/x/net v0.0.0-20180406214816-61147c48b25b -## explicit +# github.com/caarlos0/env/v6 v6.10.1 +## explicit; go 1.17 +github.com/caarlos0/env/v6 +# golang.org/x/net v0.1.0 +## explicit; go 1.17 golang.org/x/net/context