Compare commits

...

44 Commits

Author SHA1 Message Date
Chris F Ravenscroft 7288a72cc4 chore: bump minimum go version to 1.21 2023-12-18 00:36:04 +00:00
Chris F Ravenscroft 0b4f33281d chore: fix go fmt in makefile 2023-12-18 00:26:50 +00:00
Chris F Ravenscroft 0bf3d4a82a chore: release 2.3.1
Release-As: 2.3.1
2023-12-18 00:17:05 +00:00
Chris F Ravenscroft f5bb80e024 Update release-please to use master branch 2023-12-17 15:55:36 -08:00
Chris F Ravenscroft 1a37396109 fix: update tracing code to work with breaking otlp 1.20 changes 2023-12-11 01:36:58 +00:00
Chris F Ravenscroft d178281474 Merge branch 'shipperizer-otlp' 2023-12-11 00:36:53 +00:00
Chris F Ravenscroft a0d1069841 Merge branch 'master' into otlp 2023-12-10 16:34:56 -08:00
Chris F Ravenscroft 67d77324a3 chore: reweure ldap packages (#370) 2023-12-10 15:32:24 -08:00
Alessandro Cabbia 01bdb7148e startTLS: ldap over TLS functionality (#364)
* feat: introduce tls parameters in config to allow startTLS

* feat: pass tls config for startTLS to ldap server

* build: update libs to use newer ldap

* feat: allow tls config for startTLS
2023-12-10 15:20:29 -08:00
shipperizer f692394942 feat: allow tracing configuration via main config 2023-12-10 17:34:33 +00:00
shipperizer 46e49b6976 feat: introduce context for otlp spans into plugins pkg 2023-12-10 10:13:10 +00:00
shipperizer 52a0adcbcf ci: add go1.21 2023-12-10 09:59:57 +00:00
shipperizer 2ca5312696 feat: introduce otelsql 2023-12-10 09:59:55 +00:00
shipperizer 6ee78cd881 build: update deps with otlp libs 2023-12-10 09:59:04 +00:00
shipperizer 1c2b23c00f feat: wire up basic tracer 2023-12-10 09:59:04 +00:00
shipperizer d05630f66b feat: introduce context for otlp spans into handler pkg 2023-11-20 14:10:29 +00:00
shipperizer 0cf04037a2 feat: introduce otlp tracer 2023-11-20 14:10:29 +00:00
Chris F Ravenscroft 7174aba5ab chore: fix wrong goconvey hierarchy (#361) 2023-11-19 17:43:40 -08:00
Alessandro Cabbia 5ddbd4f610 docs: fix type on url link for last-commit badge (#358) 2023-11-05 00:13:49 -07:00
dependabot[bot] 0b6cfcb1b4 Bump github.com/smartystreets/goconvey from 1.6.4 to 1.8.1 in /v2 (#354)
Chore: Bumps [github.com/smartystreets/goconvey](https://github.com/smartystreets/goconvey) from 1.6.4 to 1.8.1.
- [Release notes](https://github.com/smartystreets/goconvey/releases)
- [Commits](https://github.com/smartystreets/goconvey/compare/v1.6.4...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/smartystreets/goconvey
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris F Ravenscroft <chris@voilaweb.com>
2023-10-21 15:31:19 -07:00
dependabot[bot] f3342fa709 Bump actions/checkout from 2 to 4 (#353)
Chore: Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-21 15:28:33 -07:00
dependabot[bot] 545188e5f2 Bump actions/setup-go from 2 to 4 (#352)
Chore: Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris F Ravenscroft <chris@voilaweb.com>
2023-10-21 15:17:09 -07:00
Chris F Ravenscroft c90cbfe5fb fix: formatting 2023-10-21 22:08:11 +00:00
Chris F Ravenscroft 1721e5701d Merge branch 'shipperizer-toml/vendor' 2023-10-21 22:04:14 +00:00
Chris F Ravenscroft ebdeae7073 Merge branch 'master' into toml/vendor 2023-10-21 15:02:48 -07:00
Alessandro Cabbia 94fe62cfe1 monitoring: metrics enhancements and proposal for dropping expvar (#351)
* feat: introduce new prometheus monitor object

* feat: add LDAPMonitorWatcher as a potential replacement for v0 Collector

* feat: pass monitor object as dependency and instrument core operations

* feat: instantiate monitor in main

* ci: exclude mock files

* ci: generate mocks before running tests and go vet

* ci: make vet command keep going in case of errors

reason is due to the following happening in the GetStats method of the ldap.Stats struct

```
internal/monitoring/mock_interfaces.go:92:13: assignment copies lock value to ret0: (github.com/nmcclain/ldap.Stats, bool) contains github.com/nmcclain/ldap.Stats contains sync.Mutex
internal/monitoring/mock_interfaces.go:93:9: return copies lock value: github.com/nmcclain/ldap.Stats contains sync.Mutex
internal/monitoring/ldap_test.go:23:56: call of mockLDAPServer.EXPECT().GetStats().MinTimes(1).Return copies lock value: github.com/nmcclain/ldap.Stats contains sync.Mutex
```

* deps:  move to use go.uber.org/mock/gomock
2023-10-21 11:58:12 -07:00
shipperizer f9addbc2c5 fix: upgrade to use BurntSushi/toml 2023-10-15 23:58:51 +01:00
shipperizer 5fe8aca852 fix: removed config setup from main, reshoring log configuration 2023-10-15 23:54:09 +01:00
shipperizer 24455e3988 fix: drop vendored toml 2023-10-15 23:54:09 +01:00
shipperizer 3ba8e11132 fix: move all toml parsing into a new internal package, drop the mappings in favour of toml.Primitive decoding 2023-10-15 23:54:09 +01:00
Alessandro Cabbia accf15410c refactor: separating http handlers from routing setup (#350)
* fix: offload prometheus logic to the monitoring package

* fix: offload ui logic to the asset package

* refactor: use handlers coming from assets and monitoring package

* ci: add unit test run and vet command

* fix: make vet happy and adjust code
2023-10-14 11:19:58 -07:00
Alessandro Cabbia 8739fd6e4f ci: dependabot and dependencies update (#348)
* fix: update and upgrade libraries

side note:
* using commit to trigger release-please workflow in light of recent changes regarding branch strategy

* ci: introduce dependabot
2023-10-14 11:15:59 -07:00
Alessandro Cabbia 97118ff186 docs: update last commit ref to use master branch (#347) 2023-10-14 11:07:26 -07:00
Chris F Ravenscroft 3943e7dc21 Graceful shutdown (#346)
* build: make build vars optional and passable via env

* build: use . for BUILD_FILES var to allow buildvcs flag

* build: create mkbindir, build and sha256 targets

* build: reuse build target for each platform specific compilation

* Ensure same go version for main and plugins

* chore: release 2.3.0

Release-As: 2.3.0

* release-please

* Fix(plugins)!: Rename the groups table to ldapgroups (#326)

The groups table name is a reserved keyword in mysql and requires that
the table name be escaped with backticks in order to use the groups
name. Other databases such as postgres do not support the use of the
backticks in the query statements. This change renames the groups table
to ldapgroups in oder to avoid this problem altogether.

This does have a dependency on the backend sql plugins to alter the
schema to match the new name.

Signed-off-by: Billy Olsen <billy.olsen@gmail.com>
Co-authored-by: Chris F Ravenscroft <chris@voilaweb.com>

* chore: update docker sqlite with ldapgroups (#338)

* feat: Update migration code to support table names (#339)

* chore(dev): release 2.3.0 (#337)

* ci: change pr name pattern for release please

* ci: switch to use master as default branch and plan to drop dev

* fix: exploit release-please for tagging in code

* fix: use runtime/debug to fetch git build info

* fix: use newer function to fetch git info for version

* build: drop build tags

* feat: graceful shutdown (#343)

* fix: start servers on separate goroutines

* fix: listen to signals for graceful shutdowns

* fix: exit with 0 in case of programmed termination

---------

Signed-off-by: Billy Olsen <billy.olsen@gmail.com>
Co-authored-by: shipperizer <alexcabb@gmail.com>
Co-authored-by: Billy Olsen <wolsen@users.noreply.github.com>
2023-10-07 18:04:53 -07:00
Chris F Ravenscroft 114b36f677 Final merge from Dev to Master (#344)
* build: make build vars optional and passable via env

* build: use . for BUILD_FILES var to allow buildvcs flag

* build: create mkbindir, build and sha256 targets

* build: reuse build target for each platform specific compilation

* Ensure same go version for main and plugins

* chore: release 2.3.0

Release-As: 2.3.0

* release-please

* Fix(plugins)!: Rename the groups table to ldapgroups (#326)

The groups table name is a reserved keyword in mysql and requires that
the table name be escaped with backticks in order to use the groups
name. Other databases such as postgres do not support the use of the
backticks in the query statements. This change renames the groups table
to ldapgroups in oder to avoid this problem altogether.

This does have a dependency on the backend sql plugins to alter the
schema to match the new name.

Signed-off-by: Billy Olsen <billy.olsen@gmail.com>
Co-authored-by: Chris F Ravenscroft <chris@voilaweb.com>

* chore: update docker sqlite with ldapgroups (#338)

* feat: Update migration code to support table names (#339)

* chore(dev): release 2.3.0 (#337)

* ci: change pr name pattern for release please

* ci: switch to use master as default branch and plan to drop dev

* fix: exploit release-please for tagging in code

* fix: use runtime/debug to fetch git build info

* fix: use newer function to fetch git info for version

* build: drop build tags

---------

Signed-off-by: Billy Olsen <billy.olsen@gmail.com>
Co-authored-by: shipperizer <alexcabb@gmail.com>
Co-authored-by: Billy Olsen <wolsen@users.noreply.github.com>
2023-10-07 12:51:00 -07:00
Chris F Ravenscroft 0904a24564 v2.3.0 (#341)
* build: make build vars optional and passable via env

* build: use . for BUILD_FILES var to allow buildvcs flag

* build: create mkbindir, build and sha256 targets

* build: reuse build target for each platform specific compilation

* Ensure same go version for main and plugins

* chore: release 2.3.0

Release-As: 2.3.0

* release-please

* Fix(plugins)!: Rename the groups table to ldapgroups (#326)

The groups table name is a reserved keyword in mysql and requires that
the table name be escaped with backticks in order to use the groups
name. Other databases such as postgres do not support the use of the
backticks in the query statements. This change renames the groups table
to ldapgroups in oder to avoid this problem altogether.

This does have a dependency on the backend sql plugins to alter the
schema to match the new name.

Signed-off-by: Billy Olsen <billy.olsen@gmail.com>
Co-authored-by: Chris F Ravenscroft <chris@voilaweb.com>

* chore: update docker sqlite with ldapgroups (#338)

* feat: Update migration code to support table names (#339)

* chore(dev): release 2.3.0 (#337)

---------

Signed-off-by: Billy Olsen <billy.olsen@gmail.com>
Co-authored-by: shipperizer <alexcabb@gmail.com>
Co-authored-by: Billy Olsen <wolsen@users.noreply.github.com>
2023-10-01 22:55:23 -07:00
Chris F Ravenscroft 498cad48b1 Merge branch 'dev' 2023-09-23 18:50:42 +00:00
Chris F Ravenscroft 19fa5045ee Merge branch 'master' of github.com:glauth/glauth 2023-09-23 18:50:38 +00:00
Chris F Ravenscroft 08a89a9861 Sum types, preparation (#330) 2023-09-09 13:35:42 -07:00
Daniel Barney a4450d4558 Allow custom S3 endpoint to be used (#329)
Co-authored-by: Daniel Barney <daniel@thoughtplot.io>
2023-09-09 13:31:40 -07:00
Chris F Ravenscroft 0b7910c03c users is an organizational unit (#323) 2023-09-09 13:09:52 -07:00
Billy Olsen 944d602077 Update the TRIM_FLAGS to use -trimpath (#327)
The current TRIM_FLAGS specify gcflags and asmflags directly in order to
trim the path of the resulting binaries. This results in an error where
the plugins cannot be loaded due to the error: "built with a different
version of package". Per https://github.com/golang/go/issues/51955 in the
golang repository, this is explicitly not supported. Switching to use the
go build command's -trimpath flag resolves the issue.

Signed-off-by: Billy Olsen <billy.olsen@gmail.com>
2023-09-03 19:15:25 -07:00
guangwu 596d28efbd chore: remove refs to deprecated io/ioutil (#322)
Thanks!
2023-07-22 13:02:24 -07:00
Chris F Ravenscroft 6a58f35b14 v2.2.0 merge to master
Cross-platform changes:
- No SysLog on Windows (#289)
  
Code quality:
- CodeSee integration
- Add CodeQL workflow for GitHub code scanning (#295)
  
Correctness:
- Construct DNs under ou=users for uniqueMember group attribute
- Use NameFormat/GroupFormat when constructing RDN attributes
- Brian Candler (@candlerb) completed the above by making sure they were supported where it matters
- The directory structure can be navigated up and down using any LDAP browser
- searchMaybePosixAccounts: add filter to respect searchBaseDN (#304)
- Feature: introduce legacy behavior, based on past versions (#318)
  
Security/Updates:
- Bump golang.org/x/crypto in /v2
- Update Go Versions to 1.19, 1.20 (#314)
- Added anonymousdse backend option to config sample (#316)
  
Qualify of life:
- Add devcontainer (#306)
- Add help command to Makefile (#307)
- Update go.work to 1.19 (#315)
2023-07-03 18:59:23 -07:00
85 changed files with 2449 additions and 7826 deletions
+24
View File
@@ -0,0 +1,24 @@
# 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://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# \todo Eliminate duplication when/if Dependabot supports YAML anchors
version: 2
updates:
- package-ecosystem: github-actions
directory: /
labels:
- dependencies
- actions
schedule:
day: sunday
interval: weekly
- package-ecosystem: gomod
directory: /v2
labels:
- dependencies
- go
schedule:
interval: weekly
day: friday
+4 -4
View File
@@ -4,16 +4,16 @@ on:
pull_request:
push:
branches:
- dev
- master
jobs:
build:
runs-on: ubuntu-20.04
strategy:
matrix:
go-version: [1.19.x, 1.20.x]
go-version: [1.21.x]
steps:
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Updating packages database
@@ -21,7 +21,7 @@ jobs:
- name: Install CLI Dependencies
run: sudo apt-get install -y ldap-utils oathtool libpam0g-dev
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Fetch Go dependencies
working-directory: ./v2
run: go get -t -v ./...
+2 -2
View File
@@ -2,7 +2,7 @@ name: "CodeQL"
on:
push:
branches: [ "master", "dev" ]
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
+27
View File
@@ -0,0 +1,27 @@
on:
pull_request:
push:
branches:
- master
permissions:
contents: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/release-please-action@v3
with:
release-type: go
package-name: GLAuth
path: v2
default-branch: master
pull-request-title-pattern: "ci: release ${version}"
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
extra-files: |
v2/internal/version/const.go
+11
View File
@@ -4,3 +4,14 @@ v2/bin
v2/docker/assets/linux
v2/gl.db
.vscode/
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
### Go Patch ###
/vendor/
/Godeps/
# gomocks auto-generated
**/mock_*.go
+12
View File
@@ -0,0 +1,12 @@
[submodule "v2/pkg/plugins/glauth-sqlite"]
path = v2/pkg/plugins/glauth-sqlite
url = https://github.com/glauth/glauth-sqlite
[submodule "v2/pkg/plugins/glauth-mysql"]
path = v2/pkg/plugins/glauth-mysql
url = https://github.com/glauth/glauth-mysql
[submodule "v2/pkg/plugins/glauth-postgres"]
path = v2/pkg/plugins/glauth-postgres
url = https://github.com/glauth/glauth-postgres
[submodule "v2/pkg/plugins/glauth-pam"]
path = v2/pkg/plugins/glauth-pam
url = https://github.com/glauth/glauth-pam
+1 -1
View File
@@ -7,7 +7,7 @@ Go-lang LDAP Authentication (GLAuth) is a secure, easy-to-use, LDAP server w/ co
![GitHub all releases](https://img.shields.io/github/downloads/glauth/glauth/total)
![Docker pulls](https://badgen.net/docker/pulls/glauth/glauth)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/glauth/glauth/dev)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/glauth/glauth/master)
![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability-percentage/glauth/glauth)
* Centrally manage accounts across your infrastructure
+22
View File
@@ -0,0 +1,22 @@
# Changelog
## [2.3.0](https://github.com/glauth/glauth/compare/v2.2.0...v2.3.0) (2023-10-02)
### ⚠ BREAKING CHANGES
* **plugins:** Rename the groups table to ldapgroups ([#326](https://github.com/glauth/glauth/issues/326))
### Features
* Update migration code to support table names ([#339](https://github.com/glauth/glauth/issues/339)) ([349431c](https://github.com/glauth/glauth/commit/349431c6caa0388d17ab987621eb9be5f019155e))
### Bug Fixes
* **plugins:** Rename the groups table to ldapgroups ([#326](https://github.com/glauth/glauth/issues/326)) ([675b236](https://github.com/glauth/glauth/commit/675b236328a21a65daa7876a1a3c6900b85b1964))
### Miscellaneous Chores
* release 2.3.0 ([0c11325](https://github.com/glauth/glauth/commit/0c11325a2482d5067c805a4c7ed948a5e337b8f8))
+54 -18
View File
@@ -16,14 +16,21 @@ GIT_IS_TAG_COMMIT=$(shell git describe --abbrev=0 --tags > /dev/null 2> /dev/nul
GIT_BRANCH=$(shell git rev-parse --abbrev-ref HEAD)
# Build variables
BUILD_VARS=-s -w -X main.GitCommit=${GIT_COMMIT} -X main.GitBranch=${GIT_BRANCH} -X main.BuildTime=${BUILD_TIME} -X main.GitClean=${GIT_CLEAN} -X main.LastGitTag=${LAST_GIT_TAG} -X main.GitTagIsCommit=${GIT_IS_TAG_COMMIT}
BUILD_FILES=glauth.go
TRIM_FLAGS=-gcflags "all=-trimpath=${PWD}" -asmflags "all=-trimpath=${PWD}"
BUILD_VARS?=-s -w
BUILD_FILES=.
TRIM_FLAGS>?=-trimpath
# Targets
MAIN_TARGETS=linux/amd64,linux/386,linux/arm64,linux/arm-7,darwin/amd64,darwin/arm64,windows/amd64,windows/386
PLUGIN_TARGETS=linux/amd64,linux/386,linux/arm64,linux/arm-7,darwin/amd64,darwin/arm64
# For release process
GO_RELEASE_V=$(shell go version | { read _ _ v _; echo $${v#go}; })
# Build
GOOS?=linux
GOARCH?=amd64
# Plugins
-include pkg/plugins/*/Makefile
@@ -38,9 +45,20 @@ help:
#run: @ build and run - used for development
run: setup devrun
#test: @ runs the integration test on linuxamd64 (eventually allow the binary to be set)
test: runtest
mocks:
go install go.uber.org/mock/mockgen@v0.3.0
# generate gomocks
go generate ./...
.PHONY: mocks
vet:
-go vet ./...
.PHONY: vet
#test: @ runs the integration test on linuxamd64 (eventually allow the binary to be set)
test: mocks vet
# also run unit tests for packages, skip glauth_test.go for now
go test -v -cover -coverprofile coverage.out ./pkg/... ./internal/...
$(MAKE) runtest
#all: @ run build process for all binaries
all: setup binaries verify
@@ -69,34 +87,48 @@ updatetest:
./scripts/ci/integration-test.sh
format:
go fmt
go fmt ./...
devrun:
go run ${BUILD_FILES} -c sample-simple.cfg
mkbindir:
@echo "create directory bin/$(GOOS)$(GOARCH)"
@mkdir -p bin/$(GOOS)$(GOARCH)
.PHONY: mkbindir
build: mkbindir
@go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$(GOOS)$(GOARCH)/glauth -buildvcs=false .
$(MAKE) sha256
.PHONY: build
sha256:
@sha256sum bin/$(GOOS)$(GOARCH)/glauth > bin/$(GOOS)$(GOARCH)/glauth.sha256
.PHONY: sha256
linux386:
mkdir -p bin/$@ && GOOS=linux GOARCH=386 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=linux GOARCH=386 $(MAKE) build
linuxamd64:
mkdir -p bin/$@ && GOOS=linux GOARCH=amd64 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=linux GOARCH=amd64 $(MAKE) build
linuxarm:
mkdir -p bin/$@ && GOOS=linux GOARCH=arm go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=linux GOARCH=arm $(MAKE) build
linuxarm64:
mkdir -p bin/$@ && GOOS=linux GOARCH=arm64 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=linux GOARCH=arm64 $(MAKE) build
darwinamd64:
mkdir -p bin/$@ && GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 $(MAKE) build
darwinarm64:
mkdir -p bin/$@ && GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 $(MAKE) build
win386:
mkdir -p bin/$@ && GOOS=windows GOARCH=386 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=windows GOARCH=386 $(MAKE) build
winamd64:
mkdir -p bin/$@ && GOOS=windows GOARCH=amd64 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o bin/$@/glauth ${BUILD_FILES} && cd bin/$@ && sha256sum glauth > glauth.sha256
GOOS=windows GOARCH=amd64 $(MAKE) build
verify:
@for binary in linux386 linuxamd64 linuxarm linuxarm64 darwinamd64 darwinarm64 win386 winamd64; do cd bin/$$binary && sha256sum glauth.sha256 -c && cd ../..; done
@@ -105,6 +137,7 @@ pull-plugin:
@git submodule add $U $M && \
(for pkg in $$(cat $$M/go.mod | awk '/^require [a-z]/{print $$2} /^\)/{rblock=0} rblock{if($$4!="indirect" && $$1!~"glauth"){print $$1}} /^require \(/{rblock=1}'); do if [ "$$(grep $$pkg go.mod &>/dev/null)" = "" ]; then go get $$pkg; fi; done)
#pull-base-plugins: @ pull plugins for local building
pull-base-plugins:
@U=https://github.com/glauth/glauth-sqlite M=pkg/plugins/glauth-sqlite make pull-plugin && \
U=https://github.com/glauth/glauth-mysql M=pkg/plugins/glauth-mysql make pull-plugin && \
@@ -117,21 +150,24 @@ forget-plugin:
git rm --cache $M && \
go mod tidy
#forget-plugin: @ remove plugins, restoring original file structure
forget-plugins:
for pkg in pkg/plugins/*/go.mod; do\
P=$$(echo $$pkg | cut -d'/' -f 3) M=$$(dirname $$pkg) make forget-plugin;\
done
#releasemain: @ build main binaries for distribution
releasemain:
@xgo -v -ldflags="${BUILD_VARS}" -trimpath -go 1.18.1 -out glauth -dest bin -buildvcs=false --targets="${MAIN_TARGETS}" .
@xgo -v -ldflags="${BUILD_VARS}" -trimpath -go ${GO_RELEASE_V} -out glauth -dest bin -buildvcs=false --targets="${MAIN_TARGETS}" .
releaseplugin:
@xgo -v -ldflags="${BUILD_VARS}" -trimpath -go 1.18.1 -out $P -dest bin -buildvcs=false -buildmode=plugin --targets="${PLUGIN_TARGETS}" --pkg $M/$P.go . && \
(cd bin && for lib in $$(ls $$P-*); do mv $$lib $$lib.so; done)
@xgo -v -ldflags="${BUILD_VARS}" -trimpath -go ${GO_RELEASE_V} -out $P -dest bin -buildvcs=false -buildmode=plugin --targets="${PLUGIN_TARGETS}" --pkg $M/$P.go . && \
(cd bin && for lib in $$(ls $$P-*); do sudo mv $$lib $$lib.so; done)
#releaseplugins: @ build base plugins for distribution
releaseplugins:
@for pkg in pkg/plugins/*/go.mod; do\
P=$$(echo $$pkg | cut -d'/' -f 3) && make release-$$P;\
M=pkg/plugins/$$(echo $$pkg | cut -d'/' -f 3) && P=$$(echo $$M | grep -oP 'glauth-\K\w+') && M=$$M P=$$P make releaseplugin;\
done
releasedockermain:
Binary file not shown.
+95 -603
View File
@@ -1,38 +1,33 @@
package main
import (
"bytes"
"crypto/tls"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/GeertJohan/yubigo"
"github.com/arl/statsviz"
docopt "github.com/docopt/docopt-go"
"github.com/fsnotify/fsnotify"
"github.com/glauth/glauth/v2/internal/monitoring"
_tls "github.com/glauth/glauth/v2/internal/tls"
"github.com/glauth/glauth/v2/internal/toml"
"github.com/glauth/glauth/v2/internal/tracing"
"github.com/glauth/glauth/v2/internal/version"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/frontend"
"github.com/glauth/glauth/v2/pkg/logging"
"github.com/glauth/glauth/v2/pkg/server"
"github.com/glauth/glauth/v2/pkg/stats"
"github.com/hydronica/toml"
"github.com/jinzhu/copier"
"github.com/rs/zerolog"
"gopkg.in/amz.v3/aws"
"gopkg.in/amz.v3/s3"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"time"
)
// Set with buildtime vars
var LastGitTag string
var BuildTime string
var GitCommit string
var GitClean string
var GitBranch string
var GitTagIsCommit string
"github.com/rs/zerolog"
)
const programName = "glauth"
@@ -48,6 +43,7 @@ Options:
-K <aws_key_id> AWS Key ID.
-S <aws_secret_key> AWS Secret Key.
-r <aws_region> AWS Region [default: us-east-1].
--aws_endpoint_url <url> Custom S3 endpoint.
--ldap <address> Listen address for the LDAP server.
--ldaps <address> Listen address for the LDAPS server.
--ldaps-cert <cert-file> Path to cert file for the LDAPS server.
@@ -65,49 +61,8 @@ var (
activeConfig = &config.Config{}
)
// Reads builtime vars and returns a full string containing info about
// the currently running version of the software. Primarily used by the
// --version flag at runtime.
func getVersionString() string {
var versionstr string
versionstr = "GLauth"
// Notate the git context of the build
switch {
// If a release, use the tag
case GitClean == "1" && GitTagIsCommit == "1":
versionstr += " " + LastGitTag + "\n\n"
// If this branch had a tag before, mention the branch and the tag to give a rough idea of the base version
case len(GitBranch) > 1 && len(LastGitTag) > 1:
versionstr += "\nNon-release build from branch " + GitBranch + ", based on tag " + LastGitTag + "\n\n"
// If no previous tag specified, just mention the branch
case len(GitBranch) > 1:
versionstr += "\nNon-release build from branch " + GitBranch + "\n\n"
// Fallback message, if all else fails
default:
versionstr += "\nNon-release build\n\n"
}
// Include build time
if len(BuildTime) > 1 {
versionstr += "Build time: " + BuildTime + "\n"
}
// Add commit hash
if GitClean == "1" && len(GitCommit) > 1 {
versionstr += "Commit: " + GitCommit + "\n"
}
return versionstr
}
func main() {
if err := parseArgs(); err != nil {
fmt.Println("Could not parse command-line arguments")
fmt.Println(err)
@@ -119,15 +74,35 @@ func main() {
checkConfig = true
}
}
if err := doConfig(checkConfig); err != nil {
cfg, err := toml.NewConfig(checkConfig, getConfigLocation(), args)
if err != nil {
fmt.Println("Configuration file error")
fmt.Println(err)
os.Exit(1)
}
if checkConfig {
fmt.Println("Config file seems ok (but I am not checking much at this time)")
return
}
if err := copier.Copy(activeConfig, cfg); err != nil {
log.Info().Err(err).Msg("Could not save reloaded config. Holding on to old config")
}
log = logging.InitLogging(activeConfig.Debug, activeConfig.Syslog, activeConfig.StructuredLog)
if !checkConfig {
if cfg.Debug {
log.Info().Msg("Debugging enabled")
}
if cfg.Syslog {
log.Info().Msg("Syslog enabled")
}
}
log.Info().Msg("AP start")
startService()
@@ -135,7 +110,7 @@ func main() {
func startService() {
// stats
stats.General.Set("version", stats.Stringer(LastGitTag))
stats.General.Set("version", stats.Stringer(version.Version))
// web API
if activeConfig.API.Enabled {
@@ -155,46 +130,75 @@ func startService() {
)
}
monitor := monitoring.NewMonitor(&log)
tracer := tracing.NewTracer(
tracing.NewConfig(
activeConfig.Tracing.Enabled,
activeConfig.Tracing.GRPCEndpoint,
activeConfig.Tracing.HTTPEndpoint,
&log,
),
)
startConfigWatcher()
var err error
var tlsConfig *tls.Config
if c := activeConfig.LDAP; c.Enabled && c.TLS {
// TODO check if tls params are string or bytes and change config accordingly
tlsConfig, err = _tls.MakeTLS([]byte(c.TLSCert), []byte(c.TLSKey))
if err != nil {
log.Warn().Err(err).Msg("unable to configure TLS, proceeding without....StartTLS won't be supported")
}
}
s, err := server.NewServer(
server.Logger(log),
server.Config(activeConfig),
server.TLSConfig(tlsConfig),
server.Monitor(monitor),
server.Tracer(tracer),
)
if err != nil {
log.Error().Err(err).Msg("could not create server")
os.Exit(1)
}
if activeConfig.LDAP.Enabled {
// Don't block if also starting a LDAPS server afterwards
shouldBlock := !activeConfig.LDAPS.Enabled
if shouldBlock {
go func() {
if err := s.ListenAndServe(); err != nil {
log.Error().Err(err).Msg("could not start LDAP server")
os.Exit(1)
}
} else {
go func() {
if err := s.ListenAndServe(); err != nil {
log.Error().Err(err).Msg("could not start LDAP server")
os.Exit(1)
}
}()
}
}()
}
if activeConfig.LDAPS.Enabled {
// Always block here
if err := s.ListenAndServeTLS(); err != nil {
log.Error().Err(err).Msg("could not start LDAPS server")
os.Exit(1)
}
go func() {
if err := s.ListenAndServeTLS(); err != nil {
log.Error().Err(err).Msg("could not start LDAPS server")
os.Exit(1)
}
}()
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// Block until we receive our signal.
<-c
// Doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
s.Shutdown()
// Optionally, you could run srv.Shutdown in a goroutine and block on
// <-ctx.Done() if your application should wait for other services
// to finalize based on context cancellation.
log.Info().Msg("AP exit")
os.Exit(1)
os.Exit(0)
}
func startConfigWatcher() {
@@ -233,10 +237,17 @@ func startConfigWatcher() {
isChanged, isRemoved = true, false
}
if isChanged {
if err := doConfig(false); err != nil {
cfg, err := toml.NewConfig(false, configFileLocation, args)
if err != nil {
log.Info().Err(err).Msg("Could not reload config. Holding on to old config")
} else {
log.Info().Msg("Config was reloaded")
if err := copier.Copy(activeConfig, cfg); err != nil {
log.Info().Err(err).Msg("Could not save reloaded config. Holding on to old config")
}
}
isChanged = false
}
@@ -250,7 +261,7 @@ func startConfigWatcher() {
func parseArgs() error {
var err error
if args, err = docopt.Parse(usage, nil, true, getVersionString(), false); err != nil {
if args, err = docopt.Parse(usage, nil, true, version.GetVersion(), false); err != nil {
return err
}
@@ -260,522 +271,3 @@ func parseArgs() error {
func getConfigLocation() string {
return args["--config"].(string)
}
func parseConfigFile(configFileLocation string) (*config.Config, error) {
cfg := config.Config{}
// setup defaults
cfg.LDAP.Enabled = false
cfg.LDAPS.Enabled = true
// parse the config file
if strings.HasPrefix(configFileLocation, "s3://") {
if _, present := aws.Regions[args["-r"].(string)]; present == false {
return &cfg, fmt.Errorf("invalid AWS region: %s", args["-r"])
}
region := aws.Regions[args["-r"].(string)]
auth, err := aws.EnvAuth()
if err != nil {
if args["-K"] == nil || args["-S"] == nil {
return &cfg, fmt.Errorf("AWS credentials not found: must use -K and -S flags, or set these env vars:\n\texport AWS_ACCESS_KEY_ID=\"AAA...\"\n\texport AWS_SECRET_ACCESS_KEY=\"BBBB...\"\n")
}
auth = aws.Auth{
AccessKey: args["-K"].(string),
SecretKey: args["-S"].(string),
}
}
// parse S3 url
s3url := strings.TrimPrefix(configFileLocation, "s3://")
parts := strings.SplitN(s3url, "/", 2)
if len(parts) != 2 {
return &cfg, fmt.Errorf("invalid S3 URL: %s", s3url)
}
b, err := s3.New(auth, region).Bucket(parts[0])
if err != nil {
return &cfg, err
}
tomlData, err := b.Get(parts[1])
if err != nil {
return &cfg, err
}
if _, err := toml.Decode(string(tomlData), &cfg); err != nil {
return &cfg, err
}
} else { // local config file
fInfo, err := os.Stat(configFileLocation)
if err != nil {
return &cfg, fmt.Errorf("non-existent config path: %s", configFileLocation)
}
var md toml.MetaData
if fInfo.IsDir() { // multiple files in a directory
rawCfgStruct := make(map[string]interface{})
// To keep things simple, we are not going to use the default values built in Cfg
// so far (LDAP.Enabled, LDAPS.Enabled, etc) so do not forget to specify them!
/*
sourcebuf := new(bytes.Buffer)
err = toml.NewEncoder(sourcebuf).Encode(cfg)
if err != nil {
return &cfg, err
}
var initialRawCfgStruct interface{}
if err := toml.Unmarshal(sourcebuf.Bytes(), &initialRawCfgStruct); err != nil {
return &cfg, err
}
if err = mergeConfigs(&rawCfgStruct, initialRawCfgStruct); err != nil {
return &cfg, err
}
*/
files, _ := ioutil.ReadDir(configFileLocation)
for _, f := range files {
canonicalName := filepath.Join(configFileLocation, f.Name())
bs, _ := ioutil.ReadFile(canonicalName)
var curRawCfgStruct interface{}
if err := toml.Unmarshal(bs, &curRawCfgStruct); err != nil {
return &cfg, err
}
if err = mergeConfigs(&rawCfgStruct, curRawCfgStruct); err != nil {
return &cfg, err
}
}
destbuf := new(bytes.Buffer)
err = toml.NewEncoder(destbuf).Encode(rawCfgStruct)
if err != nil {
return &cfg, err
}
fmt.Println(destbuf.String())
merged := config.Config{}
if md, err = toml.Decode(destbuf.String(), &merged); err != nil {
return &cfg, err
}
cfg = merged
} else {
md, err = toml.DecodeFile(configFileLocation, &cfg)
if err != nil {
return &cfg, err
}
}
switch users := md.Mappings()["users"].(type) {
case []map[string]interface{}:
for _, mduser := range users {
if mduser["customattributes"] != nil {
for idx, cfguser := range cfg.Users {
if cfguser.Name == mduser["name"].(string) {
switch attributes := mduser["customattributes"].(type) {
case []map[string]interface{}:
cfg.Users[idx].CustomAttrs = attributes[0]
case map[string]interface{}:
cfg.Users[idx].CustomAttrs = attributes
default:
log.Info().Interface("attributes", attributes).Msg("Unknown attribute structure in config file")
}
break
}
}
}
}
}
}
// Backward Compability
if cfg.Backend.Datastore != "" {
if cfg.Backends != nil {
return &cfg, fmt.Errorf("you cannot specify both [Backend] and [[Backends]] directives in the same configuration ")
} else {
cfg.Backends = append(cfg.Backends, cfg.Backend)
}
}
// Patch with default values where not specified
for i := range cfg.Backends {
if cfg.Backends[i].NameFormat == "" {
cfg.Backends[i].NameFormat = "cn"
}
if cfg.Backends[i].GroupFormat == "" {
cfg.Backends[i].GroupFormat = "ou"
}
if cfg.Backends[i].SSHKeyAttr == "" {
cfg.Backends[i].SSHKeyAttr = "sshPublicKey"
}
}
//
return &cfg, nil
}
func mergeConfigs(config1 interface{}, config2 interface{}) error {
var merger func(int, string, interface{}, interface{}) error
merger = func(depth int, keyName string, cfg1 interface{}, cfg2 interface{}) error {
//fmt.Println(strings.Repeat(" ", depth), "Handling element: ", keyName, " for: ", cfg2)
switch element2 := cfg2.(type) {
case map[string]interface{}:
//fmt.Println(strings.Repeat(" ", depth), " - A map")
element2, ok := cfg2.(map[string]interface{})
if !ok {
return fmt.Errorf("config source: %s is not a map", keyName)
}
element1, ok := cfg1.(*map[string]interface{})
if !ok {
return fmt.Errorf("config dest: %s is not a map", keyName)
}
for k, _ := range element2 {
//fmt.Println(strings.Repeat(" ", depth), " - key: ", k)
_, ok := (*element1)[k]
if !ok {
(*element1)[k] = element2[k]
} else {
//fmt.Println(strings.Repeat(" ", depth), " - merging: ", element2[k])
asanarrayptr, ok := (*element1)[k].([]map[string]interface{})
if ok {
if err := merger(depth+1, k, &asanarrayptr, element2[k]); err != nil {
return err
}
(*element1)[k] = asanarrayptr
} else {
asamapptr, ok := (*element1)[k].(map[string]interface{})
if ok {
if err := merger(depth+1, k, &asamapptr, element2[k]); err != nil {
return err
}
(*element1)[k] = asamapptr
} else {
return fmt.Errorf("config dest: %s does not make a valid map/array ptr", keyName)
}
}
}
}
case []map[string]interface{}:
//fmt.Println(strings.Repeat(" ", depth), " - An array")
element2, ok := cfg2.([]map[string]interface{})
if !ok {
return fmt.Errorf("config source: %s is not a map array", keyName)
}
//fmt.Println(strings.Repeat(" ", depth), " - element2: ", element2)
element1, ok := cfg1.(*[]map[string]interface{})
if !ok {
return fmt.Errorf("config dest: %s is not a map array", keyName)
}
//fmt.Println(strings.Repeat(" ", depth), " - element1: ", element1)
for index, _ := range element2 {
*element1 = append(*element1, element2[index])
}
case string:
//fmt.Println(strings.Repeat(" ", depth), " - A string")
element2, ok := cfg2.(string)
if !ok {
return fmt.Errorf("config: %s is not a string", keyName)
}
case bool:
//fmt.Println(strings.Repeat(" ", depth), " - A boolean")
element2, ok := cfg2.(bool)
if !ok {
return fmt.Errorf("config: %s is not a boolean value", keyName)
}
case float64:
//fmt.Println(strings.Repeat(" ", depth), " - A float64")
element2, ok := cfg2.(float64)
if !ok {
return fmt.Errorf("config: %s is not a float64 value", keyName)
}
case nil:
//fmt.Println(strings.Repeat(" ", depth), " - Nil")
default:
log.Info().Str("type", reflect.TypeOf(element2).String()).Msg("Unknown element type found in configuration file. Ignoring.")
}
return nil
}
err := merger(0, "TOP", config1, config2)
if err != nil {
return err
}
return nil
}
func mergeConfigsO(config1 interface{}, config2 interface{}) (interface{}, error) {
var merger func(int, string, interface{}, interface{}) (interface{}, error)
merger = func(depth int, keyName string, cfg1 interface{}, cfg2 interface{}) (interface{}, error) {
var returnElement interface{}
fmt.Println(strings.Repeat(" ", depth), "Handling element: ", keyName, " for: ", cfg1)
switch element1 := cfg1.(type) {
case map[string]interface{}:
element2, ok := cfg2.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("config: %s is not a map", keyName)
}
for k, value2 := range element2 {
value1, ok := element1[k]
if !ok {
return nil, fmt.Errorf("config: %s not found in at least one config file", k)
}
merged, err := merger(depth+1, k, value1, value2)
if err != nil {
return merged, err
}
element1[k] = merged
}
returnElement = element1
case string:
element2, ok := cfg2.(string)
if !ok {
return nil, fmt.Errorf("config: %s is not a string", keyName)
}
if element2 != "" {
element1 = element2
}
returnElement = element1
case bool:
element2, ok := cfg2.(bool)
if !ok {
return nil, fmt.Errorf("config: %s is not a boolean value", keyName)
}
if element2 {
element1 = true
}
returnElement = element1
case float64:
element2, ok := cfg2.(float64)
if !ok {
return nil, fmt.Errorf("config: %s is not a float64 value", keyName)
}
if element2 != 0 {
element1 = element2
}
returnElement = element1
case nil:
if cfg2 == nil {
returnElement = nil
} else {
element2, ok := cfg2.(map[string]interface{})
if ok {
returnElement = element2
} else {
element3, ok := cfg2.([]interface{})
if ok {
returnElement = element3
} else {
log.Info().Msg("Unexpected interface type for an assignment. Ignoring.")
returnElement = element1
}
}
}
default:
log.Info().Str("type", reflect.TypeOf(element1).String()).Msg("Unknown element type found in configuration file. Ignoring.")
returnElement = element1
}
fmt.Println(strings.Repeat(" ", depth), "Done with element: ", keyName, " for: ", returnElement)
return returnElement, nil
}
/*
flattenedConfig1, _ := json.Marshal(config1)
var jsonConfig1 interface{}
//json.Unmarshal(flattenedConfig1, &jsonConfig1)
d1 := json.NewDecoder(bytes.NewReader(flattenedConfig1))
d1.UseNumber()
if err := d1.Decode(&jsonConfig1); err != nil {
return config1, err
}
flattenedConfig2, _ := json.Marshal(config2)
var jsonConfig2 interface{}
//json.Unmarshal(flattenedConfig2, &jsonConfig2)
//spew.Dump(jsonConfig2)
d2 := json.NewDecoder(bytes.NewReader(flattenedConfig2))
d2.UseNumber()
if err := d2.Decode(&jsonConfig2); err != nil {
return config1, err
}
*/
blocks, err := merger(0, "TOP", config1, config2)
if err != nil {
return config1, err
}
//
//
// TODO: return blocks... we will convert back to toml when we are done merging!
//
buf := new(bytes.Buffer)
err = toml.NewEncoder(buf).Encode(blocks)
if err != nil {
return config1, err
}
fmt.Println(buf.String())
merged := config.Config{}
if _, err := toml.Decode(buf.String(), &merged); err != nil {
return config1, err
}
return merged, nil
}
func handleArgs(cfg config.Config) (*config.Config, error) {
// LDAP flags
if ldap, ok := args["--ldap"].(string); ok && ldap != "" {
cfg.LDAP.Enabled = true
cfg.LDAP.Listen = ldap
}
// LDAPS flags
if ldaps, ok := args["--ldaps"].(string); ok && ldaps != "" {
cfg.LDAPS.Enabled = true
cfg.LDAPS.Listen = ldaps
}
if ldapsCert, ok := args["--ldaps-cert"].(string); ok && ldapsCert != "" {
cfg.LDAPS.Cert = ldapsCert
}
if ldapsKey, ok := args["--ldaps-key"].(string); ok && ldapsKey != "" {
cfg.LDAPS.Key = ldapsKey
}
return &cfg, nil
}
func handleLegacyConfig(cfg config.Config) (*config.Config, error) {
if len(cfg.Frontend.Listen) > 0 && (len(cfg.LDAP.Listen) > 0 || len(cfg.LDAPS.Listen) > 0) {
// Both old server-config and new - dont allow
return &cfg, fmt.Errorf("both old and new server-config in use - please remove old format ([frontend]) and migrate to new format ([ldap], [ldaps])")
}
if len(cfg.Frontend.Listen) > 0 {
// We're going with old format - parse it into new
log.Info().Msg("Config [frontend] is deprecated - please move to [ldap] and [ldaps] as-per documentation")
cfg.LDAP.Enabled = !cfg.Frontend.TLS
cfg.LDAPS.Enabled = cfg.Frontend.TLS
if cfg.Frontend.TLS {
cfg.LDAPS.Listen = cfg.Frontend.Listen
} else {
cfg.LDAP.Listen = cfg.Frontend.Listen
}
if len(cfg.Frontend.Cert) > 0 {
cfg.LDAPS.Cert = cfg.Frontend.Cert
}
if len(cfg.Frontend.Key) > 0 {
cfg.LDAPS.Key = cfg.Frontend.Key
}
}
return &cfg, nil
}
func validateConfig(cfg config.Config) (*config.Config, error) {
if !cfg.LDAP.Enabled && !cfg.LDAPS.Enabled {
return &cfg, fmt.Errorf("no server configuration found: please provide either LDAP or LDAPS configuration")
}
if cfg.LDAPS.Enabled {
// LDAPS enabled - verify requirements (cert, key, listen)
if len(cfg.LDAPS.Cert) == 0 || len(cfg.LDAPS.Key) == 0 {
return &cfg, fmt.Errorf("LDAPS was enabled but no certificate or key were specified: please disable LDAPS or use the 'cert' and 'key' options")
}
if len(cfg.LDAPS.Listen) == 0 {
return &cfg, fmt.Errorf("no LDAPS bind address was specified: please disable LDAPS or use the 'listen' option")
}
}
if cfg.LDAP.Enabled {
// LDAP enabled - verify listen
if len(cfg.LDAP.Listen) == 0 {
return &cfg, fmt.Errorf("no LDAP bind address was specified: please disable LDAP or use the 'listen' option")
}
}
//spew.Dump(cfg)
for i := range cfg.Backends {
switch cfg.Backends[i].Datastore {
case "":
cfg.Backends[i].Datastore = "config"
case "config":
case "ldap":
case "owncloud":
case "plugin":
default:
return &cfg, fmt.Errorf("invalid backend %s - must be 'config', 'ldap', 'owncloud' or 'plugin'", cfg.Backends[i].Datastore)
}
}
// TODO: remove after deprecating UnixID on User and Group
for _, user := range cfg.Users {
if user.UnixID != 0 {
user.UIDNumber = user.UnixID
log.Info().Msg(fmt.Sprintf("User '%s': 'unixid' is deprecated - please move to 'uidnumber' as per documentation", user.Name))
}
}
for _, group := range cfg.Groups {
if group.UnixID != 0 {
group.GIDNumber = group.UnixID
log.Info().Msg(fmt.Sprintf("Group '%s': 'unixid' is deprecated - please move to 'gidnumber' as per documentation", group.Name))
}
}
return &cfg, nil
}
// doConfig reads the cli flags and config file
func doConfig(checkConfig bool) error {
// Parse config-file into config{} struct
cfg, err := parseConfigFile(getConfigLocation())
if err != nil {
return err
}
// Handle parsed flags
cfg, err = handleArgs(*cfg)
if err != nil {
return err
}
// Handle parsing of legacy [frontend] section into [ldap] and/or [ldaps] sections
cfg, err = handleLegacyConfig(*cfg)
if err != nil {
return err
}
cfg, err = validateConfig(*cfg)
if err != nil {
return err
}
// Before greenlighting new config entirely, lets make sure the yubiauth works - in case they changed
if activeConfig.YubikeyClientID != cfg.YubikeyClientID || activeConfig.YubikeySecret != cfg.YubikeySecret {
if len(cfg.YubikeyClientID) > 0 && len(cfg.YubikeySecret) > 0 {
_yubiAuth, err := yubigo.NewYubiAuth(cfg.YubikeyClientID, cfg.YubikeySecret)
if err != nil {
return err
}
// No errors, override
yubiAuth = _yubiAuth
}
}
// All config is validated and alright, copy to activeConfig
if err := copier.Copy(activeConfig, cfg); err != nil {
return err
}
// Handle logging settings for new config
// - we do this last to make sure we only respect a fully validated config
log = logging.InitLogging(activeConfig.Debug, activeConfig.Syslog, activeConfig.StructuredLog)
if !checkConfig {
if activeConfig.Debug {
log.Info().Msg("Debugging enabled")
}
if activeConfig.Syslog {
log.Info().Msg("Syslog enabled")
}
}
return nil
}
+53 -41
View File
@@ -1,6 +1,7 @@
package main
import (
"os"
"os/exec"
"strings"
"testing"
@@ -26,50 +27,61 @@ type testEnv struct {
func TestIntegerStuff(t *testing.T) {
Convey("Testing sample-simple local file-based LDAP server", t, func() {
svc := startSvc(SD, "bin/glauth", "-c", "sample-simple.cfg")
batteryOfTests(
t,
svc, testEnv{
checkanonymousrootDSE: true,
checkTOTP: true,
checkbindUPN: true,
expectedinfo: "supportedLDAPVersion: 3",
svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com",
svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com",
otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com",
expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com",
expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com",
checkemployeetype: "cn=hackers",
})
stopSvc(svc)
})
Convey("Preflight: Testing pre-requisites", t, func() {
Convey("bin/glauth should exist", func() {
info, err := os.Stat("bin/glauth")
So(err, ShouldEqual, nil)
Convey("bin/glauth should be executable", func() {
mode := uint32(info.Mode())
So(mode&0b001001001, ShouldNotEqual, 0)
matchingLibrary := doRunGetFirst(RD, "ls", "bin/sqlite.so")
if matchingLibrary == "bin/sqlite.so" {
Convey("Testing sample-database LDAP server", t, func() {
svc := startSvc(SD, "bin/glauth", "-c", "pkg/plugins/sample-database.cfg")
batteryOfTests(
t,
svc, testEnv{
checkanonymousrootDSE: true,
checkTOTP: false,
checkbindUPN: true,
expectedinfo: "supportedLDAPVersion: 3",
svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com",
svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com",
otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com",
expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com",
expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com",
checkemployeetype: "",
Convey("Testing sample-simple local file-based LDAP server", func() {
svc := startSvc(SD, "bin/glauth", "-c", "sample-simple.cfg")
batteryOfTests(
t,
svc, testEnv{
checkanonymousrootDSE: true,
checkTOTP: true,
checkbindUPN: true,
expectedinfo: "supportedLDAPVersion: 3",
svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com",
svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com",
otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com",
expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com",
expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com",
checkemployeetype: "cn=hackers",
})
stopSvc(svc)
})
stopSvc(svc)
matchingLibrary := doRunGetFirst(RD, "ls", "bin/sqlite.so")
if matchingLibrary == "bin/sqlite.so" {
Convey("Testing sample-database LDAP server", func() {
svc := startSvc(SD, "bin/glauth", "-c", "pkg/plugins/sample-database.cfg")
batteryOfTests(
t,
svc, testEnv{
checkanonymousrootDSE: true,
checkTOTP: false,
checkbindUPN: true,
expectedinfo: "supportedLDAPVersion: 3",
svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com",
svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com",
otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com",
expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com",
expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com",
checkemployeetype: "",
})
stopSvc(svc)
})
}
})
})
}
})
matchingContainers := doRunGetFirst(RD, "sh", "-c", "docker ps | grep openldap-service | wc -l")
if matchingContainers == "1" {
+50 -31
View File
@@ -1,44 +1,63 @@
module github.com/glauth/glauth/v2
go 1.19
// Do not mistake /vendored for /vendor!
replace github.com/hydronica/toml => ./vendored/toml
go 1.21
require (
github.com/BurntSushi/toml v1.3.2
github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e
github.com/arl/statsviz v0.4.0
github.com/arl/statsviz v0.6.0
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/fsnotify/fsnotify v1.4.9
github.com/hydronica/toml v0.4.2
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.13.0
github.com/rs/zerolog v1.28.0
github.com/smartystreets/goconvey v1.6.4
github.com/yaegashi/msgraph.go v0.1.1-0.20200221123608-2d438cf2a7cc
golang.org/x/crypto v0.1.0
github.com/fsnotify/fsnotify v1.7.0
github.com/glauth/ldap v0.0.0-20231210225823-b9bf4d1baf6e
github.com/jinzhu/copier v0.4.0
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.17.0
github.com/prometheus/client_model v0.5.0
github.com/rs/zerolog v1.31.0
github.com/smartystreets/goconvey v1.8.1
github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.3
github.com/yaegashi/msgraph.go v0.1.4
go.opentelemetry.io/contrib/propagators/jaeger v1.21.1
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0
go.opentelemetry.io/otel/trace v1.21.0
go.uber.org/mock v0.3.0
golang.org/x/crypto v0.16.0
gopkg.in/amz.v3 v3.0.0-20201001071545-24fc1eceb27b
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
golang.org/x/sys v0.1.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rickb777/date v1.12.4 // indirect
github.com/rickb777/plural v1.2.0 // indirect
github.com/smarty/assertions v1.15.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
+117 -478
View File
@@ -1,534 +1,173 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e h1:Bqtt5C+uVk+vH/t5dmB47uDCTwxw16EYHqvJnmY2aQc=
github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e/go.mod h1:njRCDrl+1RQ/A/+KVU8Ho2EWAxUSkohOWczdW3dzDG0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/arl/statsviz v0.4.0 h1:pv8DWfti0TC8M2Baold2xGnCnir9jhqv1BO6UNKQmqY=
github.com/arl/statsviz v0.4.0/go.mod h1:+5inUy/dxy11x/KSmicG3ZrEEy0Yr81AFm3dn4QC04M=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE=
github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/glauth/ldap v0.0.0-20231210225823-b9bf4d1baf6e h1:ohe1O2DUo198delHsQvA8O+CgAPP8wv1SmSxLPlO9ao=
github.com/glauth/ldap v0.0.0-20231210225823-b9bf4d1baf6e/go.mod h1:EHMcFuVSIs0huelHOhaGtj9IKMB/Dmxo+jDheipjtYA=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s=
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA=
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba h1:DO8NFYdcRv1dnyAINJIBm6Bw2XibtLvQniNFGzf2W8E=
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba/go.mod h1:4S0XndRL8HNOaQBfdViJ2F/GPCgL524xlXRuXFH12/U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/rickb777/date v1.12.4 h1:+6IzcCCS/1t17DrmnEvrznyq7nM8vPwir6/UhlyohKw=
github.com/rickb777/date v1.12.4/go.mod h1:xP0eo/I5qmUt97yRGClHZfyLZ3ikMw6v6SU5MOGZTE0=
github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE=
github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yaegashi/msgraph.go v0.1.1-0.20200221123608-2d438cf2a7cc h1:ejaC8rvIvCWmsaFrvmGOxhBuMxxhBB1xRshuM98XQ7M=
github.com/yaegashi/msgraph.go v0.1.1-0.20200221123608-2d438cf2a7cc/go.mod h1:tso14hwzqX4VbnWTNsxiL0DvMb2OwbGISFA7jDibdWc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.3 h1:LNi0Qa7869/loPjz2kmMvp/jwZZnMZ9scMJKhDJ1DIo=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.3/go.mod h1:jyigonKik3C5V895QNiAGpKYKEvFuqjw9qAEZks1mUg=
github.com/yaegashi/msgraph.go v0.1.4 h1:leDXSczAbwBpYFSmmZrdByTiPoUw8dbTfNMetAjJvbw=
github.com/yaegashi/msgraph.go v0.1.4/go.mod h1:vgeYhHa5skJt/3lTyjGXThTZhwbhRnGo6uUxzoJIGME=
github.com/yaegashi/wtz.go v0.0.2/go.mod h1:nOLA5QXsmdkRxBkP5tljhua13ADHCKirLBrzPf4PEJc=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 h1:f4beMGDKiVzg9IcX7/VuWVy+oGdjx3dNJ72YehmtY5k=
go.opentelemetry.io/contrib/propagators/jaeger v1.21.1/go.mod h1:U9jhkEl8d1LL+QXY7q3kneJWJugiN3kZJV2OWz3hkBY=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200529172331-a64b76657301/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/amz.v3 v3.0.0-20201001071545-24fc1eceb27b h1:tdSyAzD5FHJmZAZ13d5WexAo7YJkiyGphhzFfInMnDw=
gopkg.in/amz.v3 v3.0.0-20201001071545-24fc1eceb27b/go.mod h1:cE0dCGx2UfBTjLFlzEx4EXJUmoX6BXBoX9GjKOvqha4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+28
View File
@@ -0,0 +1,28 @@
package monitoring
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
)
type API struct {
logger zerolog.Logger
}
func (a *API) RegisterEndpoints(router *http.ServeMux) {
router.HandleFunc("/metrics", a.prometheusHTTP)
}
func (a *API) prometheusHTTP(w http.ResponseWriter, r *http.Request) {
promhttp.Handler().ServeHTTP(w, r)
}
func NewAPI(logger zerolog.Logger) *API {
a := new(API)
a.logger = logger
return a
}
+13
View File
@@ -0,0 +1,13 @@
package monitoring
import "github.com/glauth/ldap"
type MonitorInterface interface {
SetResponseTimeMetric(map[string]string, float64) error
SetLDAPMetric(map[string]string, float64) error
}
type LDAPServerInterface interface {
SetStats(bool)
GetStats() ldap.Stats
}
+60
View File
@@ -0,0 +1,60 @@
package monitoring
import (
"time"
"github.com/rs/zerolog"
)
type LDAPMonitorWatcher struct {
syncTicker *time.Ticker
ldap LDAPServerInterface
monitor MonitorInterface
logger *zerolog.Logger
}
func (m *LDAPMonitorWatcher) sync() {
for {
select {
case tick := <-m.syncTicker.C:
m.logger.Debug().Time("value", tick).Msg("Tick")
m.storeMetrics()
default:
continue
}
}
}
func (m *LDAPMonitorWatcher) storeMetrics() {
stats := m.ldap.GetStats()
if err := m.monitor.SetLDAPMetric(map[string]string{"type": "conns"}, float64(stats.Conns)); err != nil {
m.logger.Error().Err(err).Msg("failed to set metric")
}
if err := m.monitor.SetLDAPMetric(map[string]string{"type": "binds"}, float64(stats.Binds)); err != nil {
m.logger.Error().Err(err).Msg("failed to set metric")
}
if err := m.monitor.SetLDAPMetric(map[string]string{"type": "unbinds"}, float64(stats.Unbinds)); err != nil {
m.logger.Error().Err(err).Msg("failed to set metric")
}
if err := m.monitor.SetLDAPMetric(map[string]string{"type": "searches"}, float64(stats.Searches)); err != nil {
m.logger.Error().Err(err).Msg("failed to set metric")
}
}
func NewLDAPMonitorWatcher(ldap LDAPServerInterface, monitor MonitorInterface, logger *zerolog.Logger) *LDAPMonitorWatcher {
m := new(LDAPMonitorWatcher)
m.syncTicker = time.NewTicker(15 * time.Second)
m.ldap = ldap
m.monitor = monitor
m.logger = logger
m.ldap.SetStats(true)
go m.sync()
return m
}
+36
View File
@@ -0,0 +1,36 @@
package monitoring
import (
"testing"
"time"
"github.com/glauth/ldap"
"github.com/rs/zerolog"
"go.uber.org/mock/gomock"
)
//go:generate mockgen -build_flags=--mod=mod -package monitoring -destination ./mock_interfaces.go -source=./interfaces.go
func TestNewLDAPMonitorWatcherRunsOnASchedule(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMonitor := NewMockMonitorInterface(ctrl)
mockLDAPServer := NewMockLDAPServerInterface(ctrl)
stats := ldap.Stats{}
mockLDAPServer.EXPECT().SetStats(true).Times(1)
mockLDAPServer.EXPECT().GetStats().MinTimes(1).Return(stats)
mockMonitor.EXPECT().SetLDAPMetric(map[string]string{"type": "conns"}, float64(0)).MinTimes(1)
mockMonitor.EXPECT().SetLDAPMetric(map[string]string{"type": "binds"}, float64(0)).MinTimes(1)
mockMonitor.EXPECT().SetLDAPMetric(map[string]string{"type": "unbinds"}, float64(0)).MinTimes(1)
mockMonitor.EXPECT().SetLDAPMetric(map[string]string{"type": "searches"}, float64(0)).MinTimes(1)
logger := zerolog.Nop()
m := NewLDAPMonitorWatcher(mockLDAPServer, mockMonitor, &logger)
m.syncTicker = time.NewTicker(5 * time.Microsecond)
// allow goroutine to start and ticker to tick
time.Sleep(10 * time.Millisecond)
}
+139
View File
@@ -0,0 +1,139 @@
package monitoring
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
// The code below liberally borrows from https://github.com/albertito/prometheus-expvar-exporter
// and is adapted (just a little) for our purpose.
type Collector struct {
url string
names map[string]string
helps map[string]string
labelName map[string]string
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
return
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
resp, err := http.Get(c.url)
if err != nil {
log.Printf("Error scraping %q: %v", c.url, err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading body of %q: %v", c.url, err)
return
}
// Replace "\xNN" with "?" because the default parser doesn't handle them
// well.
re := regexp.MustCompile(`\\x..`)
body = re.ReplaceAllFunc(body, func(s []byte) []byte {
return []byte("?")
})
var vs map[string]interface{}
err = json.Unmarshal(body, &vs)
if err != nil {
log.Printf("Error unmarshalling json from %q: %v", c.url, err)
return
}
for k, v := range vs {
name := strings.ReplaceAll(k, "/", "_")
if n, ok := c.names[k]; ok {
name = n
}
help := fmt.Sprintf("expvar %q", k)
if h, ok := c.helps[k]; ok {
help = h
}
lnames := []string{}
if ln, ok := c.labelName[k]; ok {
lnames = append(lnames, ln)
}
desc := prometheus.NewDesc(name, help, lnames, nil)
switch v := v.(type) {
case float64:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue, v)
case bool:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(v))
case map[string]interface{}:
// We only support explicitly written label names.
if len(lnames) != 1 {
continue
}
for lk, lv := range v {
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(lv), lk)
}
case string:
// Not supported by Prometheus.
continue
case []interface{}:
// Not supported by Prometheus.
continue
default:
continue
}
}
}
func NewCollector(url string) *Collector {
c := new(Collector)
c.url = url
c.names = map[string]string{
"proxy": "glauth_proxy",
"proxy_frontend": "glauth_proxy_frontend",
"proxy_backend": "glauth_proxy_backend",
}
c.helps = map[string]string{
"proxy": "General Metrics",
"proxy_frontend": "Frontend Metrics",
"proxy_backend": "Backend Metrics",
}
c.labelName = map[string]string{
"proxy": "metric",
"proxy_frontend": "metric",
"proxy_backend": "metric",
}
prometheus.MustRegister(c)
return c
}
func valToFloat(v interface{}) float64 {
switch v := v.(type) {
case float64:
return v
case bool:
if v {
return 1.0
}
return 0.0
case string:
return 0.0
}
log.Printf("unexpected value type: %#v", v)
return 0.0
}
+109
View File
@@ -0,0 +1,109 @@
package monitoring
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/rs/zerolog"
)
type Monitor struct {
responseTime *prometheus.HistogramVec
ldapMetric *prometheus.GaugeVec
logger *zerolog.Logger
}
func (m *Monitor) SetResponseTimeMetric(tags map[string]string, value float64) error {
if m.responseTime == nil {
return fmt.Errorf("metric not instantiated")
}
m.responseTime.With(tags).Observe(value)
return nil
}
func (m *Monitor) SetLDAPMetric(tags map[string]string, value float64) error {
if m.ldapMetric == nil {
return fmt.Errorf("metric not instantiated")
}
m.ldapMetric.With(tags).Set(value)
return nil
}
func (m *Monitor) constLabels() map[string]string {
return map[string]string{
"library": "github.com/glauth/glauth",
}
}
func (m *Monitor) registerHistograms() {
histograms := make([]*prometheus.HistogramVec, 0)
m.responseTime = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "tcp_response_time_seconds",
Help: "tcp_response_time_seconds",
ConstLabels: m.constLabels(),
},
[]string{"operation", "status"},
)
histograms = append(histograms, m.responseTime)
for _, histogram := range histograms {
err := prometheus.Register(histogram)
switch err.(type) {
case nil:
return
case prometheus.AlreadyRegisteredError:
m.logger.Debug().Interface("metric", histogram).Msg("metric already registered")
default:
m.logger.Error().Interface("metric", histogram).Msg("metric could not be registered")
}
}
}
func (m *Monitor) registerGauges() {
gauges := make([]*prometheus.GaugeVec, 0)
m.ldapMetric = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "ldap_metric",
Help: "ldap_metric",
ConstLabels: m.constLabels(),
},
[]string{"type"},
)
gauges = append(gauges, m.ldapMetric)
for _, gauge := range gauges {
err := prometheus.Register(gauge)
switch err.(type) {
case nil:
return
case prometheus.AlreadyRegisteredError:
m.logger.Debug().Interface("metric", gauge).Msg("metric already registered")
default:
m.logger.Error().Interface("metric", gauge).Msg("metric could not be registered")
}
}
}
func NewMonitor(logger *zerolog.Logger) *Monitor {
m := new(Monitor)
m.logger = logger
m.registerHistograms()
m.registerGauges()
return m
}
+62
View File
@@ -0,0 +1,62 @@
package monitoring
import (
"reflect"
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/rs/zerolog"
)
func TestNewMonitorImplementsInterface(t *testing.T) {
logger := zerolog.Nop()
m := NewMonitor(&logger)
i := reflect.TypeOf((*MonitorInterface)(nil)).Elem()
if !reflect.TypeOf(m).Implements(i) {
t.Fatal("Monitor doesn't implement MonitorInterface")
}
}
func TestMonitorSetLDAPMetricSucceeds(t *testing.T) {
logger := zerolog.Nop()
m := NewMonitor(&logger)
labels := map[string]string{"type": "test"}
m.SetLDAPMetric(labels, float64(10))
mLDAPMetric := dto.Metric{}
m.ldapMetric.With(labels).Write(&mLDAPMetric)
if mLDAPMetric.GetGauge().GetValue() != float64(10) {
t.Fatalf("metric value should have been set to %v", float64(10))
}
}
func TestMonitorSetResponseTimeMetricSucceeds(t *testing.T) {
logger := zerolog.Nop()
m := NewMonitor(&logger)
labels := map[string]string{"operation": "test", "status": "0"}
m.SetResponseTimeMetric(labels, float64(10))
mResponseTimeMetric := dto.Metric{}
m.responseTime.With(labels).(prometheus.Metric).Write(&mResponseTimeMetric)
if mResponseTimeMetric.GetHistogram().GetSampleSum() != float64(10) {
t.Fatalf("metric value should have been set to %v", float64(10))
}
for _, bucket := range mResponseTimeMetric.GetHistogram().GetBucket() {
if bucket.GetUpperBound() < float64(10) && bucket.GetCumulativeCount() != 0 {
t.Fatal("there should be no count for this metric bucket")
}
if bucket.GetUpperBound() >= float64(10) && bucket.GetCumulativeCount() != 1 {
t.Fatal("there should be one entry into this metric bucket")
}
}
}
+81
View File
@@ -0,0 +1,81 @@
package tls
import (
tls "crypto/tls"
"crypto/x509"
"encoding/pem"
"github.com/rs/zerolog"
)
var (
log zerolog.Logger
)
// MakeTLS generates a tls.Config
func MakeTLS(clientCert, key []byte) (*tls.Config, error) {
if clientCert == nil && key == nil {
return new(tls.Config), nil
}
var err error
cert, err := tls.X509KeyPair(clientCert, key)
if err != nil {
return nil, err
}
log.Debug().Str("key", string(key))
log.Debug().Str("client.certificate", string(clientCert))
log.Debug().Interface("certificate", cert)
if err != nil {
return nil, err
}
// Get the SystemCertPool, continue with an empty pool on error
rootCAs, err := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
log.Warn().Err(err).Msg("Using empty cert-pool")
} else {
log.Info().Msg("Using system cert-pool")
}
for _, cert := range DecodePEM(clientCert).Certificate {
x509Cert, err := x509.ParseCertificate(cert)
if err != nil {
log.Error().Err(err).Msg("issue parsing cert PEM")
}
rootCAs.AddCert(x509Cert)
}
log.Debug().Interface("root.ca", rootCAs)
log.Debug().Interface("certificates", []tls.Certificate{cert})
return &tls.Config{
RootCAs: rootCAs,
MinVersion: tls.VersionTLS10,
MaxVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{cert},
}, nil
}
// DecodePEM builds a PEM certificate object
func DecodePEM(certPEM []byte) tls.Certificate {
var cert tls.Certificate
var certDER *pem.Block
for {
certDER, certPEM = pem.Decode(certPEM)
if certDER == nil {
break
}
if certDER.Type == "CERTIFICATE" {
cert.Certificate = append(cert.Certificate, certDER.Bytes)
}
}
return cert
}
+453
View File
@@ -0,0 +1,453 @@
package toml
import (
"bytes"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/BurntSushi/toml"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/rs/zerolog/log"
"gopkg.in/amz.v3/aws"
"gopkg.in/amz.v3/s3"
)
type Config struct {
Users []toml.Primitive
}
type User struct {
Name string
CustomAttributes []toml.Primitive
}
// NewConfig reads the cli flags and config file
func NewConfig(checkConfig bool, location string, args map[string]interface{}) (*config.Config, error) {
// Parse config-file into config{} struct
cfg, err := parseConfigFile(location, args)
if err != nil {
return nil, err
}
// Handle parsed flags
cfg, err = handleArgs(cfg, args)
if err != nil {
return nil, err
}
// Handle parsing of legacy [frontend] section into [ldap] and/or [ldaps] sections
cfg, err = handleLegacyConfig(cfg)
if err != nil {
return nil, err
}
cfg, err = validateConfig(cfg)
if err != nil {
return nil, err
}
// TODO @shipperizer reinstate this
// // Before greenlighting new config entirely, lets make sure the yubiauth works - in case they changed
if _, err := yubigo.NewYubiAuth(cfg.YubikeyClientID, cfg.YubikeySecret); err != nil && len(cfg.YubikeyClientID) > 0 && len(cfg.YubikeySecret) > 0 {
return nil, err
}
return cfg, nil
}
func parseConfigFile(configFileLocation string, args map[string]interface{}) (*config.Config, error) {
cfg := new(config.Config)
// setup defaults
cfg.LDAP.Enabled = false
cfg.LDAPS.Enabled = true
// parse the config file
if strings.HasPrefix(configFileLocation, "s3://") {
region, present := aws.Regions[args["-r"].(string)]
if !present {
return cfg, fmt.Errorf("invalid AWS region: %s", args["-r"])
}
if args["--aws_endpoint_url"] != nil {
region = aws.Region{
Name: "User defined",
S3Endpoint: args["--aws_endpoint_url"].(string),
}
present = true
}
auth, err := aws.EnvAuth()
if err != nil {
if args["-K"] == nil || args["-S"] == nil {
return cfg, fmt.Errorf("AWS credentials not found: must use -K and -S flags, or set these env vars:\n\texport AWS_ACCESS_KEY_ID=\"AAA...\"\n\texport AWS_SECRET_ACCESS_KEY=\"BBBB...\"\n")
}
auth = aws.Auth{
AccessKey: args["-K"].(string),
SecretKey: args["-S"].(string),
}
}
// parse S3 url
s3url := strings.TrimPrefix(configFileLocation, "s3://")
parts := strings.SplitN(s3url, "/", 2)
if len(parts) != 2 {
return cfg, fmt.Errorf("invalid S3 URL: %s", s3url)
}
b, err := s3.New(auth, region).Bucket(parts[0])
if err != nil {
return cfg, err
}
tomlData, err := b.Get(parts[1])
if err != nil {
return cfg, err
}
if _, err := toml.Decode(string(tomlData), cfg); err != nil {
return cfg, err
}
} else { // local config file
fInfo, err := os.Stat(configFileLocation)
if err != nil {
return cfg, fmt.Errorf("non-existent config path: %s", configFileLocation)
}
if fInfo.IsDir() { // multiple files in a directory
rawCfgStruct := make(map[string]interface{})
// To keep things simple, we are not going to use the default values built in Cfg
// so far (LDAP.Enabled, LDAPS.Enabled, etc) so do not forget to specify them!
/*
sourcebuf := new(bytes.Buffer)
err = toml.NewEncoder(sourcebuf).Encode(cfg)
if err != nil {
return cfg, err
}
var initialRawCfgStruct interface{}
if err := toml.Unmarshal(sourcebuf.Bytes(), &initialRawCfgStruct); err != nil {
return cfg, err
}
if err = mergeConfigs(&rawCfgStruct, initialRawCfgStruct); err != nil {
return cfg, err
}
*/
files, _ := os.ReadDir(configFileLocation)
for _, f := range files {
canonicalName := filepath.Join(configFileLocation, f.Name())
bs, _ := os.ReadFile(canonicalName)
var curRawCfgStruct interface{}
if err := toml.Unmarshal(bs, &curRawCfgStruct); err != nil {
return cfg, err
}
if err = mergeConfigs(&rawCfgStruct, curRawCfgStruct); err != nil {
return cfg, err
}
}
destbuf := new(bytes.Buffer)
err = toml.NewEncoder(destbuf).Encode(rawCfgStruct)
if err != nil {
return cfg, err
}
fmt.Println(destbuf.String())
merged := config.Config{}
if _, err = toml.Decode(destbuf.String(), &merged); err != nil {
return cfg, err
}
cfg = &merged
} else {
_, err = toml.DecodeFile(configFileLocation, cfg)
if err != nil {
return cfg, err
}
}
usersCustomAttributes(configFileLocation, cfg)
}
// Backward Compability
if cfg.Backend.Datastore != "" {
if cfg.Backends != nil {
return cfg, fmt.Errorf("you cannot specify both [Backend] and [[Backends]] directives in the same configuration ")
} else {
cfg.Backends = append(cfg.Backends, cfg.Backend)
}
}
// Patch with default values where not specified
for i := range cfg.Backends {
if cfg.Backends[i].NameFormat == "" {
cfg.Backends[i].NameFormat = "cn"
}
if cfg.Backends[i].GroupFormat == "" {
cfg.Backends[i].GroupFormat = "ou"
}
if cfg.Backends[i].SSHKeyAttr == "" {
cfg.Backends[i].SSHKeyAttr = "sshPublicKey"
}
}
//
return cfg, nil
}
// usersCustomAttributes changes config passed in by adding extra information coming from the custom attributes
func usersCustomAttributes(location string, config *config.Config) {
// TODO @shipperizer deal with multiple files like in line #126
c := new(Config)
md, err := toml.DecodeFile(location, c)
if err != nil {
log.Error().Err(err).Msg("issues parsing users...keep going")
return
}
for _, u := range c.Users {
user := new(User)
md.PrimitiveDecode(u, user)
if user.CustomAttributes == nil {
continue
}
for idx, cUser := range config.Users {
if cUser.Name != user.Name {
continue
}
x := make(map[string]interface{})
for _, attribute := range user.CustomAttributes {
err := md.PrimitiveDecode(attribute, x)
fmt.Println(err, x)
for k, v := range x {
if config.Users[idx].CustomAttrs == nil {
config.Users[idx].CustomAttrs = make(map[string]interface{})
}
config.Users[idx].CustomAttrs[k] = v
}
}
}
}
}
func mergeConfigs(config1 interface{}, config2 interface{}) error {
var merger func(int, string, interface{}, interface{}) error
merger = func(depth int, keyName string, cfg1 interface{}, cfg2 interface{}) error {
//fmt.Println(strings.Repeat(" ", depth), "Handling element: ", keyName, " for: ", cfg2)
switch element2 := cfg2.(type) {
case map[string]interface{}:
//fmt.Println(strings.Repeat(" ", depth), " - A map")
element2, ok := cfg2.(map[string]interface{})
if !ok {
return fmt.Errorf("config source: %s is not a map", keyName)
}
element1, ok := cfg1.(*map[string]interface{})
if !ok {
return fmt.Errorf("config dest: %s is not a map", keyName)
}
for k, _ := range element2 {
//fmt.Println(strings.Repeat(" ", depth), " - key: ", k)
_, ok := (*element1)[k]
if !ok {
(*element1)[k] = element2[k]
} else {
//fmt.Println(strings.Repeat(" ", depth), " - merging: ", element2[k])
asanarrayptr, ok := (*element1)[k].([]map[string]interface{})
if ok {
if err := merger(depth+1, k, &asanarrayptr, element2[k]); err != nil {
return err
}
(*element1)[k] = asanarrayptr
} else {
asamapptr, ok := (*element1)[k].(map[string]interface{})
if ok {
if err := merger(depth+1, k, &asamapptr, element2[k]); err != nil {
return err
}
(*element1)[k] = asamapptr
} else {
return fmt.Errorf("config dest: %s does not make a valid map/array ptr", keyName)
}
}
}
}
case []map[string]interface{}:
//fmt.Println(strings.Repeat(" ", depth), " - An array")
element2, ok := cfg2.([]map[string]interface{})
if !ok {
return fmt.Errorf("config source: %s is not a map array", keyName)
}
//fmt.Println(strings.Repeat(" ", depth), " - element2: ", element2)
element1, ok := cfg1.(*[]map[string]interface{})
if !ok {
return fmt.Errorf("config dest: %s is not a map array", keyName)
}
//fmt.Println(strings.Repeat(" ", depth), " - element1: ", element1)
for index, _ := range element2 {
*element1 = append(*element1, element2[index])
}
case string:
//fmt.Println(strings.Repeat(" ", depth), " - A string")
element2, ok := cfg2.(string)
if !ok {
return fmt.Errorf("config: %s is not a string", keyName)
}
case bool:
//fmt.Println(strings.Repeat(" ", depth), " - A boolean")
element2, ok := cfg2.(bool)
if !ok {
return fmt.Errorf("config: %s is not a boolean value", keyName)
}
case float64:
//fmt.Println(strings.Repeat(" ", depth), " - A float64")
element2, ok := cfg2.(float64)
if !ok {
return fmt.Errorf("config: %s is not a float64 value", keyName)
}
case nil:
//fmt.Println(strings.Repeat(" ", depth), " - Nil")
default:
log.Info().Str("type", reflect.TypeOf(element2).String()).Msg("Unknown element type found in configuration file. Ignoring.")
}
return nil
}
err := merger(0, "TOP", config1, config2)
if err != nil {
return err
}
return nil
}
func handleArgs(cfg *config.Config, args map[string]interface{}) (*config.Config, error) {
// LDAP flags
if ldap, ok := args["--ldap"].(string); ok && ldap != "" {
cfg.LDAP.Enabled = true
cfg.LDAP.Listen = ldap
}
// LDAPS flags
if ldaps, ok := args["--ldaps"].(string); ok && ldaps != "" {
cfg.LDAPS.Enabled = true
cfg.LDAPS.Listen = ldaps
}
if ldapsCert, ok := args["--ldaps-cert"].(string); ok && ldapsCert != "" {
cfg.LDAPS.Cert = ldapsCert
}
if ldapsKey, ok := args["--ldaps-key"].(string); ok && ldapsKey != "" {
cfg.LDAPS.Key = ldapsKey
}
return cfg, nil
}
func handleLegacyConfig(cfg *config.Config) (*config.Config, error) {
if len(cfg.Frontend.Listen) > 0 && (len(cfg.LDAP.Listen) > 0 || len(cfg.LDAPS.Listen) > 0) {
// Both old server-config and new - dont allow
return cfg, fmt.Errorf("both old and new server-config in use - please remove old format ([frontend]) and migrate to new format ([ldap], [ldaps])")
}
if len(cfg.Frontend.Listen) > 0 {
// We're going with old format - parse it into new
log.Info().Msg("Config [frontend] is deprecated - please move to [ldap] and [ldaps] as-per documentation")
cfg.LDAP.Enabled = !cfg.Frontend.TLS
cfg.LDAPS.Enabled = cfg.Frontend.TLS
if cfg.Frontend.TLS {
cfg.LDAPS.Listen = cfg.Frontend.Listen
} else {
cfg.LDAP.Listen = cfg.Frontend.Listen
}
if len(cfg.Frontend.Cert) > 0 {
cfg.LDAPS.Cert = cfg.Frontend.Cert
}
if len(cfg.Frontend.Key) > 0 {
cfg.LDAPS.Key = cfg.Frontend.Key
}
}
return cfg, nil
}
func validateConfig(cfg *config.Config) (*config.Config, error) {
if !cfg.LDAP.Enabled && !cfg.LDAPS.Enabled {
return cfg, fmt.Errorf("no server configuration found: please provide either LDAP or LDAPS configuration")
}
if cfg.LDAPS.Enabled {
// LDAPS enabled - verify requirements (cert, key, listen)
if len(cfg.LDAPS.Cert) == 0 || len(cfg.LDAPS.Key) == 0 {
return cfg, fmt.Errorf("LDAPS was enabled but no certificate or key were specified: please disable LDAPS or use the 'cert' and 'key' options")
}
if len(cfg.LDAPS.Listen) == 0 {
return cfg, fmt.Errorf("no LDAPS bind address was specified: please disable LDAPS or use the 'listen' option")
}
}
if cfg.LDAP.Enabled {
// LDAP enabled - verify listen
if len(cfg.LDAP.Listen) == 0 {
return cfg, fmt.Errorf("no LDAP bind address was specified: please disable LDAP or use the 'listen' option")
}
if cfg.LDAP.TLS && cfg.LDAP.TLSCert == "" && cfg.LDAP.TLSCertPath != "" {
byteData, err := os.ReadFile(cfg.LDAP.TLSCertPath)
cfg.LDAP.TLSCert = string(byteData)
if err != nil {
return cfg, fmt.Errorf("unable to read TLS certificate file")
}
}
if cfg.LDAP.TLS && cfg.LDAP.TLSKey == "" && cfg.LDAP.TLSKeyPath != "" {
byteData, err := os.ReadFile(cfg.LDAP.TLSKeyPath)
cfg.LDAP.TLSKey = string(byteData)
if err != nil {
return cfg, fmt.Errorf("unable to read TLS key file")
}
}
}
//spew.Dump(cfg)
for i := range cfg.Backends {
switch cfg.Backends[i].Datastore {
case "":
cfg.Backends[i].Datastore = "config"
case "config":
case "ldap":
case "owncloud":
case "plugin":
default:
return cfg, fmt.Errorf("invalid backend %s - must be 'config', 'ldap', 'owncloud' or 'plugin'", cfg.Backends[i].Datastore)
}
}
// TODO: remove after deprecating UnixID on User and Group
for _, user := range cfg.Users {
if user.UnixID != 0 {
user.UIDNumber = user.UnixID
log.Info().Msg(fmt.Sprintf("User '%s': 'unixid' is deprecated - please move to 'uidnumber' as per documentation", user.Name))
}
}
for _, group := range cfg.Groups {
if group.UnixID != 0 {
group.GIDNumber = group.UnixID
log.Info().Msg(fmt.Sprintf("Group '%s': 'unixid' is deprecated - please move to 'gidnumber' as per documentation", group.Name))
}
}
return cfg, nil
}
+24
View File
@@ -0,0 +1,24 @@
package tracing
import (
"github.com/rs/zerolog"
)
type Config struct {
OtelHTTPEndpoint string
OtelGRPCEndpoint string
Logger *zerolog.Logger
Enabled bool
}
func NewConfig(enabled bool, otelGRPCEndpoint, otelHTTPEndpoint string, logger *zerolog.Logger) *Config {
c := new(Config)
c.OtelGRPCEndpoint = otelGRPCEndpoint
c.OtelHTTPEndpoint = otelHTTPEndpoint
c.Logger = logger
c.Enabled = enabled
return c
}
+134
View File
@@ -0,0 +1,134 @@
package tracing
import (
"context"
"runtime/debug"
"github.com/rs/zerolog"
"go.opentelemetry.io/contrib/propagators/jaeger"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/embedded"
"go.opentelemetry.io/otel/trace/noop"
)
type Tracer struct {
embedded.Tracer
tracer trace.Tracer
logger *zerolog.Logger
}
func (t *Tracer) init(service string, e sdktrace.SpanExporter) {
traceProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithBatcher(e),
sdktrace.WithResource(
t.buildResource(service),
),
)
otel.SetTracerProvider(traceProvider)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}, jaeger.Jaeger{}))
t.tracer = otel.Tracer(service)
}
func (t *Tracer) gitRevision(settings []debug.BuildSetting) string {
for _, setting := range settings {
if setting.Key == "vcs.revision" {
return setting.Value
}
}
return "n/a"
}
func (t *Tracer) buildResource(service string) *resource.Resource {
var res *resource.Resource
res = resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(service),
semconv.ServiceVersion("n/a"),
)
if info, ok := debug.ReadBuildInfo(); ok {
if service == "" {
service = info.Path
}
res = resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(service),
attribute.String("git_sha", t.gitRevision(info.Settings)),
attribute.String("app", info.Main.Path),
)
}
return res
}
func (t *Tracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return t.tracer.Start(ctx, spanName, opts...)
}
// basic tracer implementation of trace.Tracer, just adding some extra configuration
func NewTracer(cfg *Config) *Tracer {
t := new(Tracer)
t.logger = cfg.Logger
// if tracing disabled skip the config
if !cfg.Enabled {
t.tracer = noop.NewTracerProvider().Tracer("github.com/glauth/glauth")
return t
}
var err error
var exporter sdktrace.SpanExporter
if cfg.OtelGRPCEndpoint != "" {
exporter, err = otlptrace.New(
context.TODO(),
otlptracegrpc.NewClient(
otlptracegrpc.WithEndpoint(cfg.OtelGRPCEndpoint),
otlptracegrpc.WithInsecure(),
),
)
} else if cfg.OtelHTTPEndpoint != "" {
exporter, err = otlptrace.New(
context.TODO(),
otlptracehttp.NewClient(
otlptracehttp.WithEndpoint(cfg.OtelHTTPEndpoint),
otlptracehttp.WithInsecure(),
),
)
} else {
exporter, err = stdouttrace.New(
stdouttrace.WithPrettyPrint(),
)
}
if err != nil {
t.logger.Error().Err(err).Msg("unable to initialize tracing exporter")
return nil
}
// set tracer provider and propagator properly, this is to ensure all
// instrumentation library could run well
t.init("github.com/glauth/glauth", exporter)
return t
}
+5
View File
@@ -0,0 +1,5 @@
package version
// Version is the tag of the latest release, it gets changed by the ci
// process when a release is happening
const Version = "v2.3.1" // x-release-please-version
+68
View File
@@ -0,0 +1,68 @@
package version
import (
"fmt"
"runtime/debug"
)
type gitInfo struct {
BuildTime string
Commit string
Dirty bool
}
// GetVersion reads builtime vars and returns a full string containing info about
// the currently running version of the software. Primarily used by the
// --version flag at runtime.
func GetVersion() string {
// TODO @shipperizer see if it's worth replacing with info.Main.Path
// aka github.com/glauth/glauth/v2 - not great but worth checking
name := "GLauth"
info, ok := debug.ReadBuildInfo()
if !ok {
return fmt.Sprintf(`
%s
Non-release build`,
name)
}
gitInfo := vcsInfo(info.Settings)
// If a release, use the tag
if !gitInfo.Dirty {
return fmt.Sprintf(`%s %s
Build time: %s
Commit: %s`, name, Version, gitInfo.BuildTime, gitInfo.Commit)
}
return fmt.Sprintf(`%s
Non-release build based on tag %s
Build time: %s
Commit: %s`, name, Version, gitInfo.BuildTime, gitInfo.Commit)
}
func vcsInfo(settings []debug.BuildSetting) *gitInfo {
info := new(gitInfo)
info.BuildTime = "unknown"
info.Commit = "unknown"
info.Dirty = false
for _, v := range settings {
switch v.Key {
case "vcs.revision":
info.Commit = v.Value
case "vcs.modified":
info.Dirty = v.Value == "true"
case "vcs.time":
info.BuildTime = v.Value
}
}
return info
}
+39
View File
@@ -0,0 +1,39 @@
package assets
import (
"net/http"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type API struct {
fileServer http.Handler
logger zerolog.Logger
}
func (a *API) RegisterEndpoints(router *http.ServeMux) {
router.HandleFunc("/", a.assets)
router.Handle("/assets/", http.StripPrefix("/assets/", a.fileServer))
}
func (a *API) assets(w http.ResponseWriter, r *http.Request) {
a.logger.Info().Str("path", r.URL.Path).Msg("Web")
if r.URL.Path != "/" {
log.Info().Msg("Web 404")
http.NotFound(w, r)
return
}
a.fileServer.ServeHTTP(w, r)
}
func NewAPI(logger zerolog.Logger) *API {
a := new(API)
a.logger = logger
a.fileServer = http.FileServer(http.FS(Content))
return a
}
@@ -1,21 +1,20 @@
package frontend
package assets
import (
"bytes"
"crypto/sha256"
"github.com/rs/zerolog"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/glauth/glauth/v2/pkg/assets"
"github.com/rs/zerolog"
)
// TestAPI is a small test to make sure that behavior didn't change for the
// worse when switching over to the go1.16+ "embed" package from go-bindata.
func TestAPI(t *testing.T) {
func TestAPIAssets(t *testing.T) {
tt := []struct {
Name string
Path string
@@ -26,7 +25,7 @@ func TestAPI(t *testing.T) {
Path: "/",
Check: func(t testing.TB, res *http.Response) {
if res.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %v", res.Status)
t.Fatalf("unexpected response: %v", res.Status)
return
}
h := sha256.New()
@@ -34,7 +33,7 @@ func TestAPI(t *testing.T) {
t.Error(err)
}
if got, want := h.Sum(nil), mkDigest(t, "index.html"); !bytes.Equal(got, want) {
t.Errorf("bad digest: got: %x, want %x", got, want)
t.Fatalf("bad digest: got: %x, want %x", got, want)
}
},
},
@@ -42,48 +41,47 @@ func TestAPI(t *testing.T) {
Name: "NotFound",
Path: "/glauth.js",
Check: func(t testing.TB, res *http.Response) {
if res.StatusCode == http.StatusNotFound {
return
if res.StatusCode != http.StatusNotFound {
t.Fatalf("unexpected response: %v", res.Status)
}
t.Errorf("unexpected response: %v", res.Status)
},
},
{
Name: "Content-Type",
Path: "/assets/glauth.js",
Path: "/assets/js/glauth.js",
Check: func(t testing.TB, res *http.Response) {
if res.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %v", res.Status)
t.Fatalf("unexpected response: %v", res.Status)
}
if got, want := res.Header.Get("Content-Type"), `application/javascript`; got != want {
t.Errorf("bad content-type: got: %q, want %q", got, want)
if got, want := res.Header.Get("Content-Type"), `text/javascript; charset=utf-8`; got != want {
t.Fatalf("bad content-type: got: %q, want %q", got, want)
}
h := sha256.New()
if _, err := io.Copy(h, res.Body); err != nil {
t.Error(err)
}
if got, want := h.Sum(nil), mkDigest(t, "glauth.js"); !bytes.Equal(got, want) {
t.Errorf("bad digest: got: %x, want %x", got, want)
if got, want := h.Sum(nil), mkDigest(t, "js/glauth.js"); !bytes.Equal(got, want) {
t.Fatalf("bad digest: got: %x, want %x", got, want)
}
},
},
{
Name: "Content-Type",
Path: "/assets/glauth.css",
Path: "/assets/css/glauth.css",
Check: func(t testing.TB, res *http.Response) {
if res.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %v", res.Status)
t.Fatalf("unexpected response: %v", res.Status)
}
// This isn't exactly the same, it adds a charset argument.
if got, want := res.Header.Get("Content-Type"), `text/css; charset=utf-8`; got != want {
t.Errorf("bad content-type: got: %q, want %q", got, want)
t.Fatalf("bad content-type: got: %q, want %q", got, want)
}
h := sha256.New()
if _, err := io.Copy(h, res.Body); err != nil {
t.Error(err)
}
if got, want := h.Sum(nil), mkDigest(t, "glauth.css"); !bytes.Equal(got, want) {
t.Errorf("bad digest: got: %x, want %x", got, want)
if got, want := h.Sum(nil), mkDigest(t, "css/glauth.css"); !bytes.Equal(got, want) {
t.Fatalf("bad digest: got: %x, want %x", got, want)
}
},
},
@@ -91,7 +89,7 @@ func TestAPI(t *testing.T) {
mux := http.NewServeMux()
log := zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
register(mux, log, true, "0.0.0.0:5555")
NewAPI(log).RegisterEndpoints(mux)
srv := httptest.NewServer(mux)
defer srv.Close()
for _, tc := range tt {
@@ -112,7 +110,7 @@ func TestAPI(t *testing.T) {
}
func mkDigest(t testing.TB, path string) []byte {
f, err := assets.Content.Open(path)
f, err := Content.Open(path)
if err != nil {
t.Error(err)
}
+14
View File
@@ -36,6 +36,12 @@ type Frontend struct {
type LDAP struct {
Enabled bool
Listen string
// StartTLS parameters
TLS bool
TLSCert string
TLSKey string
TLSCertPath string
TLSKeyPath string
}
type LDAPS struct {
Enabled bool
@@ -98,6 +104,13 @@ type Group struct {
GIDNumber int
IncludeGroups []int
}
type Tracing struct {
Enabled bool
GRPCEndpoint string
HTTPEndpoint string
}
type Config struct {
API API
Backend Backend // Deprecated
@@ -115,6 +128,7 @@ type Config struct {
LDAPS LDAPS
Groups []Group
Users []User
Tracing Tracing
ConfigFile string
AwsAccessKeyId string
AwsSecretAccessKey string
+1
View File
@@ -2,6 +2,7 @@ package frontend
import (
"context"
"github.com/rs/zerolog"
"github.com/glauth/glauth/v2/pkg/config"
+14 -156
View File
@@ -1,17 +1,10 @@
package frontend
import (
"encoding/json"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
"io/ioutil"
"log"
"net/http"
"regexp"
"strings"
"github.com/glauth/glauth/v2/internal/monitoring"
"github.com/glauth/glauth/v2/pkg/assets"
)
@@ -21,162 +14,27 @@ func RunAPI(opts ...Option) {
log := options.Logger
cfg := options.Config
register(http.DefaultServeMux, log, cfg.TLS, cfg.Listen)
router := http.DefaultServeMux
assets.NewAPI(log).RegisterEndpoints(router)
monitoring.NewAPI(log).RegisterEndpoints(router)
if cfg.TLS {
log.Info().Str("address", cfg.Listen).Msg("Starting HTTPS server")
err := http.ListenAndServeTLS(cfg.Listen, cfg.Cert, cfg.Key, nil)
if err != nil {
monitoring.NewCollector(fmt.Sprintf("https://%s/debug/vars", cfg.Listen))
if err := http.ListenAndServeTLS(cfg.Listen, cfg.Cert, cfg.Key, nil); err != nil {
log.Error().Err(err).Msg("error starting HTTPS server")
return
}
} else {
log.Info().Str("address", cfg.Listen).Msg("Starting HTTP server")
err := http.ListenAndServe(cfg.Listen, nil)
if err != nil {
log.Error().Err(err).Msg("error starting HTTP server")
return
}
}
}
func register(mux *http.ServeMux, log zerolog.Logger, tls bool, listen string) {
fs := http.FileServer(http.FS(assets.Content))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Info().Str("path", r.URL.Path).Msg("Web")
if r.URL.Path != "/" {
log.Info().Msg("Web 404")
http.NotFound(w, r)
return
}
fs.ServeHTTP(w, r)
})
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
var url string
if tls {
url = fmt.Sprintf("https://%s/debug/vars", listen)
} else {
url = fmt.Sprintf("http://%s/debug/vars", listen)
}
c := &collector{
url: url,
names: map[string]string{
"proxy": "glauth_proxy",
"proxy_frontend": "glauth_proxy_frontend",
"proxy_backend": "glauth_proxy_backend",
},
helps: map[string]string{
"proxy": "General Metrics",
"proxy_frontend": "Frontend Metrics",
"proxy_backend": "Backend Metrics",
},
labelName: map[string]string{
"proxy": "metric",
"proxy_frontend": "metric",
"proxy_backend": "metric",
},
}
prometheus.MustRegister(c)
mux.Handle("/metrics", promhttp.Handler())
}
// The code below liberally borrows from https://github.com/albertito/prometheus-expvar-exporter
// and is adapted (just a little) for our purpose.
type collector struct {
url string
names map[string]string
helps map[string]string
labelName map[string]string
}
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
return
}
func (c *collector) Collect(ch chan<- prometheus.Metric) {
resp, err := http.Get(c.url)
if err != nil {
log.Printf("Error scraping %q: %v", c.url, err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading body of %q: %v", c.url, err)
return
log.Info().Str("address", cfg.Listen).Msg("Starting HTTP server")
monitoring.NewCollector(fmt.Sprintf("http://%s/debug/vars", cfg.Listen))
if err := http.ListenAndServe(cfg.Listen, nil); err != nil {
log.Error().Err(err).Msg("error starting HTTP server")
}
// Replace "\xNN" with "?" because the default parser doesn't handle them
// well.
re := regexp.MustCompile(`\\x..`)
body = re.ReplaceAllFunc(body, func(s []byte) []byte {
return []byte("?")
})
var vs map[string]interface{}
err = json.Unmarshal(body, &vs)
if err != nil {
log.Printf("Error unmarshalling json from %q: %v", c.url, err)
return
}
for k, v := range vs {
name := strings.ReplaceAll(k, "/", "_")
if n, ok := c.names[k]; ok {
name = n
}
help := fmt.Sprintf("expvar %q", k)
if h, ok := c.helps[k]; ok {
help = h
}
lnames := []string{}
if ln, ok := c.labelName[k]; ok {
lnames = append(lnames, ln)
}
desc := prometheus.NewDesc(name, help, lnames, nil)
switch v := v.(type) {
case float64:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue, v)
case bool:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(v))
case map[string]interface{}:
// We only support explicitly written label names.
if len(lnames) != 1 {
continue
}
for lk, lv := range v {
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(lv), lk)
}
case string:
// Not supported by Prometheus.
continue
case []interface{}:
// Not supported by Prometheus.
continue
default:
continue
}
}
}
func valToFloat(v interface{}) float64 {
switch v := v.(type) {
case float64:
return v
case bool:
if v {
return 1.0
}
return 0.0
case string:
return 0.0
}
log.Printf("unexpected value type: %#v", v)
return 0.0
}
+109 -24
View File
@@ -1,17 +1,22 @@
package handler
import (
"context"
"fmt"
"github.com/rs/zerolog"
"net"
"regexp"
"sort"
"strings"
"time"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/v2/internal/monitoring"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/stats"
"github.com/nmcclain/ldap"
"github.com/glauth/ldap"
)
type configHandler struct {
@@ -21,6 +26,9 @@ type configHandler struct {
yubikeyAuth *yubigo.YubiAuth
ldohelper LDAPOpsHelper
attmatcher *regexp.Regexp
monitor monitoring.MonitorInterface
tracer trace.Tracer
}
// NewConfigHandler creates a new config backed handler
@@ -34,6 +42,8 @@ func NewConfigHandler(opts ...Option) Handler {
yubikeyAuth: options.YubiAuth,
ldohelper: options.LDAPHelper,
attmatcher: configattributematcher,
monitor: options.Monitor,
tracer: options.Tracer,
}
return handler
}
@@ -52,31 +62,83 @@ func (h configHandler) GetYubikeyAuth() *yubigo.YubiAuth {
}
// Bind implements a bind request against the config file
func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
return h.ldohelper.Bind(h, bindDN, bindSimplePw, conn)
func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
ctx, span := h.tracer.Start(context.Background(), "handler.configHandler.Bind")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "bind", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return h.ldohelper.Bind(ctx, h, bindDN, bindSimplePw, conn)
}
// Search implements a search request against the config file
func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
return h.ldohelper.Search(h, bindDN, searchReq, conn)
ctx, span := h.tracer.Start(context.Background(), "handler.configHandler.Search")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "search", "status": fmt.Sprintf("%v", result.ResultCode)},
time.Since(start).Seconds(),
)
}()
return h.ldohelper.Search(ctx, h, bindDN, searchReq, conn)
}
// Add is not supported for a static config file
func (h configHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Add")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "add", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Modify is not supported for a static config file
func (h configHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Modify")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "modify", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Delete is not supported for a static config file
func (h configHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Delete")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "delete", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
func (h configHandler) FindUser(userName string, searchByUPN bool) (f bool, u config.User, err error) {
func (h configHandler) FindUser(ctx context.Context, userName string, searchByUPN bool) (f bool, u config.User, err error) {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindUser")
defer span.End()
user := config.User{}
found := false
@@ -97,7 +159,9 @@ func (h configHandler) FindUser(userName string, searchByUPN bool) (f bool, u co
return found, user, nil
}
func (h configHandler) FindGroup(groupName string) (f bool, g config.Group, err error) {
func (h configHandler) FindGroup(ctx context.Context, groupName string) (f bool, g config.Group, err error) {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindGroup")
defer span.End()
// TODO Does g get erased, and above does u get erased?
// TODO and what about f?
group := config.Group{}
@@ -111,7 +175,10 @@ func (h configHandler) FindGroup(groupName string) (f bool, g config.Group, err
return found, group, nil
}
func (h configHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.Entry, err error) {
func (h configHandler) FindPosixAccounts(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error) {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindPosixAccounts")
defer span.End()
entries := []*ldap.Entry{}
for _, u := range h.cfg.Users {
@@ -127,7 +194,7 @@ func (h configHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.En
attrs = append(attrs, &ldap.EntryAttribute{Name: "sn", Values: []string{u.SN}})
}
attrs = append(attrs, &ldap.EntryAttribute{Name: "ou", Values: []string{h.getGroupName(u.PrimaryGroup)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "ou", Values: []string{h.getGroupName(ctx, u.PrimaryGroup)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "uidNumber", Values: []string{fmt.Sprintf("%d", u.UIDNumber)}})
if u.Disabled {
@@ -158,7 +225,7 @@ func (h configHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.En
attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", u.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "gecos", Values: []string{fmt.Sprintf("%s", u.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", u.PrimaryGroup)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: h.getGroupDNs(append(u.OtherGroups, u.PrimaryGroup))})
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: h.getGroupDNs(ctx, append(u.OtherGroups, u.PrimaryGroup))})
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowExpire", Values: []string{"-1"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowFlag", Values: []string{"134538308"}})
@@ -193,9 +260,9 @@ func (h configHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.En
}
var dn string
if hierarchy == "" {
dn = fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
dn = fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), h.backend.BaseDN)
} else {
dn = fmt.Sprintf("%s=%s,%s=%s,%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), hierarchy, h.backend.BaseDN)
dn = fmt.Sprintf("%s=%s,%s=%s,%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), hierarchy, h.backend.BaseDN)
}
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
}
@@ -203,7 +270,10 @@ func (h configHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.En
return entries, nil
}
func (h configHandler) FindPosixGroups(hierarchy string) (entrylist []*ldap.Entry, err error) {
func (h configHandler) FindPosixGroups(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error) {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindPosixGroups")
defer span.End()
asGroupOfUniqueNames := hierarchy == "ou=groups"
entries := []*ldap.Entry{}
@@ -214,11 +284,11 @@ func (h configHandler) FindPosixGroups(hierarchy string) (entrylist []*ldap.Entr
attrs = append(attrs, &ldap.EntryAttribute{Name: "uid", Values: []string{g.Name}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", g.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", g.GIDNumber)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMemberDNs(g.GIDNumber)})
attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMemberDNs(ctx, g.GIDNumber)})
if asGroupOfUniqueNames {
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"groupOfUniqueNames", "top"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(g.GIDNumber)})
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(ctx, g.GIDNumber)})
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixGroup", "top"}})
}
dn := fmt.Sprintf("%s=%s,%s,%s", h.backend.GroupFormat, g.Name, hierarchy, h.backend.BaseDN)
@@ -230,11 +300,17 @@ func (h configHandler) FindPosixGroups(hierarchy string) (entrylist []*ldap.Entr
// Close does not actually close anything, because the config data is kept in memory
func (h configHandler) Close(boundDn string, conn net.Conn) error {
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Close")
defer span.End()
stats.Frontend.Add("closes", 1)
return nil
}
func (h configHandler) getGroupMemberDNs(gid int) []string {
func (h configHandler) getGroupMemberDNs(ctx context.Context, gid int) []string {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupMemberDNs")
defer span.End()
var insertOuUsers string
if h.cfg.Behaviors.LegacyVersion > 0 && h.cfg.Behaviors.LegacyVersion <= 20100 {
insertOuUsers = ""
@@ -244,12 +320,12 @@ func (h configHandler) getGroupMemberDNs(gid int) []string {
members := make(map[string]bool)
for _, u := range h.cfg.Users {
if u.PrimaryGroup == gid {
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
members[dn] = true
} else {
for _, othergid := range u.OtherGroups {
if othergid == gid {
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
members[dn] = true
}
}
@@ -260,7 +336,7 @@ func (h configHandler) getGroupMemberDNs(gid int) []string {
if gid == g.GIDNumber {
for _, includegroupid := range g.IncludeGroups {
if includegroupid != gid {
includegroupmembers := h.getGroupMemberDNs(includegroupid)
includegroupmembers := h.getGroupMemberDNs(ctx, includegroupid)
for _, includegroupmember := range includegroupmembers {
members[includegroupmember] = true
@@ -280,7 +356,10 @@ func (h configHandler) getGroupMemberDNs(gid int) []string {
return m
}
func (h configHandler) getGroupMemberIDs(gid int) []string {
func (h configHandler) getGroupMemberIDs(ctx context.Context, gid int) []string {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupMemberIDs")
defer span.End()
members := make(map[string]bool)
for _, u := range h.cfg.Users {
if u.PrimaryGroup == gid {
@@ -300,7 +379,7 @@ func (h configHandler) getGroupMemberIDs(gid int) []string {
if includegroupid == gid {
h.log.Warn().Int("groupid", includegroupid).Msg("Ignoring myself as included group")
} else {
includegroupmemberids := h.getGroupMemberIDs(includegroupid)
includegroupmemberids := h.getGroupMemberIDs(ctx, includegroupid)
for _, includegroupmemberid := range includegroupmemberids {
members[includegroupmemberid] = true
@@ -321,7 +400,10 @@ func (h configHandler) getGroupMemberIDs(gid int) []string {
}
// Converts an array of GUIDs into an array of DNs
func (h configHandler) getGroupDNs(gids []int) []string {
func (h configHandler) getGroupDNs(ctx context.Context, gids []int) []string {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupDNs")
defer span.End()
groups := make(map[string]bool)
for _, gid := range gids {
for _, g := range h.cfg.Groups {
@@ -332,7 +414,7 @@ func (h configHandler) getGroupDNs(gids []int) []string {
for _, includegroupid := range g.IncludeGroups {
if includegroupid == gid && g.GIDNumber != gid {
includegroupdns := h.getGroupDNs([]int{g.GIDNumber})
includegroupdns := h.getGroupDNs(ctx, []int{g.GIDNumber})
for _, includegroupdn := range includegroupdns {
groups[includegroupdn] = true
@@ -352,7 +434,10 @@ func (h configHandler) getGroupDNs(gids []int) []string {
return g
}
func (h configHandler) getGroupName(gid int) string {
func (h configHandler) getGroupName(ctx context.Context, gid int) string {
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupName")
defer span.End()
for _, g := range h.cfg.Groups {
if g.GIDNumber == gid {
return g.Name
+5 -3
View File
@@ -1,13 +1,15 @@
package handler
import (
"context"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/nmcclain/ldap"
"github.com/glauth/ldap"
)
type HelperMaker interface {
FindUser(userName string, searchByUPN bool) (bool, config.User, error)
FindGroup(groupName string) (bool, config.Group, error)
FindUser(ctx context.Context, userName string, searchByUPN bool) (bool, config.User, error)
FindGroup(ctx context.Context, groupName string) (bool, config.Group, error)
}
// Handler is the common interface for all datastores
+78 -9
View File
@@ -1,11 +1,11 @@
package handler
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/rs/zerolog"
"net"
"net/url"
"os"
@@ -15,9 +15,13 @@ import (
"sync"
"time"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/glauth/glauth/v2/internal/monitoring"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/stats"
"github.com/nmcclain/ldap"
"github.com/glauth/ldap"
"github.com/pquerna/otp/totp"
)
@@ -34,6 +38,9 @@ type ldapHandler struct {
servers []ldapBackend
helper Handler
attm *regexp.Regexp
monitor monitoring.MonitorInterface
tracer trace.Tracer
}
// global lock for ldapHandler sessions & servers manipulation
@@ -71,6 +78,8 @@ func NewLdapHandler(opts ...Option) Handler {
helper: options.Helper,
lock: &ldaplock,
attm: ldapattributematcher,
monitor: options.Monitor,
tracer: options.Tracer,
}
// parse LDAP URLs
for _, ldapurl := range handler.backend.Servers {
@@ -88,7 +97,18 @@ func NewLdapHandler(opts ...Option) Handler {
return handler
}
func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
ctx, span := h.tracer.Start(context.Background(), "handler.ldapHandler.Bind")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "bind", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
h.log.Debug().Str("binddn", bindDN).Str("src", conn.RemoteAddr().String()).Msg("Bind request")
// if h.helper != nil {
@@ -107,7 +127,7 @@ func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCod
user := config.User{}
found := false
for i, handler := range h.handlers.Handlers {
found, user, _ = handler.FindUser(userName, false)
found, user, _ = handler.FindUser(ctx, userName, false)
if found {
break
}
@@ -154,6 +174,17 @@ func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCod
}
func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
ctx, span := h.tracer.Start(context.Background(), "handler.ldapHandler.Search")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "search", "status": fmt.Sprintf("%v", result.ResultCode)},
time.Since(start).Seconds(),
)
}()
wantAttributes := true
wantTypesOnly := false
@@ -233,7 +264,7 @@ func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn n
h.log.Debug().Str("type", "Root search detected").Msg("AP: Search Info")
}
filters := h.buildReqAttributesList(searchReq.Filter, []string{})
filters := h.buildReqAttributesList(ctx, searchReq.Filter, []string{})
for _, filter := range filters {
attbits := h.attm.FindStringSubmatch(filter)
@@ -272,7 +303,10 @@ func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn n
return ssr, nil
}
func (h ldapHandler) buildReqAttributesList(filter string, filters []string) []string {
func (h ldapHandler) buildReqAttributesList(ctx context.Context, filter string, filters []string) []string {
ctx, span := h.tracer.Start(ctx, "handler.ldapHandler.buildReqAttributesList")
defer span.End()
maxp := len(filter)
start := -1
descended := false
@@ -284,7 +318,7 @@ func (h ldapHandler) buildReqAttributesList(filter string, filters []string) []s
} else if c == ')' {
if start > -1 {
descended = true
filters = h.buildReqAttributesList(filter[start:p], filters)
filters = h.buildReqAttributesList(ctx, filter[start:p], filters)
}
start = -1
}
@@ -297,24 +331,59 @@ func (h ldapHandler) buildReqAttributesList(filter string, filters []string) []s
// Add is not yet supported for the ldap backend
func (h ldapHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "handler.ldapHandler.Add")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "add", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Modify is not yet supported for the ldap backend
func (h ldapHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "handler.ldapHandler.Modify")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "modify", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Delete is not yet supported for the ldap backend
func (h ldapHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "handler.ldapHandler.Delete")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "delete", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
func (h ldapHandler) FindUser(userName string, searchByUPN bool) (found bool, user config.User, err error) {
func (h ldapHandler) FindUser(ctx context.Context, userName string, searchByUPN bool) (found bool, user config.User, err error) {
ctx, span := h.tracer.Start(ctx, "handler.ldapHandler.FindUser")
defer span.End()
return false, config.User{}, nil
}
func (h ldapHandler) FindGroup(groupName string) (found bool, group config.Group, err error) {
func (h ldapHandler) FindGroup(ctx context.Context, groupName string) (found bool, group config.Group, err error) {
ctx, span := h.tracer.Start(ctx, "handler.ldapHandler.FindGroup")
defer span.End()
return false, config.Group{}, nil
}
+117 -68
View File
@@ -2,11 +2,10 @@ package handler
import (
"bufio"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/rs/zerolog"
"io/ioutil"
"net"
"os"
"path/filepath"
@@ -14,10 +13,13 @@ import (
"strings"
"time"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/stats"
"github.com/nmcclain/ldap"
"github.com/glauth/ldap"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
)
@@ -31,10 +33,10 @@ type LDAPOpsHandler interface {
GetCfg() *config.Config
GetYubikeyAuth() *yubigo.YubiAuth
FindUser(userName string, searchByUPN bool) (f bool, u config.User, err error)
FindGroup(groupName string) (f bool, g config.Group, err error)
FindPosixAccounts(hierarchy string) (entrylist []*ldap.Entry, err error)
FindPosixGroups(hierarchy string) (entrylist []*ldap.Entry, err error)
FindUser(ctx context.Context, userName string, searchByUPN bool) (f bool, u config.User, err error)
FindGroup(ctx context.Context, groupName string) (f bool, g config.Group, err error)
FindPosixAccounts(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error)
FindPosixGroups(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error)
}
type failedBind struct {
@@ -50,18 +52,24 @@ type sourceInfo struct {
type LDAPOpsHelper struct {
sources map[string]*sourceInfo
nextPruning time.Time
tracer trace.Tracer
}
func NewLDAPOpsHelper() LDAPOpsHelper {
func NewLDAPOpsHelper(tracer trace.Tracer) LDAPOpsHelper {
helper := LDAPOpsHelper{
sources: make(map[string]*sourceInfo),
nextPruning: time.Now(),
tracer: tracer,
}
return helper
}
func (l LDAPOpsHelper) Bind(h LDAPOpsHandler, bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
if l.isInTimeout(h, conn) {
func (l LDAPOpsHelper) Bind(ctx context.Context, h LDAPOpsHandler, bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.Bind")
defer span.End()
if l.isInTimeout(ctx, h, conn) {
return ldap.LDAPResultUnwillingToPerform, nil
}
@@ -78,7 +86,7 @@ func (l LDAPOpsHelper) Bind(h LDAPOpsHandler, bindDN, bindSimplePw string, conn
return ldap.LDAPResultSuccess, nil
}
user, ldapcode := l.findUser(h, bindDN, true /* checkGroup */)
user, ldapcode := l.findUser(ctx, h, bindDN, true /* checkGroup */)
if ldapcode != ldap.LDAPResultSuccess {
return ldapcode, nil
}
@@ -176,7 +184,7 @@ func (l LDAPOpsHelper) Bind(h LDAPOpsHandler, bindDN, bindSimplePw string, conn
}
if bcrypt.CompareHashAndPassword(decoded, []byte(bindSimplePw)) != nil {
h.GetLog().Info().Str("binddn", bindDN).Str("src", conn.RemoteAddr().String()).Msg("invalid credentials")
l.maybePutInTimeout(h, conn, true)
l.maybePutInTimeout(ctx, h, conn, true)
return ldap.LDAPResultInvalidCredentials, nil
}
}
@@ -185,7 +193,7 @@ func (l LDAPOpsHelper) Bind(h LDAPOpsHandler, bindDN, bindSimplePw string, conn
hash.Write([]byte(bindSimplePw))
if user.PassSHA256 != hex.EncodeToString(hash.Sum(nil)) {
h.GetLog().Info().Str("binddn", bindDN).Str("src", conn.RemoteAddr().String()).Msg("invalid credentials")
l.maybePutInTimeout(h, conn, true)
l.maybePutInTimeout(ctx, h, conn, true)
return ldap.LDAPResultInvalidCredentials, nil
}
}
@@ -210,8 +218,11 @@ func (l LDAPOpsHelper) Bind(h LDAPOpsHandler, bindDN, bindSimplePw string, conn
* TODO #5:
* Document roll out of schemas
*/
func (l LDAPOpsHelper) Search(h LDAPOpsHandler, bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
if l.isInTimeout(h, conn) {
func (l LDAPOpsHelper) Search(ctx context.Context, h LDAPOpsHandler, bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.Search")
defer span.End()
if l.isInTimeout(ctx, h, conn) {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultUnwillingToPerform}, fmt.Errorf("Source is in a timeout")
}
@@ -225,7 +236,7 @@ func (l LDAPOpsHelper) Search(h LDAPOpsHandler, bindDN string, searchReq ldap.Se
var ldapcode ldap.LDAPResultCode
if !anonymous {
if bindDN, boundUser, ldapcode = l.searchCheckBindDN(h, baseDN, bindDN, anonymous); ldapcode != ldap.LDAPResultSuccess {
if bindDN, boundUser, ldapcode = l.searchCheckBindDN(ctx, h, baseDN, bindDN, anonymous); ldapcode != ldap.LDAPResultSuccess {
return ldap.ServerSearchResult{ResultCode: ldapcode}, fmt.Errorf("Search Error: Potential bypass of BindDN %s", bindDN)
}
}
@@ -233,7 +244,7 @@ func (l LDAPOpsHelper) Search(h LDAPOpsHandler, bindDN string, searchReq ldap.Se
h.GetLog().Info().Str("binddn", bindDN).Str("basedn", baseDN).Str("searchbasedn", searchBaseDN).Str("src", conn.RemoteAddr().String()).Int("scope", searchReq.Scope).Str("filter", searchReq.Filter).Msg("Search request")
stats.Frontend.Add("search_reqs", 1)
switch entries, ldapcode := l.searchMaybeRootDSEQuery(h, baseDN, searchBaseDN, searchReq, anonymous); ldapcode {
switch entries, ldapcode := l.searchMaybeRootDSEQuery(ctx, h, baseDN, searchBaseDN, searchReq, anonymous); ldapcode {
case ldap.LDAPResultUnwillingToPerform:
return ldap.ServerSearchResult{ResultCode: ldapcode}, fmt.Errorf("Search Error: No BaseDN provided")
case ldap.LDAPResultInsufficientAccessRights:
@@ -247,9 +258,9 @@ func (l LDAPOpsHelper) Search(h LDAPOpsHandler, bindDN string, searchReq ldap.Se
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN)
}
switch entries, ldapcode, attributename := l.searchMaybeSchemaQuery(h, baseDN, searchBaseDN, searchReq, anonymous); ldapcode {
switch entries, ldapcode, attributename := l.searchMaybeSchemaQuery(ctx, h, baseDN, searchBaseDN, searchReq, anonymous); ldapcode {
case ldap.LDAPResultOperationsError:
return ldap.ServerSearchResult{ResultCode: ldapcode}, fmt.Errorf("Schema Error: attribute %s cannot be read", attributename)
return ldap.ServerSearchResult{ResultCode: ldapcode}, fmt.Errorf("Schema Error: attribute %s cannot be read", *attributename)
case ldap.LDAPResultSuccess:
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldapcode}, nil
}
@@ -264,21 +275,21 @@ func (l LDAPOpsHelper) Search(h LDAPOpsHandler, bindDN string, searchReq ldap.Se
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.GetBackend().BaseDN)
}
// Unless globally ignored, we will check that a user has capabilities allowing them to perform a search in the requested BaseDN
if !h.GetCfg().Behaviors.IgnoreCapabilities && !l.checkCapability(*boundUser, "search", []string{"*", searchBaseDN}) {
if !h.GetCfg().Behaviors.IgnoreCapabilities && !l.checkCapability(ctx, *boundUser, "search", []string{"*", searchBaseDN}) {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: no capability allowing BindDN %s to perform search in %s", bindDN, searchBaseDN)
}
switch entries, ldapcode := l.searchMaybeTopLevelNodes(h, baseDN, searchBaseDN, searchReq); ldapcode {
switch entries, ldapcode := l.searchMaybeTopLevelNodes(ctx, h, baseDN, searchBaseDN, searchReq); ldapcode {
case ldap.LDAPResultSuccess:
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldapcode}, nil
}
switch entries, ldapcode := l.searchMaybeTopLevelGroupsNode(h, baseDN, searchBaseDN, searchReq); ldapcode {
switch entries, ldapcode := l.searchMaybeTopLevelGroupsNode(ctx, h, baseDN, searchBaseDN, searchReq); ldapcode {
case ldap.LDAPResultSuccess:
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldapcode}, nil
}
switch entries, ldapcode := l.searchMaybeTopLevelUsersNode(h, baseDN, searchBaseDN, searchReq); ldapcode {
switch entries, ldapcode := l.searchMaybeTopLevelUsersNode(ctx, h, baseDN, searchBaseDN, searchReq); ldapcode {
case ldap.LDAPResultSuccess:
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldapcode}, nil
}
@@ -288,12 +299,12 @@ func (l LDAPOpsHelper) Search(h LDAPOpsHandler, bindDN string, searchReq ldap.Se
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter)
}
switch entries, ldapcode := l.searchMaybePosixGroups(h, baseDN, searchBaseDN, searchReq, filterEntity); ldapcode {
switch entries, ldapcode := l.searchMaybePosixGroups(ctx, h, baseDN, searchBaseDN, searchReq, filterEntity); ldapcode {
case ldap.LDAPResultSuccess:
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldapcode}, nil
}
switch entries, ldapcode := l.searchMaybePosixAccounts(h, baseDN, searchBaseDN, searchReq, filterEntity); ldapcode {
switch entries, ldapcode := l.searchMaybePosixAccounts(ctx, h, baseDN, searchBaseDN, searchReq, filterEntity); ldapcode {
case ldap.LDAPResultSuccess:
stats.Frontend.Add("search_successes", 1)
h.GetLog().Info().Str("filter", searchReq.Filter).Msg("AP: Search OK")
@@ -306,8 +317,11 @@ func (l LDAPOpsHelper) Search(h LDAPOpsHandler, bindDN string, searchReq ldap.Se
}
// Returns: LDAPResultSuccess or any ldap code returned by findUser
func (l LDAPOpsHelper) searchCheckBindDN(h LDAPOpsHandler, baseDN string, bindDN string, anonymous bool) (newBindDN string, boundUser *config.User, ldapresultcode ldap.LDAPResultCode) {
boundUser, ldapcode := l.findUser(h, bindDN, false /* checkGroup */)
func (l LDAPOpsHelper) searchCheckBindDN(ctx context.Context, h LDAPOpsHandler, baseDN string, bindDN string, anonymous bool) (newBindDN string, boundUser *config.User, ldapresultcode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchCheckBindDN")
defer span.End()
boundUser, ldapcode := l.findUser(ctx, h, bindDN, false /* checkGroup */)
if ldapcode != ldap.LDAPResultSuccess {
return "", nil, ldapcode
}
@@ -323,7 +337,10 @@ func (l LDAPOpsHelper) searchCheckBindDN(h LDAPOpsHandler, baseDN string, bindDN
// Search RootDSE and return information on the server
// Returns: LDAPResultSuccess, LDAPResultOther, LDAPResultUnwillingToPerform, LDAPResultInsufficientAccessRights
func (l LDAPOpsHelper) searchMaybeRootDSEQuery(h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, anonymous bool) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
func (l LDAPOpsHelper) searchMaybeRootDSEQuery(ctx context.Context, h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, anonymous bool) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchMaybeRootDSEQuery")
defer span.End()
if searchBaseDN != "" {
return nil, ldap.LDAPResultOther // OK
}
@@ -349,7 +366,7 @@ func (l LDAPOpsHelper) searchMaybeRootDSEQuery(h LDAPOpsHandler, baseDN string,
attrs = append(attrs, &ldap.EntryAttribute{Name: "serverName", Values: []string{"unknown"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "namingContexts", Values: []string{baseDN}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "defaultNamingContext", Values: []string{baseDN}})
attrs = l.collectRequestedAttributesBack(attrs, searchReq)
attrs = l.collectRequestedAttributesBack(ctx, attrs, searchReq)
entries = append(entries, &ldap.Entry{DN: searchBaseDN, Attributes: attrs})
stats.Frontend.Add("search_successes", 1)
h.GetLog().Info().Str("filter", searchReq.Filter).Msg("AP: Root Search OK")
@@ -358,7 +375,10 @@ func (l LDAPOpsHelper) searchMaybeRootDSEQuery(h LDAPOpsHandler, baseDN string,
// Search and return the information, after indirection from the RootDSE
// Returns: LDAPResultSuccess, LDAPResultOther, LDAPResultOperationsError
func (l LDAPOpsHelper) searchMaybeSchemaQuery(h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, anonymous bool) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode, attributename *string) {
func (l LDAPOpsHelper) searchMaybeSchemaQuery(ctx context.Context, h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, anonymous bool) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode, attributename *string) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchMaybeSchemaQuery")
defer span.End()
if searchBaseDN != "cn=schema" {
return nil, ldap.LDAPResultOther, nil // OK
}
@@ -371,7 +391,7 @@ func (l LDAPOpsHelper) searchMaybeSchemaQuery(h LDAPOpsHandler, baseDN string, s
attrs = append(attrs, &ldap.EntryAttribute{Name: "modifiersName", Values: []string{"cn=Directory Manager"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "modifyTimeStamp", Values: []string{"Mar 8, 2021, 12:46:29 PM PST (20210308204629Z)"}})
// Iterate through schema attributes provided in schema/ directory
filenames, _ := ioutil.ReadDir("schema")
filenames, _ := os.ReadDir("schema")
for _, filename := range filenames {
attributename := new(string)
*attributename = filename.Name()
@@ -388,7 +408,7 @@ func (l LDAPOpsHelper) searchMaybeSchemaQuery(h LDAPOpsHandler, baseDN string, s
}
attrs = append(attrs, &ldap.EntryAttribute{Name: filename.Name(), Values: values})
}
attrs = l.collectRequestedAttributesBack(attrs, searchReq)
attrs = l.collectRequestedAttributesBack(ctx, attrs, searchReq)
entries = append(entries, &ldap.Entry{DN: searchBaseDN, Attributes: attrs})
stats.Frontend.Add("search_successes", 1)
h.GetLog().Info().Str("filter", searchReq.Filter).Msg("AP: Schema Discovery OK")
@@ -397,25 +417,28 @@ func (l LDAPOpsHelper) searchMaybeSchemaQuery(h LDAPOpsHandler, baseDN string, s
// Retrieve the top-levell nodes, i.e. the baseDN, groups, members...
// Returns: LDAPResultSuccess, LDAPResultOther
func (l LDAPOpsHelper) searchMaybeTopLevelNodes(h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
func (l LDAPOpsHelper) searchMaybeTopLevelNodes(ctx context.Context, h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchMaybeTopLevelNodes")
defer span.End()
if baseDN != searchBaseDN {
return nil, ldap.LDAPResultOther // OK
}
h.GetLog().Info().Str("special case", "top-level browse").Msg("Search request")
entries := []*ldap.Entry{}
if searchReq.Scope == ldap.ScopeBaseObject || searchReq.Scope == ldap.ScopeWholeSubtree {
entries = append(entries, l.topLevelRootNode(searchBaseDN))
entries = append(entries, l.topLevelRootNode(ctx, searchBaseDN))
}
entries = append(entries, l.topLevelGroupsNode(searchBaseDN, "groups"))
entries = append(entries, l.topLevelUsersNode(searchBaseDN))
entries = append(entries, l.topLevelGroupsNode(ctx, searchBaseDN, "groups"))
entries = append(entries, l.topLevelUsersNode(ctx, searchBaseDN))
if searchReq.Scope == ldap.ScopeWholeSubtree {
groupentries, err := h.FindPosixGroups("ou=users")
groupentries, err := h.FindPosixGroups(ctx, "ou=users")
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
entries = append(entries, groupentries...)
userentries, err := h.FindPosixAccounts("ou=users")
userentries, err := h.FindPosixAccounts(ctx, "ou=users")
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
@@ -428,17 +451,20 @@ func (l LDAPOpsHelper) searchMaybeTopLevelNodes(h LDAPOpsHandler, baseDN string,
// Search starting from and including the ou=groups node
// Returns: LDAPResultSuccess, LDAPResultOther
func (l LDAPOpsHelper) searchMaybeTopLevelGroupsNode(h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
func (l LDAPOpsHelper) searchMaybeTopLevelGroupsNode(ctx context.Context, h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchMaybeTopLevelGroupsNode")
defer span.End()
if searchBaseDN != fmt.Sprintf("ou=groups,%s", baseDN) {
return nil, ldap.LDAPResultOther // OK
}
h.GetLog().Info().Str("special case", "top-level groups node").Msg("Search request")
entries := []*ldap.Entry{}
if searchReq.Scope == ldap.ScopeBaseObject || searchReq.Scope == ldap.ScopeWholeSubtree {
entries = append(entries, l.topLevelGroupsNode(searchBaseDN, "groups"))
entries = append(entries, l.topLevelGroupsNode(ctx, searchBaseDN, "groups"))
}
if searchReq.Scope == ldap.ScopeSingleLevel || searchReq.Scope == ldap.ScopeWholeSubtree {
groupentries, err := h.FindPosixGroups("ou=groups")
groupentries, err := h.FindPosixGroups(ctx, "ou=groups")
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
@@ -451,24 +477,27 @@ func (l LDAPOpsHelper) searchMaybeTopLevelGroupsNode(h LDAPOpsHandler, baseDN st
// Search starting from and including the ou=users node
// Returns: LDAPResultSuccess, LDAPResultOther
func (l LDAPOpsHelper) searchMaybeTopLevelUsersNode(h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
func (l LDAPOpsHelper) searchMaybeTopLevelUsersNode(ctx context.Context, h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchMaybeTopLevelUsersNode")
defer span.End()
if searchBaseDN != fmt.Sprintf("ou=users,%s", baseDN) {
return nil, ldap.LDAPResultOther // OK
}
h.GetLog().Info().Str("special case", "top-level users node").Msg("Search request")
entries := []*ldap.Entry{}
if searchReq.Scope == ldap.ScopeBaseObject || searchReq.Scope == ldap.ScopeWholeSubtree {
entries = append(entries, l.topLevelUsersNode(searchBaseDN))
entries = append(entries, l.topLevelUsersNode(ctx, searchBaseDN))
}
if searchReq.Scope == ldap.ScopeSingleLevel || searchReq.Scope == ldap.ScopeWholeSubtree {
groupentries, err := h.FindPosixGroups("ou=users")
groupentries, err := h.FindPosixGroups(ctx, "ou=users")
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
entries = append(entries, groupentries...)
}
if searchReq.Scope == ldap.ScopeWholeSubtree {
userentries, err := h.FindPosixAccounts("ou=users")
userentries, err := h.FindPosixAccounts(ctx, "ou=users")
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
@@ -481,7 +510,10 @@ func (l LDAPOpsHelper) searchMaybeTopLevelUsersNode(h LDAPOpsHandler, baseDN str
// Look up posixgroup entries, either through objectlass or parent is ou=groups or ou=users
// Returns: LDAPResultSuccess, LDAPResultOther, LDAPResultOperationsError
func (l LDAPOpsHelper) searchMaybePosixGroups(h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, filterEntity string) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
func (l LDAPOpsHelper) searchMaybePosixGroups(ctx context.Context, h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, filterEntity string) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchMaybePosixGroups")
defer span.End()
hierarchy := "ou=groups"
if filterEntity != "posixgroup" {
bits := strings.Split(strings.Replace(searchBaseDN, baseDN, "", 1), ",")
@@ -493,19 +525,19 @@ func (l LDAPOpsHelper) searchMaybePosixGroups(h LDAPOpsHandler, baseDN string, s
h.GetLog().Info().Str("special case", "posix groups").Msg("Search request")
entries := []*ldap.Entry{}
if searchReq.Scope == ldap.ScopeBaseObject || searchReq.Scope == ldap.ScopeWholeSubtree {
groupentries, err := h.FindPosixGroups(hierarchy)
groupentries, err := h.FindPosixGroups(ctx, hierarchy)
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
entries = append(entries, l.preFilterEntries(searchBaseDN, groupentries)...)
entries = append(entries, l.preFilterEntries(ctx, searchBaseDN, groupentries)...)
}
if searchReq.Scope == ldap.ScopeSingleLevel || searchReq.Scope == ldap.ScopeWholeSubtree {
if hierarchy == "ou=users" {
userentries, err := h.FindPosixAccounts("ou=users")
userentries, err := h.FindPosixAccounts(ctx, "ou=users")
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
entries = append(entries, l.preFilterEntries(searchBaseDN, userentries)...)
entries = append(entries, l.preFilterEntries(ctx, searchBaseDN, userentries)...)
}
}
stats.Frontend.Add("search_successes", 1)
@@ -516,7 +548,10 @@ func (l LDAPOpsHelper) searchMaybePosixGroups(h LDAPOpsHandler, baseDN string, s
// Lookup posixaccount entries
// Returns: LDAPResultSuccess, LDAPResultOther, LDAPResultOperationsError
// This function ignores scopes... for now
func (l LDAPOpsHelper) searchMaybePosixAccounts(h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, filterEntity string) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
func (l LDAPOpsHelper) searchMaybePosixAccounts(ctx context.Context, h LDAPOpsHandler, baseDN string, searchBaseDN string, searchReq ldap.SearchRequest, filterEntity string) (resultentries []*ldap.Entry, ldapresultcode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.searchMaybePosixAccounts")
defer span.End()
switch filterEntity {
case "posixaccount", "shadowaccount", "":
h.GetLog().Info().Str("default case", filterEntity).Msg("Search request")
@@ -530,7 +565,7 @@ func (l LDAPOpsHelper) searchMaybePosixAccounts(h LDAPOpsHandler, baseDN string,
hierarchyString = "ou=users"
}
unscopedEntries, err := h.FindPosixAccounts(hierarchyString)
unscopedEntries, err := h.FindPosixAccounts(ctx, hierarchyString)
if err != nil {
return nil, ldap.LDAPResultOperationsError
}
@@ -548,7 +583,7 @@ func (l LDAPOpsHelper) searchMaybePosixAccounts(h LDAPOpsHandler, baseDN string,
return entries, ldap.LDAPResultSuccess
}
func (l LDAPOpsHelper) topLevelRootNode(searchBaseDN string) *ldap.Entry {
func (l LDAPOpsHelper) topLevelRootNode(ctx context.Context, searchBaseDN string) *ldap.Entry {
attrs := []*ldap.EntryAttribute{}
dnBits := strings.Split(searchBaseDN, ",")
for _, dnBit := range dnBits {
@@ -559,7 +594,7 @@ func (l LDAPOpsHelper) topLevelRootNode(searchBaseDN string) *ldap.Entry {
return &ldap.Entry{DN: searchBaseDN, Attributes: attrs}
}
func (l LDAPOpsHelper) topLevelGroupsNode(searchBaseDN string, hierarchy string) *ldap.Entry {
func (l LDAPOpsHelper) topLevelGroupsNode(ctx context.Context, searchBaseDN string, hierarchy string) *ldap.Entry {
attrs := []*ldap.EntryAttribute{}
attrs = append(attrs, &ldap.EntryAttribute{Name: "ou", Values: []string{"groups"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"organizationalUnit", "top"}})
@@ -571,7 +606,7 @@ func (l LDAPOpsHelper) topLevelGroupsNode(searchBaseDN string, hierarchy string)
return &ldap.Entry{DN: dn, Attributes: attrs}
}
func (l LDAPOpsHelper) topLevelUsersNode(searchBaseDN string) *ldap.Entry {
func (l LDAPOpsHelper) topLevelUsersNode(ctx context.Context, searchBaseDN string) *ldap.Entry {
attrs := []*ldap.EntryAttribute{}
attrs = append(attrs, &ldap.EntryAttribute{Name: "ou", Values: []string{"users"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"organizationalUnit", "top"}})
@@ -586,7 +621,7 @@ func (l LDAPOpsHelper) topLevelUsersNode(searchBaseDN string) *ldap.Entry {
// querying groups and users under a certain node (e.g. ou=users) with a scope of "sub"
// (and only in this scenario!) will defeat the LDAP library's filtering capabilities.
// Some day, hopefully, I'll fix this directly in the library.
func (l LDAPOpsHelper) preFilterEntries(searchBaseDN string, entries []*ldap.Entry) (resultentries []*ldap.Entry) {
func (l LDAPOpsHelper) preFilterEntries(ctx context.Context, searchBaseDN string, entries []*ldap.Entry) (resultentries []*ldap.Entry) {
filteredEntries := []*ldap.Entry{}
for _, entry := range entries {
if strings.HasSuffix(entry.DN, searchBaseDN) {
@@ -596,7 +631,9 @@ func (l LDAPOpsHelper) preFilterEntries(searchBaseDN string, entries []*ldap.Ent
return filteredEntries
}
func (l LDAPOpsHelper) findUser(h LDAPOpsHandler, bindDN string, checkGroup bool) (userWhenFound *config.User, resultCode ldap.LDAPResultCode) {
func (l LDAPOpsHelper) findUser(ctx context.Context, h LDAPOpsHandler, bindDN string, checkGroup bool) (userWhenFound *config.User, resultCode ldap.LDAPResultCode) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.findUser")
defer span.End()
var user config.User
@@ -606,7 +643,7 @@ func (l LDAPOpsHelper) findUser(h LDAPOpsHandler, bindDN string, checkGroup bool
// Not using mail.ParseAddress/1 because we would allow incorrectly formatted UPNs
if emailmatcher.MatchString(bindDN) {
var foundUser bool // = false
foundUser, user, _ = h.FindUser(bindDN, true)
foundUser, user, _ = h.FindUser(ctx, bindDN, true)
if !foundUser {
h.GetLog().Info().Str("userprincipalname", bindDN).Msg("User not found")
return nil, ldap.LDAPResultInvalidCredentials
@@ -623,7 +660,7 @@ func (l LDAPOpsHelper) findUser(h LDAPOpsHandler, bindDN string, checkGroup bool
userName := ""
if len(parts) == 1 {
userName = strings.TrimPrefix(parts[0], h.GetBackend().NameFormat+"=")
} else if len(parts) == 2 || (len(parts) == 3 && parts[2] == fmt.Sprintf("%s=users", h.GetBackend().GroupFormat)) {
} else if len(parts) == 2 || (len(parts) == 3 && parts[2] == "ou=users") {
userName = strings.TrimPrefix(parts[0], h.GetBackend().NameFormat+"=")
groupName = strings.TrimPrefix(parts[1], h.GetBackend().GroupFormat+"=")
} else {
@@ -636,7 +673,7 @@ func (l LDAPOpsHelper) findUser(h LDAPOpsHandler, bindDN string, checkGroup bool
// find the user
var foundUser bool // = false
foundUser, user, _ = h.FindUser(userName, false)
foundUser, user, _ = h.FindUser(ctx, userName, false)
if !foundUser {
h.GetLog().Info().Str("username", userName).Msg("User not found")
return nil, ldap.LDAPResultInvalidCredentials
@@ -646,7 +683,7 @@ func (l LDAPOpsHelper) findUser(h LDAPOpsHandler, bindDN string, checkGroup bool
var group config.Group // = nil
var foundGroup bool // = false
if groupName != "" {
foundGroup, group, _ = h.FindGroup(groupName)
foundGroup, group, _ = h.FindGroup(ctx, groupName)
if !foundGroup {
h.GetLog().Info().Str("groupname", groupName).Msg("Group not found")
return nil, ldap.LDAPResultInvalidCredentials
@@ -664,7 +701,7 @@ func (l LDAPOpsHelper) findUser(h LDAPOpsHandler, bindDN string, checkGroup bool
return &user, ldap.LDAPResultSuccess
}
func (l LDAPOpsHelper) checkCapability(user config.User, action string, objects []string) bool {
func (l LDAPOpsHelper) checkCapability(ctx context.Context, user config.User, action string, objects []string) bool {
for _, capability := range user.Capabilities {
if capability.Action == action {
for _, object := range objects {
@@ -680,7 +717,10 @@ func (l LDAPOpsHelper) checkCapability(user config.User, action string, objects
// If your query is for, say 'objectClass', then our LDAP
// library will weed out this entry since it does *not* contain an objectclass attribute
// so we are going to re-inject it to keep the LDAP library happy
func (l LDAPOpsHelper) collectRequestedAttributesBack(attrs []*ldap.EntryAttribute, searchReq ldap.SearchRequest) []*ldap.EntryAttribute {
func (l LDAPOpsHelper) collectRequestedAttributesBack(ctx context.Context, attrs []*ldap.EntryAttribute, searchReq ldap.SearchRequest) []*ldap.EntryAttribute {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.collectRequestedAttributesBack")
defer span.End()
attbits := configattributematcher.FindStringSubmatch(searchReq.Filter)
if len(attbits) == 3 {
foundattname := false
@@ -699,13 +739,16 @@ func (l LDAPOpsHelper) collectRequestedAttributesBack(attrs []*ldap.EntryAttribu
}
// return true if we should not process the current operation
func (l LDAPOpsHelper) isInTimeout(handler LDAPOpsHandler, conn net.Conn) bool {
func (l LDAPOpsHelper) isInTimeout(ctx context.Context, handler LDAPOpsHandler, conn net.Conn) bool {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.isInTimeout")
defer span.End()
cfg := handler.GetCfg()
if !cfg.Behaviors.LimitFailedBinds {
return false
}
remoteAddr := l.getAddr(conn)
remoteAddr := l.getAddr(ctx, conn)
now := time.Now()
info, ok := l.sources[remoteAddr]
if !ok {
@@ -725,13 +768,16 @@ func (l LDAPOpsHelper) isInTimeout(handler LDAPOpsHandler, conn net.Conn) bool {
return false
}
func (l LDAPOpsHelper) maybePutInTimeout(handler LDAPOpsHandler, conn net.Conn, noteFailure bool) bool {
func (l LDAPOpsHelper) maybePutInTimeout(ctx context.Context, handler LDAPOpsHandler, conn net.Conn, noteFailure bool) bool {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.maybePutInTimeout")
defer span.End()
cfg := handler.GetCfg()
if !cfg.Behaviors.LimitFailedBinds {
return false
}
remoteAddr := l.getAddr(conn)
remoteAddr := l.getAddr(ctx, conn)
now := time.Now()
info, _ := l.sources[remoteAddr]
// if we have a failed bind...
@@ -765,7 +811,10 @@ func (l LDAPOpsHelper) maybePutInTimeout(handler LDAPOpsHandler, conn net.Conn,
return false
}
func (l LDAPOpsHelper) getAddr(conn net.Conn) string {
func (l LDAPOpsHelper) getAddr(ctx context.Context, conn net.Conn) string {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.getAddr")
defer span.End()
fullAddr := conn.RemoteAddr().String()
sep := strings.LastIndex(fullAddr, ":")
if sep == -1 {
+19
View File
@@ -2,9 +2,12 @@ package handler
import (
"context"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/v2/internal/monitoring"
"github.com/glauth/glauth/v2/pkg/config"
)
@@ -21,6 +24,8 @@ type Options struct {
YubiAuth *yubigo.YubiAuth
Helper Handler
LDAPHelper LDAPOpsHelper
Monitor monitoring.MonitorInterface
Tracer trace.Tracer
}
// newOptions initializes the available default options.
@@ -100,3 +105,17 @@ func LDAPHelper(val LDAPOpsHelper) Option {
o.LDAPHelper = val
}
}
// Monitor provides a function to set the monitor option.
func Monitor(val monitoring.MonitorInterface) Option {
return func(o *Options) {
o.Monitor = val
}
}
// Tracer provides a function to set the tracer option.
func Tracer(val trace.Tracer) Option {
return func(o *Options) {
o.Tracer = val
}
}
+86 -31
View File
@@ -6,16 +6,20 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/rs/zerolog"
"io/ioutil"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/glauth/glauth/v2/internal/monitoring"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/stats"
"github.com/nmcclain/ldap"
"github.com/glauth/ldap"
msgraph "github.com/yaegashi/msgraph.go/v1.0"
)
@@ -33,12 +37,46 @@ type ownCloudHandler struct {
client *http.Client
sessions map[string]ownCloudSession
lock *sync.Mutex
monitor monitoring.MonitorInterface
tracer trace.Tracer
}
// global lock for ownCloudHandler sessions & servers manipulation
var ownCloudLock sync.Mutex
func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) {
func NewOwnCloudHandler(opts ...Option) Handler {
options := newOptions(opts...)
return ownCloudHandler{
backend: options.Backend,
log: options.Logger,
sessions: make(map[string]ownCloudSession),
lock: &ownCloudLock,
client: &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: options.Backend.Insecure,
},
},
},
monitor: options.Monitor,
tracer: options.Tracer,
}
}
func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "bind", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
bindDN = strings.ToLower(bindDN)
baseDN := strings.ToLower("," + h.backend.BaseDN)
@@ -83,7 +121,15 @@ func (h ownCloudHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.
return ldap.LDAPResultSuccess, nil
}
func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "search", "status": fmt.Sprintf("%v", result.ResultCode)},
time.Since(start).Seconds(),
)
}()
bindDN = strings.ToLower(bindDN)
baseDN := strings.ToLower("," + h.backend.BaseDN)
searchBaseDN := strings.ToLower(searchReq.BaseDN)
@@ -133,7 +179,7 @@ func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, con
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: members})
}
dn := fmt.Sprintf("cn=%s,%s=groups,%s", *g.ID, h.backend.GroupFormat, h.backend.BaseDN)
dn := fmt.Sprintf("%s=%s,ou=groups,%s", h.backend.GroupFormat, *g.ID, h.backend.BaseDN)
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
}
case "posixaccount", "":
@@ -174,25 +220,55 @@ func (h ownCloudHandler) Search(bindDN string, searchReq ldap.SearchRequest, con
// Add is not yet supported for the owncloud backend
func (h ownCloudHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "add", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Modify is not yet supported for the owncloud backend
func (h ownCloudHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "modify", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Delete is not yet supported for the owncloud backend
func (h ownCloudHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Delete")
defer span.End()
start := time.Now()
defer func() {
h.monitor.SetResponseTimeMetric(
map[string]string{"operation": "delete", "status": fmt.Sprintf("%v", result)},
time.Since(start).Seconds(),
)
}()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// FindUser with the given username. Called by the ldap backend to authenticate the bind. Optional
func (h ownCloudHandler) FindUser(userName string, searchByUPN bool) (found bool, user config.User, err error) {
func (h ownCloudHandler) FindUser(ctx context.Context, userName string, searchByUPN bool) (found bool, user config.User, err error) {
_, span := h.tracer.Start(ctx, "handler.ownCloudHandler.FindUser")
defer span.End()
return false, config.User{}, nil
}
func (h ownCloudHandler) FindGroup(groupName string) (found bool, group config.Group, err error) {
func (h ownCloudHandler) FindGroup(ctx context.Context, groupName string) (found bool, group config.Group, err error) {
_, span := h.tracer.Start(ctx, "handler.ownCloudHandler.FindGroup")
defer span.End()
return false, config.Group{}, nil
}
@@ -255,7 +331,7 @@ func (s ownCloudSession) getGroups() ([]msgraph.Group, error) {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@@ -326,7 +402,7 @@ func (s ownCloudSession) getUsers(userName string) ([]msgraph.User, error) {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@@ -352,24 +428,3 @@ func (s ownCloudSession) redirectPolicyFunc(req *http.Request, via []*http.Reque
req.SetBasicAuth(s.user, s.password)
return nil
}
func NewOwnCloudHandler(opts ...Option) Handler {
options := newOptions(opts...)
return ownCloudHandler{
backend: options.Backend,
log: options.Logger,
sessions: make(map[string]ownCloudSession),
lock: &ownCloudLock,
client: &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: options.Backend.Insecure,
},
},
},
}
}
+2 -1
View File
@@ -5,7 +5,6 @@ package logging
import (
"fmt"
"github.com/rs/zerolog"
"io"
"log"
"log/syslog"
@@ -13,6 +12,8 @@ import (
"regexp"
"strings"
"time"
"github.com/rs/zerolog"
)
// We will use this package to wrap log messages coming from libraries who have no interest
+2 -1
View File
@@ -5,13 +5,14 @@ package logging
import (
"fmt"
"github.com/rs/zerolog"
"io"
"log"
"os"
"regexp"
"strings"
"time"
"github.com/rs/zerolog"
)
// We will use this package to wrap log messages coming from libraries who have no interest
+178 -82
View File
@@ -1,11 +1,11 @@
package plugins
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"github.com/rs/zerolog"
"net"
"os"
"regexp"
@@ -13,11 +13,17 @@ import (
"strconv"
"strings"
"github.com/rs/zerolog"
"github.com/uptrace/opentelemetry-go-extra/otelsql"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"go.opentelemetry.io/otel/trace"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/handler"
"github.com/glauth/glauth/v2/pkg/stats"
"github.com/nmcclain/ldap"
"github.com/glauth/ldap"
)
var configattributematcher = regexp.MustCompile(`(?i)\((?P<attribute>[a-zA-Z0-9]+)\s*=\s*(?P<value>.*)\)`)
@@ -28,7 +34,7 @@ type SqlBackend interface {
// Create db/schema if necessary
CreateSchema(db *sql.DB)
// Migrate schema if necessary
MigrateSchema(db *sql.DB, checker func(*sql.DB, string) bool)
MigrateSchema(db *sql.DB, checker func(*sql.DB, string, string) bool)
//
GetPrepareSymbol() string
}
@@ -48,13 +54,21 @@ type databaseHandler struct {
MemGroups []config.Group
ldohelper handler.LDAPOpsHelper
attmatcher *regexp.Regexp
tracer trace.Tracer
}
func NewDatabaseHandler(sqlBackend SqlBackend, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
// Note: we will never terminate this connection pool.
db, err := sql.Open(sqlBackend.GetDriverName(), options.Backend.Database)
db, err := otelsql.Open(
sqlBackend.GetDriverName(),
options.Backend.Database,
otelsql.WithAttributes(otlpDriverAttribute(sqlBackend)),
otelsql.WithDBName(options.Backend.Database),
)
if err != nil {
options.Logger.Error().Err(err).Msg(fmt.Sprintf("unable to open SQL database named '%s'", options.Backend.Database))
os.Exit(1)
@@ -78,7 +92,9 @@ func NewDatabaseHandler(sqlBackend SqlBackend, opts ...handler.Option) handler.H
sqlBackend: sqlBackend,
database: dbInfo,
ldohelper: options.LDAPHelper,
attmatcher: configattributematcher}
attmatcher: configattributematcher,
tracer: options.Tracer,
}
sqlBackend.CreateSchema(db)
sqlBackend.MigrateSchema(db, ColumnExists)
@@ -88,14 +104,24 @@ func NewDatabaseHandler(sqlBackend SqlBackend, opts ...handler.Option) handler.H
return handler
}
func ColumnExists(db *sql.DB, columnName string) bool {
func ColumnExists(db *sql.DB, tableName string, columnName string) bool {
var found string
err := db.QueryRow(fmt.Sprintf(`SELECT COUNT(%s) FROM users`, columnName)).Scan(
err := db.QueryRowContext(context.Background(), fmt.Sprintf(`SELECT COUNT(%s) FROM %s`, columnName, tableName)).Scan(
&found)
if err != nil {
return false
return err == nil
}
func otlpDriverAttribute(backend SqlBackend) attribute.KeyValue {
switch backend.GetDriverName() {
case "sqlite3":
return semconv.DBSystemSqlite
case "postgres":
return semconv.DBSystemPostgreSQL
case "mysql":
return semconv.DBSystemMySQL
default:
return semconv.DBSystemOtherSQL
}
return true
}
func (h databaseHandler) GetBackend() config.Backend {
@@ -112,29 +138,47 @@ func (h databaseHandler) GetYubikeyAuth() *yubigo.YubiAuth {
}
func (h databaseHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
return h.ldohelper.Bind(h, bindDN, bindSimplePw, conn)
ctx, span := h.tracer.Start(context.Background(), "plugins.databaseHandler.Bind")
defer span.End()
return h.ldohelper.Bind(ctx, h, bindDN, bindSimplePw, conn)
}
func (h databaseHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
return h.ldohelper.Search(h, bindDN, searchReq, conn)
ctx, span := h.tracer.Start(context.Background(), "plugins.databaseHandler.Search")
defer span.End()
return h.ldohelper.Search(ctx, h, bindDN, searchReq, conn)
}
// Add is not yet supported for the sql backend
func (h databaseHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "plugins.databaseHandler.Add")
defer span.End()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Modify is not yet supported for the sql backend
func (h databaseHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "plugins.databaseHandler.Modify")
defer span.End()
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Delete is not yet supported for the sql backend
func (h databaseHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
_, span := h.tracer.Start(context.Background(), "plugins.databaseHandler.Delete")
defer span.End()
return ldap.LDAPResultInsufficientAccessRights, nil
}
func (h databaseHandler) FindUser(userName string, searchByUPN bool) (f bool, u config.User, err error) {
func (h databaseHandler) FindUser(ctx context.Context, userName string, searchByUPN bool) (f bool, u config.User, err error) {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.FindUser")
defer span.End()
var criterion string
if searchByUPN {
criterion = "lower(u.mail)"
@@ -146,11 +190,14 @@ func (h databaseHandler) FindUser(userName string, searchByUPN bool) (f bool, u
found := false
var disabled int
err = h.database.cnx.QueryRow(fmt.Sprintf(`
err = h.database.cnx.QueryRowContext(
ctx,
fmt.Sprintf(`
SELECT u.uidnumber,u.primarygroup,u.passbcrypt,u.passsha256,u.otpsecret,u.yubikey,u.disabled
FROM users u WHERE %s=%s`,
criterion,
h.sqlBackend.GetPrepareSymbol()), userName).Scan(
criterion,
h.sqlBackend.GetPrepareSymbol(),
), userName).Scan(
&user.UIDNumber, &user.PrimaryGroup, &user.PassBcrypt, &user.PassSHA256, &user.OTPSecret, &user.Yubikey, &disabled)
if err == nil {
user.Disabled = h.intToBool(disabled)
@@ -159,7 +206,7 @@ func (h databaseHandler) FindUser(userName string, searchByUPN bool) (f bool, u
if !h.cfg.Behaviors.IgnoreCapabilities {
capability := config.Capability{}
rows, err := h.database.cnx.Query(fmt.Sprintf(`
rows, err := h.database.cnx.QueryContext(ctx, fmt.Sprintf(`
SELECT c.action,c.object
FROM capabilities c WHERE userid=%s`,
h.sqlBackend.GetPrepareSymbol()), user.UIDNumber)
@@ -179,12 +226,17 @@ func (h databaseHandler) FindUser(userName string, searchByUPN bool) (f bool, u
return found, user, err
}
func (h databaseHandler) FindGroup(groupName string) (f bool, g config.Group, err error) {
func (h databaseHandler) FindGroup(ctx context.Context, groupName string) (f bool, g config.Group, err error) {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.FindGroup")
defer span.End()
group := config.Group{}
found := false
err = h.database.cnx.QueryRow(fmt.Sprintf(`
SELECT g.gidnumber FROM groups g WHERE lower(name)=%s`, h.sqlBackend.GetPrepareSymbol()), groupName).Scan(
err = h.database.cnx.QueryRowContext(
ctx,
fmt.Sprintf(`
SELECT g.gidnumber FROM ldapgroups g WHERE lower(name)=%s`, h.sqlBackend.GetPrepareSymbol()), groupName).Scan(
&group.GIDNumber)
if err == nil {
found = true
@@ -193,15 +245,20 @@ func (h databaseHandler) FindGroup(groupName string) (f bool, g config.Group, er
return found, group, err
}
func (h databaseHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.Entry, err error) {
func (h databaseHandler) FindPosixAccounts(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error) {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.FindPosixAccounts")
defer span.End()
entries := []*ldap.Entry{}
h.MemGroups, err = h.memoizeGroups()
h.MemGroups, err = h.memoizeGroups(ctx)
if err != nil {
return entries, err
}
rows, err := h.database.cnx.Query(`
rows, err := h.database.cnx.QueryContext(
ctx,
`
SELECT u.name,u.uidnumber,u.primarygroup,u.passbcrypt,u.passsha256,u.otpsecret,u.yubikey,u.othergroups,u.givenname,u.sn,u.mail,u.loginshell,u.homedirectory,u.disabled,u.sshkeys,u.custattr
FROM users u`)
if err != nil {
@@ -219,11 +276,11 @@ func (h databaseHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.
if err != nil {
return entries, err
}
u.OtherGroups = h.commaListToIntTable(otherGroups)
u.OtherGroups = h.commaListToIntTable(ctx, otherGroups)
u.Disabled = h.intToBool(disabled)
u.SSHKeys = h.commaListToStringTable(sshKeys)
u.SSHKeys = h.commaListToStringTable(ctx, sshKeys)
entry := h.getAccount(hierarchy, u)
entry := h.getAccount(ctx, hierarchy, u)
if custattrstr != "{}" {
var r map[string]interface{}
@@ -256,16 +313,19 @@ func (h databaseHandler) FindPosixAccounts(hierarchy string) (entrylist []*ldap.
return entries, nil
}
func (h databaseHandler) FindPosixGroups(hierarchy string) (entrylist []*ldap.Entry, err error) {
func (h databaseHandler) FindPosixGroups(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error) {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.FindPosixGroups")
defer span.End()
entries := []*ldap.Entry{}
h.MemGroups, err = h.memoizeGroups()
h.MemGroups, err = h.memoizeGroups(ctx)
if err != nil {
return entries, err
}
for _, g := range h.MemGroups {
info := h.getGroup(hierarchy, g)
info := h.getGroup(ctx, hierarchy, g)
if hierarchy != "groups" {
info.DN = strings.Replace(info.DN, ",ou=groups,", fmt.Sprintf(",%s,", hierarchy), 1)
}
@@ -276,6 +336,9 @@ func (h databaseHandler) FindPosixGroups(hierarchy string) (entrylist []*ldap.En
}
func (h databaseHandler) Close(boundDn string, conn net.Conn) error {
_, span := h.tracer.Start(context.Background(), "plugins.databaseHandler.Close")
defer span.End()
stats.Frontend.Add("closes", 1)
return nil
}
@@ -287,7 +350,10 @@ func (h databaseHandler) intToBool(value int) bool {
return true
}
func (h databaseHandler) commaListToIntTable(commaList string) []int {
func (h databaseHandler) commaListToIntTable(ctx context.Context, commaList string) []int {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.commaListToIntTable")
defer span.End()
if len(commaList) == 0 {
return make([]int, 0)
}
@@ -303,20 +369,28 @@ func (h databaseHandler) commaListToIntTable(commaList string) []int {
return rowsAsInts
}
func (h databaseHandler) commaListToStringTable(commaList string) []string {
func (h databaseHandler) commaListToStringTable(ctx context.Context, commaList string) []string {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.commaListToStringTable")
defer span.End()
if len(commaList) == 0 {
return make([]string, 0)
}
return strings.Split(commaList, ",")
}
func (h databaseHandler) memoizeGroups() ([]config.Group, error) {
func (h databaseHandler) memoizeGroups(ctx context.Context) ([]config.Group, error) {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.memoizeGroups")
defer span.End()
workMemGroups := make([]*config.Group, 0)
rows, err := h.database.cnx.Query(`
SELECT g1.name,g1.gidnumber,ig.includegroupid
FROM groups g1
rows, err := h.database.cnx.QueryContext(
ctx,
`
SELECT g1.name,g1.gidnumber,ig.includegroupid
FROM ldapgroups g1
LEFT JOIN includegroups ig ON g1.gidnumber=ig.parentgroupid
LEFT JOIN groups g2 ON ig.includegroupid=g2.gidnumber`)
LEFT JOIN ldapgroups g2 ON ig.includegroupid=g2.gidnumber`)
if err != nil {
return nil, errors.New("Unable to memoize groups list")
}
@@ -349,7 +423,10 @@ func (h databaseHandler) memoizeGroups() ([]config.Group, error) {
return memGroups, nil
}
func (h databaseHandler) getGroupMemberDNs(gid int) []string {
func (h databaseHandler) getGroupMemberDNs(ctx context.Context, gid int) []string {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.getGroupMemberDNs")
defer span.End()
var insertOuUsers string
if h.cfg.Behaviors.LegacyVersion > 0 && h.cfg.Behaviors.LegacyVersion <= 20100 {
insertOuUsers = ""
@@ -358,7 +435,9 @@ func (h databaseHandler) getGroupMemberDNs(gid int) []string {
}
members := make(map[string]bool)
rows, err := h.database.cnx.Query(`
rows, err := h.database.cnx.QueryContext(
ctx,
`
SELECT u.name,u.uidnumber,u.primarygroup,u.passbcrypt,u.passsha256,u.otpsecret,u.yubikey,u.othergroups
FROM users u`)
if err != nil {
@@ -375,13 +454,13 @@ func (h databaseHandler) getGroupMemberDNs(gid int) []string {
return []string{}
}
if u.PrimaryGroup == gid {
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
members[dn] = true
} else {
u.OtherGroups = h.commaListToIntTable(otherGroups)
u.OtherGroups = h.commaListToIntTable(ctx, otherGroups)
for _, othergid := range u.OtherGroups {
if othergid == gid {
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
members[dn] = true
}
}
@@ -392,7 +471,7 @@ func (h databaseHandler) getGroupMemberDNs(gid int) []string {
if gid == g.GIDNumber {
for _, includegroupid := range g.IncludeGroups {
if includegroupid != gid {
includegroupmembers := h.getGroupMemberDNs(includegroupid)
includegroupmembers := h.getGroupMemberDNs(ctx, includegroupid)
for _, includegroupmember := range includegroupmembers {
members[includegroupmember] = true
@@ -413,9 +492,14 @@ func (h databaseHandler) getGroupMemberDNs(gid int) []string {
}
// Used exclusively when looking up Posix Groups
func (h databaseHandler) getGroupMemberIDs(gid int) []string {
func (h databaseHandler) getGroupMemberIDs(ctx context.Context, gid int) []string {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.getGroupMemberIDs")
defer span.End()
members := make(map[string]bool)
rows, err := h.database.cnx.Query(`
rows, err := h.database.cnx.QueryContext(
ctx,
`
SELECT u.name,u.uidnumber,u.primarygroup,u.passbcrypt,u.passsha256,u.otpsecret,u.yubikey,u.othergroups
FROM users u`)
if err != nil {
@@ -434,7 +518,7 @@ func (h databaseHandler) getGroupMemberIDs(gid int) []string {
if u.PrimaryGroup == gid {
members[u.Name] = true
} else {
u.OtherGroups = h.commaListToIntTable(otherGroups)
u.OtherGroups = h.commaListToIntTable(ctx, otherGroups)
for _, othergid := range u.OtherGroups {
if othergid == gid {
members[u.Name] = true
@@ -449,7 +533,7 @@ func (h databaseHandler) getGroupMemberIDs(gid int) []string {
if includegroupid == gid {
h.log.Warn().Msg(fmt.Sprintf("Group: %d - Ignoring myself as included group", includegroupid))
} else {
includegroupmemberids := h.getGroupMemberIDs(includegroupid)
includegroupmemberids := h.getGroupMemberIDs(ctx, includegroupid)
for _, includegroupmemberid := range includegroupmemberids {
members[includegroupmemberid] = true
@@ -470,7 +554,10 @@ func (h databaseHandler) getGroupMemberIDs(gid int) []string {
}
// Invoked for every user being returned from our database
func (h databaseHandler) getGroupDNs(gids []int) []string {
func (h databaseHandler) getGroupDNs(ctx context.Context, gids []int) []string {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.getGroupDNs")
defer span.End()
groups := make(map[string]bool)
for _, gid := range gids {
for _, g := range h.MemGroups {
@@ -481,7 +568,7 @@ func (h databaseHandler) getGroupDNs(gids []int) []string {
for _, includegroupid := range g.IncludeGroups {
if includegroupid == gid && g.GIDNumber != gid {
includegroupdns := h.getGroupDNs([]int{g.GIDNumber})
includegroupdns := h.getGroupDNs(ctx, []int{g.GIDNumber})
for _, includegroupdn := range includegroupdns {
groups[includegroupdn] = true
@@ -502,7 +589,10 @@ func (h databaseHandler) getGroupDNs(gids []int) []string {
}
// Invoked for every user being returned from our database
func (h databaseHandler) getGroupName(gid int) string {
func (h databaseHandler) getGroupName(ctx context.Context, gid int) string {
_, span := h.tracer.Start(ctx, "plugins.databaseHandler.getGroupName")
defer span.End()
for _, g := range h.MemGroups {
if g.GIDNumber == gid {
return g.Name
@@ -512,77 +602,83 @@ func (h databaseHandler) getGroupName(gid int) string {
}
// Toolbox
func (h databaseHandler) getGroup(hierarchy string, g config.Group) *ldap.Entry {
func (h databaseHandler) getGroup(ctx context.Context, hierarchy string, g config.Group) *ldap.Entry {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.getGroup")
defer span.End()
asGroupOfUniqueNames := hierarchy == "ou=groups"
attrs := []*ldap.EntryAttribute{}
attrs = append(attrs, &ldap.EntryAttribute{h.backend.GroupFormat, []string{g.Name}})
attrs = append(attrs, &ldap.EntryAttribute{"description", []string{fmt.Sprintf("%s via LDAP", g.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{"gidNumber", []string{fmt.Sprintf("%d", g.GIDNumber)}})
attrs = append(attrs, &ldap.EntryAttribute{"uniqueMember", h.getGroupMemberDNs(g.GIDNumber)})
attrs = append(attrs, &ldap.EntryAttribute{Name: h.backend.GroupFormat, Values: []string{g.Name}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s via LDAP", g.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", g.GIDNumber)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMemberDNs(ctx, g.GIDNumber)})
if asGroupOfUniqueNames {
attrs = append(attrs, &ldap.EntryAttribute{"objectClass", []string{"groupOfUniqueNames", "top"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"groupOfUniqueNames", "top"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{"memberUid", h.getGroupMemberIDs(g.GIDNumber)})
attrs = append(attrs, &ldap.EntryAttribute{"objectClass", []string{"posixGroup", "top"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(ctx, g.GIDNumber)})
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixGroup", "top"}})
}
dn := fmt.Sprintf("%s=%s,ou=groups,%s", h.backend.GroupFormat, g.Name, h.backend.BaseDN)
return &ldap.Entry{dn, attrs}
return &ldap.Entry{DN: dn, Attributes: attrs}
}
func (h databaseHandler) getAccount(hierarchy string, u config.User) *ldap.Entry {
func (h databaseHandler) getAccount(ctx context.Context, hierarchy string, u config.User) *ldap.Entry {
ctx, span := h.tracer.Start(ctx, "plugins.databaseHandler.getAccount")
defer span.End()
attrs := []*ldap.EntryAttribute{}
attrs = append(attrs, &ldap.EntryAttribute{h.backend.NameFormat, []string{u.Name}})
attrs = append(attrs, &ldap.EntryAttribute{"uid", []string{u.Name}})
attrs = append(attrs, &ldap.EntryAttribute{Name: h.backend.NameFormat, Values: []string{u.Name}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "uid", Values: []string{u.Name}})
if len(u.GivenName) > 0 {
attrs = append(attrs, &ldap.EntryAttribute{"givenName", []string{u.GivenName}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "givenName", Values: []string{u.GivenName}})
}
if len(u.SN) > 0 {
attrs = append(attrs, &ldap.EntryAttribute{"sn", []string{u.SN}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "sn", Values: []string{u.SN}})
}
attrs = append(attrs, &ldap.EntryAttribute{"ou", []string{h.getGroupName(u.PrimaryGroup)}})
attrs = append(attrs, &ldap.EntryAttribute{"uidNumber", []string{fmt.Sprintf("%d", u.UIDNumber)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "ou", Values: []string{h.getGroupName(ctx, u.PrimaryGroup)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "uidNumber", Values: []string{fmt.Sprintf("%d", u.UIDNumber)}})
if u.Disabled {
attrs = append(attrs, &ldap.EntryAttribute{"accountStatus", []string{"inactive"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{"accountStatus", []string{"active"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}})
}
if len(u.Mail) > 0 {
attrs = append(attrs, &ldap.EntryAttribute{"mail", []string{u.Mail}})
attrs = append(attrs, &ldap.EntryAttribute{"userPrincipalName", []string{u.Mail}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "mail", Values: []string{u.Mail}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "userPrincipalName", Values: []string{u.Mail}})
}
attrs = append(attrs, &ldap.EntryAttribute{"objectClass", []string{"posixAccount"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixAccount"}})
if len(u.LoginShell) > 0 {
attrs = append(attrs, &ldap.EntryAttribute{"loginShell", []string{u.LoginShell}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "loginShell", Values: []string{u.LoginShell}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{"loginShell", []string{"/bin/bash"}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "loginShell", Values: []string{"/bin/bash"}})
}
if len(u.Homedir) > 0 {
attrs = append(attrs, &ldap.EntryAttribute{"homeDirectory", []string{u.Homedir}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "homeDirectory", Values: []string{u.Homedir}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{"homeDirectory", []string{"/home/" + u.Name}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "homeDirectory", Values: []string{"/home/" + u.Name}})
}
attrs = append(attrs, &ldap.EntryAttribute{"description", []string{fmt.Sprintf("%s via LDAP", u.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{"gecos", []string{fmt.Sprintf("%s via LDAP", u.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{"gidNumber", []string{fmt.Sprintf("%d", u.PrimaryGroup)}})
attrs = append(attrs, &ldap.EntryAttribute{"memberOf", h.getGroupDNs(append(u.OtherGroups, u.PrimaryGroup))})
attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s via LDAP", u.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "gecos", Values: []string{fmt.Sprintf("%s via LDAP", u.Name)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", u.PrimaryGroup)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: h.getGroupDNs(ctx, append(u.OtherGroups, u.PrimaryGroup))})
if len(u.SSHKeys) > 0 {
attrs = append(attrs, &ldap.EntryAttribute{"sshPublicKey", u.SSHKeys})
attrs = append(attrs, &ldap.EntryAttribute{Name: "sshPublicKey", Values: u.SSHKeys})
}
var dn string
if hierarchy == "" {
dn = fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.backend.BaseDN)
dn = fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), h.backend.BaseDN)
} else {
dn = fmt.Sprintf("%s=%s,%s=%s,%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(u.PrimaryGroup), hierarchy, h.backend.BaseDN)
dn = fmt.Sprintf("%s=%s,%s=%s,%s,%s", h.backend.NameFormat, u.Name, h.backend.GroupFormat, h.getGroupName(ctx, u.PrimaryGroup), hierarchy, h.backend.BaseDN)
}
return &ldap.Entry{dn, attrs}
return &ldap.Entry{DN: dn, Attributes: attrs}
}
+32 -4
View File
@@ -2,8 +2,12 @@ package server
import (
"context"
"github.com/rs/zerolog"
"crypto/tls"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/glauth/glauth/v2/internal/monitoring"
"github.com/glauth/glauth/v2/pkg/config"
)
@@ -12,9 +16,12 @@ type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger zerolog.Logger
Config *config.Config
Context context.Context
Logger zerolog.Logger
Config *config.Config
TLSConfig *tls.Config
Monitor monitoring.MonitorInterface
Tracer trace.Tracer
Context context.Context
}
// newOptions initializes the available default options.
@@ -48,3 +55,24 @@ func Context(val context.Context) Option {
o.Context = val
}
}
// TLSConfig provides a function to set the TLS config option.
func TLSConfig(val *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = val
}
}
// Monitor provides a function to set the monitor option.
func Monitor(val monitoring.MonitorInterface) Option {
return func(o *Options) {
o.Monitor = val
}
}
// Tracer provides a function to set the tracer option.
func Tracer(val trace.Tracer) Option {
return func(o *Options) {
o.Tracer = val
}
}
+32 -6
View File
@@ -3,28 +3,36 @@ package server
import (
"errors"
"fmt"
"github.com/rs/zerolog"
"plugin"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/GeertJohan/yubigo"
"github.com/glauth/glauth/v2/internal/monitoring"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/glauth/v2/pkg/handler"
"github.com/nmcclain/ldap"
"github.com/glauth/ldap"
)
type LdapSvc struct {
log zerolog.Logger
c *config.Config
yubiAuth *yubigo.YubiAuth
l *ldap.Server
monitor monitoring.MonitorInterface
tracer trace.Tracer
log zerolog.Logger
}
func NewServer(opts ...Option) (*LdapSvc, error) {
options := newOptions(opts...)
s := LdapSvc{
log: options.Logger,
c: options.Config,
log: options.Logger,
c: options.Config,
monitor: options.Monitor,
tracer: options.Tracer,
}
var err error
@@ -39,7 +47,7 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
var helper handler.Handler
loh := handler.NewLDAPOpsHelper()
loh := handler.NewLDAPOpsHelper(s.tracer)
// instantiate the helper, if any
if s.c.Helper.Enabled {
@@ -50,6 +58,7 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
handler.Config(s.c),
handler.YubiAuth(s.yubiAuth),
handler.LDAPHelper(loh),
handler.Tracer(s.tracer),
)
case "plugin":
plug, err := plugin.Open(s.c.Helper.Plugin)
@@ -72,6 +81,7 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
handler.Config(s.c),
handler.YubiAuth(s.yubiAuth),
handler.LDAPHelper(loh),
handler.Tracer(s.tracer),
)
default:
return nil, fmt.Errorf("unsupported helper %s - must be one of 'config', 'plugin'", s.c.Helper.Datastore)
@@ -85,6 +95,12 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
// configure the backends
s.l = ldap.NewServer()
s.l.EnforceLDAP = true
if tlsConfig := options.TLSConfig; tlsConfig != nil {
s.l.TLSConfig = tlsConfig
s.log.Info().Interface("tls.certificates", tlsConfig.Certificates).Msg("enabling LDAP over TLS")
}
for i, backend := range s.c.Backends {
var h handler.Handler
switch backend.Datastore {
@@ -94,11 +110,15 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
handler.Handlers(allHandlers),
handler.Logger(&s.log),
handler.Helper(helper),
handler.Monitor(s.monitor),
handler.Tracer(s.tracer),
)
case "owncloud":
h = handler.NewOwnCloudHandler(
handler.Backend(backend),
handler.Logger(&s.log),
handler.Monitor(s.monitor),
handler.Tracer(s.tracer),
)
case "config":
h = handler.NewConfigHandler(
@@ -107,6 +127,8 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
handler.Config(s.c), // TODO only used to access Users and Groups, move that to dedicated options
handler.YubiAuth(s.yubiAuth),
handler.LDAPHelper(loh),
handler.Monitor(s.monitor),
handler.Tracer(s.tracer),
)
case "plugin":
plug, err := plugin.Open(backend.Plugin)
@@ -130,6 +152,8 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
handler.Config(s.c),
handler.YubiAuth(s.yubiAuth),
handler.LDAPHelper(loh),
handler.Monitor(s.monitor),
handler.Tracer(s.tracer),
)
default:
return nil, fmt.Errorf("unsupported backend %s - must be one of 'config', 'ldap','owncloud' or 'plugin'", backend.Datastore)
@@ -148,6 +172,8 @@ func NewServer(opts ...Option) (*LdapSvc, error) {
backendCounter++
}
monitoring.NewLDAPMonitorWatcher(s.l, s.monitor, &s.log)
return &s, nil
}
+13
View File
@@ -21,6 +21,9 @@ debug = true
enabled = true
# run on a non privileged port
listen = "0.0.0.0:3893"
tls = false # enable StartTLS support
tlsCertPath = "glauth.crt"
tlsKeyPath = "glauth.key"
[ldaps]
# to enable ldaps generate a certificate, eg. with:
@@ -31,6 +34,16 @@ debug = true
key = "glauth.key"
#################
# Tracing section controls the tracer configuration
[tracing]
# if enabled is set to false, a no-op tracer will be used
enabled = true
# if both grpcEndpoint and httpEndpoint are unset, the default stdout provider will be used
# TODO add allowGRPCInsecure: right now grpc otlp is using the WithInsecure flag so traffic
# will always go without verifying server certificates
# grpcEndpoint = "otlp.monitoring.io:4317"
# httpEndpoint = "http://otlp.monitoring.io:4318"
#################
# The backend section controls the data store.
[backend]
datastore = "config"
-13
View File
@@ -1,13 +0,0 @@
Vendored libraries. Not to be mistaken for libraries included under `vendor/` which is a go convention.
To upgrade toml:
1. clone from upstream
2. add to `decode_meta.go`:
```
func (md *MetaData) Mappings() map[string]interface{} {
return md.mapping
}
```
3. Run `gofmt -w ./` in `vendored/`
-3
View File
@@ -1,3 +0,0 @@
Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md)
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 TOML authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-19
View File
@@ -1,19 +0,0 @@
install:
go install ./...
test: install
go test -v
toml-test toml-test-decoder
toml-test -encoder toml-test-encoder
fmt:
gofmt -w *.go */*.go
colcheck *.go */*.go
tags:
find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS
push:
git push origin master
git push github master
-226
View File
@@ -1,226 +0,0 @@
[![Build Status](https://travis-ci.org/hydronica/toml.svg?branch=master)](https://travis-ci.org/hydronica/toml)
This is a modified and opinionated fork of https://github.com/BurntSushi/toml with the following changes
- time.Duration is always encoded as a string ("10s" or "10ns")
- support encoding comments (similar functionality to https://github.com/pelletier/go-toml.v1)
- comments can be added using the comment tag `comment:"a well thought-out comment"`
- valid toml can be commented out using the commented tag `commented:"true"`
- Only guarantees last two years of golang compatibility (1.10 to current)
## TOML parser and encoder for Go with reflection
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
reflection interface similar to Go's standard library `json` and `xml`
packages. This package also supports the `encoding.TextUnmarshaler` and
`encoding.TextMarshaler` interfaces so that you can define custom data
representations. (There is an example of this below.)
Spec: https://github.com/toml-lang/toml
Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
Documentation: https://godoc.org/github.com/BurntSushi/toml
Installation:
```bash
go get github.com/hydronica/toml
```
Try the toml validator:
```bash
go get github.com/hydronica/toml/cmd/tomlv
tomlv some-toml-file.toml
```
### Testing
This package passes all tests in
[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder
and the encoder.
### Examples
This package works similarly to how the Go standard library handles `XML`
and `JSON`. Namely, data is loaded into Go values via reflection.
For the simplest example, consider some TOML file as just a list of keys
and values:
```toml
Age = 25
Cats = [ "Cauchy", "Plato" ]
Pi = 3.14
Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z
```
Which could be defined in Go as:
```go
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time // requires `import time`
}
```
And then decoded with:
```go
var conf Config
if _, err := toml.Decode(tomlData, &conf); err != nil {
// handle error
}
```
You can also use struct tags if your struct field name doesn't map to a TOML
key value directly:
```toml
some_key_NAME = "wat"
```
```go
type TOML struct {
ObscureKey string `toml:"some_key_NAME"`
}
```
### Using the `encoding.TextUnmarshaler` interface
Here's an example that automatically parses duration strings into
`time.Duration` values:
```toml
[[song]]
name = "Thunder Road"
duration = "4m49s"
[[song]]
name = "Stairway to Heaven"
duration = "8m03s"
```
Which can be decoded with:
```go
type song struct {
Name string
Duration duration
}
type songs struct {
Song []song
}
var favorites songs
if _, err := toml.Decode(blob, &favorites); err != nil {
log.Fatal(err)
}
for _, s := range favorites.Song {
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}
```
And you'll also need a `duration` type that satisfies the
`encoding.TextUnmarshaler` interface:
```go
type duration struct {
time.Duration
}
func (d *duration) UnmarshalText(text []byte) error {
var err error
d.Duration, err = time.ParseDuration(string(text))
return err
}
```
### More complex usage
Here's an example of how to load the example from the official spec page:
```toml
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
```
And the corresponding Go types are:
```go
type tomlConfig struct {
Title string
Owner ownerInfo
DB database `toml:"database"`
Servers map[string]server
Clients clients
}
type ownerInfo struct {
Name string
Org string `toml:"organization"`
Bio string
DOB time.Time
}
type database struct {
Server string
Ports []int
ConnMax int `toml:"connection_max"`
Enabled bool
}
type server struct {
IP string
DC string
}
type clients struct {
Data [][]interface{}
Hosts []string
}
```
Note that a case insensitive match will be tried if an exact match can't be
found.
A working example of the above can be found in `_examples/example.{go,toml}`.
-61
View File
@@ -1,61 +0,0 @@
package main
import (
"fmt"
"time"
"github.com/hydronica/toml"
)
type tomlConfig struct {
Title string
Owner ownerInfo
DB database `toml:"database"`
Servers map[string]server
Clients clients
}
type ownerInfo struct {
Name string
Org string `toml:"organization"`
Bio string
DOB time.Time
}
type database struct {
Server string
Ports []int
ConnMax int `toml:"connection_max"`
Enabled bool
}
type server struct {
IP string
DC string
}
type clients struct {
Data [][]interface{}
Hosts []string
}
func main() {
var config tomlConfig
if _, err := toml.DecodeFile("example.toml", &config); err != nil {
fmt.Println(err)
return
}
fmt.Printf("Title: %s\n", config.Title)
fmt.Printf("Owner: %s (%s, %s), Born: %s\n",
config.Owner.Name, config.Owner.Org, config.Owner.Bio,
config.Owner.DOB)
fmt.Printf("Database: %s %v (Max conn. %d), Enabled? %v\n",
config.DB.Server, config.DB.Ports, config.DB.ConnMax,
config.DB.Enabled)
for serverName, server := range config.Servers {
fmt.Printf("Server: %s (%s, %s)\n", serverName, server.IP, server.DC)
}
fmt.Printf("Client data: %v\n", config.Clients.Data)
fmt.Printf("Client hosts: %v\n", config.Clients.Hosts)
}
-35
View File
@@ -1,35 +0,0 @@
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
-22
View File
@@ -1,22 +0,0 @@
# Test file for TOML
# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate
# This part you'll really hate
[the]
test_string = "You'll hate me after this - #" # " Annoying, isn't it?
[the.hard]
test_array = [ "] ", " # "] # ] There you go, parse this!
test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ]
# You didn't think it'd as easy as chucking out the last #, did you?
another_test_string = " Same thing, but with a string #"
harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too"
# Things will get harder
[the.hard.bit#]
what? = "You don't think some user won't do that?"
multi_line_array = [
"]",
# ] Oh yes I did
]
-4
View File
@@ -1,4 +0,0 @@
# [x] you
# [x.y] don't
# [x.y.z] need these
[x.y.z.w] # for this to work
@@ -1,6 +0,0 @@
# DO NOT WANT
[fruit]
type = "apple"
[fruit.type]
apple = "yes"
-35
View File
@@ -1,35 +0,0 @@
# This is an INVALID TOML document. Boom.
# Can you spot the error without help?
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T7:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
-5
View File
@@ -1,5 +0,0 @@
Age = 25
Cats = [ "Cauchy", "Plato" ]
Pi = 3.14
Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z
-1
View File
@@ -1 +0,0 @@
some_key_NAME = "wat"
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 TOML authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -1,13 +0,0 @@
# Implements the TOML test suite interface
This is an implementation of the interface expected by
[toml-test](https://github.com/BurntSushi/toml-test) for my
[toml parser written in Go](https://github.com/BurntSushi/toml).
In particular, it maps TOML data on `stdin` to a JSON format on `stdout`.
Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
Compatible with `toml-test` version
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
@@ -1,90 +0,0 @@
// Command toml-test-decoder satisfies the toml-test interface for testing
// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path"
"time"
"github.com/hydronica/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
log.Fatalf("Error decoding TOML: %s", err)
}
typedTmp := translate(tmp)
if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
}
func translate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = translate(v)
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v)
}
// We don't really need to tag arrays, but let's be future proof.
// (If TOML ever supports tuples, we'll need this.)
return tag("array", typed)
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
return tag("float", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 TOML authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -1,13 +0,0 @@
# Implements the TOML test suite interface for TOML encoders
This is an implementation of the interface expected by
[toml-test](https://github.com/BurntSushi/toml-test) for the
[TOML encoder](https://github.com/BurntSushi/toml).
In particular, it maps JSON data on `stdin` to a TOML format on `stdout`.
Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
Compatible with `toml-test` version
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
@@ -1,131 +0,0 @@
// Command toml-test-encoder satisfies the toml-test interface for testing
// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
package main
import (
"encoding/json"
"flag"
"log"
"os"
"path"
"strconv"
"time"
"github.com/hydronica/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
log.Fatalf("Error decoding JSON: %s", err)
}
tomlData := translate(tmp)
if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
log.Fatalf("Error encoding TOML: %s", err)
}
}
func translate(typedJson interface{}) interface{} {
switch v := typedJson.(type) {
case map[string]interface{}:
if len(v) == 2 && in("type", v) && in("value", v) {
return untag(v)
}
m := make(map[string]interface{}, len(v))
for k, v2 := range v {
m[k] = translate(v2)
}
return m
case []interface{}:
tabArray := make([]map[string]interface{}, len(v))
for i := range v {
if m, ok := translate(v[i]).(map[string]interface{}); ok {
tabArray[i] = m
} else {
log.Fatalf("JSON arrays may only contain objects. This " +
"corresponds to only tables being allowed in " +
"TOML table arrays.")
}
}
return tabArray
}
log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
panic("unreachable")
}
func untag(typed map[string]interface{}) interface{} {
t := typed["type"].(string)
v := typed["value"]
switch t {
case "string":
return v.(string)
case "integer":
v := v.(string)
n, err := strconv.Atoi(v)
if err != nil {
log.Fatalf("Could not parse '%s' as integer: %s", v, err)
}
return n
case "float":
v := v.(string)
f, err := strconv.ParseFloat(v, 64)
if err != nil {
log.Fatalf("Could not parse '%s' as float64: %s", v, err)
}
return f
case "datetime":
v := v.(string)
t, err := time.Parse("2006-01-02T15:04:05Z", v)
if err != nil {
log.Fatalf("Could not parse '%s' as a datetime: %s", v, err)
}
return t
case "bool":
v := v.(string)
switch v {
case "true":
return true
case "false":
return false
}
log.Fatalf("Could not parse '%s' as a boolean.", v)
case "array":
v := v.([]interface{})
array := make([]interface{}, len(v))
for i := range v {
if m, ok := v[i].(map[string]interface{}); ok {
array[i] = untag(m)
} else {
log.Fatalf("Arrays may only contain other arrays or "+
"primitive values, but found a '%T'.", m)
}
}
return array
}
log.Fatalf("Unrecognized tag type '%s'.", t)
panic("unreachable")
}
func in(key string, m map[string]interface{}) bool {
_, ok := m[key]
return ok
}
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 TOML authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-21
View File
@@ -1,21 +0,0 @@
# TOML Validator
If Go is installed, it's simple to try it out:
```bash
go get github.com/BurntSushi/toml/cmd/tomlv
tomlv some-toml-file.toml
```
You can see the types of every key in a TOML file with:
```bash
tomlv -types some-toml-file.toml
```
At the moment, only one error message is reported at a time. Error messages
include line numbers. No output means that the files given are valid TOML, or
there is a bug in `tomlv`.
Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
-61
View File
@@ -1,61 +0,0 @@
// Command tomlv validates TOML documents and prints each key's type.
package main
import (
"flag"
"fmt"
"log"
"os"
"path"
"strings"
"text/tabwriter"
"github.com/hydronica/toml"
)
var (
flagTypes = false
)
func init() {
log.SetFlags(0)
flag.BoolVar(&flagTypes, "types", flagTypes,
"When set, the types of every defined key will be shown.")
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s toml-file [ toml-file ... ]\n",
path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() < 1 {
flag.Usage()
}
for _, f := range flag.Args() {
var tmp interface{}
md, err := toml.DecodeFile(f, &tmp)
if err != nil {
log.Fatalf("Error in '%s': %s", f, err)
}
if flagTypes {
printTypes(md)
}
}
}
func printTypes(md toml.MetaData) {
tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for _, key := range md.Keys() {
fmt.Fprintf(tabw, "%s%s\t%s\n",
strings.Repeat(" ", len(key)-1), key, md.Type(key...))
}
tabw.Flush()
}
-519
View File
@@ -1,519 +0,0 @@
package toml
import (
"fmt"
"io"
"io/ioutil"
"math"
"reflect"
"strings"
"time"
)
func e(format string, args ...interface{}) error {
return fmt.Errorf("toml: "+format, args...)
}
// Unmarshaler is the interface implemented by objects that can unmarshal a
// TOML description of themselves.
type Unmarshaler interface {
UnmarshalTOML(interface{}) error
}
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
func Unmarshal(p []byte, v interface{}) error {
_, err := Decode(string(p), v)
return err
}
// Primitive is a TOML value that hasn't been decoded into a Go value.
// When using the various `Decode*` functions, the type `Primitive` may
// be given to any value, and its decoding will be delayed.
//
// A `Primitive` value can be decoded using the `PrimitiveDecode` function.
//
// The underlying representation of a `Primitive` value is subject to change.
// Do not rely on it.
//
// N.B. Primitive values are still parsed, so using them will only avoid
// the overhead of reflection. They can be useful when you don't know the
// exact type of TOML data until run time.
type Primitive struct {
undecoded interface{}
context Key
}
// DEPRECATED!
//
// Use MetaData.PrimitiveDecode instead.
func PrimitiveDecode(primValue Primitive, v interface{}) error {
md := MetaData{decoded: make(map[string]bool)}
return md.unify(primValue.undecoded, rvalue(v))
}
// PrimitiveDecode is just like the other `Decode*` functions, except it
// decodes a TOML value that has already been parsed. Valid primitive values
// can *only* be obtained from values filled by the decoder functions,
// including this method. (i.e., `v` may contain more `Primitive`
// values.)
//
// Meta data for primitive values is included in the meta data returned by
// the `Decode*` functions with one exception: keys returned by the Undecoded
// method will only reflect keys that were decoded. Namely, any keys hidden
// behind a Primitive will be considered undecoded. Executing this method will
// update the undecoded keys in the meta data. (See the example.)
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
md.context = primValue.context
defer func() { md.context = nil }()
return md.unify(primValue.undecoded, rvalue(v))
}
// Decode will decode the contents of `data` in TOML format into a pointer
// `v`.
//
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
// used interchangeably.)
//
// TOML arrays of tables correspond to either a slice of structs or a slice
// of maps.
//
// TOML datetimes correspond to Go `time.Time` values.
//
// All other TOML types (float, string, int, bool and array) correspond
// to the obvious Go types.
//
// An exception to the above rules is if a type implements the
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
// (floats, strings, integers, booleans and datetimes) will be converted to
// a byte string and given to the value's UnmarshalText method. See the
// Unmarshaler example for a demonstration with time duration strings.
//
// # Key mapping
//
// TOML keys can map to either keys in a Go map or field names in a Go
// struct. The special `toml` struct tag may be used to map TOML keys to
// struct fields that don't match the key name exactly. (See the example.)
// A case insensitive match to struct names will be tried if an exact match
// can't be found.
//
// The mapping between TOML values and Go values is loose. That is, there
// may exist TOML values that cannot be placed into your representation, and
// there may be parts of your representation that do not correspond to
// TOML values. This loose mapping can be made stricter by using the IsDefined
// and/or Undecoded methods on the MetaData returned.
//
// This decoder will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
func Decode(data string, v interface{}) (MetaData, error) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
}
if rv.IsNil() {
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
}
p, err := parse(data)
if err != nil {
return MetaData{}, err
}
md := MetaData{
p.mapping, p.types, p.ordered,
make(map[string]bool, len(p.ordered)), nil,
}
return md, md.unify(p.mapping, indirect(rv))
}
// DecodeFile is just like Decode, except it will automatically read the
// contents of the file at `fpath` and decode it for you.
func DecodeFile(fpath string, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadFile(fpath)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// DecodeReader is just like Decode, except it will consume all bytes
// from the reader and decode it for you.
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadAll(r)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// unify performs a sort of type unification based on the structure of `rv`,
// which is the client representation.
//
// Any type mismatch produces an error. Finding a type that we don't know
// how to handle produces an unsupported type error.
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
// Special case. Look for a `Primitive` value.
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
// Save the undecoded data and the key context into the primitive
// value.
context := make(Key, len(md.context))
copy(context, md.context)
rv.Set(reflect.ValueOf(Primitive{
undecoded: data,
context: context,
}))
return nil
}
// Special case. Unmarshaler Interface support.
if rv.CanAddr() {
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
return v.UnmarshalTOML(data)
}
}
if rv.Type() == reflect.TypeOf(time.Second) {
return md.unifyDuration(data, rv)
}
// Special case. Look for a value satisfying the TextUnmarshaler interface.
if v, ok := rv.Interface().(TextUnmarshaler); ok {
return md.unifyText(data, v)
}
// BUG(burntsushi)
// The behavior here is incorrect whenever a Go type satisfies the
// encoding.TextUnmarshaler interface but also corresponds to a TOML
// hash or array. In particular, the unmarshaler should only be applied
// to primitive TOML values. But at this point, it will be applied to
// all kinds of values and produce an incorrect error whenever those values
// are hashes or arrays (including arrays of tables).
k := rv.Kind()
// laziness
if k >= reflect.Int && k <= reflect.Uint64 {
return md.unifyInt(data, rv)
}
switch k {
case reflect.Ptr:
elem := reflect.New(rv.Type().Elem())
err := md.unify(data, reflect.Indirect(elem))
if err != nil {
return err
}
rv.Set(elem)
return nil
case reflect.Struct:
return md.unifyStruct(data, rv)
case reflect.Map:
return md.unifyMap(data, rv)
case reflect.Array:
return md.unifyArray(data, rv)
case reflect.Slice:
return md.unifySlice(data, rv)
case reflect.String:
return md.unifyString(data, rv)
case reflect.Bool:
return md.unifyBool(data, rv)
case reflect.Interface:
// we only support empty interfaces.
if rv.NumMethod() > 0 {
return e("unsupported type %s", rv.Type())
}
return md.unifyAnything(data, rv)
case reflect.Float32:
fallthrough
case reflect.Float64:
return md.unifyFloat64(data, rv)
}
return e("unsupported type %s", rv.Kind())
}
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
if mapping == nil {
return nil
}
return e("type mismatch for %s: expected table but found %T",
rv.Type().String(), mapping)
}
for key, datum := range tmap {
var f *field
fields := cachedTypeFields(rv.Type())
for i := range fields {
ff := &fields[i]
if ff.name == key {
f = ff
break
}
if f == nil && strings.EqualFold(ff.name, key) {
f = ff
}
}
if f != nil {
subv := rv
for _, i := range f.index {
subv = indirect(subv.Field(i))
}
if isUnifiable(subv) {
md.decoded[md.context.add(key).String()] = true
md.context = append(md.context, key)
if err := md.unify(datum, subv); err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
} else if f.name != "" {
// Bad user! No soup for you!
return e("cannot write unexported field %s.%s",
rv.Type().String(), f.name)
}
}
}
return nil
}
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
if tmap == nil {
return nil
}
return badtype("map", mapping)
}
if rv.IsNil() {
rv.Set(reflect.MakeMap(rv.Type()))
}
for k, v := range tmap {
md.decoded[md.context.add(k).String()] = true
md.context = append(md.context, k)
rvkey := indirect(reflect.New(rv.Type().Key()))
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
if err := md.unify(v, rvval); err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
rvkey.SetString(k)
rv.SetMapIndex(rvkey, rvval)
}
return nil
}
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
sliceLen := datav.Len()
if sliceLen != rv.Len() {
return e("expected array length %d; got TOML array of length %d",
rv.Len(), sliceLen)
}
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
n := datav.Len()
if rv.IsNil() || rv.Cap() < n {
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
}
rv.SetLen(n)
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
sliceLen := data.Len()
for i := 0; i < sliceLen; i++ {
v := data.Index(i).Interface()
sliceval := indirect(rv.Index(i))
if err := md.unify(v, sliceval); err != nil {
return err
}
}
return nil
}
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
if _, ok := data.(time.Time); ok {
rv.Set(reflect.ValueOf(data))
return nil
}
return badtype("time.Time", data)
}
func (md *MetaData) unifyDuration(data interface{}, rv reflect.Value) error {
switch v := data.(type) {
case string:
d, err := time.ParseDuration(v)
if err != nil {
return e("invalid time.duration format %s", v)
}
rv.SetInt(int64(d))
return nil
case int, int64, int32, int16, int8:
return md.unifyInt(data, rv)
}
return badtype("time.duration", data)
}
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
if s, ok := data.(string); ok {
rv.SetString(s)
return nil
}
return badtype("string", data)
}
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
if num, ok := data.(float64); ok {
switch rv.Kind() {
case reflect.Float32:
fallthrough
case reflect.Float64:
rv.SetFloat(num)
default:
panic("bug")
}
return nil
}
return badtype("float", data)
}
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
if num, ok := data.(int64); ok {
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
switch rv.Kind() {
case reflect.Int, reflect.Int64:
// No bounds checking necessary.
case reflect.Int8:
if num < math.MinInt8 || num > math.MaxInt8 {
return e("value %d is out of range for int8", num)
}
case reflect.Int16:
if num < math.MinInt16 || num > math.MaxInt16 {
return e("value %d is out of range for int16", num)
}
case reflect.Int32:
if num < math.MinInt32 || num > math.MaxInt32 {
return e("value %d is out of range for int32", num)
}
}
rv.SetInt(num)
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
unum := uint64(num)
switch rv.Kind() {
case reflect.Uint, reflect.Uint64:
// No bounds checking necessary.
case reflect.Uint8:
if num < 0 || unum > math.MaxUint8 {
return e("value %d is out of range for uint8", num)
}
case reflect.Uint16:
if num < 0 || unum > math.MaxUint16 {
return e("value %d is out of range for uint16", num)
}
case reflect.Uint32:
if num < 0 || unum > math.MaxUint32 {
return e("value %d is out of range for uint32", num)
}
}
rv.SetUint(unum)
} else {
panic("unreachable")
}
return nil
}
return badtype("integer", data)
}
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
if b, ok := data.(bool); ok {
rv.SetBool(b)
return nil
}
return badtype("boolean", data)
}
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
rv.Set(reflect.ValueOf(data))
return nil
}
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
var s string
switch sdata := data.(type) {
case TextMarshaler:
text, err := sdata.MarshalText()
if err != nil {
return err
}
s = string(text)
case fmt.Stringer:
s = sdata.String()
case string:
s = sdata
case bool:
s = fmt.Sprintf("%v", sdata)
case int64:
s = fmt.Sprintf("%d", sdata)
case float64:
s = fmt.Sprintf("%f", sdata)
default:
return badtype("primitive (string-like)", data)
}
if err := v.UnmarshalText([]byte(s)); err != nil {
return err
}
return nil
}
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
func rvalue(v interface{}) reflect.Value {
return indirect(reflect.ValueOf(v))
}
// indirect returns the value pointed to by a pointer.
// Pointers are followed until the value is not a pointer.
// New values are allocated for each nil pointer.
//
// An exception to this rule is if the value satisfies an interface of
// interest to us (like encoding.TextUnmarshaler).
func indirect(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Ptr {
if v.CanSet() {
pv := v.Addr()
if _, ok := pv.Interface().(TextUnmarshaler); ok {
return pv
}
}
return v
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
return indirect(reflect.Indirect(v))
}
func isUnifiable(rv reflect.Value) bool {
if rv.CanSet() {
return true
}
if _, ok := rv.Interface().(TextUnmarshaler); ok {
return true
}
return false
}
func badtype(expected string, data interface{}) error {
return e("cannot load TOML value of type %T into a Go %s", data, expected)
}
-125
View File
@@ -1,125 +0,0 @@
package toml
import "strings"
// MetaData allows access to meta information about TOML data that may not
// be inferrable via reflection. In particular, whether a key has been defined
// and the TOML type of a key.
type MetaData struct {
mapping map[string]interface{}
types map[string]tomlType
keys []Key
decoded map[string]bool
context Key // Used only during decoding.
}
// IsDefined returns true if the key given exists in the TOML data. The key
// should be specified hierarchially. e.g.,
//
// // access the TOML key 'a.b.c'
// IsDefined("a", "b", "c")
//
// IsDefined will return false if an empty key given. Keys are case sensitive.
func (md *MetaData) IsDefined(key ...string) bool {
if len(key) == 0 {
return false
}
var hash map[string]interface{}
var ok bool
var hashOrVal interface{} = md.mapping
for _, k := range key {
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
return false
}
if hashOrVal, ok = hash[k]; !ok {
return false
}
}
return true
}
// Type returns a string representation of the type of the key specified.
//
// Type will return the empty string if given an empty key or a key that
// does not exist. Keys are case sensitive.
func (md *MetaData) Type(key ...string) string {
fullkey := strings.Join(key, ".")
if typ, ok := md.types[fullkey]; ok {
return typ.typeString()
}
return ""
}
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys
// to get values of this type.
type Key []string
func (k Key) String() string {
return strings.Join(k, ".")
}
func (k Key) maybeQuotedAll() string {
var ss []string
for i := range k {
ss = append(ss, k.maybeQuoted(i))
}
return strings.Join(ss, ".")
}
func (k Key) maybeQuoted(i int) string {
quote := false
for _, c := range k[i] {
if !isBareKeyChar(c) {
quote = true
break
}
}
if quote {
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
}
return k[i]
}
func (k Key) add(piece string) Key {
newKey := make(Key, len(k)+1)
copy(newKey, k)
newKey[len(k)] = piece
return newKey
}
// Keys returns a slice of every key in the TOML data, including key groups.
// Each key is itself a slice, where the first element is the top of the
// hierarchy and the last is the most specific.
//
// The list will have the same order as the keys appeared in the TOML data.
//
// All keys returned are non-empty.
func (md *MetaData) Keys() []Key {
return md.keys
}
// Undecoded returns all keys that have not been decoded in the order in which
// they appear in the original TOML document.
//
// This includes keys that haven't been decoded because of a Primitive value.
// Once the Primitive value is decoded, the keys will be considered decoded.
//
// Also note that decoding into an empty interface will result in no decoding,
// and so no keys will be considered decoded.
//
// In this sense, the Undecoded keys correspond to keys in the TOML document
// that do not have a concrete type in your representation.
func (md *MetaData) Undecoded() []Key {
undecoded := make([]Key, 0, len(md.keys))
for _, key := range md.keys {
if !md.decoded[key.String()] {
undecoded = append(undecoded, key)
}
}
return undecoded
}
func (md *MetaData) Mappings() map[string]interface{} {
return md.mapping
}
File diff suppressed because it is too large Load Diff
-27
View File
@@ -1,27 +0,0 @@
/*
Package toml provides facilities for decoding and encoding TOML configuration
files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the
MetaData type.
The specification implemented: https://github.com/toml-lang/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the
type of each key in a TOML document.
# Testing
There are two important types of tests used for this package. The first is
contained inside '*_test.go' files and uses the standard Go unit testing
framework. These tests are primarily devoted to holistically testing the
decoder and encoder.
The second type of testing is used to verify the implementation's adherence
to the TOML specification. These tests have been factored into their own
project: https://github.com/BurntSushi/toml-test
The reason the tests are in a separate project is so that they can be used by
any implementation of TOML. Namely, it is language agnostic.
*/
package toml
-605
View File
@@ -1,605 +0,0 @@
package toml
import (
"bufio"
"errors"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
type tomlEncodeError struct{ error }
var (
errArrayMixedElementTypes = errors.New(
"toml: cannot encode array with mixed element types")
errArrayNilElement = errors.New(
"toml: cannot encode array with nil element")
errNonString = errors.New(
"toml: cannot encode a map with non-string key type")
errAnonNonStruct = errors.New(
"toml: cannot encode an anonymous field that is not a struct")
errArrayNoTable = errors.New(
"toml: TOML array element cannot contain a table")
errNoKey = errors.New(
"toml: top-level values must be Go maps or structs")
errAnything = errors.New("") // used in testing
)
var quotedReplacer = strings.NewReplacer(
"\t", "\\t",
"\n", "\\n",
"\r", "\\r",
"\"", "\\\"",
"\\", "\\\\",
)
const (
// tagComment generates a descriptive comment above an object
tagComment = "comment"
// tagCommented will generate the corresponding comment file, but it will be commented out
tagCommented = "commented"
)
// Encoder controls the encoding of Go values to a TOML document to some
// io.Writer.
//
// The indentation level can be controlled with the Indent field.
type Encoder struct {
// A single indentation level. By default it is two spaces.
Indent string
// indicates in the line should be generated commented out
commented bool
// hasWritten is whether we have written any output to w yet.
hasWritten bool
w *bufio.Writer
}
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
// given. By default, a single indentation level is 2 spaces.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: bufio.NewWriter(w),
Indent: " ",
}
}
// Encode writes a TOML representation of the Go value to the underlying
// io.Writer. If the value given cannot be encoded to a valid TOML document,
// then an error is returned.
//
// The mapping between Go values and TOML values should be precisely the same
// as for the Decode* functions. Similarly, the TextMarshaler interface is
// supported by encoding the resulting bytes as strings. (If you want to write
// arbitrary binary data then you will need to use something like base64 since
// TOML does not have any binary types.)
//
// When encoding TOML hashes (i.e., Go maps or structs), keys without any
// sub-hashes are encoded first.
//
// If a Go map is encoded, then its keys are sorted alphabetically for
// deterministic output. More control over this behavior may be provided if
// there is demand for it.
//
// Encoding Go values without a corresponding TOML representation---like map
// types with non-string keys---will cause an error to be returned. Similarly
// for mixed arrays/slices, arrays/slices with nil elements, embedded
// non-struct types and nested slices containing maps or structs.
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK
// and so is []map[string][]string.)
func (enc *Encoder) Encode(v interface{}) error {
rv := eindirect(reflect.ValueOf(v))
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
return err
}
return enc.w.Flush()
}
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
defer func() {
if r := recover(); r != nil {
if terr, ok := r.(tomlEncodeError); ok {
err = terr.error
return
}
panic(r)
}
}()
enc.encode(key, rv)
return nil
}
func (enc *Encoder) encode(key Key, rv reflect.Value) {
// Special case. Time needs to be in ISO8601 format.
// Special case. If we can marshal the type to text, then we used that.
// Basically, this prevents the encoder for handling these types as
// generic structs (or whatever the underlying type of a TextMarshaler is).
switch rv.Interface().(type) {
case time.Time, TextMarshaler, time.Duration:
enc.keyEqElement(key, rv)
return
}
k := rv.Kind()
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
enc.keyEqElement(key, rv)
case reflect.Array, reflect.Slice:
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
enc.eArrayOfTables(key, rv)
} else {
enc.keyEqElement(key, rv)
}
case reflect.Interface:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Map:
if rv.IsNil() {
return
}
enc.eTable(key, rv)
case reflect.Ptr:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Struct:
enc.eTable(key, rv)
default:
panic(e("unsupported type for key '%s': %s", key, k))
}
}
// eElement encodes any value that can be an array element (primitives and
// arrays).
func (enc *Encoder) eElement(rv reflect.Value) {
switch v := rv.Interface().(type) {
case time.Time:
// Special case time.Time as a primitive. Has to come before
// TextMarshaler below because time.Time implements
// encoding.TextMarshaler, but we need to always use UTC.
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
return
case TextMarshaler:
// Special case. Use text marshaler if it's available for this value.
if s, err := v.MarshalText(); err != nil {
encPanic(err)
} else {
enc.writeQuoted(string(s))
}
return
case time.Duration:
s := v.String()
enc.writeQuoted(s)
return
}
switch rv.Kind() {
case reflect.Bool:
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
case reflect.Float64:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
case reflect.Interface:
enc.eElement(rv.Elem())
case reflect.String:
enc.writeQuoted(rv.String())
default:
panic(e("unexpected primitive type: %s", rv.Kind()))
}
}
// By the TOML spec, all floats must have a decimal with at least one
// number on either side.
func floatAddDecimal(fstr string) string {
if !strings.Contains(fstr, ".") {
return fstr + ".0"
}
return fstr
}
func (enc *Encoder) writeQuoted(s string) {
enc.wf("\"%s\"", quotedReplacer.Replace(s))
}
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len()
enc.wf("[")
for i := 0; i < length; i++ {
elem := rv.Index(i)
enc.eElement(elem)
if i != length-1 {
enc.wf(", ")
}
}
enc.wf("]")
}
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
for i := 0; i < rv.Len(); i++ {
trv := rv.Index(i)
if isNil(trv) {
continue
}
panicIfInvalidKey(key)
enc.newline()
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
enc.eMapOrStruct(key, trv)
}
}
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key)
if len(key) == 1 {
// Output an extra newline between top-level tables.
// (The newline isn't written if nothing else has been written though.)
enc.newline()
}
format := "%s[%s]"
if enc.commented {
format = "%s#[%s]"
}
if len(key) > 0 {
enc.wf(format, enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
}
enc.eMapOrStruct(key, rv)
}
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
switch rv := eindirect(rv); rv.Kind() {
case reflect.Map:
enc.eMap(key, rv)
case reflect.Struct:
enc.eStruct(key, rv)
default:
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
}
}
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
rt := rv.Type()
if rt.Key().Kind() != reflect.String {
encPanic(errNonString)
}
// Sort keys so that we have deterministic output. And write keys directly
// underneath this key first, before writing sub-structs or sub-maps.
var mapKeysDirect, mapKeysSub []string
for _, mapKey := range rv.MapKeys() {
k := mapKey.String()
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
mapKeysSub = append(mapKeysSub, k)
} else {
mapKeysDirect = append(mapKeysDirect, k)
}
}
var writeMapKeys = func(mapKeys []string) {
sort.Strings(mapKeys)
for _, mapKey := range mapKeys {
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
if isNil(mrv) {
// Don't write anything for nil fields.
continue
}
enc.encode(key.add(mapKey), mrv)
}
}
writeMapKeys(mapKeysDirect)
writeMapKeys(mapKeysSub)
}
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
// Write keys for fields directly under this key first, because if we write
// a field that creates a new table, then all keys under it will be in that
// table (not the one we're writing here).
rt := rv.Type()
var fieldsDirect, fieldsSub [][]int
var addFields func(rt reflect.Type, rv reflect.Value, start []int)
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
// skip unexported fields
if f.PkgPath != "" && !f.Anonymous {
continue
}
frv := rv.Field(i)
if f.Anonymous {
t := f.Type
switch t.Kind() {
case reflect.Struct:
// Treat anonymous struct fields with
// tag names as though they are not
// anonymous, like encoding/json does.
if getOptions(f.Tag).name == "" {
addFields(t, frv, f.Index)
continue
}
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct &&
getOptions(f.Tag).name == "" {
if !frv.IsNil() {
addFields(t.Elem(), frv.Elem(), f.Index)
}
continue
}
// Fall through to the normal field encoding logic below
// for non-struct anonymous fields.
}
}
if typeIsHash(tomlTypeOfGo(frv)) {
fieldsSub = append(fieldsSub, append(start, f.Index...))
} else {
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
}
}
}
addFields(rt, rv, nil)
var writeFields = func(fields [][]int) {
for _, fieldIndex := range fields {
sft := rt.FieldByIndex(fieldIndex)
sf := rv.FieldByIndex(fieldIndex)
if isNil(sf) {
// Don't write anything for nil fields.
continue
}
opts := getOptions(sft.Tag)
if opts.skip {
continue
}
keyName := sft.Name
if opts.name != "" {
keyName = opts.name
}
if opts.omitempty && isEmpty(sf) {
continue
}
if opts.omitzero && isZero(sf) {
continue
}
if s := sft.Tag.Get(tagCommented); s == "true" {
enc.commented = true
enc.addComment(key.add(keyName), sft.Tag)
enc.encode(key.add(keyName), sf)
enc.commented = false
continue
}
enc.addComment(key.add(keyName), sft.Tag)
enc.encode(key.add(keyName), sf)
}
}
writeFields(fieldsDirect)
writeFields(fieldsSub)
}
func (enc *Encoder) addComment(key Key, tag reflect.StructTag) {
if s := tag.Get(tagComment); s != "" {
enc.wf("%s# %s\n", enc.indentStr(key), s)
enc.hasWritten = false
}
}
// tomlTypeName returns the TOML type name of the Go value's type. It is
// used to determine whether the types of array elements are mixed (which is
// forbidden). If the Go value is nil, then it is illegal for it to be an array
// element, and valueIsNil is returned as true.
// Returns the TOML type of a Go value. The type may be `nil`, which means
// no concrete TOML type could be found.
func tomlTypeOfGo(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() {
return nil
}
switch rv.Kind() {
case reflect.Bool:
return tomlBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
return tomlInteger
case reflect.Float32, reflect.Float64:
return tomlFloat
case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) {
return tomlArrayHash
}
return tomlArray
case reflect.Ptr, reflect.Interface:
return tomlTypeOfGo(rv.Elem())
case reflect.String:
return tomlString
case reflect.Map:
return tomlHash
case reflect.Struct:
switch rv.Interface().(type) {
case time.Time:
return tomlDatetime
case TextMarshaler:
return tomlString
default:
return tomlHash
}
default:
panic("unexpected reflect.Kind: " + rv.Kind().String())
}
}
// tomlArrayType returns the element type of a TOML array. The type returned
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
// slize). This function may also panic if it finds a type that cannot be
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
// nested arrays of tables).
func tomlArrayType(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
return nil
}
firstType := tomlTypeOfGo(rv.Index(0))
if firstType == nil {
encPanic(errArrayNilElement)
}
rvlen := rv.Len()
for i := 1; i < rvlen; i++ {
elem := rv.Index(i)
switch elemType := tomlTypeOfGo(elem); {
case elemType == nil:
encPanic(errArrayNilElement)
case !typeEqual(firstType, elemType):
encPanic(errArrayMixedElementTypes)
}
}
// If we have a nested array, then we must make sure that the nested
// array contains ONLY primitives.
// This checks arbitrarily nested arrays.
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
nest := tomlArrayType(eindirect(rv.Index(0)))
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
encPanic(errArrayNoTable)
}
}
return firstType
}
type tagOptions struct {
skip bool // "-"
name string
omitempty bool
omitzero bool
}
func getOptions(tag reflect.StructTag) tagOptions {
t := tag.Get("toml")
if t == "-" {
return tagOptions{skip: true}
}
var opts tagOptions
parts := strings.Split(t, ",")
opts.name = parts[0]
for _, s := range parts[1:] {
switch s {
case "omitempty":
opts.omitempty = true
case "omitzero":
opts.omitzero = true
}
}
return opts
}
func isZero(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rv.Uint() == 0
case reflect.Float32, reflect.Float64:
return rv.Float() == 0.0
}
return false
}
func isEmpty(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len() == 0
case reflect.Bool:
return !rv.Bool()
}
return false
}
func (enc *Encoder) newline() {
if enc.hasWritten {
enc.wf("\n")
}
}
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key)
format := "%s%s = "
if enc.commented {
format = "%s#%s = "
}
enc.wf(format, enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
enc.newline()
}
func (enc *Encoder) wf(format string, v ...interface{}) {
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
encPanic(err)
}
enc.hasWritten = true
}
func (enc *Encoder) indentStr(key Key) string {
return strings.Repeat(enc.Indent, len(key)-1)
}
func encPanic(err error) {
panic(tomlEncodeError{err})
}
func eindirect(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
return eindirect(v.Elem())
default:
return v
}
}
func isNil(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
func panicIfInvalidKey(key Key) {
for _, k := range key {
if len(k) == 0 {
encPanic(e("Key '%s' is not a valid table name. Key names "+
"cannot be empty.", key.maybeQuotedAll()))
}
}
}
func isValidKeyName(s string) bool {
return len(s) != 0
}
-697
View File
@@ -1,697 +0,0 @@
package toml
import (
"bytes"
"fmt"
"log"
"net"
"testing"
"time"
)
func TestEncodeRoundTrip(t *testing.T) {
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time
Ipaddress net.IP
}
var inputs = Config{
13,
[]string{"one", "two", "three"},
3.145,
[]int{11, 2, 3, 4},
time.Now(),
net.ParseIP("192.168.59.254"),
}
var firstBuffer bytes.Buffer
e := NewEncoder(&firstBuffer)
err := e.Encode(inputs)
if err != nil {
t.Fatal(err)
}
var outputs Config
if _, err := Decode(firstBuffer.String(), &outputs); err != nil {
t.Logf("Could not decode:\n-----\n%s\n-----\n",
firstBuffer.String())
t.Fatal(err)
}
// could test each value individually, but I'm lazy
var secondBuffer bytes.Buffer
e2 := NewEncoder(&secondBuffer)
err = e2.Encode(outputs)
if err != nil {
t.Fatal(err)
}
if firstBuffer.String() != secondBuffer.String() {
t.Error(
firstBuffer.String(),
"\n\n is not identical to\n\n",
secondBuffer.String())
}
}
// XXX(burntsushi)
// I think these tests probably should be removed. They are good, but they
// ought to be obsolete by toml-test.
func TestEncode(t *testing.T) {
type Embedded struct {
Int int `toml:"_int"`
}
type NonStruct int
date := time.Date(2014, 5, 11, 20, 30, 40, 0, time.FixedZone("IST", 3600))
dateStr := "2014-05-11T19:30:40Z"
tests := map[string]struct {
input interface{}
wantOutput string
wantError error
}{
"bool field": {
input: struct {
BoolTrue bool
BoolFalse bool
}{true, false},
wantOutput: "BoolTrue = true\nBoolFalse = false\n",
},
"int fields": {
input: struct {
Int int
Int8 int8
Int16 int16
Int32 int32
Int64 int64
}{1, 2, 3, 4, 5},
wantOutput: "Int = 1\nInt8 = 2\nInt16 = 3\nInt32 = 4\nInt64 = 5\n",
},
"uint fields": {
input: struct {
Uint uint
Uint8 uint8
Uint16 uint16
Uint32 uint32
Uint64 uint64
}{1, 2, 3, 4, 5},
wantOutput: "Uint = 1\nUint8 = 2\nUint16 = 3\nUint32 = 4" +
"\nUint64 = 5\n",
},
"float fields": {
input: struct {
Float32 float32
Float64 float64
}{1.5, 2.5},
wantOutput: "Float32 = 1.5\nFloat64 = 2.5\n",
},
"string field": {
input: struct{ String string }{"foo"},
wantOutput: "String = \"foo\"\n",
},
"string field and unexported field": {
input: struct {
String string
unexported int
}{"foo", 0},
wantOutput: "String = \"foo\"\n",
},
"datetime field in UTC": {
input: struct{ Date time.Time }{date},
wantOutput: fmt.Sprintf("Date = %s\n", dateStr),
},
"datetime field as primitive": {
// Using a map here to fail if isStructOrMap() returns true for
// time.Time.
input: map[string]interface{}{
"Date": date,
"Int": 1,
},
wantOutput: fmt.Sprintf("Date = %s\nInt = 1\n", dateStr),
},
"array fields": {
input: struct {
IntArray0 [0]int
IntArray3 [3]int
}{[0]int{}, [3]int{1, 2, 3}},
wantOutput: "IntArray0 = []\nIntArray3 = [1, 2, 3]\n",
},
"slice fields": {
input: struct{ IntSliceNil, IntSlice0, IntSlice3 []int }{
nil, []int{}, []int{1, 2, 3},
},
wantOutput: "IntSlice0 = []\nIntSlice3 = [1, 2, 3]\n",
},
"datetime slices": {
input: struct{ DatetimeSlice []time.Time }{
[]time.Time{date, date},
},
wantOutput: fmt.Sprintf("DatetimeSlice = [%s, %s]\n",
dateStr, dateStr),
},
"nested arrays and slices": {
input: struct {
SliceOfArrays [][2]int
ArrayOfSlices [2][]int
SliceOfArraysOfSlices [][2][]int
ArrayOfSlicesOfArrays [2][][2]int
SliceOfMixedArrays [][2]interface{}
ArrayOfMixedSlices [2][]interface{}
}{
[][2]int{{1, 2}, {3, 4}},
[2][]int{{1, 2}, {3, 4}},
[][2][]int{
{
{1, 2}, {3, 4},
},
{
{5, 6}, {7, 8},
},
},
[2][][2]int{
{
{1, 2}, {3, 4},
},
{
{5, 6}, {7, 8},
},
},
[][2]interface{}{
{1, 2}, {"a", "b"},
},
[2][]interface{}{
{1, 2}, {"a", "b"},
},
},
wantOutput: `SliceOfArrays = [[1, 2], [3, 4]]
ArrayOfSlices = [[1, 2], [3, 4]]
SliceOfArraysOfSlices = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
ArrayOfSlicesOfArrays = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
SliceOfMixedArrays = [[1, 2], ["a", "b"]]
ArrayOfMixedSlices = [[1, 2], ["a", "b"]]
`,
},
"empty slice": {
input: struct{ Empty []interface{} }{[]interface{}{}},
wantOutput: "Empty = []\n",
},
"(error) slice with element type mismatch (string and integer)": {
input: struct{ Mixed []interface{} }{[]interface{}{1, "a"}},
wantError: errArrayMixedElementTypes,
},
"(error) slice with element type mismatch (integer and float)": {
input: struct{ Mixed []interface{} }{[]interface{}{1, 2.5}},
wantError: errArrayMixedElementTypes,
},
"slice with elems of differing Go types, same TOML types": {
input: struct {
MixedInts []interface{}
MixedFloats []interface{}
}{
[]interface{}{
int(1), int8(2), int16(3), int32(4), int64(5),
uint(1), uint8(2), uint16(3), uint32(4), uint64(5),
},
[]interface{}{float32(1.5), float64(2.5)},
},
wantOutput: "MixedInts = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n" +
"MixedFloats = [1.5, 2.5]\n",
},
"(error) slice w/ element type mismatch (one is nested array)": {
input: struct{ Mixed []interface{} }{
[]interface{}{1, []interface{}{2}},
},
wantError: errArrayMixedElementTypes,
},
"(error) slice with 1 nil element": {
input: struct{ NilElement1 []interface{} }{[]interface{}{nil}},
wantError: errArrayNilElement,
},
"(error) slice with 1 nil element (and other non-nil elements)": {
input: struct{ NilElement []interface{} }{
[]interface{}{1, nil},
},
wantError: errArrayNilElement,
},
"simple map": {
input: map[string]int{"a": 1, "b": 2},
wantOutput: "a = 1\nb = 2\n",
},
"map with interface{} value type": {
input: map[string]interface{}{"a": 1, "b": "c"},
wantOutput: "a = 1\nb = \"c\"\n",
},
"map with interface{} value type, some of which are structs": {
input: map[string]interface{}{
"a": struct{ Int int }{2},
"b": 1,
},
wantOutput: "b = 1\n\n[a]\n Int = 2\n",
},
"nested map": {
input: map[string]map[string]int{
"a": {"b": 1},
"c": {"d": 2},
},
wantOutput: "[a]\n b = 1\n\n[c]\n d = 2\n",
},
"nested struct": {
input: struct{ Struct struct{ Int int } }{
struct{ Int int }{1},
},
wantOutput: "[Struct]\n Int = 1\n",
},
"nested struct and non-struct field": {
input: struct {
Struct struct{ Int int }
Bool bool
}{struct{ Int int }{1}, true},
wantOutput: "Bool = true\n\n[Struct]\n Int = 1\n",
},
"2 nested structs": {
input: struct{ Struct1, Struct2 struct{ Int int } }{
struct{ Int int }{1}, struct{ Int int }{2},
},
wantOutput: "[Struct1]\n Int = 1\n\n[Struct2]\n Int = 2\n",
},
"deeply nested structs": {
input: struct {
Struct1, Struct2 struct{ Struct3 *struct{ Int int } }
}{
struct{ Struct3 *struct{ Int int } }{&struct{ Int int }{1}},
struct{ Struct3 *struct{ Int int } }{nil},
},
wantOutput: "[Struct1]\n [Struct1.Struct3]\n Int = 1" +
"\n\n[Struct2]\n",
},
"nested struct with nil struct elem": {
input: struct {
Struct struct{ Inner *struct{ Int int } }
}{
struct{ Inner *struct{ Int int } }{nil},
},
wantOutput: "[Struct]\n",
},
"nested struct with no fields": {
input: struct {
Struct struct{ Inner struct{} }
}{
struct{ Inner struct{} }{struct{}{}},
},
wantOutput: "[Struct]\n [Struct.Inner]\n",
},
"struct with tags": {
input: struct {
Struct struct {
Int int `toml:"_int"`
} `toml:"_struct"`
Bool bool `toml:"_bool"`
}{
struct {
Int int `toml:"_int"`
}{1}, true,
},
wantOutput: "_bool = true\n\n[_struct]\n _int = 1\n",
},
"embedded struct": {
input: struct{ Embedded }{Embedded{1}},
wantOutput: "_int = 1\n",
},
"embedded *struct": {
input: struct{ *Embedded }{&Embedded{1}},
wantOutput: "_int = 1\n",
},
"nested embedded struct": {
input: struct {
Struct struct{ Embedded } `toml:"_struct"`
}{struct{ Embedded }{Embedded{1}}},
wantOutput: "[_struct]\n _int = 1\n",
},
"nested embedded *struct": {
input: struct {
Struct struct{ *Embedded } `toml:"_struct"`
}{struct{ *Embedded }{&Embedded{1}}},
wantOutput: "[_struct]\n _int = 1\n",
},
"embedded non-struct": {
input: struct{ NonStruct }{5},
wantOutput: "NonStruct = 5\n",
},
"array of tables": {
input: struct {
Structs []*struct{ Int int } `toml:"struct"`
}{
[]*struct{ Int int }{{1}, {3}},
},
wantOutput: "[[struct]]\n Int = 1\n\n[[struct]]\n Int = 3\n",
},
"array of tables order": {
input: map[string]interface{}{
"map": map[string]interface{}{
"zero": 5,
"arr": []map[string]int{
{
"friend": 5,
},
},
},
},
wantOutput: "[map]\n zero = 5\n\n [[map.arr]]\n friend = 5\n",
},
"(error) top-level slice": {
input: []struct{ Int int }{{1}, {2}, {3}},
wantError: errNoKey,
},
"(error) slice of slice": {
input: struct {
Slices [][]struct{ Int int }
}{
[][]struct{ Int int }{{{1}}, {{2}}, {{3}}},
},
wantError: errArrayNoTable,
},
"(error) map no string key": {
input: map[int]string{1: ""},
wantError: errNonString,
},
"(error) empty key name": {
input: map[string]int{"": 1},
wantError: errAnything,
},
"(error) empty map name": {
input: map[string]interface{}{
"": map[string]int{"v": 1},
},
wantError: errAnything,
},
"simple fields with comments": {
input: struct {
Int1 int `comment:"an integer"`
Float float64 `comment:"a float"`
Bool bool `comment:"a bool"`
}{0, 0.0, false},
wantOutput: "# an integer\nInt1 = 0\n# a float\nFloat = 0.0\n# a bool\nBool = false\n",
},
"embedded struct with comments": {
input: struct {
Parent struct {
Name string `comment:"my name"`
Value int `comment:"my value"`
} `comment:"parent"`
}{struct {
Name string `comment:"my name"`
Value int `comment:"my value"`
}{"hello", 10}},
wantOutput: "# parent\n[Parent]\n # my name\n Name = \"hello\"\n # my value\n Value = 10\n",
},
"slice with comments": {
input: struct {
Primes []int `comment:"prime numbers"`
}{[]int{1, 2, 3, 5, 7}},
wantOutput: "# prime numbers\nPrimes = [1, 2, 3, 5, 7]\n",
},
"map with comments": {
input: struct {
Library map[string]interface{} `comment:"a library"`
}{map[string]interface{}{"name": "hello", "age": 99}},
wantOutput: "# a library\n[Library]\n age = 99\n name = \"hello\"\n",
},
"2 layer embedded struct with comments": {
input: struct {
Parent struct {
Child struct {
Int int `comment:"child int"`
} `comment:"child struct"`
} `comment:"parent struct"`
}{},
wantOutput: "# parent struct\n[Parent]\n # child struct\n [Parent.Child]\n # child int\n Int = 0\n",
},
"commented value": {
input: struct {
Int int `commented:"true"`
}{},
wantOutput: "#Int = 0\n",
},
"commented struct": {
input: struct {
Data struct {
Int int `comment:"data count"`
Name string
} `commented:"true"`
}{},
wantOutput: "#[Data]\n # data count\n #Int = 0\n #Name = \"\"\n",
},
"commented slice": {
input: struct {
Slice []string `commented:"true"`
}{Slice: []string{"apple", "orange", "mango"}},
wantOutput: "#Slice = [\"apple\", \"orange\", \"mango\"]\n",
},
"commented map": {
input: struct {
Map map[string]string `commented:"true"`
}{Map: map[string]string{"hello": "world"}},
wantOutput: "#[Map]\n #hello = \"world\"\n",
},
"struct in structs": {
input: struct {
Parent struct {
Child struct {
Int int
} `commented:"true" comment:"child"`
} `commented:"true"`
Cousin struct {
Amount time.Duration
}
}{},
wantOutput: "#[Parent]\n # child\n #[Parent.Child]\n #Int = 0\n\n[Cousin]\n Amount = \"0s\"\n",
},
}
for label, test := range tests {
encodeExpected(t, label, test.input, test.wantOutput, test.wantError)
}
}
func TestEncodeNestedTableArrays(t *testing.T) {
type song struct {
Name string `toml:"name"`
}
type album struct {
Name string `toml:"name"`
Songs []song `toml:"songs"`
}
type springsteen struct {
Albums []album `toml:"albums"`
}
value := springsteen{
[]album{
{"Born to Run",
[]song{{"Jungleland"}, {"Meeting Across the River"}}},
{"Born in the USA",
[]song{{"Glory Days"}, {"Dancing in the Dark"}}},
},
}
expected := `[[albums]]
name = "Born to Run"
[[albums.songs]]
name = "Jungleland"
[[albums.songs]]
name = "Meeting Across the River"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
[[albums.songs]]
name = "Dancing in the Dark"
`
encodeExpected(t, "nested table arrays", value, expected, nil)
}
func TestEncodeArrayHashWithNormalHashOrder(t *testing.T) {
type Alpha struct {
V int
}
type Beta struct {
V int
}
type Conf struct {
V int
A Alpha
B []Beta
}
val := Conf{
V: 1,
A: Alpha{2},
B: []Beta{{3}},
}
expected := "V = 1\n\n[A]\n V = 2\n\n[[B]]\n V = 3\n"
encodeExpected(t, "array hash with normal hash order", val, expected, nil)
}
func TestEncodeWithOmitEmpty(t *testing.T) {
type simple struct {
Bool bool `toml:"bool,omitempty"`
String string `toml:"string,omitempty"`
Array [0]byte `toml:"array,omitempty"`
Slice []int `toml:"slice,omitempty"`
Map map[string]string `toml:"map,omitempty"`
}
var v simple
encodeExpected(t, "fields with omitempty are omitted when empty", v, "", nil)
v = simple{
Bool: true,
String: " ",
Slice: []int{2, 3, 4},
Map: map[string]string{"foo": "bar"},
}
expected := `bool = true
string = " "
slice = [2, 3, 4]
[map]
foo = "bar"
`
encodeExpected(t, "fields with omitempty are not omitted when non-empty",
v, expected, nil)
}
func TestEncodeWithOmitZero(t *testing.T) {
type simple struct {
Number int `toml:"number,omitzero"`
Real float64 `toml:"real,omitzero"`
Unsigned uint `toml:"unsigned,omitzero"`
}
value := simple{0, 0.0, uint(0)}
expected := ""
encodeExpected(t, "simple with omitzero, all zero", value, expected, nil)
value.Number = 10
value.Real = 20
value.Unsigned = 5
expected = `number = 10
real = 20.0
unsigned = 5
`
encodeExpected(t, "simple with omitzero, non-zero", value, expected, nil)
}
func TestEncodeOmitemptyWithEmptyName(t *testing.T) {
type simple struct {
S []int `toml:",omitempty"`
}
v := simple{[]int{1, 2, 3}}
expected := "S = [1, 2, 3]\n"
encodeExpected(t, "simple with omitempty, no name, non-empty field",
v, expected, nil)
}
func TestEncodeAnonymousStruct(t *testing.T) {
type Inner struct{ N int }
type Outer0 struct{ Inner }
type Outer1 struct {
Inner `toml:"inner"`
}
v0 := Outer0{Inner{3}}
expected := "N = 3\n"
encodeExpected(t, "embedded anonymous untagged struct", v0, expected, nil)
v1 := Outer1{Inner{3}}
expected = "[inner]\n N = 3\n"
encodeExpected(t, "embedded anonymous tagged struct", v1, expected, nil)
}
func TestEncodeAnonymousStructPointerField(t *testing.T) {
type Inner struct{ N int }
type Outer0 struct{ *Inner }
type Outer1 struct {
*Inner `toml:"inner"`
}
v0 := Outer0{}
expected := ""
encodeExpected(t, "nil anonymous untagged struct pointer field", v0, expected, nil)
v0 = Outer0{&Inner{3}}
expected = "N = 3\n"
encodeExpected(t, "non-nil anonymous untagged struct pointer field", v0, expected, nil)
v1 := Outer1{}
expected = ""
encodeExpected(t, "nil anonymous tagged struct pointer field", v1, expected, nil)
v1 = Outer1{&Inner{3}}
expected = "[inner]\n N = 3\n"
encodeExpected(t, "non-nil anonymous tagged struct pointer field", v1, expected, nil)
}
func TestEncodeIgnoredFields(t *testing.T) {
type simple struct {
Number int `toml:"-"`
}
value := simple{}
expected := ""
encodeExpected(t, "ignored field", value, expected, nil)
}
func encodeExpected(
t *testing.T, label string, val interface{}, wantStr string, wantErr error,
) {
var buf bytes.Buffer
enc := NewEncoder(&buf)
err := enc.Encode(val)
if err != wantErr {
if wantErr != nil {
if wantErr == errAnything && err != nil {
return
}
t.Errorf("%s: want Encode error %v, got %v", label, wantErr, err)
} else {
t.Errorf("%s: Encode failed: %s", label, err)
}
}
if err != nil {
return
}
if got := buf.String(); wantStr != got {
t.Errorf("%s: want\n-----\n%q\n-----\nbut got\n-----\n%q\n-----\n",
label, wantStr, got)
}
}
func ExampleEncoder_Encode() {
date, _ := time.Parse(time.RFC822, "14 Mar 10 18:00 UTC")
var config = map[string]interface{}{
"date": date,
"counts": []int{1, 1, 2, 3, 5, 8},
"hash": map[string]string{
"key1": "val1",
"key2": "val2",
},
}
buf := new(bytes.Buffer)
if err := NewEncoder(buf).Encode(config); err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
// Output:
// counts = [1, 1, 2, 3, 5, 8]
// date = 2010-03-14T18:00:00Z
//
// [hash]
// key1 = "val1"
// key2 = "val2"
}
-20
View File
@@ -1,20 +0,0 @@
//go:build go1.2
// +build go1.2
package toml
// In order to support Go 1.1, we define our own TextMarshaler and
// TextUnmarshaler types. For Go 1.2+, we just alias them with the
// standard library interfaces.
import (
"encoding"
)
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler encoding.TextUnmarshaler
-19
View File
@@ -1,19 +0,0 @@
//go:build !go1.2
// +build !go1.2
package toml
// These interfaces were introduced in Go 1.2, so we add them manually when
// compiling for Go 1.1.
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}
-3
View File
@@ -1,3 +0,0 @@
module github.com/hydronica/toml
go 1.11
View File
-953
View File
@@ -1,953 +0,0 @@
package toml
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type itemType int
const (
itemError itemType = iota
itemNIL // used in the parser to indicate no type
itemEOF
itemText
itemString
itemRawString
itemMultilineString
itemRawMultilineString
itemBool
itemInteger
itemFloat
itemDatetime
itemArray // the start of an array
itemArrayEnd
itemTableStart
itemTableEnd
itemArrayTableStart
itemArrayTableEnd
itemKeyStart
itemCommentStart
itemInlineTableStart
itemInlineTableEnd
)
const (
eof = 0
comma = ','
tableStart = '['
tableEnd = ']'
arrayTableStart = '['
arrayTableEnd = ']'
tableSep = '.'
keySep = '='
arrayStart = '['
arrayEnd = ']'
commentStart = '#'
stringStart = '"'
stringEnd = '"'
rawStringStart = '\''
rawStringEnd = '\''
inlineTableStart = '{'
inlineTableEnd = '}'
)
type stateFn func(lx *lexer) stateFn
type lexer struct {
input string
start int
pos int
line int
state stateFn
items chan item
// Allow for backing up up to three runes.
// This is necessary because TOML contains 3-rune tokens (""" and ''').
prevWidths [3]int
nprev int // how many of prevWidths are in use
// If we emit an eof, we can still back up, but it is not OK to call
// next again.
atEOF bool
// A stack of state functions used to maintain context.
// The idea is to reuse parts of the state machine in various places.
// For example, values can appear at the top level or within arbitrarily
// nested arrays. The last state on the stack is used after a value has
// been lexed. Similarly for comments.
stack []stateFn
}
type item struct {
typ itemType
val string
line int
}
func (lx *lexer) nextItem() item {
for {
select {
case item := <-lx.items:
return item
default:
lx.state = lx.state(lx)
}
}
}
func lex(input string) *lexer {
lx := &lexer{
input: input,
state: lexTop,
line: 1,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
}
return lx
}
func (lx *lexer) push(state stateFn) {
lx.stack = append(lx.stack, state)
}
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
return lx.errorf("BUG in lexer: no states to pop")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
return last
}
func (lx *lexer) current() string {
return lx.input[lx.start:lx.pos]
}
func (lx *lexer) emit(typ itemType) {
lx.items <- item{typ, lx.current(), lx.line}
lx.start = lx.pos
}
func (lx *lexer) emitTrim(typ itemType) {
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line}
lx.start = lx.pos
}
func (lx *lexer) next() (r rune) {
if lx.atEOF {
panic("next called after EOF")
}
if lx.pos >= len(lx.input) {
lx.atEOF = true
return eof
}
if lx.input[lx.pos] == '\n' {
lx.line++
}
lx.prevWidths[2] = lx.prevWidths[1]
lx.prevWidths[1] = lx.prevWidths[0]
if lx.nprev < 3 {
lx.nprev++
}
r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.prevWidths[0] = w
lx.pos += w
return r
}
// ignore skips over the pending input before this point.
func (lx *lexer) ignore() {
lx.start = lx.pos
}
// backup steps back one rune. Can be called only twice between calls to next.
func (lx *lexer) backup() {
if lx.atEOF {
lx.atEOF = false
return
}
if lx.nprev < 1 {
panic("backed up too far")
}
w := lx.prevWidths[0]
lx.prevWidths[0] = lx.prevWidths[1]
lx.prevWidths[1] = lx.prevWidths[2]
lx.nprev--
lx.pos -= w
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
lx.line--
}
}
// accept consumes the next rune if it's equal to `valid`.
func (lx *lexer) accept(valid rune) bool {
if lx.next() == valid {
return true
}
lx.backup()
return false
}
// peek returns but does not consume the next rune in the input.
func (lx *lexer) peek() rune {
r := lx.next()
lx.backup()
return r
}
// skip ignores all input that matches the given predicate.
func (lx *lexer) skip(pred func(rune) bool) {
for {
r := lx.next()
if pred(r) {
continue
}
lx.backup()
lx.ignore()
return
}
}
// errorf stops all lexing by emitting an error and returning `nil`.
// Note that any value that is a character is escaped if it's a special
// character (newlines, tabs, etc.).
func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
lx.items <- item{
itemError,
fmt.Sprintf(format, values...),
lx.line,
}
return nil
}
// lexTop consumes elements at the top level of TOML data.
func lexTop(lx *lexer) stateFn {
r := lx.next()
if isWhitespace(r) || isNL(r) {
return lexSkip(lx, lexTop)
}
switch r {
case commentStart:
lx.push(lexTop)
return lexCommentStart
case tableStart:
return lexTableStart
case eof:
if lx.pos > lx.start {
return lx.errorf("unexpected EOF")
}
lx.emit(itemEOF)
return nil
}
// At this point, the only valid item can be a key, so we back up
// and let the key lexer do the rest.
lx.backup()
lx.push(lexTopEnd)
return lexKeyStart
}
// lexTopEnd is entered whenever a top-level item has been consumed. (A value
// or a table.) It must see only whitespace, and will turn back to lexTop
// upon a newline. If it sees EOF, it will quit the lexer successfully.
func lexTopEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case r == commentStart:
// a comment will read to a newline for us.
lx.push(lexTop)
return lexCommentStart
case isWhitespace(r):
return lexTopEnd
case isNL(r):
lx.ignore()
return lexTop
case r == eof:
lx.emit(itemEOF)
return nil
}
return lx.errorf("expected a top-level item to end with a newline, "+
"comment, or EOF, but got %q instead", r)
}
// lexTable lexes the beginning of a table. Namely, it makes sure that
// it starts with a character other than '.' and ']'.
// It assumes that '[' has already been consumed.
// It also handles the case that this is an item in an array of tables.
// e.g., '[[name]]'.
func lexTableStart(lx *lexer) stateFn {
if lx.peek() == arrayTableStart {
lx.next()
lx.emit(itemArrayTableStart)
lx.push(lexArrayTableEnd)
} else {
lx.emit(itemTableStart)
lx.push(lexTableEnd)
}
return lexTableNameStart
}
func lexTableEnd(lx *lexer) stateFn {
lx.emit(itemTableEnd)
return lexTopEnd
}
func lexArrayTableEnd(lx *lexer) stateFn {
if r := lx.next(); r != arrayTableEnd {
return lx.errorf("expected end of table array name delimiter %q, "+
"but got %q instead", arrayTableEnd, r)
}
lx.emit(itemArrayTableEnd)
return lexTopEnd
}
func lexTableNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
case r == tableEnd || r == eof:
return lx.errorf("unexpected end of table name " +
"(table names cannot be empty)")
case r == tableSep:
return lx.errorf("unexpected table separator " +
"(table names cannot be empty)")
case r == stringStart || r == rawStringStart:
lx.ignore()
lx.push(lexTableNameEnd)
return lexValue // reuse string lexing
default:
return lexBareTableName
}
}
// lexBareTableName lexes the name of a table. It assumes that at least one
// valid character for the table has already been read.
func lexBareTableName(lx *lexer) stateFn {
r := lx.next()
if isBareKeyChar(r) {
return lexBareTableName
}
lx.backup()
lx.emit(itemText)
return lexTableNameEnd
}
// lexTableNameEnd reads the end of a piece of a table name, optionally
// consuming whitespace.
func lexTableNameEnd(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.next(); {
case isWhitespace(r):
return lexTableNameEnd
case r == tableSep:
lx.ignore()
return lexTableNameStart
case r == tableEnd:
return lx.pop()
default:
return lx.errorf("expected '.' or ']' to end table name, "+
"but got %q instead", r)
}
}
// lexKeyStart consumes a key name up until the first non-whitespace character.
// lexKeyStart will ignore whitespace.
func lexKeyStart(lx *lexer) stateFn {
r := lx.peek()
switch {
case r == keySep:
return lx.errorf("unexpected key separator %q", keySep)
case isWhitespace(r) || isNL(r):
lx.next()
return lexSkip(lx, lexKeyStart)
case r == stringStart || r == rawStringStart:
lx.ignore()
lx.emit(itemKeyStart)
lx.push(lexKeyEnd)
return lexValue // reuse string lexing
default:
lx.ignore()
lx.emit(itemKeyStart)
return lexBareKey
}
}
// lexBareKey consumes the text of a bare key. Assumes that the first character
// (which is not whitespace) has not yet been consumed.
func lexBareKey(lx *lexer) stateFn {
switch r := lx.next(); {
case isBareKeyChar(r):
return lexBareKey
case isWhitespace(r):
lx.backup()
lx.emit(itemText)
return lexKeyEnd
case r == keySep:
lx.backup()
lx.emit(itemText)
return lexKeyEnd
default:
return lx.errorf("bare keys cannot contain %q", r)
}
}
// lexKeyEnd consumes the end of a key and trims whitespace (up to the key
// separator).
func lexKeyEnd(lx *lexer) stateFn {
switch r := lx.next(); {
case r == keySep:
return lexSkip(lx, lexValue)
case isWhitespace(r):
return lexSkip(lx, lexKeyEnd)
default:
return lx.errorf("expected key separator %q, but got %q instead",
keySep, r)
}
}
// lexValue starts the consumption of a value anywhere a value is expected.
// lexValue will ignore whitespace.
// After a value is lexed, the last state on the next is popped and returned.
func lexValue(lx *lexer) stateFn {
// We allow whitespace to precede a value, but NOT newlines.
// In array syntax, the array states are responsible for ignoring newlines.
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexValue)
case isDigit(r):
lx.backup() // avoid an extra state and use the same as above
return lexNumberOrDateStart
}
switch r {
case arrayStart:
lx.ignore()
lx.emit(itemArray)
return lexArrayValue
case inlineTableStart:
lx.ignore()
lx.emit(itemInlineTableStart)
return lexInlineTableValue
case stringStart:
if lx.accept(stringStart) {
if lx.accept(stringStart) {
lx.ignore() // Ignore """
return lexMultilineString
}
lx.backup()
}
lx.ignore() // ignore the '"'
return lexString
case rawStringStart:
if lx.accept(rawStringStart) {
if lx.accept(rawStringStart) {
lx.ignore() // Ignore """
return lexMultilineRawString
}
lx.backup()
}
lx.ignore() // ignore the "'"
return lexRawString
case '+', '-':
return lexNumberStart
case '.': // special error case, be kind to users
return lx.errorf("floats must start with a digit, not '.'")
}
if unicode.IsLetter(r) {
// Be permissive here; lexBool will give a nice error if the
// user wrote something like
// x = foo
// (i.e. not 'true' or 'false' but is something else word-like.)
lx.backup()
return lexBool
}
return lx.errorf("expected value but found %q instead", r)
}
// lexArrayValue consumes one value in an array. It assumes that '[' or ','
// have already been consumed. All whitespace and newlines are ignored.
func lexArrayValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValue)
case r == commentStart:
lx.push(lexArrayValue)
return lexCommentStart
case r == comma:
return lx.errorf("unexpected comma")
case r == arrayEnd:
// NOTE(caleb): The spec isn't clear about whether you can have
// a trailing comma or not, so we'll allow it.
return lexArrayEnd
}
lx.backup()
lx.push(lexArrayValueEnd)
return lexValue
}
// lexArrayValueEnd consumes everything between the end of an array value and
// the next value (or the end of the array): it ignores whitespace and newlines
// and expects either a ',' or a ']'.
func lexArrayValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValueEnd)
case r == commentStart:
lx.push(lexArrayValueEnd)
return lexCommentStart
case r == comma:
lx.ignore()
return lexArrayValue // move on to the next value
case r == arrayEnd:
return lexArrayEnd
}
return lx.errorf(
"expected a comma or array terminator %q, but got %q instead",
arrayEnd, r,
)
}
// lexArrayEnd finishes the lexing of an array.
// It assumes that a ']' has just been consumed.
func lexArrayEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemArrayEnd)
return lx.pop()
}
// lexInlineTableValue consumes one key/value pair in an inline table.
// It assumes that '{' or ',' have already been consumed. Whitespace is ignored.
func lexInlineTableValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValue)
return lexCommentStart
case r == comma:
return lx.errorf("unexpected comma")
case r == inlineTableEnd:
return lexInlineTableEnd
}
lx.backup()
lx.push(lexInlineTableValueEnd)
return lexKeyStart
}
// lexInlineTableValueEnd consumes everything between the end of an inline table
// key/value pair and the next pair (or the end of the table):
// it ignores whitespace and expects either a ',' or a '}'.
func lexInlineTableValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValueEnd)
return lexCommentStart
case r == comma:
lx.ignore()
return lexInlineTableValue
case r == inlineTableEnd:
return lexInlineTableEnd
}
return lx.errorf("expected a comma or an inline table terminator %q, "+
"but got %q instead", inlineTableEnd, r)
}
// lexInlineTableEnd finishes the lexing of an inline table.
// It assumes that a '}' has just been consumed.
func lexInlineTableEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemInlineTableEnd)
return lx.pop()
}
// lexString consumes the inner contents of a string. It assumes that the
// beginning '"' has already been consumed and ignored.
func lexString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r):
return lx.errorf("strings cannot contain newlines")
case r == '\\':
lx.push(lexString)
return lexStringEscape
case r == stringEnd:
lx.backup()
lx.emit(itemString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexString
}
// lexMultilineString consumes the inner contents of a string. It assumes that
// the beginning '"""' has already been consumed and ignored.
func lexMultilineString(lx *lexer) stateFn {
switch lx.next() {
case eof:
return lx.errorf("unexpected EOF")
case '\\':
return lexMultilineStringEscape
case stringEnd:
if lx.accept(stringEnd) {
if lx.accept(stringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineString
}
// lexRawString consumes a raw string. Nothing can be escaped in such a string.
// It assumes that the beginning "'" has already been consumed and ignored.
func lexRawString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r):
return lx.errorf("strings cannot contain newlines")
case r == rawStringEnd:
lx.backup()
lx.emit(itemRawString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexRawString
}
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such
// a string. It assumes that the beginning "”'" has already been consumed and
// ignored.
func lexMultilineRawString(lx *lexer) stateFn {
switch lx.next() {
case eof:
return lx.errorf("unexpected EOF")
case rawStringEnd:
if lx.accept(rawStringEnd) {
if lx.accept(rawStringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemRawMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineRawString
}
// lexMultilineStringEscape consumes an escaped character. It assumes that the
// preceding '\\' has already been consumed.
func lexMultilineStringEscape(lx *lexer) stateFn {
// Handle the special case first:
if isNL(lx.next()) {
return lexMultilineString
}
lx.backup()
lx.push(lexMultilineString)
return lexStringEscape(lx)
}
func lexStringEscape(lx *lexer) stateFn {
r := lx.next()
switch r {
case 'b':
fallthrough
case 't':
fallthrough
case 'n':
fallthrough
case 'f':
fallthrough
case 'r':
fallthrough
case '"':
fallthrough
case '\\':
return lx.pop()
case 'u':
return lexShortUnicodeEscape
case 'U':
return lexLongUnicodeEscape
}
return lx.errorf("invalid escape character %q; only the following "+
"escape characters are allowed: "+
`\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r)
}
func lexShortUnicodeEscape(lx *lexer) stateFn {
var r rune
for i := 0; i < 4; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf(`expected four hexadecimal digits after '\u', `+
"but got %q instead", lx.current())
}
}
return lx.pop()
}
func lexLongUnicodeEscape(lx *lexer) stateFn {
var r rune
for i := 0; i < 8; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf(`expected eight hexadecimal digits after '\U', `+
"but got %q instead", lx.current())
}
}
return lx.pop()
}
// lexNumberOrDateStart consumes either an integer, a float, or datetime.
func lexNumberOrDateStart(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumberOrDate
}
switch r {
case '_':
return lexNumber
case 'e', 'E':
return lexFloat
case '.':
return lx.errorf("floats must start with a digit, not '.'")
}
return lx.errorf("expected a digit but got %q", r)
}
// lexNumberOrDate consumes either an integer, float or datetime.
func lexNumberOrDate(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumberOrDate
}
switch r {
case '-':
return lexDatetime
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexDatetime consumes a Datetime, to a first approximation.
// The parser validates that it matches one of the accepted formats.
func lexDatetime(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexDatetime
}
switch r {
case '-', 'T', ':', '.', 'Z', '+':
return lexDatetime
}
lx.backup()
lx.emit(itemDatetime)
return lx.pop()
}
// lexNumberStart consumes either an integer or a float. It assumes that a sign
// has already been read, but that *no* digits have been consumed.
// lexNumberStart will move to the appropriate integer or float states.
func lexNumberStart(lx *lexer) stateFn {
// We MUST see a digit. Even floats have to start with a digit.
r := lx.next()
if !isDigit(r) {
if r == '.' {
return lx.errorf("floats must start with a digit, not '.'")
}
return lx.errorf("expected a digit but got %q", r)
}
return lexNumber
}
// lexNumber consumes an integer or a float after seeing the first digit.
func lexNumber(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumber
}
switch r {
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexFloat consumes the elements of a float. It allows any sequence of
// float-like characters, so floats emitted by the lexer are only a first
// approximation and must be validated by the parser.
func lexFloat(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexFloat
}
switch r {
case '_', '.', '-', '+', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemFloat)
return lx.pop()
}
// lexBool consumes a bool string: 'true' or 'false.
func lexBool(lx *lexer) stateFn {
var rs []rune
for {
r := lx.next()
if !unicode.IsLetter(r) {
lx.backup()
break
}
rs = append(rs, r)
}
s := string(rs)
switch s {
case "true", "false":
lx.emit(itemBool)
return lx.pop()
}
return lx.errorf("expected value but found %q instead", s)
}
// lexCommentStart begins the lexing of a comment. It will emit
// itemCommentStart and consume no characters, passing control to lexComment.
func lexCommentStart(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemCommentStart)
return lexComment
}
// lexComment lexes an entire comment. It assumes that '#' has been consumed.
// It will consume *up to* the first newline character, and pass control
// back to the last state on the stack.
func lexComment(lx *lexer) stateFn {
r := lx.peek()
if isNL(r) || r == eof {
lx.emit(itemText)
return lx.pop()
}
lx.next()
return lexComment
}
// lexSkip ignores all slurped input and moves on to the next state.
func lexSkip(lx *lexer, nextState stateFn) stateFn {
return func(lx *lexer) stateFn {
lx.ignore()
return nextState
}
}
// isWhitespace returns true if `r` is a whitespace character according
// to the spec.
func isWhitespace(r rune) bool {
return r == '\t' || r == ' '
}
func isNL(r rune) bool {
return r == '\n' || r == '\r'
}
func isDigit(r rune) bool {
return r >= '0' && r <= '9'
}
func isHexadecimal(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
func isBareKeyChar(r rune) bool {
return (r >= 'A' && r <= 'Z') ||
(r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') ||
r == '_' ||
r == '-'
}
func (itype itemType) String() string {
switch itype {
case itemError:
return "Error"
case itemNIL:
return "NIL"
case itemEOF:
return "EOF"
case itemText:
return "Text"
case itemString, itemRawString, itemMultilineString, itemRawMultilineString:
return "String"
case itemBool:
return "Bool"
case itemInteger:
return "Integer"
case itemFloat:
return "Float"
case itemDatetime:
return "DateTime"
case itemTableStart:
return "TableStart"
case itemTableEnd:
return "TableEnd"
case itemKeyStart:
return "KeyStart"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemCommentStart:
return "CommentStart"
}
panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype)))
}
func (item item) String() string {
return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val)
}
-592
View File
@@ -1,592 +0,0 @@
package toml
import (
"fmt"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
)
type parser struct {
mapping map[string]interface{}
types map[string]tomlType
lx *lexer
// A list of keys in the order that they appear in the TOML data.
ordered []Key
// the full key for the current hash in scope
context Key
// the base key name for everything except hashes
currentKey string
// rough approximation of line number
approxLine int
// A map of 'key.group.names' to whether they were created implicitly.
implicits map[string]bool
}
type parseError string
func (pe parseError) Error() string {
return string(pe)
}
func parse(data string) (p *parser, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
if err, ok = r.(parseError); ok {
return
}
panic(r)
}
}()
p = &parser{
mapping: make(map[string]interface{}),
types: make(map[string]tomlType),
lx: lex(data),
ordered: make([]Key, 0),
implicits: make(map[string]bool),
}
for {
item := p.next()
if item.typ == itemEOF {
break
}
p.topLevel(item)
}
return p, nil
}
func (p *parser) panicf(format string, v ...interface{}) {
msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
p.approxLine, p.current(), fmt.Sprintf(format, v...))
panic(parseError(msg))
}
func (p *parser) next() item {
it := p.lx.nextItem()
if it.typ == itemError {
p.panicf("%s", it.val)
}
return it
}
func (p *parser) bug(format string, v ...interface{}) {
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
}
func (p *parser) expect(typ itemType) item {
it := p.next()
p.assertEqual(typ, it.typ)
return it
}
func (p *parser) assertEqual(expected, got itemType) {
if expected != got {
p.bug("Expected '%s' but got '%s'.", expected, got)
}
}
func (p *parser) topLevel(item item) {
switch item.typ {
case itemCommentStart:
p.approxLine = item.line
p.expect(itemText)
case itemTableStart:
kg := p.next()
p.approxLine = kg.line
var key Key
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
}
p.assertEqual(itemTableEnd, kg.typ)
p.establishContext(key, false)
p.setType("", tomlHash)
p.ordered = append(p.ordered, key)
case itemArrayTableStart:
kg := p.next()
p.approxLine = kg.line
var key Key
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
}
p.assertEqual(itemArrayTableEnd, kg.typ)
p.establishContext(key, true)
p.setType("", tomlArrayHash)
p.ordered = append(p.ordered, key)
case itemKeyStart:
kname := p.next()
p.approxLine = kname.line
p.currentKey = p.keyString(kname)
val, typ := p.value(p.next())
p.setValue(p.currentKey, val)
p.setType(p.currentKey, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
p.currentKey = ""
default:
p.bug("Unexpected type at top level: %s", item.typ)
}
}
// Gets a string for a key (or part of a key in a table name).
func (p *parser) keyString(it item) string {
switch it.typ {
case itemText:
return it.val
case itemString, itemMultilineString,
itemRawString, itemRawMultilineString:
s, _ := p.value(it)
return s.(string)
default:
p.bug("Unexpected key type: %s", it.typ)
panic("unreachable")
}
}
// value translates an expected value from the lexer into a Go value wrapped
// as an empty interface.
func (p *parser) value(it item) (interface{}, tomlType) {
switch it.typ {
case itemString:
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
case itemMultilineString:
trimmed := stripFirstNewline(stripEscapedWhitespace(it.val))
return p.replaceEscapes(trimmed), p.typeOfPrimitive(it)
case itemRawString:
return it.val, p.typeOfPrimitive(it)
case itemRawMultilineString:
return stripFirstNewline(it.val), p.typeOfPrimitive(it)
case itemBool:
switch it.val {
case "true":
return true, p.typeOfPrimitive(it)
case "false":
return false, p.typeOfPrimitive(it)
}
p.bug("Expected boolean value, but got '%s'.", it.val)
case itemInteger:
if !numUnderscoresOK(it.val) {
p.panicf("Invalid integer %q: underscores must be surrounded by digits",
it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseInt(val, 10, 64)
if err != nil {
// Distinguish integer values. Normally, it'd be a bug if the lexer
// provides an invalid integer, but it's possible that the number is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Integer '%s' is out of the range of 64-bit "+
"signed integers.", it.val)
} else {
p.bug("Expected integer value, but got '%s'.", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemFloat:
parts := strings.FieldsFunc(it.val, func(r rune) bool {
switch r {
case '.', 'e', 'E':
return true
}
return false
})
for _, part := range parts {
if !numUnderscoresOK(part) {
p.panicf("Invalid float %q: underscores must be "+
"surrounded by digits", it.val)
}
}
if !numPeriodsOK(it.val) {
// As a special case, numbers like '123.' or '1.e2',
// which are valid as far as Go/strconv are concerned,
// must be rejected because TOML says that a fractional
// part consists of '.' followed by 1+ digits.
p.panicf("Invalid float %q: '.' must be followed "+
"by one or more digits", it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseFloat(val, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Float '%s' is out of the range of 64-bit "+
"IEEE-754 floating-point numbers.", it.val)
} else {
p.panicf("Invalid float value: %q", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemDatetime:
var t time.Time
var ok bool
var err error
for _, format := range []string{
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05",
"2006-01-02",
} {
t, err = time.ParseInLocation(format, it.val, time.Local)
if err == nil {
ok = true
break
}
}
if !ok {
p.panicf("Invalid TOML Datetime: %q.", it.val)
}
return t, p.typeOfPrimitive(it)
case itemArray:
array := make([]interface{}, 0)
types := make([]tomlType, 0)
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
val, typ := p.value(it)
array = append(array, val)
types = append(types, typ)
}
return array, p.typeOfArray(types)
case itemInlineTableStart:
var (
hash = make(map[string]interface{})
outerContext = p.context
outerKey = p.currentKey
)
p.context = append(p.context, p.currentKey)
p.currentKey = ""
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
if it.typ != itemKeyStart {
p.bug("Expected key start but instead found %q, around line %d",
it.val, p.approxLine)
}
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
// retrieve key
k := p.next()
p.approxLine = k.line
kname := p.keyString(k)
// retrieve value
p.currentKey = kname
val, typ := p.value(p.next())
// make sure we keep metadata up to date
p.setType(kname, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
hash[kname] = val
}
p.context = outerContext
p.currentKey = outerKey
return hash, tomlHash
}
p.bug("Unexpected value type: %s", it.typ)
panic("unreachable")
}
// numUnderscoresOK checks whether each underscore in s is surrounded by
// characters that are not underscores.
func numUnderscoresOK(s string) bool {
accept := false
for _, r := range s {
if r == '_' {
if !accept {
return false
}
accept = false
continue
}
accept = true
}
return accept
}
// numPeriodsOK checks whether every period in s is followed by a digit.
func numPeriodsOK(s string) bool {
period := false
for _, r := range s {
if period && !isDigit(r) {
return false
}
period = r == '.'
}
return !period
}
// establishContext sets the current context of the parser,
// where the context is either a hash or an array of hashes. Which one is
// set depends on the value of the `array` parameter.
//
// Establishing the context also makes sure that the key isn't a duplicate, and
// will create implicit hashes automatically.
func (p *parser) establishContext(key Key, array bool) {
var ok bool
// Always start at the top level and drill down for our context.
hashContext := p.mapping
keyContext := make(Key, 0)
// We only need implicit hashes for key[0:-1]
for _, k := range key[0 : len(key)-1] {
_, ok = hashContext[k]
keyContext = append(keyContext, k)
// No key? Make an implicit hash and move on.
if !ok {
p.addImplicit(keyContext)
hashContext[k] = make(map[string]interface{})
}
// If the hash context is actually an array of tables, then set
// the hash context to the last element in that array.
//
// Otherwise, it better be a table, since this MUST be a key group (by
// virtue of it not being the last element in a key).
switch t := hashContext[k].(type) {
case []map[string]interface{}:
hashContext = t[len(t)-1]
case map[string]interface{}:
hashContext = t
default:
p.panicf("Key '%s' was already created as a hash.", keyContext)
}
}
p.context = keyContext
if array {
// If this is the first element for this array, then allocate a new
// list of tables for it.
k := key[len(key)-1]
if _, ok := hashContext[k]; !ok {
hashContext[k] = make([]map[string]interface{}, 0, 5)
}
// Add a new table. But make sure the key hasn't already been used
// for something else.
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
hashContext[k] = append(hash, make(map[string]interface{}))
} else {
p.panicf("Key '%s' was already created and cannot be used as "+
"an array.", keyContext)
}
} else {
p.setValue(key[len(key)-1], make(map[string]interface{}))
}
p.context = append(p.context, key[len(key)-1])
}
// setValue sets the given key to the given value in the current context.
// It will make sure that the key hasn't already been defined, account for
// implicit key groups.
func (p *parser) setValue(key string, value interface{}) {
var tmpHash interface{}
var ok bool
hash := p.mapping
keyContext := make(Key, 0)
for _, k := range p.context {
keyContext = append(keyContext, k)
if tmpHash, ok = hash[k]; !ok {
p.bug("Context for key '%s' has not been established.", keyContext)
}
switch t := tmpHash.(type) {
case []map[string]interface{}:
// The context is a table of hashes. Pick the most recent table
// defined as the current hash.
hash = t[len(t)-1]
case map[string]interface{}:
hash = t
default:
p.bug("Expected hash to have type 'map[string]interface{}', but "+
"it has '%T' instead.", tmpHash)
}
}
keyContext = append(keyContext, key)
if _, ok := hash[key]; ok {
// Typically, if the given key has already been set, then we have
// to raise an error since duplicate keys are disallowed. However,
// it's possible that a key was previously defined implicitly. In this
// case, it is allowed to be redefined concretely. (See the
// `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.)
//
// But we have to make sure to stop marking it as an implicit. (So that
// another redefinition provokes an error.)
//
// Note that since it has already been defined (as a hash), we don't
// want to overwrite it. So our business is done.
if p.isImplicit(keyContext) {
p.removeImplicit(keyContext)
return
}
// Otherwise, we have a concrete key trying to override a previous
// key, which is *always* wrong.
p.panicf("Key '%s' has already been defined.", keyContext)
}
hash[key] = value
}
// setType sets the type of a particular value at a given key.
// It should be called immediately AFTER setValue.
//
// Note that if `key` is empty, then the type given will be applied to the
// current context (which is either a table or an array of tables).
func (p *parser) setType(key string, typ tomlType) {
keyContext := make(Key, 0, len(p.context)+1)
for _, k := range p.context {
keyContext = append(keyContext, k)
}
if len(key) > 0 { // allow type setting for hashes
keyContext = append(keyContext, key)
}
p.types[keyContext.String()] = typ
}
// addImplicit sets the given Key as having been created implicitly.
func (p *parser) addImplicit(key Key) {
p.implicits[key.String()] = true
}
// removeImplicit stops tagging the given key as having been implicitly
// created.
func (p *parser) removeImplicit(key Key) {
p.implicits[key.String()] = false
}
// isImplicit returns true if the key group pointed to by the key was created
// implicitly.
func (p *parser) isImplicit(key Key) bool {
return p.implicits[key.String()]
}
// current returns the full key name of the current context.
func (p *parser) current() string {
if len(p.currentKey) == 0 {
return p.context.String()
}
if len(p.context) == 0 {
return p.currentKey
}
return fmt.Sprintf("%s.%s", p.context, p.currentKey)
}
func stripFirstNewline(s string) string {
if len(s) == 0 || s[0] != '\n' {
return s
}
return s[1:]
}
func stripEscapedWhitespace(s string) string {
esc := strings.Split(s, "\\\n")
if len(esc) > 1 {
for i := 1; i < len(esc); i++ {
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
}
}
return strings.Join(esc, "")
}
func (p *parser) replaceEscapes(str string) string {
var replaced []rune
s := []byte(str)
r := 0
for r < len(s) {
if s[r] != '\\' {
c, size := utf8.DecodeRune(s[r:])
r += size
replaced = append(replaced, c)
continue
}
r += 1
if r >= len(s) {
p.bug("Escape sequence at end of string.")
return ""
}
switch s[r] {
default:
p.bug("Expected valid escape code after \\, but got %q.", s[r])
return ""
case 'b':
replaced = append(replaced, rune(0x0008))
r += 1
case 't':
replaced = append(replaced, rune(0x0009))
r += 1
case 'n':
replaced = append(replaced, rune(0x000A))
r += 1
case 'f':
replaced = append(replaced, rune(0x000C))
r += 1
case 'r':
replaced = append(replaced, rune(0x000D))
r += 1
case '"':
replaced = append(replaced, rune(0x0022))
r += 1
case '\\':
replaced = append(replaced, rune(0x005C))
r += 1
case 'u':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
replaced = append(replaced, escaped)
r += 5
case 'U':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
replaced = append(replaced, escaped)
r += 9
}
}
return string(replaced)
}
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
s := string(bs)
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
if err != nil {
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
"lexer claims it's OK: %s", s, err)
}
if !utf8.ValidRune(rune(hex)) {
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
}
return rune(hex)
}
func isStringType(ty itemType) bool {
return ty == itemString || ty == itemMultilineString ||
ty == itemRawString || ty == itemRawMultilineString
}
-1
View File
@@ -1 +0,0 @@
au BufWritePost *.go silent!make tags > /dev/null 2>&1
-91
View File
@@ -1,91 +0,0 @@
package toml
// tomlType represents any Go type that corresponds to a TOML type.
// While the first draft of the TOML spec has a simplistic type system that
// probably doesn't need this level of sophistication, we seem to be militating
// toward adding real composite types.
type tomlType interface {
typeString() string
}
// typeEqual accepts any two types and returns true if they are equal.
func typeEqual(t1, t2 tomlType) bool {
if t1 == nil || t2 == nil {
return false
}
return t1.typeString() == t2.typeString()
}
func typeIsHash(t tomlType) bool {
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
}
type tomlBaseType string
func (btype tomlBaseType) typeString() string {
return string(btype)
}
func (btype tomlBaseType) String() string {
return btype.typeString()
}
var (
tomlInteger tomlBaseType = "Integer"
tomlFloat tomlBaseType = "Float"
tomlDatetime tomlBaseType = "Datetime"
tomlString tomlBaseType = "String"
tomlBool tomlBaseType = "Bool"
tomlArray tomlBaseType = "Array"
tomlHash tomlBaseType = "Hash"
tomlArrayHash tomlBaseType = "ArrayHash"
)
// typeOfPrimitive returns a tomlType of any primitive value in TOML.
// Primitive values are: Integer, Float, Datetime, String and Bool.
//
// Passing a lexer item other than the following will cause a BUG message
// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime.
func (p *parser) typeOfPrimitive(lexItem item) tomlType {
switch lexItem.typ {
case itemInteger:
return tomlInteger
case itemFloat:
return tomlFloat
case itemDatetime:
return tomlDatetime
case itemString:
return tomlString
case itemMultilineString:
return tomlString
case itemRawString:
return tomlString
case itemRawMultilineString:
return tomlString
case itemBool:
return tomlBool
}
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
panic("unreachable")
}
// typeOfArray returns a tomlType for an array given a list of types of its
// values.
//
// In the current spec, if an array is homogeneous, then its type is always
// "Array". If the array is not homogeneous, an error is generated.
func (p *parser) typeOfArray(types []tomlType) tomlType {
// Empty arrays are cool.
if len(types) == 0 {
return tomlArray
}
theType := types[0]
for _, t := range types[1:] {
if !typeEqual(theType, t) {
p.panicf("Array contains values of type '%s' and '%s', but "+
"arrays must be homogeneous.", theType, t)
}
}
return tomlArray
}
-242
View File
@@ -1,242 +0,0 @@
package toml
// Struct field handling is adapted from code in encoding/json:
//
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the Go distribution.
import (
"reflect"
"sort"
"sync"
)
// A field represents a single field found in a struct.
type field struct {
name string // the name of the field (`toml` tag included)
tag bool // whether field has a `toml` tag
index []int // represents the depth of an anonymous field
typ reflect.Type // the type of the field
}
// byName sorts field by name, breaking ties with depth,
// then breaking ties with "name came from toml tag", then
// breaking ties with index sequence.
type byName []field
func (x byName) Len() int { return len(x) }
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byName) Less(i, j int) bool {
if x[i].name != x[j].name {
return x[i].name < x[j].name
}
if len(x[i].index) != len(x[j].index) {
return len(x[i].index) < len(x[j].index)
}
if x[i].tag != x[j].tag {
return x[i].tag
}
return byIndex(x).Less(i, j)
}
// byIndex sorts field by index sequence.
type byIndex []field
func (x byIndex) Len() int { return len(x) }
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byIndex) Less(i, j int) bool {
for k, xik := range x[i].index {
if k >= len(x[j].index) {
return false
}
if xik != x[j].index[k] {
return xik < x[j].index[k]
}
}
return len(x[i].index) < len(x[j].index)
}
// typeFields returns a list of fields that TOML should recognize for the given
// type. The algorithm is breadth-first search over the set of structs to
// include - the top struct and then any reachable anonymous structs.
func typeFields(t reflect.Type) []field {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
// Count of queued names for current level and the next.
count := map[reflect.Type]int{}
nextCount := map[reflect.Type]int{}
// Types already visited at an earlier level.
visited := map[reflect.Type]bool{}
// Fields found.
var fields []field
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, map[reflect.Type]int{}
for _, f := range current {
if visited[f.typ] {
continue
}
visited[f.typ] = true
// Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
opts := getOptions(sf.Tag)
if opts.skip {
continue
}
index := make([]int, len(f.index)+1)
copy(index, f.index)
index[len(f.index)] = i
ft := sf.Type
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
// Follow pointer.
ft = ft.Elem()
}
// Record found field and index sequence.
if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := opts.name != ""
name := opts.name
if name == "" {
name = sf.Name
}
fields = append(fields, field{name, tagged, index, ft})
if count[f.typ] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, fields[len(fields)-1])
}
continue
}
// Record new anonymous struct to explore in next round.
nextCount[ft]++
if nextCount[ft] == 1 {
f := field{name: ft.Name(), index: index, typ: ft}
next = append(next, f)
}
}
}
}
sort.Sort(byName(fields))
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with TOML tags are promoted.
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0]
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.name
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.name != name {
break
}
}
if advance == 1 { // Only one field with this name
out = append(out, fi)
continue
}
dominant, ok := dominantField(fields[i : i+advance])
if ok {
out = append(out, dominant)
}
}
fields = out
sort.Sort(byIndex(fields))
return fields
}
// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// TOML tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order. The winner
// must therefore be one with the shortest index length. Drop all
// longer entries, which is easy: just truncate the slice.
length := len(fields[0].index)
tagged := -1 // Index of first tagged field.
for i, f := range fields {
if len(f.index) > length {
fields = fields[:i]
break
}
if f.tag {
if tagged >= 0 {
// Multiple tagged fields at the same level: conflict.
// Return no field.
return field{}, false
}
tagged = i
}
}
if tagged >= 0 {
return fields[tagged], true
}
// All remaining fields have the same length. If there's more than one,
// we have a conflict (two fields named "X" at the same level) and we
// return no field.
if len(fields) > 1 {
return field{}, false
}
return fields[0], true
}
var fieldCache struct {
sync.RWMutex
m map[reflect.Type][]field
}
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
fieldCache.RLock()
f := fieldCache.m[t]
fieldCache.RUnlock()
if f != nil {
return f
}
// Compute fields without lock.
// Might duplicate effort but won't hold other computations back.
f = typeFields(t)
if f == nil {
f = []field{}
}
fieldCache.Lock()
if fieldCache.m == nil {
fieldCache.m = map[reflect.Type][]field{}
}
fieldCache.m[t] = f
fieldCache.Unlock()
return f
}