From 6d4bbb233e34ec0160ac0f8dcaa8fa01c5dbe459 Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Thu, 14 Jan 2021 20:58:59 +0500 Subject: [PATCH] Deep application refactoring, a lot of changes (#64) ### Changed - Package name changed from `mikrotik-hosts-parser` to `github.com/tarampampam/mikrotik-hosts-parser` - Config file location now is `./configs/config.yml` (instead `./serve.yml`) - Docker image now contains only one layer - More strict linter settings - Config file now contains only sources list and script generator options - Default values for the next `serve` sub-command flags: - For `--config` now is `%binary_file_dir%/configs/config.yml` (instead nothing) - For `--resources-dir` now is `%binary_file_dir%/web` (instead nothing) - For `--listen` flag now is `8080` (instead nothing) - For static files serving disabling you can set `--resources-dir` empty value (`""`) - Large performance improvements - HTTP requests log records contains request processing duration - Panics inside HTTP handlers now will be logged and JSON-formatted string will be returned (instead empty response) - Frontend dependencies updated ### Added - Docker healthcheck - Healthcheck sub-command (hidden in CLI help) that makes a simple HTTP request (with user-agent `HealthChecker/internal`) to the `http://127.0.0.1:8080/live` endpoint. Port number can be changed using `--port`, `-p` flag or `LISTEN_PORT` environment variable - Two caching engines (memory and redis) instead file-based cache - `serve` sub-command flags: - `--cache-ttl` for cache entries lifetime setting (examples: `50s`, `1h30m`); `30m` by default; environment variable: `CACHE_TTL` - `--caching-engine` for caching engine changing (`memory|redis`); `memory` by default; environment variable: `CACHING_ENGINE` - `--redis-dsn` for redis server URL setting; `redis://127.0.0.1:6379/0` by default; environment variable: `REDIS_DSN`. This flag is required only if `redis` caching engine is set - Global (available for all sub-commands) flags: - `--log-json` for logging using JSON format (`stderr`) - `--debug` for debug information for logging messages - `--verbose` for verbose output - Graceful shutdown support for `serve` sub-command - HTTP endpoints: - `/live` for liveness probe - `/ready` for readiness probe ### Removed - File-based cache support - HTTP `/api/routes` handler --- .github/workflows/tests.yml | 1 - .golangci.yml | 169 +- CHANGELOG.md | 37 + Dockerfile | 13 +- Makefile | 4 +- README.md | 6 +- cmd/mikrotik-hosts-parser/main.go | 31 +- cmd/mikrotik-hosts-parser/main_test.go | 38 +- codecov.yml | 2 + serve.yml => configs/config.yml | 29 +- docker-compose.yml | 47 +- go.mod | 16 +- go.sum | 404 +- internal/pkg/breaker/os_signal.go | 54 + internal/pkg/breaker/os_signal_test.go | 55 + internal/pkg/cache/cacher.go | 20 + internal/pkg/cache/docs.go | 2 + internal/pkg/cache/errors.go | 14 + internal/pkg/cache/inmemory.go | 165 + internal/pkg/cache/inmemory_test.go | 208 + internal/pkg/cache/redis.go | 88 + internal/pkg/cache/redis_test.go | 88 + internal/pkg/checkers/docs.go | 2 + internal/pkg/checkers/health.go | 56 + internal/pkg/checkers/health_test.go | 47 + internal/pkg/checkers/live.go | 10 + internal/pkg/checkers/live_test.go | 11 + internal/pkg/checkers/ready.go | 29 + internal/pkg/checkers/ready_test.go | 42 + internal/pkg/cli/healthcheck/command.go | 50 + internal/pkg/cli/healthcheck/command_test.go | 93 + internal/pkg/cli/root.go | 67 + internal/pkg/cli/root_test.go | 80 + internal/pkg/cli/serve/command.go | 177 + internal/pkg/cli/serve/command_test.go | 421 + internal/pkg/cli/serve/flags.go | 158 + internal/pkg/cli/version/command.go | 24 + internal/pkg/cli/version/command_test.go | 29 + internal/pkg/cmd/options.go | 11 - internal/pkg/cmd/options_test.go | 59 - internal/pkg/cmd/serve/command.go | 134 - internal/pkg/cmd/serve/command_test.go | 457 - internal/pkg/cmd/version/command.go | 16 - internal/pkg/cmd/version/command_test.go | 70 - internal/pkg/config/config.go | 84 + internal/pkg/config/config_test.go | 221 + internal/pkg/env/env.go | 37 + internal/pkg/env/env_test.go | 49 + internal/pkg/http/api/router_test.go | 47 - internal/pkg/http/api/routes.go | 34 - internal/pkg/http/api/settings.go | 94 - internal/pkg/http/api/settings_test.go | 115 - internal/pkg/http/api/version.go | 23 - internal/pkg/http/api/version_test.go | 32 - internal/pkg/http/fileserver/errors.go | 79 + internal/pkg/http/fileserver/errors_test.go | 87 + internal/pkg/http/fileserver/fileserver.go | 203 +- .../pkg/http/fileserver/fileserver_test.go | 352 +- .../pkg/http/handlers/api/settings/handler.go | 77 + .../handlers/api/settings/handler_test.go | 58 + .../pkg/http/handlers/api/version/handler.go | 25 + .../http/handlers/api/version/handler_test.go | 21 + .../pkg/http/handlers/generate/handler.go | 475 + .../http/handlers/generate/handler_test.go | 276 + internal/pkg/http/handlers/healthz/handler.go | 26 + .../pkg/http/handlers/healthz/handler_test.go | 38 + internal/pkg/http/middlewares.go | 14 - .../pkg/http/middlewares/logreq/logreq.go | 59 + .../http/middlewares/logreq/logreq_test.go | 117 + .../pkg/http/middlewares/nocache/nocache.go | 21 + .../http/middlewares/nocache/nocache_test.go | 33 + internal/pkg/http/middlewares/panic/panic.go | 67 + .../pkg/http/middlewares/panic/panic_test.go | 86 + internal/pkg/http/middlewares_test.go | 51 - internal/pkg/http/routes.go | 82 +- internal/pkg/http/routes_test.go | 109 - internal/pkg/http/script/cache.go | 15 - internal/pkg/http/script/http_client.go | 81 - internal/pkg/http/script/query_parameters.go | 136 - .../pkg/http/script/query_parameters_test.go | 279 - internal/pkg/http/script/source.go | 236 - internal/pkg/http/script/source_test.go | 157 - internal/pkg/http/server.go | 137 +- internal/pkg/http/server_test.go | 168 +- internal/pkg/logger/logger.go | 38 + internal/pkg/logger/logger_test.go | 79 + internal/pkg/settings/serve/settings.go | 123 - internal/pkg/settings/serve/settings_test.go | 340 - internal/pkg/version/version.go | 11 +- internal/pkg/version/version_test.go | 2 +- pkg/hostsfile/docs.go | 2 + pkg/hostsfile/hostname_validator.go | 9799 +++++++++++++++++ pkg/hostsfile/hostsfile.go | 11 - pkg/hostsfile/hostsfile_test.go | 22 - pkg/hostsfile/parser.go | 186 + pkg/hostsfile/parser/parser.go | 103 - pkg/hostsfile/parser/parser_test.go | 396 - pkg/hostsfile/parser_test.go | 198 + pkg/hostsfile/record.go | 8 + pkg/mikrotik/dns/static_entry.go | 135 - .../dns/static_entry_internal_test.go | 253 - pkg/mikrotik/dns_static_entries.go | 47 + pkg/mikrotik/dns_static_entries_test.go | 145 + pkg/mikrotik/dns_static_entry.go | 75 + pkg/mikrotik/dns_static_entry_test.go | 113 + pkg/mikrotik/docs.go | 2 + pkg/mikrotik/errors.go | 18 + pkg/mikrotik/errors_test.go | 36 + test/testdata/hosts/foo.txt | 23 + web/404.html | 133 - web/__error__.html | 29 + web/components/app.vue | 12 +- web/index.html | 15 +- 113 files changed, 15958 insertions(+), 4231 deletions(-) create mode 100644 codecov.yml rename serve.yml => configs/config.yml (74%) create mode 100644 internal/pkg/breaker/os_signal.go create mode 100644 internal/pkg/breaker/os_signal_test.go create mode 100644 internal/pkg/cache/cacher.go create mode 100644 internal/pkg/cache/docs.go create mode 100644 internal/pkg/cache/errors.go create mode 100644 internal/pkg/cache/inmemory.go create mode 100644 internal/pkg/cache/inmemory_test.go create mode 100644 internal/pkg/cache/redis.go create mode 100644 internal/pkg/cache/redis_test.go create mode 100644 internal/pkg/checkers/docs.go create mode 100644 internal/pkg/checkers/health.go create mode 100644 internal/pkg/checkers/health_test.go create mode 100644 internal/pkg/checkers/live.go create mode 100644 internal/pkg/checkers/live_test.go create mode 100644 internal/pkg/checkers/ready.go create mode 100644 internal/pkg/checkers/ready_test.go create mode 100644 internal/pkg/cli/healthcheck/command.go create mode 100644 internal/pkg/cli/healthcheck/command_test.go create mode 100644 internal/pkg/cli/root.go create mode 100644 internal/pkg/cli/root_test.go create mode 100644 internal/pkg/cli/serve/command.go create mode 100644 internal/pkg/cli/serve/command_test.go create mode 100644 internal/pkg/cli/serve/flags.go create mode 100644 internal/pkg/cli/version/command.go create mode 100644 internal/pkg/cli/version/command_test.go delete mode 100644 internal/pkg/cmd/options.go delete mode 100644 internal/pkg/cmd/options_test.go delete mode 100644 internal/pkg/cmd/serve/command.go delete mode 100644 internal/pkg/cmd/serve/command_test.go delete mode 100644 internal/pkg/cmd/version/command.go delete mode 100644 internal/pkg/cmd/version/command_test.go create mode 100644 internal/pkg/config/config.go create mode 100644 internal/pkg/config/config_test.go create mode 100644 internal/pkg/env/env.go create mode 100644 internal/pkg/env/env_test.go delete mode 100644 internal/pkg/http/api/router_test.go delete mode 100644 internal/pkg/http/api/routes.go delete mode 100644 internal/pkg/http/api/settings.go delete mode 100644 internal/pkg/http/api/settings_test.go delete mode 100644 internal/pkg/http/api/version.go delete mode 100644 internal/pkg/http/api/version_test.go create mode 100644 internal/pkg/http/fileserver/errors.go create mode 100644 internal/pkg/http/fileserver/errors_test.go create mode 100644 internal/pkg/http/handlers/api/settings/handler.go create mode 100644 internal/pkg/http/handlers/api/settings/handler_test.go create mode 100644 internal/pkg/http/handlers/api/version/handler.go create mode 100644 internal/pkg/http/handlers/api/version/handler_test.go create mode 100644 internal/pkg/http/handlers/generate/handler.go create mode 100644 internal/pkg/http/handlers/generate/handler_test.go create mode 100644 internal/pkg/http/handlers/healthz/handler.go create mode 100644 internal/pkg/http/handlers/healthz/handler_test.go delete mode 100644 internal/pkg/http/middlewares.go create mode 100644 internal/pkg/http/middlewares/logreq/logreq.go create mode 100644 internal/pkg/http/middlewares/logreq/logreq_test.go create mode 100644 internal/pkg/http/middlewares/nocache/nocache.go create mode 100644 internal/pkg/http/middlewares/nocache/nocache_test.go create mode 100644 internal/pkg/http/middlewares/panic/panic.go create mode 100644 internal/pkg/http/middlewares/panic/panic_test.go delete mode 100644 internal/pkg/http/middlewares_test.go delete mode 100644 internal/pkg/http/routes_test.go delete mode 100644 internal/pkg/http/script/cache.go delete mode 100644 internal/pkg/http/script/http_client.go delete mode 100644 internal/pkg/http/script/query_parameters.go delete mode 100644 internal/pkg/http/script/query_parameters_test.go delete mode 100644 internal/pkg/http/script/source.go delete mode 100644 internal/pkg/http/script/source_test.go create mode 100644 internal/pkg/logger/logger.go create mode 100644 internal/pkg/logger/logger_test.go delete mode 100644 internal/pkg/settings/serve/settings.go delete mode 100644 internal/pkg/settings/serve/settings_test.go create mode 100644 pkg/hostsfile/docs.go create mode 100644 pkg/hostsfile/hostname_validator.go delete mode 100644 pkg/hostsfile/hostsfile.go delete mode 100644 pkg/hostsfile/hostsfile_test.go create mode 100644 pkg/hostsfile/parser.go delete mode 100644 pkg/hostsfile/parser/parser.go delete mode 100644 pkg/hostsfile/parser/parser_test.go create mode 100644 pkg/hostsfile/parser_test.go create mode 100644 pkg/hostsfile/record.go delete mode 100644 pkg/mikrotik/dns/static_entry.go delete mode 100644 pkg/mikrotik/dns/static_entry_internal_test.go create mode 100644 pkg/mikrotik/dns_static_entries.go create mode 100644 pkg/mikrotik/dns_static_entries_test.go create mode 100644 pkg/mikrotik/dns_static_entry.go create mode 100644 pkg/mikrotik/dns_static_entry_test.go create mode 100644 pkg/mikrotik/docs.go create mode 100644 pkg/mikrotik/errors.go create mode 100644 pkg/mikrotik/errors_test.go create mode 100644 test/testdata/hosts/foo.txt delete mode 100644 web/404.html create mode 100644 web/__error__.html diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 206db9b1..dba77a64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,7 +65,6 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} file: /tmp/coverage.txt - fail_ci_if_error: false build: name: Build for ${{ matrix.os }} diff --git a/.golangci.yml b/.golangci.yml index be4f37d7..f464e88c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,83 +1,132 @@ -# TODO Update linter config: +# Documentation: + +run: + timeout: 1m + skip-dirs: + - .github + - .git + modules-download-mode: readonly + allow-parallel-runners: true + +output: + format: colored-line-number # colored-line-number|line-number|json|tab|checkstyle|code-climate linters-settings: govet: check-shadowing: true + enable-all: true golint: - min-confidence: 0 + min-confidence: 0.3 gocyclo: min-complexity: 15 - maligned: - suggest-new: true + godot: + scope: declarations # declarations|toplevel|all + capital: true dupl: threshold: 100 goconst: min-len: 2 - min-occurrences: 2 + min-occurrences: 3 misspell: locale: US - ignore-words: - - routeros lll: - line-length: 140 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - whyNoLint - - wrapperFunc - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - octalLiteral - funlen: - lines: 100 - statements: 50 + line-length: 120 + maligned: + suggest-new: true + prealloc: + simple: true + range-loops: true + for-loops: true + nolintlint: + allow-leading-space: false + require-specific: true -linters: +linters: # All available linters list: disable-all: true enable: - - bodyclose - - deadcode - - depguard - - dogsled - - dupl - - errcheck - - funlen - - gochecknoinits - - goconst - - gocritic - - gocyclo - - gofmt - - goimports - - golint - - gosec - - gosimple - - govet - - ineffassign - - interfacer - - lll - - misspell - - nakedret - - scopelint - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - varcheck - - whitespace - - godox + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers + - bodyclose # Checks whether HTTP response body is closed successfully + - deadcode # Finds unused code + - depguard # Go linter that checks if package imports are in a list of acceptable packages + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + - dupl # Tool for code clone detection + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases + - errorlint # find code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - exhaustive # check exhaustiveness of enum switch statements + #- exhaustivestruct # Checks if all struct's fields are initialized + - exportloopref # checks for pointers to enclosing loop variables + - funlen # Tool for detection of long functions + #- gci # Gci control golang package import order and make it always deterministic + - gochecknoglobals # Checks that no globals are present in Go code + - gochecknoinits # Checks that no init functions are present in Go code + - gocognit # Computes and checks the cognitive complexity of functions + - goconst # Finds repeated strings that could be replaced by a constant + - gocritic # The most opinionated Go source code linter + - gocyclo # Computes and checks the cyclomatic complexity of functions + - godox # Tool for detection of FIXME, TODO and other comment keywords + #- goerr113 # Golang linter to check the errors handling expressions + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification + #- gofumpt # Gofumpt checks whether code was gofumpt-ed + #- goheader # Checks is file header matches to pattern + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports + - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + - gomnd # An analyzer to detect magic numbers + #- gomodguard # Allow and block list linter for direct Go module dependencies + - goprintffuncname # Checks that printf-like functions are named with `f` at the end + - gosec # Inspects source code for security problems + - gosimple # Linter for Go source code that specializes in simplifying a code + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # Detects when assignments to existing variables are not used + - interfacer # Linter that suggests narrower interface types + - lll # Reports long lines + - maligned # Tool to detect Go structs that would take less memory if their fields were sorted + - misspell # Finds commonly misspelled English words in comments + - nakedret # Finds naked returns in functions greater than a specified function length + - nestif # Reports deeply nested if statements + - nlreturn # checks for a new line before return and branch statements to increase code clarity + - noctx # finds sending http request without context.Context + - nolintlint # Reports ill-formed or insufficient nolint directives + #- paralleltest # detects missing usage of t.Parallel() method in your Go test + - prealloc # Finds slice declarations that could potentially be preallocated + - rowserrcheck # Checks whether Err of rows is checked successfully + - scopelint # Scopelint checks for unpinned variables in go programs + #- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks + - structcheck # Finds unused struct fields + - stylecheck # Stylecheck is a replacement for golint + #- testpackage # linter that makes you use a separate _test package + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code + - unconvert # Remove unnecessary type conversions + - unparam # Reports unused function parameters + - unused # Checks Go code for unused constants, variables, functions and types + - varcheck # Finds unused global variables and constants + - whitespace # Tool for detection of leading and trailing whitespace + #- wrapcheck # Checks that errors returned from external packages are wrapped + - wsl # Whitespace Linter - Forces you to use empty lines! + - godot # Check if comments end in a period issues: + exclude-use-default: false + exclude: + # EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok + - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked + # EXC0003 golint: False positive when tests are defined in package 'test' + #- func name will be used as test\.Test.* by other packages, and that stutters; consider calling this + # EXC0006 gosec: Too many false-positives on 'unsafe' usage + - Use of unsafe calls should be audited + # EXC0007 gosec: Too many false-positives for parametrized shell calls + - Subprocess launch(ed with variable|ing should be audited) + # EXC0008 gosec: Duplicated errcheck checks + - (G104|G307) + # EXC0009 gosec: Too many issues in popular repos + - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) + # EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' + - Potential file inclusion via variable + max-same-issues: 0 # Maximum count of issues with the same text. Set to 0 to disable. Default is 3 exclude-rules: - path: _test\.go linters: - - scopelint - funlen - - godox + - gocognit + - noctx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be753fe..497722ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,45 @@ The format is based on [Keep a Changelog][keepachangelog] and this project adher - GitHub actions updated - Docker image based on `scratch` (instead `alpine` image) - Go version updated from `1.13` up to `1.15` +- Package name changed from `mikrotik-hosts-parser` to `github.com/tarampampam/mikrotik-hosts-parser` - Directory `public` renamed to `web` +- Config file location now is `./configs/config.yml` (instead `./serve.yml`) - App packages refactored +- Docker image now contains only one layer +- More strict linter settings +- Config file now contains only sources list and script generator options +- Default values for the next `serve` sub-command flags: + - For `--config` now is `%binary_file_dir%/configs/config.yml` (instead nothing) + - For `--resources-dir` now is `%binary_file_dir%/web` (instead nothing) + - For `--listen` flag now is `8080` (instead nothing) +- For static files serving disabling you can set `--resources-dir` empty value (`""`) +- Large performance improvements +- HTTP requests log records contains request processing duration +- Panics inside HTTP handlers now will be logged and JSON-formatted string will be returned (instead empty response) +- Frontend dependencies updated + +### Added + +- Docker healthcheck +- Healthcheck sub-command (hidden in CLI help) that makes a simple HTTP request (with user-agent `HealthChecker/internal`) to the `http://127.0.0.1:8080/live` endpoint. Port number can be changed using `--port`, `-p` flag or `LISTEN_PORT` environment variable +- Two caching engines (memory and redis) instead file-based cache +- `serve` sub-command flags: + - `--cache-ttl` for cache entries lifetime setting (examples: `50s`, `1h30m`); `30m` by default; environment variable: `CACHE_TTL` + - `--caching-engine` for caching engine changing (`memory|redis`); `memory` by default; environment variable: `CACHING_ENGINE` + - `--redis-dsn` for redis server URL setting; `redis://127.0.0.1:6379/0` by default; environment variable: `REDIS_DSN`. This flag is required only if `redis` caching engine is set +- Global (available for all sub-commands) flags: + - `--log-json` for logging using JSON format (`stderr`) + - `--debug` for debug information for logging messages + - `--verbose` for verbose output +- Graceful shutdown support for `serve` sub-command +- HTTP endpoints: + - `/live` for liveness probe + - `/ready` for readiness probe + +### Removed + +- File-based cache support +- HTTP `/api/routes` handler ## v3.0.3 diff --git a/Dockerfile b/Dockerfile index c4154cd7..f977f469 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,9 +36,10 @@ RUN set -x \ && mkdir -p /tmp/rootfs/etc/ssl \ && mkdir -p /tmp/rootfs/bin \ && mkdir -p /tmp/rootfs/opt/mikrotik-hosts-parser \ + && mkdir -p --mode=777 /tmp/rootfs/tmp \ && cp -R /etc/ssl/certs /tmp/rootfs/etc/ssl/certs \ && cp -R /src/web /tmp/rootfs/opt/mikrotik-hosts-parser/web \ - && cp /src/serve.yml /tmp/rootfs/etc/serve.yml \ + && cp /src/configs/config.yml /tmp/rootfs/etc/config.yml \ && echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > /tmp/rootfs/etc/passwd \ && mv /tmp/mikrotik-hosts-parser /tmp/rootfs/bin/mikrotik-hosts-parser @@ -62,12 +63,18 @@ COPY --from=builder /tmp/rootfs / # Use an unprivileged user USER appuser +# Docs: +HEALTHCHECK --interval=15s --timeout=3s --start-period=1s CMD [ \ + "/bin/mikrotik-hosts-parser", "healthcheck", \ + "--port", "8080" \ +] + ENTRYPOINT ["/bin/mikrotik-hosts-parser"] CMD [ \ "serve", \ - "--config", "/etc/serve.yml", \ - "--listen", "0.0.0.0", \ + "--log-json", \ + "--config", "/etc/config.yml", \ "--port", "8080", \ "--resources-dir", "/opt/mikrotik-hosts-parser/web" \ ] diff --git a/Makefile b/Makefile index a636f927..7ed1aec9 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ lint: ## Run app linters docker-compose run --rm --no-deps golint golangci-lint run gotest: ## Run app tests - docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 5s ./... + docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 10s ./... test: lint gotest ## Run app tests and linters @@ -46,7 +46,7 @@ cover: ## Run app tests with coverage report -sensible-browser ./coverage.html && sleep 2 && rm -f ./coverage.html up: ## Create and start containers - docker-compose up --detach --build web + docker-compose up --detach web @printf "\n \e[30;42m %s \033[0m\n\n" 'Navigate your browser to ⇒ http://127.0.0.1:8080'; down: ## Stop and remove containers, networks, images, and volumes diff --git a/README.md b/README.md index 144f10ce..27bd223f 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,19 @@ More information can be [found here][link_habr_post]. ## Usage +// TODO redis server can be used from `redis.io` + For local application starting using binary file, you must compile application _(after repository cloning)_ using `GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ./mikrotik-hosts-parser ./cmd/mikrotik-hosts-parser/` or `make build`, and then execute: ```bash $ ./mikrotik-hosts-parser serve \ - --config ./serve.yml \ + --config ./config.yml \ --listen 0.0.0.0 \ --port 8080 \ --resources-dir ./web ``` -This command will start HTTP server using configuration from `./serve.yml` on port `8080` and use directory `./web` for serving static files. Configuration file well-documented, so, feel free to change any settings on your choice! +This command will start HTTP server using configuration from `./config.yml` on port `8080` and use directory `./web` for serving static files. Configuration file well-documented, so, feel free to change any settings on your choice! > Configuration file allows you to use environment variables with default values! diff --git a/cmd/mikrotik-hosts-parser/main.go b/cmd/mikrotik-hosts-parser/main.go index 742353f9..fc883d12 100644 --- a/cmd/mikrotik-hosts-parser/main.go +++ b/cmd/mikrotik-hosts-parser/main.go @@ -1,19 +1,30 @@ +// Main CLI application entrypoint. package main import ( "os" + "path/filepath" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cmd" - - "github.com/jessevdk/go-flags" + "github.com/fatih/color" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cli" ) -func main() { - // parse the arguments - if _, err := flags.NewParser(&cmd.Options{}, flags.Default).Parse(); err != nil { - // make error type checking - if e, ok := err.(*flags.Error); (ok && e.Type != flags.ErrHelp) || !ok { - os.Exit(1) - } +// exitFn is a function for application exiting. +var exitFn = os.Exit //nolint:gochecknoglobals + +// main CLI application entrypoint. +func main() { exitFn(run()) } + +// run this CLI application. +// Exit codes documentation: +func run() int { + cmd := cli.NewCommand(filepath.Base(os.Args[0])) + + if err := cmd.Execute(); err != nil { + _, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error()) + + return 1 } + + return 0 } diff --git a/cmd/mikrotik-hosts-parser/main_test.go b/cmd/mikrotik-hosts-parser/main_test.go index 66345242..168a7c13 100644 --- a/cmd/mikrotik-hosts-parser/main_test.go +++ b/cmd/mikrotik-hosts-parser/main_test.go @@ -1,7 +1,41 @@ package main -import "testing" +import ( + "os" + "testing" + + "github.com/kami-zh/go-capturer" + "github.com/stretchr/testify/assert" +) func Test_Main(t *testing.T) { - t.Skip("Not implemented yet") // @todo: implement + os.Args = []string{"", "--help"} + exitFn = func(code int) { assert.Equal(t, 0, code) } + + output := capturer.CaptureStdout(main) + + assert.Contains(t, output, "Usage:") + assert.Contains(t, output, "Available Commands:") + assert.Contains(t, output, "Flags:") +} + +func Test_MainWithoutCommands(t *testing.T) { + os.Args = []string{""} + exitFn = func(code int) { assert.Equal(t, 0, code) } + + output := capturer.CaptureStdout(main) + + assert.Contains(t, output, "Usage:") + assert.Contains(t, output, "Available Commands:") + assert.Contains(t, output, "Flags:") +} + +func Test_MainUnknownSubcommand(t *testing.T) { + os.Args = []string{"", "foobar"} + exitFn = func(code int) { assert.Equal(t, 1, code) } + + output := capturer.CaptureStderr(main) + + assert.Contains(t, output, "unknown command") + assert.Contains(t, output, "foobar") } diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..5df13a65 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "pkg/hostsfile/hostname_validator.go" # generated file diff --git a/serve.yml b/configs/config.yml similarity index 74% rename from serve.yml rename to configs/config.yml index 22fb6f97..ce8ce32d 100644 --- a/serve.yml +++ b/configs/config.yml @@ -1,19 +1,3 @@ -# http server configuration -listen: - # hostname or IP address for listening - address: ${LISTEN_ADDR:-0.0.0.0} - # port number to listen - port: ${LISTEN_PORT:-8080} - -# public resources settings -resources: - # path to the directory with public assets - dir: ${RESOURCES_DIR} - # index file name (content from this file will be used as directory index) - index_name: index.html - # file name with 404 error page content - error_404_name: 404.html - # provided sources configuration (for usage in frontend using API request) sources: - uri: https://cdn.jsdelivr.net/gh/tarampampam/mikrotik-hosts-parser@master/.hosts/basic.txt @@ -55,21 +39,12 @@ sources: enabled: false count: 46000 -# caching settings -cache: - # file-based cache - files: - # path to the directory for cache files - dir: ${TMPDIR:-/tmp} - # cached entries lifetime duration (in seconds) - lifetime_sec: ${CACHE_LIFETIME_SEC:-1800} - -# router script generation settings +# router script generation config router_script: # default requests redirection redirect: address: 127.0.0.1 - # default excluding settings + # default excluding config exclude: # this hosts will be excluded by default (will be set only in UI) hosts: diff --git a/docker-compose.yml b/docker-compose.yml index 3b7122f5..3dcefb62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,34 +1,47 @@ -version: '3.2' +version: '3.4' volumes: tmp-data: + redis-data: + golint-cache: + +x-app: &app-service # application service template + image: golang:1.15-buster # Image page: + working_dir: /src + environment: + HOME: /tmp + GOPATH: /tmp + volumes: + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + - .:/src:rw + - tmp-data:/tmp:rw services: app: - image: golang:1.15-buster # Image page: - working_dir: /src - environment: - HOME: /tmp - GOPATH: /tmp - volumes: - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - - .:/src:cached - - tmp-data:/tmp:cached + <<: *app-service web: - build: - context: . - dockerfile: Dockerfile + <<: *app-service ports: - - 8080:8080/tcp + - '8080:8080/tcp' # Open + command: go run ./cmd/mikrotik-hosts-parser serve --config ./configs/config.yml --resources-dir ./web #--caching-engine redis --redis-dsn "redis://redis:6379/0" + depends_on: + - redis + + redis: + image: redis:6.0.9-alpine # Image page: volumes: - - ./serve.yml:/etc/serve.yml:ro - - ./web:/opt/web:ro + - redis-data:/data:cached + ports: + - 6379 golint: image: golangci/golangci-lint:v1.33-alpine # Image page: + environment: + GOLANGCI_LINT_CACHE: /tmp/golint # volumes: - .:/src:ro + - golint-cache:/tmp/golint:rw working_dir: /src command: /bin/true diff --git a/go.mod b/go.mod index 5878829c..eb52f169 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,16 @@ go 1.15 require ( github.com/a8m/envsubst v1.2.0 - github.com/gorilla/handlers v1.5.1 + github.com/alicebob/miniredis/v2 v2.14.1 + github.com/fatih/color v1.10.0 + github.com/felixge/httpsnoop v1.0.1 + github.com/go-redis/redis/v8 v8.4.8 github.com/gorilla/mux v1.8.0 - github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4 - github.com/kr/pretty v0.2.0 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/tarampampam/go-filecache v1.0.2 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d + github.com/spf13/cobra v1.1.1 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.6.1 + github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect + go.uber.org/zap v1.16.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 2c3f626e..979c8dca 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,402 @@ +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/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/a8m/envsubst v1.2.0 h1:yvzAhJD2QKdo35Ut03wIfXQmg+ta3wC/1bskfZynz+Q= github.com/a8m/envsubst v1.2.0/go.mod h1:PpvLvNWa+Rvu/10qXmFbFiGICIU5hZvFJNPCCkUaObg= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.14.1 h1:GjlbSeoJ24bzdLRs13HoMEeaRZx9kg5nHoRW7QV/nCs= +github.com/alicebob/miniredis/v2 v2.14.1/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/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/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +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-redis/redis/v8 v8.4.8 h1:sEG4g6Jq4hvQzbrNsVDNTDdxFCUnFC0jxuOp6tgALlA= +github.com/go-redis/redis/v8 v8.4.8/go.mod h1:/cTZsrSn1DPqRuOnSDuyH2OSvd9iX0iUGT0s7hYGIAg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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/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.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.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +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.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4 h1:xKkUL6QBojwguhKKetf1SocCAKqc6W7S/mGm9xEGllo= -github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +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/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +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/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0= +github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/tarampampam/go-filecache v1.0.2 h1:vTA6yDzsRJBqrGFPAa30bvxgeN6JOp5751iZNdF5FV4= -github.com/tarampampam/go-filecache v1.0.2/go.mod h1:qmQUDSPdLOVN6w3T0LdLysnAjfPpMk5w3/arpKWGFcs= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= +github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +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/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= +github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg= +github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw= +go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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/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 h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +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/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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20190204203706-41f3e6584952/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-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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-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-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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/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/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/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/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.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +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.8/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= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/pkg/breaker/os_signal.go b/internal/pkg/breaker/os_signal.go new file mode 100644 index 00000000..41d532a6 --- /dev/null +++ b/internal/pkg/breaker/os_signal.go @@ -0,0 +1,54 @@ +// Package breaker provides OSSignals struct for OS signals handling (with context). +package breaker + +import ( + "context" + "os" + "os/signal" + "syscall" +) + +// OSSignals allows to subscribe for system signals. +type OSSignals struct { + ctx context.Context + ch chan os.Signal +} + +// NewOSSignals creates new subscriber for system signals. +func NewOSSignals(ctx context.Context) OSSignals { + return OSSignals{ + ctx: ctx, + ch: make(chan os.Signal, 1), + } +} + +// Subscribe for some of system signals (call Stop for stopping). +func (oss *OSSignals) Subscribe(onSignal func(os.Signal), signals ...os.Signal) { + if len(signals) == 0 { + signals = []os.Signal{os.Interrupt, syscall.SIGINT, syscall.SIGTERM} // default signals + } + + signal.Notify(oss.ch, signals...) + + go func(ch <-chan os.Signal) { + select { + case <-oss.ctx.Done(): + break + + case sig, opened := <-ch: + if oss.ctx.Err() != nil { + break + } + + if opened && sig != nil { + onSignal(sig) + } + } + }(oss.ch) +} + +// Stop system signals listening. +func (oss *OSSignals) Stop() { + signal.Stop(oss.ch) + close(oss.ch) +} diff --git a/internal/pkg/breaker/os_signal_test.go b/internal/pkg/breaker/os_signal_test.go new file mode 100644 index 00000000..d96432ea --- /dev/null +++ b/internal/pkg/breaker/os_signal_test.go @@ -0,0 +1,55 @@ +package breaker + +import ( + "context" + "os" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewOSSignals(t *testing.T) { + oss := NewOSSignals(context.Background()) + + gotSignal := make(chan os.Signal, 1) + + oss.Subscribe(func(signal os.Signal) { + gotSignal <- signal + }, syscall.SIGUSR2) + + defer oss.Stop() + + proc, err := os.FindProcess(os.Getpid()) + assert.NoError(t, err) + + assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal + + time.Sleep(time.Millisecond * 5) + + assert.Equal(t, syscall.SIGUSR2, <-gotSignal) +} + +func TestNewOSSignalCtxCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + oss := NewOSSignals(ctx) + + gotSignal := make(chan os.Signal, 1) + + oss.Subscribe(func(signal os.Signal) { + gotSignal <- signal + }, syscall.SIGUSR2) + + defer oss.Stop() + + proc, err := os.FindProcess(os.Getpid()) + assert.NoError(t, err) + + cancel() + + assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal + + assert.Empty(t, gotSignal) +} diff --git a/internal/pkg/cache/cacher.go b/internal/pkg/cache/cacher.go new file mode 100644 index 00000000..543c894c --- /dev/null +++ b/internal/pkg/cache/cacher.go @@ -0,0 +1,20 @@ +package cache + +import ( + "time" +) + +// Cacher is a byte-based cache with TTL. +type Cacher interface { + // TTL returns current cache values time-to-live. + TTL() time.Duration + + // Get value associated with the key from the storage. + Get(key string) (found bool, data []byte, ttl time.Duration, err error) + + // Put value into the storage. + Put(key string, data []byte) error + + // Delete value from the storage with passed key. + Delete(key string) (bool, error) +} diff --git a/internal/pkg/cache/docs.go b/internal/pkg/cache/docs.go new file mode 100644 index 00000000..20ae27f7 --- /dev/null +++ b/internal/pkg/cache/docs.go @@ -0,0 +1,2 @@ +// Package cache contains different byte-cache implementations. +package cache diff --git a/internal/pkg/cache/errors.go b/internal/pkg/cache/errors.go new file mode 100644 index 00000000..eaaeda58 --- /dev/null +++ b/internal/pkg/cache/errors.go @@ -0,0 +1,14 @@ +package cache + +import "errors" + +var ( + // ErrEmptyKey means "empty key is used". + ErrEmptyKey = errors.New("empty key") + + // ErrEmptyData means "no data provided". + ErrEmptyData = errors.New("empty data") + + // ErrClosed means "cache was closed". + ErrClosed = errors.New("cache closed") +) diff --git a/internal/pkg/cache/inmemory.go b/internal/pkg/cache/inmemory.go new file mode 100644 index 00000000..0bb1b2ed --- /dev/null +++ b/internal/pkg/cache/inmemory.go @@ -0,0 +1,165 @@ +package cache + +import ( + "sync" + "time" +) + +type ( + // InMemoryCache is an inmemory cache (with TTL) implementation. + InMemoryCache struct { + ttl time.Duration + ci time.Duration // cleanup interval + + storageMu sync.RWMutex + storage map[string]inmemoryItem + + close chan struct{} + closedMu sync.RWMutex + closed bool + } + + inmemoryItem struct { + data []byte + expiresAtNano int64 + } +) + +// NewInMemoryCache creates inmemory storage with TTL. +func NewInMemoryCache(ttl time.Duration, ci time.Duration) *InMemoryCache { + cache := &InMemoryCache{ttl: ttl, ci: ci, storage: make(map[string]inmemoryItem), close: make(chan struct{}, 1)} + go cache.cleanup() + + return cache +} + +func (c *InMemoryCache) cleanup() { + defer close(c.close) + + timer := time.NewTimer(c.ci) + defer timer.Stop() + + for { + select { + case <-c.close: + c.storageMu.Lock() + for key := range c.storage { + delete(c.storage, key) + } + c.storageMu.Unlock() + + return + + case <-timer.C: + c.storageMu.Lock() + var now = time.Now().UnixNano() + + for key, item := range c.storage { + if now > item.expiresAtNano { + delete(c.storage, key) + } + } + c.storageMu.Unlock() + + timer.Reset(c.ci) + } + } +} + +func (c *InMemoryCache) isClosed() bool { + c.closedMu.RLock() + defer c.closedMu.RUnlock() + + return c.closed +} + +// Close current in memory storage with data invalidation. +func (c *InMemoryCache) Close() error { + if c.isClosed() { + return ErrClosed + } + + c.closedMu.Lock() + c.closed = true + c.closedMu.Unlock() + + c.close <- struct{}{} + + return nil +} + +// TTL returns current cache values time-to-live. +func (c *InMemoryCache) TTL() time.Duration { return c.ttl } + +// Get value associated with the key from the storage. +func (c *InMemoryCache) Get(key string) (bool, []byte, time.Duration, error) { + if c.isClosed() { + return false, nil, 0, ErrClosed + } + + if key == "" { + return false, nil, 0, ErrEmptyKey + } + + c.storageMu.RLock() + item, ok := c.storage[key] + c.storageMu.RUnlock() + + if ok { + now := time.Now() + + // item has been expired? + if now.UnixNano() > item.expiresAtNano { + c.storageMu.Lock() + delete(c.storage, key) + c.storageMu.Unlock() + + return false, nil, 0, nil + } + + return true, item.data, time.Unix(0, item.expiresAtNano).Sub(now), nil + } + + return false, nil, 0, nil +} + +// Put value into the storage. +func (c *InMemoryCache) Put(key string, data []byte) error { + if c.isClosed() { + return ErrClosed + } + + if key == "" { + return ErrEmptyKey + } else if len(data) == 0 { + return ErrEmptyData + } + + c.storageMu.Lock() + c.storage[key] = inmemoryItem{data: data, expiresAtNano: time.Now().Add(c.ttl).UnixNano()} + c.storageMu.Unlock() + + return nil +} + +// Delete value from the storage with passed key. +func (c *InMemoryCache) Delete(key string) (bool, error) { + if c.isClosed() { + return false, ErrClosed + } + + if key == "" { + return false, ErrEmptyKey + } + + c.storageMu.Lock() + defer c.storageMu.Unlock() + + if _, ok := c.storage[key]; ok { + delete(c.storage, key) + + return true, nil + } + + return false, nil +} diff --git a/internal/pkg/cache/inmemory_test.go b/internal/pkg/cache/inmemory_test.go new file mode 100644 index 00000000..71ae9525 --- /dev/null +++ b/internal/pkg/cache/inmemory_test.go @@ -0,0 +1,208 @@ +package cache + +import ( + "context" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +/* +func BenchmarkInMemoryCache_Put(b *testing.B) { + b.ReportAllocs() + + cache := NewInMemoryCache(time.Minute, time.Second) + defer cache.Close() + + data := []byte(strings.Repeat("xxxxxxxxx", 102400)) + + for n := 0; n < b.N; n++ { + cache.Put("foo" + strconv.Itoa(n), data) + } +} +*/ + +func TestInMemoryCache_GetPutDelete(t *testing.T) { + cache := NewInMemoryCache(time.Minute, time.Second) + defer cache.Close() + + const testKeyName = "foo" + + // try to get non-existing entry + found, data, ttl, err := cache.Get(testKeyName) + assert.False(t, found) + assert.Nil(t, data) + assert.Zero(t, ttl) + assert.NoError(t, err) + + // put valid value with the same key + assert.NoError(t, cache.Put(testKeyName, []byte{1, 2, 3})) + + // and now all must be fine + found, data, ttl, err = cache.Get(testKeyName) + assert.True(t, found) + assert.Equal(t, []byte{1, 2, 3}, data) + assert.InDelta(t, time.Minute.Milliseconds(), ttl.Milliseconds(), 3) + assert.NoError(t, err) + + // delete the key + deleted, err := cache.Delete(testKeyName) + assert.True(t, deleted) + assert.NoError(t, err) + + // try to delete non-existing key + deleted, err = cache.Delete(testKeyName) + assert.False(t, deleted) + assert.NoError(t, err) +} + +func TestInMemoryCache_CloserInterface(t *testing.T) { + var cache Cacher //nolint:gosimple + + cache = NewInMemoryCache(time.Minute, time.Second) + defer cache.(io.Closer).Close() + + _, ok := cache.(io.Closer) + + assert.True(t, ok) +} + +func TestInMemoryCache_Expiration(t *testing.T) { + const testKeyName = "foo" + + cache := NewInMemoryCache(time.Millisecond*100, time.Millisecond) + defer cache.Close() + + assert.NoError(t, cache.Put(testKeyName, []byte{1, 2, 3})) + + found, _, _, _ := cache.Get(testKeyName) //nolint:dogsled + assert.True(t, found) + + <-time.After(time.Millisecond * 98) + + found, _, _, _ = cache.Get(testKeyName) //nolint:dogsled + assert.True(t, found) + + <-time.After(time.Millisecond * 3) + + found, _, _, _ = cache.Get(testKeyName) //nolint:dogsled + assert.False(t, found) +} + +func TestInMemoryCache_ConcurrentAccess(t *testing.T) { + cache := NewInMemoryCache(time.Minute, time.Microsecond) + defer cache.Close() + + testCtx, testCancel := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-testCtx.Done(): + return + default: + _, _, _, _ = cache.Get("foo") //nolint:dogsled + } + } + }() + + go func() { + for { + select { + case <-testCtx.Done(): + return + default: + _, _ = cache.Delete("foo") + } + } + }() + + go func() { + for { + select { + case <-testCtx.Done(): + return + default: + _ = cache.Put("foo", []byte{1, 2, 3}) + } + } + }() + + <-time.After(time.Millisecond * 50) + assert.NoError(t, cache.Close()) + + <-time.After(time.Millisecond * 50) + testCancel() +} + +func TestInMemoryCache_Close(t *testing.T) { + const testKeyName = "foo" + + cache := NewInMemoryCache(time.Millisecond*100, time.Millisecond) + defer cache.Close() + + assert.NoError(t, cache.Put(testKeyName, []byte{1, 2, 3})) + + assert.NoError(t, cache.Close()) + + <-time.After(time.Millisecond * 5) + + found, _, _, err := cache.Get(testKeyName) + assert.False(t, found) + assert.Equal(t, ErrClosed, err) + + err = cache.Put(testKeyName, []byte{1}) + assert.Equal(t, ErrClosed, err) + + ok, err := cache.Delete(testKeyName) + assert.False(t, ok) + assert.Equal(t, ErrClosed, err) + + err = cache.Close() + assert.Equal(t, ErrClosed, err) +} + +func TestInMemoryCache_GetWithEmptyKey(t *testing.T) { + cache := NewInMemoryCache(time.Minute, time.Second) + defer cache.Close() + + found, data, ttl, err := cache.Get("") + assert.False(t, found) + assert.Nil(t, data) + assert.Zero(t, ttl) + assert.Error(t, err) +} + +func TestInMemoryCache_PutWithEmptyKey(t *testing.T) { + cache := NewInMemoryCache(time.Minute, time.Second) + defer cache.Close() + + err := cache.Put("", []byte{1}) + assert.Error(t, err) + assert.Equal(t, ErrEmptyKey, err) +} + +func TestInMemoryCache_PutWithEmptyData(t *testing.T) { + cache := NewInMemoryCache(time.Minute, time.Second) + defer cache.Close() + + err := cache.Put("foo", []byte{}) + assert.Error(t, err) + assert.Equal(t, ErrEmptyData, err) + + err = cache.Put("foo", nil) + assert.Error(t, err) + assert.Equal(t, ErrEmptyData, err) +} + +func TestInMemoryCache_DeleteWithEmptyKey(t *testing.T) { + cache := NewInMemoryCache(time.Minute, time.Second) + defer cache.Close() + + deleted, err := cache.Delete("") + assert.False(t, deleted) + assert.Error(t, err) + assert.Equal(t, ErrEmptyKey, err) +} diff --git a/internal/pkg/cache/redis.go b/internal/pkg/cache/redis.go new file mode 100644 index 00000000..0751c372 --- /dev/null +++ b/internal/pkg/cache/redis.go @@ -0,0 +1,88 @@ +package cache + +import ( + "context" + "crypto/md5" //nolint:gosec + "encoding/hex" + "errors" + "time" + + "github.com/go-redis/redis/v8" +) + +// RedisCache is a redis cache implementation. +type RedisCache struct { + ctx context.Context + redis *redis.Client + ttl time.Duration +} + +// NewRedisCache creates new redis cache instance. +func NewRedisCache(ctx context.Context, client *redis.Client, ttl time.Duration) *RedisCache { + return &RedisCache{ctx: ctx, redis: client, ttl: ttl} +} + +// key generates cache entry key using passed string. +func (c *RedisCache) key(s string) string { + h := md5.Sum([]byte(s)) //nolint:gosec + + return "cache:" + hex.EncodeToString(h[:]) +} + +// TTL returns current cache values time-to-live. +func (c *RedisCache) TTL() time.Duration { return c.ttl } + +// Get retrieves value for the key from the storage. +func (c *RedisCache) Get(key string) (found bool, data []byte, ttl time.Duration, err error) { + if key == "" { + err = ErrEmptyKey + + return // wrong argument + } + + k := c.key(key) + + if data, err = c.redis.Get(c.ctx, k).Bytes(); err != nil { + if errors.Is(err, redis.Nil) { + err = nil + + return // not found + } + + return // key getting failed + } + + found = true + + if ttl, err = c.redis.TTL(c.ctx, k).Result(); err != nil { + return // ttl getting failed + } + + return // all is ok +} + +// Put value into the storage. +func (c *RedisCache) Put(key string, data []byte) error { + if key == "" { + return ErrEmptyKey + } else if len(data) == 0 { + return ErrEmptyData + } + + return c.redis.Set(c.ctx, c.key(key), data, c.ttl).Err() +} + +// Delete value from the storage with passed key. +func (c *RedisCache) Delete(key string) (bool, error) { + if key == "" { + return false, ErrEmptyKey + } + + if count, err := c.redis.Del(c.ctx, c.key(key)).Result(); err != nil { + return false, err + } else if count <= 0 { + return false, nil + } + + return true, nil +} diff --git a/internal/pkg/cache/redis_test.go b/internal/pkg/cache/redis_test.go new file mode 100644 index 00000000..636a2ff7 --- /dev/null +++ b/internal/pkg/cache/redis_test.go @@ -0,0 +1,88 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/assert" +) + +func TestRedisCache_GetPutDelete(t *testing.T) { + mini, err := miniredis.Run() + assert.NoError(t, err) + + defer mini.Close() + + cache := NewRedisCache(context.Background(), redis.NewClient(&redis.Options{Addr: mini.Addr()}), time.Minute) + + const testKeyName = "foo" + + // try to get non-existing entry + found, data, ttl, err := cache.Get(testKeyName) + assert.False(t, found) + assert.Nil(t, data) + assert.Zero(t, ttl) + assert.NoError(t, err) + + // put valid value with the same key + assert.NoError(t, cache.Put(testKeyName, []byte{1, 2, 3})) + + // and now all must be fine + found, data, ttl, err = cache.Get(testKeyName) + assert.True(t, found) + assert.Equal(t, []byte{1, 2, 3}, data) + assert.InDelta(t, time.Minute.Milliseconds(), ttl.Milliseconds(), 3) + assert.NoError(t, err) + + // delete the key + deleted, err := cache.Delete(testKeyName) + assert.True(t, deleted) + assert.NoError(t, err) + + // try to delete non-existing key + deleted, err = cache.Delete(testKeyName) + assert.False(t, deleted) + assert.NoError(t, err) +} + +func TestRedisCache_GetWithEmptyKey(t *testing.T) { + cache := NewRedisCache(context.Background(), nil, time.Minute) + + found, data, ttl, err := cache.Get("") + assert.False(t, found) + assert.Nil(t, data) + assert.Zero(t, ttl) + assert.Error(t, err) +} + +func TestRedisCache_PutWithEmptyKey(t *testing.T) { + cache := NewRedisCache(context.Background(), nil, time.Minute) + + err := cache.Put("", []byte{1}) + assert.Error(t, err) + assert.Equal(t, ErrEmptyKey, err) +} + +func TestRedisCache_PutWithEmptyData(t *testing.T) { + cache := NewRedisCache(context.Background(), nil, time.Minute) + + err := cache.Put("foo", []byte{}) + assert.Error(t, err) + assert.Equal(t, ErrEmptyData, err) + + err = cache.Put("foo", nil) + assert.Error(t, err) + assert.Equal(t, ErrEmptyData, err) +} + +func TestRedisCache_DeleteWithEmptyKey(t *testing.T) { + cache := NewRedisCache(context.Background(), nil, time.Minute) + + deleted, err := cache.Delete("") + assert.False(t, deleted) + assert.Error(t, err) + assert.Equal(t, ErrEmptyKey, err) +} diff --git a/internal/pkg/checkers/docs.go b/internal/pkg/checkers/docs.go new file mode 100644 index 00000000..9838f61b --- /dev/null +++ b/internal/pkg/checkers/docs.go @@ -0,0 +1,2 @@ +// Package checkers contains different checkers. +package checkers diff --git a/internal/pkg/checkers/health.go b/internal/pkg/checkers/health.go new file mode 100644 index 00000000..f2aecbfb --- /dev/null +++ b/internal/pkg/checkers/health.go @@ -0,0 +1,56 @@ +package checkers + +import ( + "context" + "fmt" + "net/http" + "time" +) + +type httpClient interface { + Do(*http.Request) (*http.Response, error) +} + +// HealthChecker is a heals checker. +type HealthChecker struct { + ctx context.Context + httpClient httpClient +} + +const defaultHTTPClientTimeout = time.Second * 3 + +// NewHealthChecker creates heals checker. +func NewHealthChecker(ctx context.Context, client ...httpClient) *HealthChecker { + var c httpClient + + if len(client) == 1 { + c = client[0] + } else { + c = &http.Client{Timeout: defaultHTTPClientTimeout} // default + } + + return &HealthChecker{ctx: ctx, httpClient: c} +} + +// Check application using liveness probe. +func (c *HealthChecker) Check(port uint16) error { + req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/live", port), nil) + if err != nil { + return err + } + + req.Header.Set("User-Agent", "HealthChecker/internal") + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + + _ = resp.Body.Close() + + if code := resp.StatusCode; code != http.StatusOK { + return fmt.Errorf("wrong status code [%d] from live endpoint", code) + } + + return nil +} diff --git a/internal/pkg/checkers/health_test.go b/internal/pkg/checkers/health_test.go new file mode 100644 index 00000000..a95ac21d --- /dev/null +++ b/internal/pkg/checkers/health_test.go @@ -0,0 +1,47 @@ +package checkers + +import ( + "bytes" + "context" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +type httpClientFunc func(*http.Request) (*http.Response, error) + +func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) } + +func TestHealthChecker_CheckSuccess(t *testing.T) { + var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) { + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, "http://127.0.0.1:123/live", req.URL.String()) + assert.Equal(t, "HealthChecker/internal", req.Header.Get("User-Agent")) + + return &http.Response{ + Body: ioutil.NopCloser(bytes.NewReader([]byte{})), + StatusCode: http.StatusOK, + }, nil + } + + checker := NewHealthChecker(context.Background(), httpMock) + + assert.NoError(t, checker.Check(123)) +} + +func TestHealthChecker_CheckFail(t *testing.T) { + var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) { + return &http.Response{ + Body: ioutil.NopCloser(bytes.NewReader([]byte{})), + StatusCode: http.StatusBadGateway, + }, nil + } + + checker := NewHealthChecker(context.Background(), httpMock) + + err := checker.Check(123) + assert.Error(t, err) + assert.Contains(t, err.Error(), "wrong status code") +} diff --git a/internal/pkg/checkers/live.go b/internal/pkg/checkers/live.go new file mode 100644 index 00000000..10c666c4 --- /dev/null +++ b/internal/pkg/checkers/live.go @@ -0,0 +1,10 @@ +package checkers + +// LiveChecker is a liveness checker. +type LiveChecker struct{} + +// NewLiveChecker creates liveness checker. +func NewLiveChecker() *LiveChecker { return &LiveChecker{} } + +// Check application is alive? +func (*LiveChecker) Check() error { return nil } diff --git a/internal/pkg/checkers/live_test.go b/internal/pkg/checkers/live_test.go new file mode 100644 index 00000000..f27e89c6 --- /dev/null +++ b/internal/pkg/checkers/live_test.go @@ -0,0 +1,11 @@ +package checkers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLiveChecker_Check(t *testing.T) { + assert.NoError(t, NewLiveChecker().Check()) +} diff --git a/internal/pkg/checkers/ready.go b/internal/pkg/checkers/ready.go new file mode 100644 index 00000000..21097e33 --- /dev/null +++ b/internal/pkg/checkers/ready.go @@ -0,0 +1,29 @@ +package checkers + +import ( + "context" + + "github.com/go-redis/redis/v8" +) + +// ReadyChecker is a readiness checker. +type ReadyChecker struct { + ctx context.Context + rdb *redis.Client // can be nil +} + +// NewReadyChecker creates readiness checker. +func NewReadyChecker(ctx context.Context, rdb *redis.Client) *ReadyChecker { + return &ReadyChecker{ctx: ctx, rdb: rdb} +} + +// Check application is ready for incoming requests processing? +func (c *ReadyChecker) Check() error { + if c.rdb != nil { + if err := c.rdb.Ping(c.ctx).Err(); err != nil { + return err + } + } + + return nil +} diff --git a/internal/pkg/checkers/ready_test.go b/internal/pkg/checkers/ready_test.go new file mode 100644 index 00000000..6d6907ce --- /dev/null +++ b/internal/pkg/checkers/ready_test.go @@ -0,0 +1,42 @@ +package checkers + +import ( + "context" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/assert" +) + +func TestReadyChecker_CheckWithoutRedisClient(t *testing.T) { + assert.NoError(t, NewReadyChecker(context.Background(), nil).Check()) +} + +func TestReadyChecker_CheckSuccessWithRedisClient(t *testing.T) { + // start mini-redis + mini, err := miniredis.Run() + assert.NoError(t, err) + + defer mini.Close() + + rdb := redis.NewClient(&redis.Options{Addr: mini.Addr()}) + defer rdb.Close() + + assert.NoError(t, NewReadyChecker(context.Background(), rdb).Check()) +} + +func TestReadyChecker_CheckFailedWithRedisClient(t *testing.T) { + // start mini-redis + mini, err := miniredis.Run() + assert.NoError(t, err) + + defer mini.Close() + + rdb := redis.NewClient(&redis.Options{Addr: mini.Addr()}) + defer rdb.Close() + + mini.SetError("foo err") + assert.Error(t, NewReadyChecker(context.Background(), rdb).Check()) + mini.SetError("") +} diff --git a/internal/pkg/cli/healthcheck/command.go b/internal/pkg/cli/healthcheck/command.go new file mode 100644 index 00000000..4ab1ca5d --- /dev/null +++ b/internal/pkg/cli/healthcheck/command.go @@ -0,0 +1,50 @@ +// Package healthcheck contains CLI `healthcheck` command implementation. +package healthcheck + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/env" +) + +type checker interface { + Check(port uint16) error +} + +// NewCommand creates `healthcheck` command. +func NewCommand(checker checker) *cobra.Command { + var port uint16 + + cmd := &cobra.Command{ + Use: "healthcheck", + Aliases: []string{"chk", "health", "check"}, + Short: "Health checker for the http server. Use case - docker healthcheck.", + Hidden: true, + PreRunE: func(*cobra.Command, []string) error { + if envPort, exists := env.ListenPort.Lookup(); exists && envPort != "" { + if p, err := strconv.ParseUint(envPort, 10, 16); err == nil { + port = uint16(p) + } else { + return fmt.Errorf("wrong TCP port environment variable [%s] value", envPort) + } + } + + return nil + }, + RunE: func(*cobra.Command, []string) error { + return checker.Check(port) + }, + } + + cmd.Flags().Uint16VarP( + &port, + "port", + "p", + 8080, + fmt.Sprintf("TCP port number [$%s]", env.ListenPort), + ) + + return cmd +} diff --git a/internal/pkg/cli/healthcheck/command_test.go b/internal/pkg/cli/healthcheck/command_test.go new file mode 100644 index 00000000..54bd3da0 --- /dev/null +++ b/internal/pkg/cli/healthcheck/command_test.go @@ -0,0 +1,93 @@ +package healthcheck + +import ( + "errors" + "os" + "testing" + + "github.com/kami-zh/go-capturer" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +type fakeChecker struct{ err error } + +func (c *fakeChecker) Check(port uint16) error { return c.err } + +func TestProperties(t *testing.T) { + cmd := NewCommand(&fakeChecker{err: nil}) + + assert.Equal(t, "healthcheck", cmd.Use) + assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases) + assert.NotNil(t, cmd.RunE) +} + +func TestCommandRun(t *testing.T) { + cmd := NewCommand(&fakeChecker{err: nil}) + cmd.SetArgs([]string{}) + + output := capturer.CaptureOutput(func() { + assert.NoError(t, cmd.Execute()) + }) + + assert.Empty(t, output) +} + +func TestCommandRunFailed(t *testing.T) { + cmd := NewCommand(&fakeChecker{err: errors.New("foo err")}) + cmd.SetArgs([]string{}) + + output := capturer.CaptureStderr(func() { + assert.Error(t, cmd.Execute()) + }) + + assert.Contains(t, output, "foo err") +} + +func TestPortFlagWrongArgument(t *testing.T) { + cmd := NewCommand(&fakeChecker{err: nil}) + cmd.SetArgs([]string{"-p", "65536"}) // 65535 is max + + var executed bool + + cmd.RunE = func(*cobra.Command, []string) error { + executed = true + + return nil + } + + output := capturer.CaptureStderr(func() { + assert.Error(t, cmd.Execute()) + }) + + assert.Contains(t, output, "invalid argument") + assert.Contains(t, output, "65536") + assert.Contains(t, output, "value out of range") + assert.False(t, executed) +} + +func TestPortFlagWrongEnvValue(t *testing.T) { + cmd := NewCommand(&fakeChecker{err: nil}) + cmd.SetArgs([]string{"-p", "8090"}) // `-p` flag must be ignored + + assert.NoError(t, os.Setenv("LISTEN_PORT", "65536")) // 65535 is max + + defer func() { assert.NoError(t, os.Unsetenv("LISTEN_PORT")) }() + + var executed bool + + cmd.RunE = func(*cobra.Command, []string) error { + executed = true + + return nil + } + + output := capturer.CaptureStderr(func() { + assert.Error(t, cmd.Execute()) + }) + + assert.Contains(t, output, "wrong TCP port") + assert.Contains(t, output, "environment variable") + assert.Contains(t, output, "65536") + assert.False(t, executed) +} diff --git a/internal/pkg/cli/root.go b/internal/pkg/cli/root.go new file mode 100644 index 00000000..4b6503ab --- /dev/null +++ b/internal/pkg/cli/root.go @@ -0,0 +1,67 @@ +// Package cli contains CLI command handlers. +package cli + +import ( + "context" + + "github.com/spf13/cobra" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/checkers" + healthcheckCmd "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cli/healthcheck" + serveCmd "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cli/serve" + versionCmd "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cli/version" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/logger" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/version" +) + +// NewCommand creates root command. +func NewCommand(appName string) *cobra.Command { + var ( + verbose bool + debug bool + logJSON bool + ) + + ctx := context.Background() // main CLI context + + // create "default" logger (will be overwritten later with customized) + log, err := logger.New(false, false, false) + if err != nil { + panic(err) + } + + cmd := &cobra.Command{ + Use: appName, + PersistentPreRunE: func(*cobra.Command, []string) error { + _ = log.Sync() // sync previous logger instance + + customizedLog, e := logger.New(verbose, debug, logJSON) + if e != nil { + return e + } + + *log = *customizedLog // override "default" logger with customized + + return nil + }, + PersistentPostRun: func(*cobra.Command, []string) { + // error ignoring reasons: + // - + // - + _ = log.Sync() + }, + SilenceErrors: true, + SilenceUsage: true, + } + + cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") + cmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "debug output") + cmd.PersistentFlags().BoolVarP(&logJSON, "log-json", "", false, "logs in JSON format") + + cmd.AddCommand( + versionCmd.NewCommand(version.Version()), + serveCmd.NewCommand(ctx, log), + healthcheckCmd.NewCommand(checkers.NewHealthChecker(ctx)), + ) + + return cmd +} diff --git a/internal/pkg/cli/root_test.go b/internal/pkg/cli/root_test.go new file mode 100644 index 00000000..7cc49c6c --- /dev/null +++ b/internal/pkg/cli/root_test.go @@ -0,0 +1,80 @@ +package cli + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestSubcommands(t *testing.T) { + cmd := NewCommand("unit test") + + cases := []struct { + giveName string + }{ + {giveName: "serve"}, + {giveName: "version"}, + } + + // get all existing subcommands and put into the map + subcommands := make(map[string]*cobra.Command) + for _, sub := range cmd.Commands() { + subcommands[sub.Name()] = sub + } + + for _, tt := range cases { + tt := tt + t.Run(tt.giveName, func(t *testing.T) { + if _, exists := subcommands[tt.giveName]; !exists { + assert.Failf(t, "command not found", "command [%s] was not found", tt.giveName) + } + }) + } +} + +func TestFlags(t *testing.T) { + cmd := NewCommand("unit test") + + cases := []struct { + giveName string + wantShorthand string + wantDefault string + }{ + {giveName: "verbose", wantShorthand: "v", wantDefault: "false"}, + {giveName: "debug", wantShorthand: "", wantDefault: "false"}, + {giveName: "log-json", wantShorthand: "", wantDefault: "false"}, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.giveName, func(t *testing.T) { + flag := cmd.Flag(tt.giveName) + + if flag == nil { + assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName) + + return + } + + assert.Equal(t, tt.wantShorthand, flag.Shorthand) + assert.Equal(t, tt.wantDefault, flag.DefValue) + }) + } +} + +func TestExecuting(t *testing.T) { + cmd := NewCommand("unit test") + cmd.SetArgs([]string{}) + + var executed bool + + if cmd.Run == nil { // override "Run" property for test (if it was not set) + cmd.Run = func(cmd *cobra.Command, args []string) { + executed = true + } + } + + assert.NoError(t, cmd.Execute()) + assert.True(t, executed) +} diff --git a/internal/pkg/cli/serve/command.go b/internal/pkg/cli/serve/command.go new file mode 100644 index 00000000..5341cc70 --- /dev/null +++ b/internal/pkg/cli/serve/command.go @@ -0,0 +1,177 @@ +// Package serve contains CLI `serve` command implementation. +package serve + +import ( + "context" + "errors" + "io" + "net/http" + "os" + "time" + + "github.com/go-redis/redis/v8" + "github.com/spf13/cobra" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/breaker" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cache" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/config" + appHttp "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http" + "go.uber.org/zap" +) + +const cachingEngineMemory, cachingEngineRedis = "memory", "redis" + +// NewCommand creates `serve` command. +func NewCommand(ctx context.Context, log *zap.Logger) *cobra.Command { + var f flags + + cmd := &cobra.Command{ + Use: "serve", + Aliases: []string{"s", "server"}, + Short: "Start HTTP server.\n\nEnvironment variables have higher priority then flags.", + PreRunE: func(*cobra.Command, []string) error { + if err := f.overrideUsingEnv(); err != nil { + return err + } + + return f.validate() + }, + RunE: func(*cobra.Command, []string) error { + cfg, err := config.FromYamlFile(f.configPath, true) + if err != nil { + return err + } + + return run(ctx, log, cfg, &f) + }, + } + + f.init(cmd.Flags()) + + return cmd +} + +const serverShutdownTimeout = 5 * time.Second + +// run current command. +func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f *flags) error { //nolint:funlen,gocyclo + var ( + ctx, cancel = context.WithCancel(parentCtx) // serve context creation + oss = breaker.NewOSSignals(ctx) // OS signals listener + ) + + // subscribe for system signals + oss.Subscribe(func(sig os.Signal) { + log.Warn("Stopping by OS signal..", zap.String("signal", sig.String())) + + cancel() + }) + + defer func() { + cancel() // call the cancellation function after all + oss.Stop() // stop system signals listening + }() + + var ( + cacheTTL time.Duration + rdb *redis.Client // optional, can be nil + cacher cache.Cacher + ) + + cacheTTL, _ = time.ParseDuration(f.cache.ttl) + + switch f.cache.engine { + case cachingEngineMemory: + inmemory := cache.NewInMemoryCache(cacheTTL, time.Second) + + defer func() { _ = inmemory.Close() }() + + cacher = inmemory + + case cachingEngineRedis: + opt, _ := redis.ParseURL(f.redisDSN) + rdb = redis.NewClient(opt).WithContext(ctx) + + defer func() { _ = rdb.Close() }() + + if pingErr := rdb.Ping(ctx).Err(); pingErr != nil { + return pingErr + } + + cacher = cache.NewRedisCache(ctx, rdb, cacheTTL) + + default: + return errors.New("unsupported caching engine") + } + + // create HTTP server + server := appHttp.NewServer(ctx, log, cacher, f.resourcesDir, cfg, rdb) + + // register server routes, middlewares, etc. + if err := server.Register(); err != nil { + return err + } + + startingErrCh := make(chan error, 1) // channel for server starting error + + // start HTTP server in separate goroutine + go func(errCh chan<- error) { + defer close(errCh) + + fields := []zap.Field{ + zap.String("addr", f.listen.ip), + zap.Uint16("port", f.listen.port), + zap.String("resources", f.resourcesDir), + zap.String("config file", f.configPath), + zap.String("caching engine", f.cache.engine), + zap.Duration("cache ttl", cacheTTL), + } + + if f.cache.engine == cachingEngineRedis { + fields = append(fields, zap.String("redis dsn", f.redisDSN)) + } + + log.Info("Server starting", fields...) + + if f.resourcesDir == "" { + log.Warn("Resources directory was not provided") + } + + if err := server.Start(f.listen.ip, f.listen.port); err != nil && !errors.Is(err, http.ErrServerClosed) { + errCh <- err + } + }(startingErrCh) + + // and wait for.. + select { + case err := <-startingErrCh: // ..server starting error + return err + + case <-ctx.Done(): // ..or context cancellation + log.Debug("Server stopping") + + // create context for server graceful shutdown + ctxShutdown, ctxCancelShutdown := context.WithTimeout(context.Background(), serverShutdownTimeout) + defer ctxCancelShutdown() + + // stop the server using created context above + if err := server.Stop(ctxShutdown); err != nil { + return err + } + + // close cacher (if it is possible) + if c, ok := cacher.(io.Closer); ok { + if err := c.Close(); err != nil { + return err + } + } + + // and close redis connection + if rdb != nil { + if err := rdb.Close(); err != nil { + return err + } + } + } + + return nil +} diff --git a/internal/pkg/cli/serve/command_test.go b/internal/pkg/cli/serve/command_test.go new file mode 100644 index 00000000..c88df2cb --- /dev/null +++ b/internal/pkg/cli/serve/command_test.go @@ -0,0 +1,421 @@ +package serve + +import ( + "context" + "fmt" + "net" + "os" + "path" + "path/filepath" + "strconv" + "syscall" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/kami-zh/go-capturer" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestProperties(t *testing.T) { + cmd := NewCommand(context.Background(), zap.NewNop()) + + assert.Equal(t, "serve", cmd.Use) + assert.ElementsMatch(t, []string{"s", "server"}, cmd.Aliases) + assert.NotNil(t, cmd.RunE) +} + +func TestFlags(t *testing.T) { + cmd := NewCommand(context.Background(), zap.NewNop()) + exe, _ := os.Executable() + exe = path.Dir(exe) + + cases := []struct { + giveName string + wantShorthand string + wantDefault string + }{ + {giveName: "listen", wantShorthand: "l", wantDefault: "0.0.0.0"}, + {giveName: "port", wantShorthand: "p", wantDefault: "8080"}, + {giveName: "resources-dir", wantShorthand: "r", wantDefault: filepath.Join(exe, "web")}, + {giveName: "config", wantShorthand: "c", wantDefault: filepath.Join(exe, "configs", "config.yml")}, + {giveName: "caching-engine", wantShorthand: "", wantDefault: "memory"}, + {giveName: "redis-dsn", wantShorthand: "", wantDefault: "redis://127.0.0.1:6379/0"}, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.giveName, func(t *testing.T) { + flag := cmd.Flag(tt.giveName) + + if flag == nil { + assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName) + + return + } + + assert.Equal(t, tt.wantShorthand, flag.Shorthand) + assert.Equal(t, tt.wantDefault, flag.DefValue) + }) + } +} + +const configFilePath = "../../../../configs/config.yml" + +func TestSuccessfulFlagsPreparing(t *testing.T) { + cmd := NewCommand(context.Background(), zap.NewNop()) + cmd.SetArgs([]string{"-r", "", "-c", configFilePath}) + + var executed bool + + cmd.RunE = func(*cobra.Command, []string) error { + executed = true + + return nil + } + + output := capturer.CaptureOutput(func() { + assert.NoError(t, cmd.Execute()) + }) + + assert.Empty(t, output) + assert.True(t, executed) +} + +func executeCommandWithoutRunning(t *testing.T, args []string) string { + cmd := NewCommand(context.Background(), zap.NewNop()) + cmd.SetArgs(args) + + var executed bool + + cmd.RunE = func(*cobra.Command, []string) error { + executed = true + + return nil + } + + output := capturer.CaptureStderr(func() { + assert.Error(t, cmd.Execute()) + }) + + assert.False(t, executed) + + return output +} + +func TestListenFlagWrongArgument(t *testing.T) { + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "-l", "256.256.256.256", // 255 is max + }) + + assert.Contains(t, output, "wrong IP address") + assert.Contains(t, output, "256.256.256.256") +} + +func TestListenFlagWrongEnvValue(t *testing.T) { + assert.NoError(t, os.Setenv("LISTEN_ADDR", "256.256.256.256")) // 255 is max + + defer func() { assert.NoError(t, os.Unsetenv("LISTEN_ADDR")) }() + + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "-l", "0.0.0.0", // `-l` flag must be ignored + }) + + assert.Contains(t, output, "wrong IP address") + assert.Contains(t, output, "256.256.256.256") +} + +func TestPortFlagWrongArgument(t *testing.T) { + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "-p", "65536", // 65535 is max + }) + + assert.Contains(t, output, "invalid argument") + assert.Contains(t, output, "65536") + assert.Contains(t, output, "value out of range") +} + +func TestPortFlagWrongEnvValue(t *testing.T) { + assert.NoError(t, os.Setenv("LISTEN_PORT", "65536")) // 65535 is max + + defer func() { assert.NoError(t, os.Unsetenv("LISTEN_PORT")) }() + + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "-p", "8090", // `-p` flag must be ignored + }) + + assert.Contains(t, output, "wrong TCP port") + assert.Contains(t, output, "environment variable") + assert.Contains(t, output, "65536") +} + +func TestResourcesDirFlagWrongArgument(t *testing.T) { + output := executeCommandWithoutRunning(t, []string{ + "-r", "/tmp/nonexistent/bar/baz", + "-c", configFilePath, + }) + + assert.Contains(t, output, "wrong resources directory") + assert.Contains(t, output, "/tmp/nonexistent/bar/baz") +} + +func TestResourcesDirFlagWrongEnvValue(t *testing.T) { + assert.NoError(t, os.Setenv("RESOURCES_DIR", "/tmp/nonexistent/bar/baz")) + + defer func() { assert.NoError(t, os.Unsetenv("RESOURCES_DIR")) }() + + output := executeCommandWithoutRunning(t, []string{ + "-c", configFilePath, + "-r", ".", // `-r` flag must be ignored + }) + + assert.Contains(t, output, "wrong resources directory") + assert.Contains(t, output, "/tmp/nonexistent/bar/baz") +} + +func TestCachingEngineFlagWrongArgument(t *testing.T) { + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "--caching-engine", "foobarEngine", + }) + + assert.Contains(t, output, "unsupported caching engine") + assert.Contains(t, output, "foobarEngine") +} + +func TestCachingEngineFlagWrongEnvValue(t *testing.T) { + assert.NoError(t, os.Setenv("CACHING_ENGINE", "barEngine")) + + defer func() { assert.NoError(t, os.Unsetenv("CACHING_ENGINE")) }() + + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "--caching-engine", "foobarEngine", + }) + + assert.Contains(t, output, "unsupported caching engine") + assert.Contains(t, output, "barEngine") +} + +func TestRedisDSNFlagWrongArgument(t *testing.T) { + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "--caching-engine", "redis", + "--redis-dsn", "foo://bar", + }) + + assert.Contains(t, output, "wrong redis DSN") + assert.Contains(t, output, "foo://bar") +} + +func TestRedisDSNFlagWrongEnvValue(t *testing.T) { + assert.NoError(t, os.Setenv("REDIS_DSN", "bar://baz")) + + defer func() { assert.NoError(t, os.Unsetenv("REDIS_DSN")) }() + + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, + "--caching-engine", "redis", + "--redis-dsn", "foo://bar", // `--redis-dsn` flag must be ignored + }) + + assert.Contains(t, output, "wrong redis DSN") + assert.Contains(t, output, "bar://baz") +} + +func TestConfigFlagWrongArgument(t *testing.T) { + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", "/tmp/nonexistent/bar.baz", + }) + + assert.Contains(t, output, "config file") + assert.Contains(t, output, "/tmp/nonexistent/bar.baz") + assert.Contains(t, output, "not found") +} + +func TestConfigFlagWrongEnvValue(t *testing.T) { + assert.NoError(t, os.Setenv("CONFIG_PATH", "/tmp/nonexistent/foo.baz")) + + defer func() { assert.NoError(t, os.Unsetenv("CONFIG_PATH")) }() + + output := executeCommandWithoutRunning(t, []string{ + "-r", "", + "-c", configFilePath, // `-c` flag must be ignored + }) + + assert.Contains(t, output, "config file") + assert.Contains(t, output, "/tmp/nonexistent/foo.baz") + assert.Contains(t, output, "not found") +} + +func getRandomTCPPort(t *testing.T) (int, error) { + t.Helper() + + // zero port means randomly (os) chosen port + l, err := net.Listen("tcp", ":0") //nolint:gosec + if err != nil { + return 0, err + } + + port := l.Addr().(*net.TCPAddr).Port + + if closingErr := l.Close(); closingErr != nil { + return 0, closingErr + } + + return port, nil +} + +func checkTCPPortIsBusy(t *testing.T, port int) bool { + t.Helper() + + l, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + return true + } + + _ = l.Close() + + return false +} + +func startAndStopServer(t *testing.T, port int, args []string) string { + var ( + output string + executedCh = make(chan struct{}) + ) + + // start HTTP server + go func(ch chan<- struct{}) { + defer close(ch) + + output = capturer.CaptureStderr(func() { + // create command with valid flags to run + log, _ := zap.NewDevelopment() + cmd := NewCommand(context.Background(), log) + cmd.SilenceUsage = true + cmd.SetArgs(args) + + assert.NoError(t, cmd.Execute()) + }) + + ch <- struct{}{} + }(executedCh) + + portBusyCh := make(chan struct{}) + + // check port "busy" (by HTTP server) state + go func(ch chan<- struct{}) { + defer close(ch) + + for i := 0; i < 2000; i++ { + if checkTCPPortIsBusy(t, port) { + ch <- struct{}{} + + return + } + + <-time.After(time.Millisecond * 2) + } + + t.Error("port opening timeout exceeded") + }(portBusyCh) + + <-portBusyCh // wait for server starting + + // send OS signal for server stopping + proc, err := os.FindProcess(os.Getpid()) + assert.NoError(t, err) + assert.NoError(t, proc.Signal(syscall.SIGINT)) // send the signal + + <-executedCh // wait until server has been stopped + + return output +} + +func TestSuccessfulCommandRunningUsingRedisCacheEngine(t *testing.T) { + // get TCP port number for a test + port, err := getRandomTCPPort(t) + assert.NoError(t, err) + + // start mini-redis + mini, err := miniredis.Run() + assert.NoError(t, err) + + defer mini.Close() + + output := startAndStopServer(t, port, []string{ + "-r", "", + "--port", strconv.Itoa(port), + "-c", configFilePath, + "--caching-engine", "redis", + "--redis-dsn", fmt.Sprintf("redis://127.0.0.1:%s/0", mini.Port()), + }) + + assert.Contains(t, output, "Server starting") + assert.Contains(t, output, "Stopping by OS signal") + assert.Contains(t, output, "Server stopping") +} + +func TestSuccessfulCommandRunningUsingDefaultCacheEngine(t *testing.T) { + // get TCP port number for a test + port, err := getRandomTCPPort(t) + assert.NoError(t, err) + + output := startAndStopServer(t, port, []string{ + "-r", "", + "--port", strconv.Itoa(port), + "-c", configFilePath, + }) + + assert.Contains(t, output, "Server starting") + assert.Contains(t, output, "Stopping by OS signal") + assert.Contains(t, output, "Server stopping") +} + +func TestRunningUsingBusyPortFailing(t *testing.T) { + port, err := getRandomTCPPort(t) + assert.NoError(t, err) + + // occupy a TCP port + l, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + assert.NoError(t, err) + + defer func() { assert.NoError(t, l.Close()) }() + + // create command with valid flags to run + cmd := NewCommand(context.Background(), zap.NewNop()) + cmd.SilenceUsage = true + cmd.SetArgs([]string{"-r", "", "--port", strconv.Itoa(port), "-c", configFilePath}) + + executedCh := make(chan struct{}) + + // start HTTP server + go func(ch chan<- struct{}) { + defer close(ch) + + err := cmd.Execute() + + assert.Error(t, err) + assert.Contains(t, err.Error(), "address already in use") + + ch <- struct{}{} + }(executedCh) + + <-executedCh // wait until server has been stopped +} diff --git a/internal/pkg/cli/serve/flags.go b/internal/pkg/cli/serve/flags.go new file mode 100644 index 00000000..66cdfe0f --- /dev/null +++ b/internal/pkg/cli/serve/flags.go @@ -0,0 +1,158 @@ +package serve + +import ( + "fmt" + "net" + "os" + "path" + "path/filepath" + "strconv" + "time" + + "github.com/go-redis/redis/v8" + "github.com/spf13/pflag" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/env" +) + +type flags struct { + listen struct { + ip string + port uint16 + } + + resourcesDir string // can be empty + configPath string + + cache struct { + ttl string + engine string + } + + // redisDSN allows to setup redis server using single string. Examples: + // redis://:@:/ + // unix://:@?db= + redisDSN string +} + +func (f *flags) init(flagSet *pflag.FlagSet) { + exe, _ := os.Executable() + exe = path.Dir(exe) + + flagSet.StringVarP( + &f.listen.ip, + "listen", + "l", + "0.0.0.0", + fmt.Sprintf("IP address to listen on [$%s]", env.ListenAddr), + ) + flagSet.Uint16VarP( + &f.listen.port, + "port", + "p", + 8080, + fmt.Sprintf("TCP port number [$%s]", env.ListenPort), + ) + flagSet.StringVarP( + &f.resourcesDir, + "resources-dir", + "r", + filepath.Join(exe, "web"), + fmt.Sprintf("path to the directory with public assets [$%s]", env.ResourcesDir), + ) + flagSet.StringVarP( + &f.configPath, + "config", + "c", + filepath.Join(exe, "configs", "config.yml"), + fmt.Sprintf("config file path [$%s]", env.ConfigPath), + ) + flagSet.StringVarP( + &f.cache.engine, + "caching-engine", + "", + cachingEngineMemory, + fmt.Sprintf("caching endine (%s|%s) [$%s]", cachingEngineMemory, cachingEngineRedis, env.CachingEngine), + ) + flagSet.StringVarP( + &f.cache.ttl, + "cache-ttl", + "", + "30m", + fmt.Sprintf("cache entries lifetime (examples: 50s, 1h30m) [$%s]", env.CacheTTL), + ) + flagSet.StringVarP( + &f.redisDSN, + "redis-dsn", + "", + "redis://127.0.0.1:6379/0", + fmt.Sprintf("redis server DSN (format \"redis://:@:/\") [$%s]", env.RedisDSN), //nolint:lll + ) +} + +func (f *flags) overrideUsingEnv() error { + if envVar, exists := env.ListenAddr.Lookup(); exists { + f.listen.ip = envVar + } + + if envVar, exists := env.ListenPort.Lookup(); exists { + if p, err := strconv.ParseUint(envVar, 10, 16); err == nil { + f.listen.port = uint16(p) + } else { + return fmt.Errorf("wrong TCP port environment variable [%s] value", envVar) + } + } + + if envVar, exists := env.ResourcesDir.Lookup(); exists { + f.resourcesDir = envVar + } + + if envVar, exists := env.ConfigPath.Lookup(); exists { + f.configPath = envVar + } + + if envVar, exists := env.CachingEngine.Lookup(); exists { + f.cache.engine = envVar + } + + if envVar, exists := env.CacheTTL.Lookup(); exists { + f.cache.ttl = envVar + } + + if envVar, exists := env.RedisDSN.Lookup(); exists { + f.redisDSN = envVar + } + + return nil +} + +func (f *flags) validate() error { + if net.ParseIP(f.listen.ip) == nil { + return fmt.Errorf("wrong IP address [%s] for listening", f.listen.ip) + } + + if f.resourcesDir != "" { + if info, err := os.Stat(f.resourcesDir); err != nil || !info.Mode().IsDir() { + return fmt.Errorf("wrong resources directory [%s] path", f.resourcesDir) + } + } + + if info, err := os.Stat(f.configPath); err != nil || !info.Mode().IsRegular() { + return fmt.Errorf("config file [%s] was not found", f.configPath) + } + + switch f.cache.engine { + case cachingEngineMemory: + case cachingEngineRedis: + if _, err := redis.ParseURL(f.redisDSN); err != nil { + return fmt.Errorf("wrong redis DSN [%s]: %w", f.redisDSN, err) + } + default: + return fmt.Errorf("unsupported caching engine: %s", f.cache.engine) + } + + if _, err := time.ParseDuration(f.cache.ttl); err != nil { + return fmt.Errorf("wrong cache lifetime [%s] period", f.cache.ttl) + } + + return nil +} diff --git a/internal/pkg/cli/version/command.go b/internal/pkg/cli/version/command.go new file mode 100644 index 00000000..e3b4b9d6 --- /dev/null +++ b/internal/pkg/cli/version/command.go @@ -0,0 +1,24 @@ +// Package version contains CLI `version` command implementation. +package version + +import ( + "fmt" + "os" + "runtime" + + "github.com/spf13/cobra" +) + +// NewCommand creates `version` command. +func NewCommand(ver string) *cobra.Command { + return &cobra.Command{ + Use: "version", + Aliases: []string{"v", "ver"}, + Short: "Display application version", + RunE: func(*cobra.Command, []string) (err error) { + _, err = fmt.Fprintf(os.Stdout, "app version:\t%s (%s)\n", ver, runtime.Version()) + + return + }, + } +} diff --git a/internal/pkg/cli/version/command_test.go b/internal/pkg/cli/version/command_test.go new file mode 100644 index 00000000..cc3d488a --- /dev/null +++ b/internal/pkg/cli/version/command_test.go @@ -0,0 +1,29 @@ +package version + +import ( + "runtime" + "testing" + + "github.com/kami-zh/go-capturer" + "github.com/stretchr/testify/assert" +) + +func TestProperties(t *testing.T) { + cmd := NewCommand("") + + assert.Equal(t, "version", cmd.Use) + assert.ElementsMatch(t, []string{"v", "ver"}, cmd.Aliases) + assert.NotNil(t, cmd.RunE) +} + +func TestCommandRun(t *testing.T) { + cmd := NewCommand("1.2.3@foobar") + cmd.SetArgs([]string{}) + + output := capturer.CaptureStdout(func() { + assert.NoError(t, cmd.Execute()) + }) + + assert.Contains(t, output, "1.2.3@foobar") + assert.Contains(t, output, runtime.Version()) +} diff --git a/internal/pkg/cmd/options.go b/internal/pkg/cmd/options.go deleted file mode 100644 index bbaf88a6..00000000 --- a/internal/pkg/cmd/options.go +++ /dev/null @@ -1,11 +0,0 @@ -package cmd - -import ( - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cmd/serve" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cmd/version" -) - -type Options struct { - Serve serve.Command `command:"serve" alias:"s" description:"Start web-server"` - Version version.Command `command:"version" alias:"v" description:"Display application version"` -} diff --git a/internal/pkg/cmd/options_test.go b/internal/pkg/cmd/options_test.go deleted file mode 100644 index 0419bfd7..00000000 --- a/internal/pkg/cmd/options_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package cmd - -import ( - "reflect" - "testing" -) - -func TestOptions_Struct(t *testing.T) { - tests := []struct { - element func() reflect.StructField - wantCommand string - wantAlias string - wantDescription string - }{ - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(Options{}).FieldByName("Serve") - return field - }, - wantCommand: "serve", - wantAlias: "s", - wantDescription: "Start web-server", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(Options{}).FieldByName("Version") - return field - }, - wantCommand: "version", - wantAlias: "v", - wantDescription: "Display application version", - }, - } - for _, tt := range tests { - t.Run(tt.wantDescription, func(t *testing.T) { - el := tt.element() - if tt.wantCommand != "" { - value, _ := el.Tag.Lookup("command") - if value != tt.wantCommand { - t.Errorf("Wrong value for 'command' tag. Want: %v, got: %v", tt.wantCommand, value) - } - } - - if tt.wantAlias != "" { - value, _ := el.Tag.Lookup("alias") - if value != tt.wantAlias { - t.Errorf("Wrong value for 'alias' tag. Want: %v, got: %v", tt.wantAlias, value) - } - } - - if tt.wantDescription != "" { - value, _ := el.Tag.Lookup("description") - if value != tt.wantDescription { - t.Errorf("Wrong value for 'description' tag. Want: %v, got: %v", tt.wantDescription, value) - } - } - }) - } -} diff --git a/internal/pkg/cmd/serve/command.go b/internal/pkg/cmd/serve/command.go deleted file mode 100644 index bc24305c..00000000 --- a/internal/pkg/cmd/serve/command.go +++ /dev/null @@ -1,134 +0,0 @@ -package serve - -import ( - "errors" - "fmt" - "net" - "os" - "strconv" - "time" - - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http" - serveSettings "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" -) - -type ( - listenAddress string - listenPort int - resourcesDirPath string - configFilePath string - - listenOptions struct { - Address listenAddress `short:"l" long:"listen" env:"LISTEN_ADDR" description:"Address (IP) to listen on"` - Port listenPort `short:"p" long:"port" env:"LISTEN_PORT" description:"TCP port number"` - } - - resourcesOptions struct { - ResourcesDir resourcesDirPath `short:"r" long:"resources-dir" env:"RESOURCES_DIR" description:"Resources directory path"` - } -) - -type ( - Command struct { - ConfigFile configFilePath `short:"c" long:"config" env:"CONFIG_PATH" required:"true" description:"Config file path"` - - ResourcesOptions resourcesOptions `group:"Resources"` - ServingOptions listenOptions `group:"Listening"` - } -) - -// Convert struct into string representation. -func (s listenAddress) String() string { - return string(s) -} - -// Convert struct into string representation. -func (s resourcesDirPath) String() string { - return string(s) -} - -// Convert struct into string representation. -func (s configFilePath) String() string { - return string(s) -} - -// Validate address for listening on. -func (listenAddress) IsValidValue(ip string) error { - if net.ParseIP(ip) == nil { - return errors.New("wrong address for listening value (invalid IP address)") - } - return nil -} - -// Validate config file path -func (configFilePath) IsValidValue(value string) error { - if info, err := os.Stat(value); err != nil || !info.Mode().IsRegular() { - return fmt.Errorf("config file [%s] was not found", value) - } - - return nil -} - -// Validate port for listening -func (listenPort) IsValidValue(value string) error { - portNum, err := strconv.Atoi(value) - if err != nil { - return errors.New("wrong port value (cannot be converted into number)") - } - - if portNum <= 0 || portNum > 65535 { - return errors.New("wrong port number (must be in interval 1..65535)") - } - - return nil -} - -// Validate resources directory path. -func (resourcesDirPath) IsValidValue(value string) error { - if info, err := os.Stat(value); err != nil || !info.Mode().IsDir() { - return fmt.Errorf("resources directory [%s] was not found", value) - } - - return nil -} - -// Get serving settings -func (c *Command) getSettings(filepath string) (*serveSettings.Settings, error) { - sets, err := serveSettings.FromYamlFile(filepath, true) - if err != nil { - return nil, err - } - - // override settings using passed command options - if len(c.ServingOptions.Address) > 0 { - sets.Listen.Address = c.ServingOptions.Address.String() - } - if c.ServingOptions.Port != 0 { - sets.Listen.Port = int(c.ServingOptions.Port) - } - if len(c.ResourcesOptions.ResourcesDir) > 0 { - sets.Resources.DirPath = c.ResourcesOptions.ResourcesDir.String() - } - - return sets, nil -} - -// Execute the command. -func (c *Command) Execute(_ []string) error { - settings, err := c.getSettings(c.ConfigFile.String()) - if err != nil { - return err - } - - server := http.NewServer(&http.ServerSettings{ - WriteTimeout: time.Second * 15, - ReadTimeout: time.Second * 15, - KeepAliveEnabled: false, - }, settings) - - server.RegisterHandlers() - - _ = settings.PrintInfo(os.Stdout) - - return server.Start() -} diff --git a/internal/pkg/cmd/serve/command_test.go b/internal/pkg/cmd/serve/command_test.go deleted file mode 100644 index a5abc45a..00000000 --- a/internal/pkg/cmd/serve/command_test.go +++ /dev/null @@ -1,457 +0,0 @@ -package serve - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" -) - -func TestCommand_Structures(t *testing.T) { - tests := []struct { - element func() reflect.StructField - wantShort string - wantLong string - wantEnv string - wantDescription string - wantRequired string - wantGroup string - }{ - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(listenOptions{}).FieldByName("Address") - return field - }, - wantShort: "l", - wantLong: "listen", - wantEnv: "LISTEN_ADDR", - wantDescription: "Address (IP) to listen on", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(listenOptions{}).FieldByName("Port") - return field - }, - wantShort: "p", - wantLong: "port", - wantEnv: "LISTEN_PORT", - wantDescription: "TCP port number", - }, - - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(resourcesOptions{}).FieldByName("ResourcesDir") - return field - }, - wantShort: "r", - wantLong: "resources-dir", - wantEnv: "RESOURCES_DIR", - wantDescription: "Resources directory path", - }, - - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(Command{}).FieldByName("ConfigFile") - return field - }, - wantShort: "c", - wantLong: "config", - wantEnv: "CONFIG_PATH", - wantDescription: "Config file path", - wantRequired: "true", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(Command{}).FieldByName("ResourcesOptions") - return field - }, - wantGroup: "Resources", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(Command{}).FieldByName("ServingOptions") - return field - }, - wantGroup: "Listening", - }, - } - - for _, tt := range tests { - t.Run(tt.wantDescription, func(t *testing.T) { - el := tt.element() - if tt.wantShort != "" { - value, _ := el.Tag.Lookup("short") - if value != tt.wantShort { - t.Errorf("Wrong value for 'short' tag. Want: %v, got: %v", tt.wantShort, value) - } - } - - if tt.wantLong != "" { - value, _ := el.Tag.Lookup("long") - if value != tt.wantLong { - t.Errorf("Wrong value for 'long' tag. Want: %v, got: %v", tt.wantLong, value) - } - } - - if tt.wantEnv != "" { - value, _ := el.Tag.Lookup("env") - if value != tt.wantEnv { - t.Errorf("Wrong value for 'env' tag. Want: %v, got: %v", tt.wantEnv, value) - } - } - - if tt.wantDescription != "" { - value, _ := el.Tag.Lookup("description") - if value != tt.wantDescription { - t.Errorf("Wrong value for 'description' tag. Want: %v, got: %v", tt.wantDescription, value) - } - } - - if tt.wantRequired != "" { - value, _ := el.Tag.Lookup("required") - if value != tt.wantRequired { - t.Errorf("Wrong value for 'required' tag. Want: %v, got: %v", tt.wantRequired, value) - } - } - - if tt.wantGroup != "" { - value, _ := el.Tag.Lookup("group") - if value != tt.wantGroup { - t.Errorf("Wrong value for 'group' tag. Want: %v, got: %v", tt.wantGroup, value) - } - } - }) - } -} - -func TestStringableStruct_String(t *testing.T) { - if listenAddress("foo").String() != "foo" { - t.Error("Wrong convertation into string") - } - - if resourcesDirPath("bar").String() != "bar" { - t.Error("Wrong convertation into string") - } - - if configFilePath("baz").String() != "baz" { - t.Error("Wrong convertation into string") - } -} - -func TestConfigFilePath_IsValidValue(t *testing.T) { - // create temp dir (and delete if after test) - tmpDir, dirErr := ioutil.TempDir("", "test-") - if dirErr != nil { - t.Fatal(dirErr) - } - defer func() { - if err := os.RemoveAll(tmpDir); err != nil { - t.Fatal(err) - } - }() - - // create temp file in temp dir - tmpFile, fileErr := os.Create(filepath.Join(tmpDir, "test-file")) - if fileErr != nil { - t.Fatal(fileErr) - } - _ = tmpFile.Close() // is not needed - - tests := []struct { - name string - giveValue string - wantError error - }{ - { - name: "Correct path", - giveValue: tmpFile.Name(), - wantError: nil, - }, - { - name: "Some directory path passed", - giveValue: tmpDir, - wantError: fmt.Errorf("config file [%s] was not found", tmpDir), - }, - { - name: "Wrong file path", - giveValue: "abracadabra !", - wantError: errors.New("config file [abracadabra !] was not found"), - }, - { - name: "Empty value passed", - giveValue: "", - wantError: errors.New("config file [] was not found"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - res := configFilePath("").IsValidValue(tt.giveValue) - - if res != nil { - if tt.wantError == nil { - t.Errorf("Unexpected error %v returned", res) - } else if res.Error() != tt.wantError.Error() { - t.Errorf("Wrong error returned. Want: %v, got: %v", tt.wantError, res) - } - } - }) - } -} - -func TestListenPort_IsValidValue(t *testing.T) { - var defaultErrorMessage = "wrong port number (must be in interval 1..65535)" - - tests := []struct { - name string - giveValue string - wantError error - }{ - { - name: "Correct port", - giveValue: "8080", - wantError: nil, - }, - { - name: "Too much port number", - giveValue: "65536", - wantError: errors.New(defaultErrorMessage), - }, - { - name: "Too low port number", - giveValue: "-1", - wantError: errors.New(defaultErrorMessage), - }, - { - name: "Empty value passed", - giveValue: "", - wantError: errors.New("wrong port value (cannot be converted into number)"), - }, - { - name: "Alpha-string", - giveValue: "foo bar", - wantError: errors.New("wrong port value (cannot be converted into number)"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - res := listenPort(0).IsValidValue(tt.giveValue) - - if res != nil { - if tt.wantError == nil { - t.Errorf("Unexpected error %v returned", res) - } else if res.Error() != tt.wantError.Error() { - t.Errorf("Wrong error returned. Want: %v, got: %v", tt.wantError, res) - } - } - }) - } -} - -func TestResourcesDirPath_IsValidValue(t *testing.T) { - // create temp dir (and delete if after test) - tmpDir, dirErr := ioutil.TempDir("", "test-") - if dirErr != nil { - t.Fatal(dirErr) - } - defer func() { - if err := os.RemoveAll(tmpDir); err != nil { - t.Fatal(err) - } - }() - - // create temp file in temp dir - tmpFile, fileErr := os.Create(filepath.Join(tmpDir, "test-file")) - if fileErr != nil { - t.Fatal(fileErr) - } - _ = tmpFile.Close() // is not needed - - tests := []struct { - name string - giveValue string - wantError error - }{ - { - name: "Correct path", - giveValue: tmpDir, - wantError: nil, - }, - { - name: "Some file path passed", - giveValue: tmpFile.Name(), - wantError: fmt.Errorf("resources directory [%s] was not found", tmpFile.Name()), - }, - { - name: "Wrong file path", - giveValue: "abracadabra !", - wantError: errors.New("resources directory [abracadabra !] was not found"), - }, - { - name: "Empty value passed", - giveValue: "", - wantError: errors.New("resources directory [] was not found"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - res := resourcesDirPath("").IsValidValue(tt.giveValue) - - if res != nil { - if tt.wantError == nil { - t.Errorf("Unexpected error %v returned", res) - } else if res.Error() != tt.wantError.Error() { - t.Errorf("Wrong error returned. Want: %v, got: %v", tt.wantError, res) - } - } - }) - } -} - -func TestCommand_getSettings(t *testing.T) { //nolint:gocyclo - // Create temporary file inside just created temporary directory. - createTempFile := func(t *testing.T) (*os.File, string) { - t.Helper() - - tmpDir, err := ioutil.TempDir("", "test-") - if err != nil { - t.Fatal(err) - } - - tmpFile, fileErr := os.Create(filepath.Join(tmpDir, "test-file")) - if fileErr != nil { - t.Fatal(fileErr) - } - - return tmpFile, tmpDir - } - - tests := []struct { - name string - giveCommand *Command - giveFilePath func(t *testing.T) string - wantSettings *serve.Settings - wantError bool - }{ - { - name: "Without overriding settings from config file", - giveCommand: &Command{}, - giveFilePath: func(t *testing.T) string { - tmpFile, _ := createTempFile(t) - defer func() { - if err := tmpFile.Close(); err != nil { - t.Fatal(err) - } - }() - - _, _ = tmpFile.Write([]byte(` -listen: - address: '1.2.3.4' - port: 321 -resources: - dir: /tmp -`)) - - return tmpFile.Name() - }, - wantSettings: &serve.Settings{ - Listen: serve.Listen{ - Address: "1.2.3.4", - Port: 321, - }, - Resources: serve.Resources{ - DirPath: "/tmp", - }, - }, - }, - { - name: "With settings overriding", - giveCommand: &Command{ - ServingOptions: listenOptions{ - Address: "8.8.8.8", - Port: 666, - }, - ResourcesOptions: resourcesOptions{ - ResourcesDir: "/tmp/foo/bar", - }, - }, - giveFilePath: func(t *testing.T) string { - tmpFile, _ := createTempFile(t) - defer func() { - if err := tmpFile.Close(); err != nil { - t.Fatal(err) - } - }() - - _, _ = tmpFile.Write([]byte(` -listen: - address: '1.2.3.4' - port: 321 -resources: - dir: /tmp -`)) - - return tmpFile.Name() - }, - wantSettings: &serve.Settings{ - Listen: serve.Listen{ - Address: "8.8.8.8", - Port: 666, - }, - Resources: serve.Resources{ - DirPath: "/tmp/foo/bar", - }, - }, - }, - { - name: "With missing config file path", - giveCommand: &Command{}, - giveFilePath: func(t *testing.T) string { - return "foo bar" - }, - wantSettings: &serve.Settings{}, - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - filePath := tt.giveFilePath(t) - defer func(tmpFile string) { - if info, err := os.Stat(tmpFile); err != nil || !info.Mode().IsRegular() { - return - } - dirPath, err := filepath.Abs(filepath.Dir(tmpFile)) - if err != nil { - t.Fatal(err) - } - if err := os.RemoveAll(dirPath); err != nil { - t.Fatal(err) - } - }(filePath) - - gotSettings, err := tt.giveCommand.getSettings(filePath) - - if err != nil && !tt.wantError { - t.Errorf("Unexpected error [%v] returned", err) - } else if tt.wantError && err == nil { - t.Error("Expects error, but nothing returned") - } - - if !tt.wantError && !reflect.DeepEqual(gotSettings, tt.wantSettings) { - t.Errorf("Unexpected settings returned. Want: %v, got: %v", tt.wantSettings, gotSettings) - } - }) - } -} - -func TestCommand_Execute(t *testing.T) { - t.Skip("Not implemented yet") -} diff --git a/internal/pkg/cmd/version/command.go b/internal/pkg/cmd/version/command.go deleted file mode 100644 index 028b68e9..00000000 --- a/internal/pkg/cmd/version/command.go +++ /dev/null @@ -1,16 +0,0 @@ -package version - -import ( - "fmt" - - ver "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/version" -) - -type Command struct{} - -// Execute version command. -func (*Command) Execute(_ []string) error { - fmt.Printf("Version: %s\n", ver.Version()) - - return nil -} diff --git a/internal/pkg/cmd/version/command_test.go b/internal/pkg/cmd/version/command_test.go deleted file mode 100644 index 32b910b9..00000000 --- a/internal/pkg/cmd/version/command_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package version - -import ( - "bytes" - "io" - "os" - "strings" - "testing" -) - -func TestCommand_Execute(t *testing.T) { - captureOutput := func(f func()) string { - t.Helper() - - r, w, err := os.Pipe() - if err != nil { - panic(err) - } - - stdout := os.Stdout - os.Stdout = w - defer func() { - os.Stdout = stdout - }() - f() - _ = w.Close() - - var buf bytes.Buffer - _, _ = io.Copy(&buf, r) - return buf.String() - } - - tests := []struct { - name string - giveArgs []string - wantOutput []string - wantErr bool - wantErrorMessage string - }{ - { - name: "By default", - giveArgs: []string{}, - wantOutput: []string{"Version:", "undefined@undefined", "\n"}, - wantErr: false, - wantErrorMessage: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var err error - var cmd = Command{} - - output := captureOutput(func() { - err = cmd.Execute(tt.giveArgs) - }) - - if tt.wantOutput != nil { - for _, line := range tt.wantOutput { - if !strings.Contains(output, line) { - t.Errorf("Expected line [%s] in output [%s] was not found", line, output) - } - } - } - - if tt.wantErr && err.Error() != tt.wantErrorMessage { - t.Errorf("Expected error message [%s] was not found in %v", tt.wantErrorMessage, err) - } - }) - } -} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go new file mode 100644 index 00000000..cfde793d --- /dev/null +++ b/internal/pkg/config/config.go @@ -0,0 +1,84 @@ +// Package config contains application configuration struct with the most useful functions. +package config + +import ( + "io/ioutil" + + "github.com/a8m/envsubst" + "gopkg.in/yaml.v2" +) + +// Config is main application configuration. +type Config struct { + Sources []source `yaml:"sources"` + + RouterScript struct { + Redirect struct { + Address string `yaml:"address"` + } `yaml:"redirect"` + Exclude struct { + Hosts []string `yaml:"hosts"` + } `yaml:"exclude"` + Comment string `yaml:"comment"` + MaxSourcesCount uint16 `yaml:"max_sources"` + MaxSourceSizeBytes uint32 `yaml:"max_source_size"` + } `yaml:"router_script"` +} + +type source struct { + URI string `yaml:"uri"` + Name string `yaml:"name"` + Description string `yaml:"description"` + EnabledByDefault bool `yaml:"enabled"` + RecordsCount uint `yaml:"count"` // approximate quantity +} + +// AddSource into sources list. +func (cfg *Config) AddSource(uri, name, description string, enabledByDefault bool, recordsCount uint) { + cfg.Sources = append(cfg.Sources, source{ + URI: uri, + Name: name, + Description: description, + EnabledByDefault: enabledByDefault, + RecordsCount: recordsCount, + }) +} + +// FromYaml configures itself using YAML content. +func (cfg *Config) FromYaml(in []byte, expandEnv bool) error { + if expandEnv { + parsed, err := envsubst.Bytes(in) + if err != nil { + return err + } + + in = parsed + } + + if err := yaml.UnmarshalStrict(in, cfg); err != nil { + return err + } + + return nil +} + +// FromYaml creates new config instance using YAML-structured content. +func FromYaml(in []byte, expandEnv bool) (*Config, error) { + config := &Config{} + + if err := config.FromYaml(in, expandEnv); err != nil { + return nil, err + } + + return config, nil +} + +// FromYamlFile creates new config instance using YAML file. +func FromYamlFile(filename string, expandEnv bool) (*Config, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + return FromYaml(bytes, expandEnv) +} diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go new file mode 100644 index 00000000..a8a92516 --- /dev/null +++ b/internal/pkg/config/config_test.go @@ -0,0 +1,221 @@ +package config + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_AddSource(t *testing.T) { + cfg := Config{} + + assert.Len(t, cfg.Sources, 0) + cfg.AddSource("https://foo", "foo", "foo desc", true, 123) + assert.Len(t, cfg.Sources, 1) + assert.Equal(t, "https://foo", cfg.Sources[0].URI) + assert.Equal(t, "foo", cfg.Sources[0].Name) + assert.Equal(t, "foo desc", cfg.Sources[0].Description) + assert.True(t, cfg.Sources[0].EnabledByDefault) + assert.Equal(t, uint(123), cfg.Sources[0].RecordsCount) +} + +func TestFromYaml(t *testing.T) { + var cases = []struct { //nolint:maligned + name string + giveYaml []byte + giveExpandEnv bool + giveEnv map[string]string + wantErr bool + checkResultFn func(*testing.T, *Config) + wantConfig *Config + }{ + + { + name: "With all possible values", + giveExpandEnv: true, + giveYaml: []byte(` +# Some comment + +sources: + - uri: http://goo.gl/hosts.txt + name: Foo name + description: Foo desc + enabled: true + count: 123 + - uri: http://example.com/txt.stsoh # inline comment + name: Bar name + description: Bar desc + enabled: false + count: 321 + - uri: http://goo.gl/txt.stsoh + count: 2 + +router_script: + redirect: + address: 0.1.1.0 + exclude: + hosts: + - "foo" + - bar + comment: " [ blah ] " + max_sources: 1 + max_source_size: 4 +`), + wantErr: false, + checkResultFn: func(t *testing.T, config *Config) { + assert.Equal(t, "http://goo.gl/hosts.txt", config.Sources[0].URI) + assert.Equal(t, "Foo name", config.Sources[0].Name) + assert.Equal(t, "Foo desc", config.Sources[0].Description) + assert.True(t, config.Sources[0].EnabledByDefault) + assert.Equal(t, uint(123), config.Sources[0].RecordsCount) + + assert.Equal(t, "http://example.com/txt.stsoh", config.Sources[1].URI) + assert.Equal(t, "Bar name", config.Sources[1].Name) + assert.Equal(t, "Bar desc", config.Sources[1].Description) + assert.False(t, config.Sources[1].EnabledByDefault) + assert.Equal(t, uint(321), config.Sources[1].RecordsCount) + + assert.Equal(t, "http://goo.gl/txt.stsoh", config.Sources[2].URI) + assert.Equal(t, uint(2), config.Sources[2].RecordsCount) + + assert.Equal(t, "0.1.1.0", config.RouterScript.Redirect.Address) + assert.ElementsMatch(t, []string{"foo", "bar"}, config.RouterScript.Exclude.Hosts) + assert.Equal(t, " [ blah ] ", config.RouterScript.Comment) + assert.Equal(t, uint16(1), config.RouterScript.MaxSourcesCount) + assert.Equal(t, uint32(4), config.RouterScript.MaxSourceSizeBytes) + }, + }, + + { + name: "ENV variables expanded", + giveExpandEnv: true, + giveEnv: map[string]string{"__TEST_ADDR": "1.2.3.4", "__TEST_COMMENT": "foo"}, + giveYaml: []byte(` +router_script: + redirect: + address: ${__TEST_ADDR} + comment: ${__TEST_COMMENT} +`), + wantErr: false, + checkResultFn: func(t *testing.T, config *Config) { + assert.Equal(t, "1.2.3.4", config.RouterScript.Redirect.Address) + assert.Equal(t, "foo", config.RouterScript.Comment) + }, + }, + { + name: "ENV variables NOT expanded", + giveExpandEnv: false, + giveYaml: []byte(` +router_script: + redirect: + address: ${__TEST_ADDR} +`), + wantErr: false, + checkResultFn: func(t *testing.T, config *Config) { + assert.Equal(t, "${__TEST_ADDR}", config.RouterScript.Redirect.Address) + }, + }, + { + name: "ENV variables defaults", + giveExpandEnv: true, + giveYaml: []byte(` +router_script: + redirect: + address: ${__TEST_ADDR:-2.3.4.5} + comment: ${__TEST_COMMENT:-foo} +`), + wantErr: false, + checkResultFn: func(t *testing.T, config *Config) { + assert.Equal(t, "2.3.4.5", config.RouterScript.Redirect.Address) + assert.Equal(t, "foo", config.RouterScript.Comment) + }, + }, + { + name: "broken yaml", + giveYaml: []byte(`foo bar`), + wantErr: true, + }, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if tt.giveEnv != nil { + for key, value := range tt.giveEnv { + assert.NoError(t, os.Setenv(key, value)) + } + } + + conf, err := FromYaml(tt.giveYaml, tt.giveExpandEnv) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.Nil(t, err) + tt.checkResultFn(t, conf) + } + + if tt.giveEnv != nil { + for key := range tt.giveEnv { + assert.NoError(t, os.Unsetenv(key)) + } + } + }) + } +} + +func TestFromYamlFile(t *testing.T) { + var cases = []struct { + name string + giveYaml []byte + giveExpandEnv bool + wantError bool + checkResultFn func(*testing.T, *Config) + }{ + { + name: "Using correct yaml", + giveExpandEnv: true, + giveYaml: []byte(` +router_script: + redirect: + address: 0.1.1.0 + comment: "foo" +`), + checkResultFn: func(t *testing.T, config *Config) { + assert.Equal(t, "0.1.1.0", config.RouterScript.Redirect.Address) + assert.Equal(t, "foo", config.RouterScript.Comment) + }, + }, + { + name: "Using broken file (wrong format)", + giveExpandEnv: true, + giveYaml: []byte(`!foo bar`), + wantError: true, + }, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + file, err := ioutil.TempFile("", "unit-test-") + assert.NoError(t, err) + + _, err = file.Write(tt.giveYaml) + assert.NoError(t, err) + assert.NoError(t, file.Close()) + + defer func() { assert.NoError(t, os.Remove(file.Name())) }() // cleanup + + conf, loadingErr := FromYamlFile(file.Name(), tt.giveExpandEnv) + + if tt.wantError { + assert.Error(t, loadingErr) + } else { + assert.NoError(t, loadingErr) + tt.checkResultFn(t, conf) + } + }) + } +} diff --git a/internal/pkg/env/env.go b/internal/pkg/env/env.go new file mode 100644 index 00000000..a0891d64 --- /dev/null +++ b/internal/pkg/env/env.go @@ -0,0 +1,37 @@ +// Package env contains all about environment variables, that can be used by current application. +package env + +import "os" + +type envVariable string + +const ( + // ListenAddr is IP address for listening. + ListenAddr envVariable = "LISTEN_ADDR" + + // ListenPort is port number for listening. + ListenPort envVariable = "LISTEN_PORT" + + // ResourcesDir is a directory with resources. + ResourcesDir envVariable = "RESOURCES_DIR" + + // ConfigPath is a path to the configuration file. + ConfigPath envVariable = "CONFIG_PATH" + + // CachingEngine is a caching engine name (like "redis", "memory" or something else). + CachingEngine envVariable = "CACHING_ENGINE" + + // CacheTTL is a cache items life time. + CacheTTL envVariable = "CACHE_TTL" + + // RedisDSN is URL-like redis connection string . + RedisDSN envVariable = "REDIS_DSN" +) + +// String returns environment variable name in the string representation. +func (e envVariable) String() string { return string(e) } + +// Lookup retrieves the value of the environment variable. If the variable is present in the environment the value +// (which may be empty) is returned and the boolean is true. Otherwise the returned value will be empty and the +// boolean will be false. +func (e envVariable) Lookup() (string, bool) { return os.LookupEnv(string(e)) } diff --git a/internal/pkg/env/env_test.go b/internal/pkg/env/env_test.go new file mode 100644 index 00000000..9a72b32b --- /dev/null +++ b/internal/pkg/env/env_test.go @@ -0,0 +1,49 @@ +package env + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConstants(t *testing.T) { + assert.Equal(t, "LISTEN_ADDR", string(ListenAddr)) + assert.Equal(t, "LISTEN_PORT", string(ListenPort)) + assert.Equal(t, "RESOURCES_DIR", string(ResourcesDir)) + assert.Equal(t, "CONFIG_PATH", string(ConfigPath)) + assert.Equal(t, "CACHING_ENGINE", string(CachingEngine)) + assert.Equal(t, "CACHE_TTL", string(CacheTTL)) + assert.Equal(t, "REDIS_DSN", string(RedisDSN)) +} + +func TestEnvVariable_Lookup(t *testing.T) { + cases := []struct { + giveEnv envVariable + }{ + {giveEnv: ListenAddr}, + {giveEnv: ListenPort}, + {giveEnv: ResourcesDir}, + {giveEnv: ConfigPath}, + {giveEnv: CachingEngine}, + {giveEnv: CacheTTL}, + {giveEnv: RedisDSN}, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.giveEnv.String(), func(t *testing.T) { + defer func() { assert.NoError(t, os.Unsetenv(tt.giveEnv.String())) }() + + value, exists := tt.giveEnv.Lookup() + assert.False(t, exists) + assert.Empty(t, value) + + assert.NoError(t, os.Setenv(tt.giveEnv.String(), "foo")) + + value, exists = tt.giveEnv.Lookup() + assert.True(t, exists) + assert.Equal(t, "foo", value) + }) + } +} diff --git a/internal/pkg/http/api/router_test.go b/internal/pkg/http/api/router_test.go deleted file mode 100644 index 11134b62..00000000 --- a/internal/pkg/http/api/router_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" -) - -func TestGetRoutesHandlerFunc(t *testing.T) { - var ( - req, _ = http.NewRequest("GET", "http://testing", nil) - rr = httptest.NewRecorder() - router = mux.NewRouter() - ) - - router. - HandleFunc("/gen", func(http.ResponseWriter, *http.Request) {}). - Methods("GET"). - Name("script_generator") - - router. - HandleFunc("/foo", func(http.ResponseWriter, *http.Request) {}). - Methods("GET"). - Name("foo_bar") // must be skipped - - GetRoutesHandlerFunc(router)(rr, req) - - if rr.Code != http.StatusOK { - t.Errorf("Wrong response HTTP code. Want %d, got %d", http.StatusOK, rr.Code) - } - - data := make(map[string]interface{}) - if err := json.Unmarshal(rr.Body.Bytes(), &data); err != nil { - t.Fatal(err) - } - - if len(data) != 1 { - t.Errorf("Wrong routes count. Expected length is 1, actual: %v", len(data)) - } - - if data["script_generator"].(map[string]interface{})["path"] != "/gen" { - t.Errorf("Required route for `script_generator` was mot found in %v", data) - } -} diff --git a/internal/pkg/http/api/routes.go b/internal/pkg/http/api/routes.go deleted file mode 100644 index 70565957..00000000 --- a/internal/pkg/http/api/routes.go +++ /dev/null @@ -1,34 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" -) - -// supported route names -var routeNames = [...]string{"script_generator"} - -type ( - route struct { - Path string `json:"path"` - } -) - -// GetSettingsHandlerFunc returns handler function that writes json response with version data into response writer. -func GetRoutesHandlerFunc(router *mux.Router) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - - response := make(map[string]route) - - for _, name := range routeNames { - if path, err := router.Get(name).GetPathTemplate(); err == nil { - response[name] = route{Path: path} - } - } - - _ = json.NewEncoder(w).Encode(response) - } -} diff --git a/internal/pkg/http/api/settings.go b/internal/pkg/http/api/settings.go deleted file mode 100644 index 402b5866..00000000 --- a/internal/pkg/http/api/settings.go +++ /dev/null @@ -1,94 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" -) - -type ( - providedSource struct { - URI string `json:"uri"` - Name string `json:"name"` - Description string `json:"description"` - ByDefault bool `json:"default"` - Count int `json:"count"` - } - - sources struct { - Provided []providedSource `json:"provided"` - Max int `json:"max"` - MaxSourceSize int `json:"max_source_size"` // in bytes - } - - redirect struct { - Addr string `json:"addr"` - } - - records struct { - Comment string `json:"comment"` - } - - excludes struct { - Hosts []string `json:"hosts"` - } - - settingsResponse struct { - Sources sources `json:"sources"` - Redirect redirect `json:"redirect"` - Records records `json:"records"` - Excludes excludes `json:"excludes"` - Cache cache `json:"cache"` - } - - cache struct { - LifetimeSec int `json:"lifetime_sec"` - } -) - -// GetSettingsHandlerFunc returns handler function that writes json response with possible settings into response writer. -func GetSettingsHandlerFunc(serveSettings *serve.Settings) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(convertServeSettingsIntoResponse(serveSettings)) - } -} - -// convertServeSettingsIntoResponse converts serving settings into internal response format. -func convertServeSettingsIntoResponse(settings *serve.Settings) *settingsResponse { - // set basic properties - response := &settingsResponse{ - Sources: sources{ - Max: settings.RouterScript.MaxSources, - MaxSourceSize: settings.RouterScript.MaxSourceSize, - }, - Redirect: redirect{ - Addr: settings.RouterScript.Redirect.Address, - }, - Records: records{ - Comment: settings.RouterScript.Comment, - }, - Cache: cache{ - LifetimeSec: settings.Cache.LifetimeSec, - }, - Excludes: excludes{}, - } - - // append excluded hosts list - response.Excludes.Hosts = append(response.Excludes.Hosts, settings.RouterScript.Exclude.Hosts...) - - // append sources list entries - for _, source := range settings.Sources { - response.Sources.Provided = append(response.Sources.Provided, providedSource{ - URI: source.URI, - Name: source.Name, - Description: source.Description, - ByDefault: source.EnabledByDefault, - Count: source.RecordsCount, - }) - } - - return response -} diff --git a/internal/pkg/http/api/settings_test.go b/internal/pkg/http/api/settings_test.go deleted file mode 100644 index 1f6947c1..00000000 --- a/internal/pkg/http/api/settings_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" -) - -func TestGetSettingsHandlerFunc(t *testing.T) { //nolint:gocyclo - var ( - req, _ = http.NewRequest("GET", "http://testing", nil) - rr = httptest.NewRecorder() - serveSettings = serve.Settings{ - Sources: []serve.Source{{ - URI: "http://goo.gl/hosts.txt", - Name: "Foo name", - Description: "Foo desc", - EnabledByDefault: true, - RecordsCount: 123, - }, { - URI: "http://face.book/hosts.txt", - Name: "Bar name", - Description: "Bar desc", - EnabledByDefault: false, - RecordsCount: -321, - }}, - RouterScript: serve.RouterScript{ - Redirect: serve.Redirect{ - Address: "0.1.1.0", - }, - Exclude: serve.Excludes{ - Hosts: []string{"foo", "bar"}, - }, - MaxSources: 1, - Comment: " [ blah ] ", - MaxSourceSize: 666, - }, - Cache: serve.Cache{ - LifetimeSec: 1234, - }, - } - ) - - GetSettingsHandlerFunc(&serveSettings)(rr, req) - - if rr.Code != http.StatusOK { - t.Errorf("Wrong response HTTP code. Want %d, got %d", http.StatusOK, rr.Code) - } - - data := make(map[string]interface{}) - if err := json.Unmarshal(rr.Body.Bytes(), &data); err != nil { - t.Fatal(err) - } - - var ( - sourcesProvided = data["sources"].(map[string]interface{})["provided"].([]interface{}) - sourcesMax = int(data["sources"].(map[string]interface{})["max"].(float64)) - maxSourceSize = int(data["sources"].(map[string]interface{})["max_source_size"].(float64)) - redirectAddr = data["redirect"].(map[string]interface{})["addr"].(string) - recordsComment = data["records"].(map[string]interface{})["comment"].(string) - cacheLifetimeSec = int(data["cache"].(map[string]interface{})["lifetime_sec"].(float64)) - excludesHosts = data["excludes"].(map[string]interface{})["hosts"].([]interface{}) - ) - - if len(serveSettings.Sources) != len(sourcesProvided) { - t.Errorf("Wrong source records count. Want: %v, got: %v", len(serveSettings.Sources), len(sourcesProvided)) - } - - for i := range serveSettings.Sources { - if sourcesProvided[i].(map[string]interface{})["name"] != serveSettings.Sources[i].Name { - t.Errorf("Unexpected source name found in: %v", sourcesProvided[i]) - } - if int(sourcesProvided[i].(map[string]interface{})["count"].(float64)) != serveSettings.Sources[i].RecordsCount { - t.Errorf("Unexpected records count found in: %v", sourcesProvided[i]) - } - if sourcesProvided[i].(map[string]interface{})["default"] != serveSettings.Sources[i].EnabledByDefault { - t.Errorf("Unexpected default value found in: %v", sourcesProvided[i]) - } - if sourcesProvided[i].(map[string]interface{})["description"] != serveSettings.Sources[i].Description { - t.Errorf("Unexpected source description found in: %v", sourcesProvided[i]) - } - if sourcesProvided[i].(map[string]interface{})["uri"] != serveSettings.Sources[i].URI { - t.Errorf("Unexpected URI found in: %v", sourcesProvided[i]) - } - } - - if sourcesMax != serveSettings.RouterScript.MaxSources { - t.Errorf("Unexpected max sources: got %v, want %v", sourcesMax, serveSettings.RouterScript.MaxSources) - } - - if maxSourceSize != serveSettings.RouterScript.MaxSourceSize { - t.Errorf("Unexpected max source size: got %v, want %v", maxSourceSize, serveSettings.RouterScript.MaxSourceSize) - } - - if redirectAddr != serveSettings.RouterScript.Redirect.Address { - t.Errorf("Unexpected redirect address comment: got %v, want %v", redirectAddr, serveSettings.RouterScript.Redirect.Address) - } - - if cacheLifetimeSec != serveSettings.Cache.LifetimeSec { - t.Errorf("Unexpected cache lifetime: got %v, want %v", cacheLifetimeSec, serveSettings.Cache.LifetimeSec) - } - - if recordsComment != serveSettings.RouterScript.Comment { - t.Errorf("Unexpected records comment: got %v, want %v", recordsComment, serveSettings.RouterScript.Comment) - } - - for i := range serveSettings.RouterScript.Exclude.Hosts { - if excludesHosts[i] != serveSettings.RouterScript.Exclude.Hosts[i] { - t.Errorf("Unexpected excluded host found: %v", excludesHosts[i]) - } - } -} diff --git a/internal/pkg/http/api/version.go b/internal/pkg/http/api/version.go deleted file mode 100644 index bc6d5512..00000000 --- a/internal/pkg/http/api/version.go +++ /dev/null @@ -1,23 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - ver "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/version" -) - -type ( - version struct { - Version string `json:"version"` - } -) - -// GetVersionHandler writes json response with version data into response writer. -func GetVersionHandler(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(version{ - Version: ver.Version(), - }) -} diff --git a/internal/pkg/http/api/version_test.go b/internal/pkg/http/api/version_test.go deleted file mode 100644 index 46b35cc6..00000000 --- a/internal/pkg/http/api/version_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - ver "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/version" -) - -func TestGetVersionHandler(t *testing.T) { - var ( - req, _ = http.NewRequest("GET", "http://testing", nil) - rr = httptest.NewRecorder() - ) - - GetVersionHandler(rr, req) - - if rr.Code != http.StatusOK { - t.Errorf("Wrong response HTTP code. Want %d, got %d", http.StatusOK, rr.Code) - } - - data := make(map[string]interface{}) - if err := json.Unmarshal(rr.Body.Bytes(), &data); err != nil { - t.Fatal(err) - } - - if version, _ := data["version"].(string); version != ver.Version() { - t.Errorf("unexpected version: got %v want %v", version, ver.Version()) - } -} diff --git a/internal/pkg/http/fileserver/errors.go b/internal/pkg/http/fileserver/errors.go new file mode 100644 index 00000000..20a811ec --- /dev/null +++ b/internal/pkg/http/fileserver/errors.go @@ -0,0 +1,79 @@ +package fileserver + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path" + "strconv" + "strings" +) + +// ErrorPageTemplate is error page template in string representation. Is allowed to use basic "replacing patterns" +// like `{{ code }}` or `{{ message }}`. +type ErrorPageTemplate string + +// String converts template into string representation. +func (t ErrorPageTemplate) String() string { return string(t) } + +// Build makes registered patterns replacing. +func (t ErrorPageTemplate) Build(errorCode int) string { + out := t.String() + + for k, v := range map[string]string{ + "code": strconv.Itoa(errorCode), + "message": http.StatusText(errorCode), + } { + out = strings.ReplaceAll(out, fmt.Sprintf("{{ %s }}", k), v) + } + + return out +} + +type jsonError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// JSONErrorHandler respond with simple json-formatted response, if json format was requested (defined in `Accept` +// header). +func JSONErrorHandler() ErrorHandlerFunc { + return func(w http.ResponseWriter, r *http.Request, fs *FileServer, errorCode int) bool { + if strings.Contains(r.Header.Get("Accept"), "json") { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(errorCode) + + _ = json.NewEncoder(w).Encode(jsonError{ + Code: errorCode, + Message: http.StatusText(errorCode), + }) + + return true + } + + return false + } +} + +// StaticHTMLPageErrorHandler allows to use user-defined local file with HTML for error page generating. +func StaticHTMLPageErrorHandler() ErrorHandlerFunc { //nolint:gocognit + return func(w http.ResponseWriter, r *http.Request, fs *FileServer, errorCode int) bool { + if len(fs.Settings.ErrorFileName) > 0 { + if f, err := os.Open(path.Join(fs.Settings.FilesRoot, fs.Settings.ErrorFileName)); err == nil { + defer func() { _ = f.Close() }() + + if data, err := ioutil.ReadAll(f); err == nil { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(errorCode) + _, _ = w.Write([]byte(ErrorPageTemplate(data).Build(errorCode))) + + return true + } + } + } + + return false + } +} diff --git a/internal/pkg/http/fileserver/errors_test.go b/internal/pkg/http/fileserver/errors_test.go new file mode 100644 index 00000000..b4f5525e --- /dev/null +++ b/internal/pkg/http/fileserver/errors_test.go @@ -0,0 +1,87 @@ +package fileserver + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrorPageTemplate_String(t *testing.T) { + assert.Equal(t, "foo", ErrorPageTemplate("foo").String()) +} + +func TestErrorPageTemplate_Build(t *testing.T) { + assert.Equal(t, + "foo 200 <> OK", + ErrorPageTemplate("foo {{ code }} <> {{ message }}").Build(200), + ) +} + +func TestJSONErrorHandler(t *testing.T) { + tmpDir, _ := ioutil.TempDir("", "test-") + defer func(d string) { assert.NoError(t, os.RemoveAll(d)) }(tmpDir) + + fs, _ := NewFileServer(Settings{FilesRoot: tmpDir}) + assert.NotNil(t, fs) + + handler := JSONErrorHandler() + + var ( + req, _ = http.NewRequest(http.MethodGet, "", nil) + rr = httptest.NewRecorder() + ) + + assert.False(t, handler(rr, req, fs, http.StatusNotFound)) + + req, _ = http.NewRequest(http.MethodGet, "", nil) + req.Header.Add("Accept", "application/json") + + rr = httptest.NewRecorder() + + assert.True(t, handler(rr, req, fs, http.StatusNotFound)) + assert.Equal(t, "application/json; charset=utf-8", rr.Header().Get("Content-Type")) + assert.JSONEq(t, `{"code":404,"message":"Not Found"}`, rr.Body.String()) +} + +func TestStaticHtmlPageErrorHandler(t *testing.T) { + tmpDir, _ := ioutil.TempDir("", "test-") + defer func(d string) { assert.NoError(t, os.RemoveAll(d)) }(tmpDir) + + fs, _ := NewFileServer(Settings{ + FilesRoot: tmpDir, + }) + assert.NotNil(t, fs) + + handler := StaticHTMLPageErrorHandler() + + var ( + req, _ = http.NewRequest(http.MethodGet, "", nil) + rr = httptest.NewRecorder() + ) + + assert.False(t, handler(rr, req, fs, http.StatusNotFound)) + + // create template file + file, _ := os.Create(filepath.Join(tmpDir, "error.html")) + _, _ = file.Write([]byte("template: {{ message }} | {{ code }}")) + file.Close() + + fs.Settings.ErrorFileName = "error.html" + + req, _ = http.NewRequest(http.MethodGet, "", nil) + rr = httptest.NewRecorder() + + assert.True(t, handler(rr, req, fs, http.StatusBadGateway)) + assert.Equal(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type")) + assert.Equal(t, `template: Bad Gateway | 502`, rr.Body.String()) + + rr = httptest.NewRecorder() + + assert.True(t, handler(rr, req, fs, http.StatusNotFound)) + assert.Equal(t, `template: Not Found | 404`, rr.Body.String()) +} diff --git a/internal/pkg/http/fileserver/fileserver.go b/internal/pkg/http/fileserver/fileserver.go index d9cd957e..b0e31ca0 100644 --- a/internal/pkg/http/fileserver/fileserver.go +++ b/internal/pkg/http/fileserver/fileserver.go @@ -1,128 +1,141 @@ +// Package fileserver contains static files server implementation. package fileserver import ( "fmt" - "io" "net/http" "os" "path" "path/filepath" "strings" - "time" ) -type ( - FileNotFoundHandler func(http.ResponseWriter, *http.Request) +const ( + defaultFallbackErrorContent = "

Error {{ code }}

{{ message }}

" + defaultIndexFileName = "index.html" +) - Settings struct { - Root http.Dir - NotFoundHandler FileNotFoundHandler // optionally - IndexFile string - Error404file string - } +// ErrorHandlerFunc is used as handler for errors processing. If func return `true` - next handler will be NOT executed. +type ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, fs *FileServer, errorCode int) (doNotContinue bool) - FileServer struct { - Settings Settings - } -) +// FileServer is a main file server structure (implements `http.Handler` interface). +type FileServer struct { + // Server settings (some of them can be changed in runtime). + Settings Settings -// Serve requests to the "public" files and directories. -func (fileServer *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { //nolint:gocyclo,funlen - // redirect .../index.html to .../ - if strings.HasSuffix(r.URL.Path, "/"+fileServer.Settings.IndexFile) { - http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len(fileServer.Settings.IndexFile)], http.StatusMovedPermanently) - return + // If all error handlers fails - this content will be used as fallback for error page generating. + FallbackErrorContent string + + // Error handlers stack. + ErrorHandlers []ErrorHandlerFunc +} + +// Settings describes file server options. +type Settings struct { + // Directory path, where files for serving is located. + FilesRoot string + + // File name (relative path to the file) that will be used as an index (like ). + IndexFileName string + + // File name (relative path to the file) that will be used as error page template. + ErrorFileName string + + // Respond "index file" request with redirection to the root (`example.com/index.html` -> `example.com/`). + RedirectIndexFileToRoot bool +} + +// NewFileServer creates new file server with default settings. Feel free to change default behavior. +func NewFileServer(s Settings) (*FileServer, error) { //nolint:gocritic + if info, err := os.Stat(s.FilesRoot); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf(`directory "%s" does not exists`, s.FilesRoot) + } + + return nil, err + } else if !info.IsDir() { + return nil, fmt.Errorf(`"%s" is not directory`, s.FilesRoot) } - // if empty, set current directory - dir := string(fileServer.Settings.Root) - if dir == "" { - dir = "." + if s.IndexFileName == "" { + s.IndexFileName = defaultIndexFileName } - // add prefix and clean - upath := r.URL.Path - if !strings.HasPrefix(upath, "/") { - upath = "/" + upath - r.URL.Path = upath + fs := &FileServer{ + Settings: s, + FallbackErrorContent: defaultFallbackErrorContent, } - // add index file name if requested directory (or server root) - if upath[len(upath)-1] == '/' { - upath += fileServer.Settings.IndexFile + + fs.ErrorHandlers = []ErrorHandlerFunc{ + JSONErrorHandler(), + StaticHTMLPageErrorHandler(), } - // make path clean - upath = path.Clean(upath) - - // path to file - name := path.Join(dir, filepath.FromSlash(upath)) - - // if files server root directory is set - try to find file and serve them - if len(fileServer.Settings.Root) > 0 { - // check for file exists - if f, err := os.Open(name); err == nil { - // file exists and opened - defer func() { - if err := f.Close(); err != nil { - panic(err) - } - }() - // file (or directory) exists - if stat, statErr := os.Stat(name); statErr == nil && stat.Mode().IsRegular() { - // requested file is file (not directory) - var modTime time.Time - // Try to extract file modified time - if info, err := f.Stat(); err == nil { - modTime = info.ModTime() - } else { - modTime = time.Now() // fallback - } - // serve fie content - http.ServeContent( - w, - r, - filepath.Base(upath), - modTime, - f, - ) + + return fs, nil +} + +func (fs *FileServer) handleError(w http.ResponseWriter, r *http.Request, errorCode int) { + if fs.ErrorHandlers != nil && len(fs.ErrorHandlers) > 0 { + for _, handler := range fs.ErrorHandlers { + if handler(w, r, fs, errorCode) { return } } } - // If all tries for content serving above has been failed - file was not found (HTTP 404) - if fileServer.Settings.NotFoundHandler != nil { - // If "file not found" handler is set - call them - fileServer.Settings.NotFoundHandler(w, r) + // fallback + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(errorCode) + + _, _ = w.Write([]byte(ErrorPageTemplate(fs.FallbackErrorContent).Build(errorCode))) +} + +// ServeHTTP responds to an HTTP request. +func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + fs.handleError(w, r, http.StatusMethodNotAllowed) + return } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusNotFound) - - // at first - we try to find local file with error content - if len(fileServer.Settings.Root) > 0 { - var errPage = string(fileServer.Settings.Root) + "/" + fileServer.Settings.Error404file - if f, err := os.Open(errPage); err == nil { - // file exists and opened - defer func() { - if err := f.Close(); err != nil { - panic(err) - } - }() - // file (or directory) exists - if stat, statErr := os.Stat(errPage); statErr == nil && stat.Mode().IsRegular() { - // requested file is file (not directory) - if _, writeErr := io.Copy(w, f); writeErr != nil { - panic(writeErr) - } - return - } + if fs.Settings.RedirectIndexFileToRoot && len(fs.Settings.IndexFileName) > 0 { + // redirect .../index.html to .../ + if strings.HasSuffix(r.URL.Path, "/"+fs.Settings.IndexFileName) { + http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len(fs.Settings.IndexFileName)], http.StatusMovedPermanently) + + return } } - // fallback - if _, err := fmt.Fprint(w, "

ERROR 404

Requested file was not found

"); err != nil { - panic(err) + urlPath := r.URL.Path + + // add leading `/` (if required) + if urlPath == "" || !strings.HasPrefix(urlPath, "/") { + urlPath = "/" + r.URL.Path + } + + // if directory requested (or server root) - add index file name + if len(fs.Settings.IndexFileName) > 0 && urlPath[len(urlPath)-1] == '/' { + urlPath += fs.Settings.IndexFileName } + + // prepare target file path + filePath := path.Join(fs.Settings.FilesRoot, filepath.FromSlash(path.Clean(urlPath))) + + // check for file existence + if stat, err := os.Stat(filePath); err == nil && stat.Mode().IsRegular() { + if file, err := os.Open(filePath); err == nil { + defer func() { _ = file.Close() }() + + http.ServeContent(w, r, filepath.Base(filePath), stat.ModTime(), file) + + return + } + + fs.handleError(w, r, http.StatusInternalServerError) + + return + } + + fs.handleError(w, r, http.StatusNotFound) } diff --git a/internal/pkg/http/fileserver/fileserver_test.go b/internal/pkg/http/fileserver/fileserver_test.go index ee8615ab..0689e00d 100644 --- a/internal/pkg/http/fileserver/fileserver_test.go +++ b/internal/pkg/http/fileserver/fileserver_test.go @@ -2,218 +2,280 @@ package fileserver import ( "io/ioutil" + "math/rand" "net/http" "net/http/httptest" "os" "path/filepath" - "reflect" "testing" + "time" + + "github.com/stretchr/testify/assert" ) -func TestFileServer_ServeHTTP(t *testing.T) { //nolint:gocyclo,funlen - // Create directory in temporary - createTempDir := func() string { - t.Helper() - if dir, err := ioutil.TempDir("", "test-"); err != nil { - panic(err) - } else { - return dir - } +func init() { //nolint:gochecknoinits + rand.Seed(time.Now().UnixNano()) +} + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") //nolint:gochecknoglobals + +func RandStringRunes(t *testing.T, n int) string { + t.Helper() + + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] //nolint:gosec } - tests := []struct { - name string - giveDirs []string - giveFiles map[string][]byte - giveNotFoundHandler FileNotFoundHandler - giveIndexFile string - giveError404file string - giveRequestURI string - giveRequestMethod string - wantResponseCode int - wantResponseBody []byte - wantContentType string - wantRedirectTo string + return string(b) +} + +func TestNewFileServer_WrongDirectoryError(t *testing.T) { + fs, err := NewFileServer(Settings{ + FilesRoot: RandStringRunes(t, 32), + }) + + assert.Nil(t, fs) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not exists") + + tmpDir, _ := ioutil.TempDir("", "test-") + defer func(d string) { assert.NoError(t, os.RemoveAll(d)) }(tmpDir) + file, _ := os.Create(filepath.Join(tmpDir, "foo")) + file.Close() + + fs, err = NewFileServer(Settings{ + FilesRoot: file.Name(), + }) + + assert.Nil(t, fs) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not directory") +} + +func TestFileServer_ServeHTTP(t *testing.T) { + var cases = []struct { + name string + giveDirs []string + giveFiles map[string][]byte + giveSettings Settings + giveRequestMethod string + giveRequestURI string + giveRequestHeaders map[string]string + beforeServing func(fs *FileServer) + wantResponseHTTPCode int + wantResponseContent string + wantResponseSubstrings []string + resultCheckingFn func(t *testing.T, rr *httptest.ResponseRecorder) }{ { - name: "Static TEXT file serving from local FS", + name: "serving request without URI", + giveRequestURI: "", + wantResponseHTTPCode: http.StatusNotFound, + wantResponseSubstrings: []string{"Not Found"}, + }, + { + name: "static file serving", + giveRequestURI: "/test", giveFiles: map[string][]byte{ - "test1.txt": []byte("test content"), + "test": []byte("test content"), }, - giveRequestURI: "/test1.txt", - giveRequestMethod: "GET", - wantResponseCode: http.StatusOK, - wantResponseBody: []byte("test content"), - wantContentType: "text/plain; charset=utf-8", + wantResponseHTTPCode: http.StatusOK, + wantResponseContent: "test content", }, { - name: "Static HTML file serving from local FS", + name: "static HTML file serving", + giveRequestURI: "/test.html", giveFiles: map[string][]byte{ - "test1.html": []byte("test content"), + "test.html": []byte("

test html content

"), + }, + wantResponseHTTPCode: http.StatusOK, + wantResponseContent: "

test html content

", + resultCheckingFn: func(t *testing.T, rr *httptest.ResponseRecorder) { + assert.Equal(t, rr.Header().Get("Content-Type"), "text/html; charset=utf-8") }, - giveRequestURI: "/test1.html", - giveRequestMethod: "GET", - wantResponseCode: http.StatusOK, - wantResponseBody: []byte("test content"), - wantContentType: "text/html; charset=utf-8", }, { - name: "Redirect from .../index.html to .../", - giveIndexFile: "indx.html", - giveRequestURI: "/indx.html", - giveRequestMethod: "GET", - wantResponseCode: http.StatusMovedPermanently, - wantRedirectTo: "/", + name: "directory above (./../) requested", + giveRequestURI: "/../../../../etc/passwd", + wantResponseHTTPCode: http.StatusNotFound, }, { - name: "Redirect from .../index.html to .../ insime some directory", - giveIndexFile: "indx.html", - giveRequestURI: "/some/indx.html", - giveRequestMethod: "GET", - wantResponseCode: http.StatusMovedPermanently, - wantRedirectTo: "/some/", + name: "disabled redirection from", + giveSettings: Settings{ + IndexFileName: "idx.html", + }, + giveRequestURI: "/foo/idx.html", + wantResponseHTTPCode: http.StatusNotFound, }, { - name: "Request root", + name: "redirect from ./{indexFileName} to ./", + giveSettings: Settings{ + IndexFileName: "idx.html", + RedirectIndexFileToRoot: true, + }, + giveRequestURI: "/idx.html", + wantResponseHTTPCode: http.StatusMovedPermanently, + resultCheckingFn: func(t *testing.T, rr *httptest.ResponseRecorder) { + assert.Equal(t, "/", rr.Header().Get("Location")) + }, + }, + { + name: "redirect from ./foo/{indexFileName} to ./foo/", + giveSettings: Settings{ + IndexFileName: "idx.html", + RedirectIndexFileToRoot: true, + }, + giveRequestURI: "/foo/idx.html", + wantResponseHTTPCode: http.StatusMovedPermanently, + resultCheckingFn: func(t *testing.T, rr *httptest.ResponseRecorder) { + assert.Equal(t, "/foo/", rr.Header().Get("Location")) + }, + }, + { + name: "index file in root directory serving", + giveSettings: Settings{ + IndexFileName: "idx.html", + }, + giveRequestURI: "/", giveFiles: map[string][]byte{ - "indx.html": []byte("test content"), - }, - giveIndexFile: "indx.html", - giveRequestURI: "", - giveRequestMethod: "GET", - wantResponseBody: []byte("test content"), - wantResponseCode: http.StatusOK, - wantContentType: "text/html; charset=utf-8", + "idx.html": []byte("index content"), + }, + wantResponseHTTPCode: http.StatusOK, + wantResponseContent: "index content", }, { - name: "Index file from some directory", - giveDirs: []string{"foo"}, + name: "index file in sub-directory serving", + giveSettings: Settings{ + IndexFileName: "idx.html", + }, + giveRequestURI: "/foo/", + giveDirs: []string{"foo"}, giveFiles: map[string][]byte{ - "indx.html": []byte("index in root"), - filepath.Join("foo", "indx.html"): []byte("index in foo"), + "idx.html": []byte("index in root"), + filepath.Join("foo", "idx.html"): []byte("index in foo"), }, - giveIndexFile: "indx.html", - giveRequestURI: "/foo/", - giveRequestMethod: "GET", - wantResponseBody: []byte("index in foo"), - wantResponseCode: http.StatusOK, - wantContentType: "text/html; charset=utf-8", + wantResponseHTTPCode: http.StatusOK, + wantResponseContent: "index in foo", }, { - name: "404 on directory request", + name: "404 on directory request", + giveSettings: Settings{ + IndexFileName: "indx.html", + }, giveDirs: []string{"foo"}, giveFiles: map[string][]byte{ "indx.html": []byte("index in root"), filepath.Join("foo", "indx.html"): []byte("index in foo"), }, - giveIndexFile: "indx.html", - giveRequestURI: "/foo", - giveRequestMethod: "GET", - wantResponseCode: http.StatusNotFound, + giveRequestURI: "/foo", + wantResponseHTTPCode: http.StatusNotFound, }, { - name: "NotFoundHandler handling", - giveIndexFile: "indx.html", - giveRequestURI: "/foo", - giveRequestMethod: "GET", - giveNotFoundHandler: func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(444) - _, _ = w.Write([]byte("foo bar")) - w.Header().Set("Content-Type", "blah blah") - }, - wantResponseCode: 444, - wantResponseBody: []byte("foo bar"), - wantContentType: "blah blah", + name: "custom error handler", + beforeServing: func(fs *FileServer) { + fs.ErrorHandlers = []ErrorHandlerFunc{ + func(w http.ResponseWriter, r *http.Request, fs *FileServer, errorCode int) bool { + w.WriteHeader(444) + _, _ = w.Write([]byte("foo bar")) + w.Header().Set("Content-Type", "blah blah") + + return true + }, + } + }, + giveRequestURI: "/foo", + wantResponseHTTPCode: 444, + wantResponseContent: "foo bar", + resultCheckingFn: func(t *testing.T, rr *httptest.ResponseRecorder) { + assert.Equal(t, "blah blah", rr.Header().Get("Content-Type")) + }, }, { - name: "Error 404 file serving from local FS", - giveFiles: map[string][]byte{ - "404.html": []byte("error 404 file"), - }, - giveRequestURI: "/foo", - giveError404file: "404.html", - giveRequestMethod: "GET", - wantResponseCode: http.StatusNotFound, - wantResponseBody: []byte("error 404 file"), - wantContentType: "text/html; charset=utf-8", + name: "custom error handler fallback", + beforeServing: func(fs *FileServer) { + fs.ErrorHandlers = []ErrorHandlerFunc{ + func(w http.ResponseWriter, r *http.Request, fs *FileServer, errorCode int) bool { + return false + }, + } + }, + giveRequestURI: "/foo", + wantResponseHTTPCode: http.StatusNotFound, + wantResponseSubstrings: []string{"", "Error 404", "Not Found", ""}, }, { - name: "Error 404 fallback", - giveRequestURI: "/foo", - giveError404file: "404.html", - giveRequestMethod: "GET", - wantResponseCode: http.StatusNotFound, - wantResponseBody: []byte("

ERROR 404

Requested file was not found

"), - wantContentType: "text/html; charset=utf-8", + name: "error in json format when json requested", + giveRequestURI: "/foo", + giveRequestHeaders: map[string]string{"accept": "application/json"}, + wantResponseHTTPCode: http.StatusNotFound, + resultCheckingFn: func(t *testing.T, rr *httptest.ResponseRecorder) { + assert.JSONEq(t, `{"code":404,"message":"Not Found"}`, rr.Body.String()) + }, }, } - for _, tt := range tests { + for _, tt := range cases { + tt := tt t.Run(tt.name, func(t *testing.T) { - var root http.Dir - - if len(tt.giveDirs) > 0 || len(tt.giveFiles) > 0 { - tmpDir := createTempDir() - root = http.Dir(tmpDir) + tmpDir, tmpDirErr := ioutil.TempDir("", "test-") + assert.NoError(t, tmpDirErr) - defer func(d string) { - if err := os.RemoveAll(d); err != nil { - panic(err) - } - }(tmpDir) + defer func(d string) { assert.NoError(t, os.RemoveAll(d)) }(tmpDir) - // Create directories + if len(tt.giveDirs) > 0 || len(tt.giveFiles) > 0 { for _, d := range tt.giveDirs { - if err := os.Mkdir(filepath.Join(tmpDir, d), 0777); err != nil { - panic(err) - } + assert.NoError(t, os.Mkdir(filepath.Join(tmpDir, d), 0777)) } - // Create files for name, content := range tt.giveFiles { - if f, err := os.Create(filepath.Join(tmpDir, name)); err != nil { - panic(err) - } else { - if _, err := f.Write(content); err != nil { - panic(err) - } - if err := f.Close(); err != nil { - panic(err) - } - } + file, createErr := os.Create(filepath.Join(tmpDir, name)) + assert.NoError(t, createErr) + _, fileWritingErr := file.Write(content) + assert.NoError(t, fileWritingErr) + assert.NoError(t, file.Close()) } - } else { - root = "" } - fileServer := &FileServer{Settings: Settings{ - Root: root, - NotFoundHandler: tt.giveNotFoundHandler, - IndexFile: tt.giveIndexFile, - Error404file: tt.giveError404file, - }} + if tt.giveSettings.FilesRoot == "" { + tt.giveSettings.FilesRoot = tmpDir + } + + fs, fsErr := NewFileServer(tt.giveSettings) + + assert.NoError(t, fsErr) var ( req, _ = http.NewRequest(tt.giveRequestMethod, tt.giveRequestURI, nil) rr = httptest.NewRecorder() ) - fileServer.ServeHTTP(rr, req) + if tt.giveRequestHeaders != nil { + for key, value := range tt.giveRequestHeaders { + req.Header.Set(key, value) + } + } - if rr.Code != tt.wantResponseCode { - t.Errorf("Wrong response HTTP code. Want %d, got %d", tt.wantResponseCode, rr.Code) + if tt.beforeServing != nil { + tt.beforeServing(fs) } - if len(tt.wantResponseBody) > 0 && !reflect.DeepEqual(rr.Body.Bytes(), tt.wantResponseBody) { - t.Errorf("Wrong HTTP response. Want [%s], got [%s]", tt.wantResponseBody, rr.Body.String()) + fs.ServeHTTP(rr, req) + + assert.Equal(t, tt.wantResponseHTTPCode, rr.Code) + + if tt.wantResponseContent != "" { + assert.Equal(t, tt.wantResponseContent, rr.Body.String()) } - if ct := rr.Header().Get("Content-Type"); tt.wantContentType != "" && ct != tt.wantContentType { - t.Errorf("Wrong response content type header. Want %s, got %s", tt.wantContentType, ct) + if len(tt.wantResponseSubstrings) > 0 { + for _, expected := range tt.wantResponseSubstrings { + assert.Contains(t, rr.Body.String(), expected) + } } - if rt := rr.Header().Get("Location"); tt.wantRedirectTo != "" && tt.wantRedirectTo != rt { - t.Errorf("Wrong redirect to location. Want %s, got %s", tt.wantRedirectTo, rt) + if tt.resultCheckingFn != nil { + tt.resultCheckingFn(t, rr) } }) } diff --git a/internal/pkg/http/handlers/api/settings/handler.go b/internal/pkg/http/handlers/api/settings/handler.go new file mode 100644 index 00000000..63c9b369 --- /dev/null +++ b/internal/pkg/http/handlers/api/settings/handler.go @@ -0,0 +1,77 @@ +// Package settings contains API handler for application settings getting. +package settings + +import ( + "encoding/json" + "net/http" + + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cache" + + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/config" +) + +type ( + response struct { + Sources struct { + Provided []providedSource `json:"provided"` + Max int `json:"max"` + MaxSourceSize int `json:"max_source_size"` // in bytes + } `json:"sources"` + Redirect struct { + Addr string `json:"addr"` + } `json:"redirect"` + Records struct { + Comment string `json:"comment"` + } `json:"records"` + Excludes struct { + Hosts []string `json:"hosts"` + } `json:"excludes"` + Cache struct { + LifetimeSec int `json:"lifetime_sec"` + } `json:"cache"` + } + + providedSource struct { + URI string `json:"uri"` + Name string `json:"name"` + Description string `json:"description"` + ByDefault bool `json:"default"` + Count int `json:"count"` + } +) + +// NewHandler creates settings handler. +func NewHandler(cfg config.Config, cacher cache.Cacher) http.HandlerFunc { //nolint:gocritic + var c []byte // response in-memory cache + + return func(w http.ResponseWriter, _ *http.Request) { + if c == nil { + // set basic properties + resp := &response{} + resp.Sources.Max = int(cfg.RouterScript.MaxSourcesCount) + resp.Sources.MaxSourceSize = int(cfg.RouterScript.MaxSourceSizeBytes) + resp.Redirect.Addr = cfg.RouterScript.Redirect.Address + resp.Records.Comment = cfg.RouterScript.Comment + resp.Cache.LifetimeSec = int(cacher.TTL().Seconds()) + + // append excluded hosts list + resp.Excludes.Hosts = append(resp.Excludes.Hosts, cfg.RouterScript.Exclude.Hosts...) + + // append sources list entries + for _, source := range cfg.Sources { + resp.Sources.Provided = append(resp.Sources.Provided, providedSource{ + URI: source.URI, + Name: source.Name, + Description: source.Description, + ByDefault: source.EnabledByDefault, + Count: int(source.RecordsCount), + }) + } + + c, _ = json.Marshal(resp) + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write(c) + } +} diff --git a/internal/pkg/http/handlers/api/settings/handler_test.go b/internal/pkg/http/handlers/api/settings/handler_test.go new file mode 100644 index 00000000..a7080c5b --- /dev/null +++ b/internal/pkg/http/handlers/api/settings/handler_test.go @@ -0,0 +1,58 @@ +package settings + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cache" + + "github.com/stretchr/testify/assert" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/config" +) + +func TestNewHandler(t *testing.T) { + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing", nil) + rr = httptest.NewRecorder() + cfg config.Config + ) + + cfg.AddSource("http://goo.gl/hosts.txt", "Foo", "Foo desc", false, 123) + cfg.AddSource("http://face.book/hosts.txt", "Bar", "Bar desc", true, 321) + cfg.RouterScript.Redirect.Address = "0.1.1.0" + cfg.RouterScript.Exclude.Hosts = []string{"foo", "bar"} + cfg.RouterScript.MaxSourcesCount = 1 + cfg.RouterScript.Comment = " [ blah ] " + cfg.RouterScript.MaxSourceSizeBytes = 666 + + cacher := cache.NewInMemoryCache(time.Second*123, time.Second) + + NewHandler(cfg, cacher)(rr, req) + + assert.Equal(t, rr.Code, http.StatusOK) + + assert.JSONEq(t, `{ + "sources":{ + "provided":[ + {"uri":"http://goo.gl/hosts.txt","name":"Foo","description":"Foo desc","default":false,"count":123}, + {"uri":"http://face.book/hosts.txt","name":"Bar","description":"Bar desc","default":true,"count":321} + ], + "max":1, + "max_source_size":666 + }, + "redirect":{ + "addr":"0.1.1.0" + }, + "records":{ + "comment":" [ blah ] " + }, + "excludes":{ + "hosts":["foo", "bar"] + }, + "cache":{ + "lifetime_sec":123 + } + }`, rr.Body.String()) +} diff --git a/internal/pkg/http/handlers/api/version/handler.go b/internal/pkg/http/handlers/api/version/handler.go new file mode 100644 index 00000000..4f81096d --- /dev/null +++ b/internal/pkg/http/handlers/api/version/handler.go @@ -0,0 +1,25 @@ +// Package version contains version API handler. +package version + +import ( + "encoding/json" + "net/http" +) + +// NewHandler creates version handler. +func NewHandler(ver string) http.HandlerFunc { + var cache []byte + + return func(w http.ResponseWriter, _ *http.Request) { + if cache == nil { + cache, _ = json.Marshal(struct { + Version string `json:"version"` + }{ + Version: ver, + }) + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write(cache) + } +} diff --git a/internal/pkg/http/handlers/api/version/handler_test.go b/internal/pkg/http/handlers/api/version/handler_test.go new file mode 100644 index 00000000..a25cdaa2 --- /dev/null +++ b/internal/pkg/http/handlers/api/version/handler_test.go @@ -0,0 +1,21 @@ +package version + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewHandler(t *testing.T) { + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing", nil) + rr = httptest.NewRecorder() + ) + + NewHandler("1.2.3@foo")(rr, req) + + assert.Equal(t, rr.Code, http.StatusOK) + assert.JSONEq(t, `{"version":"1.2.3@foo"}`, rr.Body.String()) +} diff --git a/internal/pkg/http/handlers/generate/handler.go b/internal/pkg/http/handlers/generate/handler.go new file mode 100644 index 00000000..1f7ecc1d --- /dev/null +++ b/internal/pkg/http/handlers/generate/handler.go @@ -0,0 +1,475 @@ +// Package generate contains RouterOS script generation handler. +package generate + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cache" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/config" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/version" + "github.com/tarampampam/mikrotik-hosts-parser/pkg/hostsfile" + "github.com/tarampampam/mikrotik-hosts-parser/pkg/mikrotik" + "go.uber.org/zap" +) + +type handler struct { + ctx context.Context + log *zap.Logger + cacher cache.Cacher + cfg *config.Config + + defaultRedirectIP net.IP + + httpClient interface { + Do(*http.Request) (*http.Response, error) + } +} + +const ( + httpClientTimeout = time.Second * 10 + httpClientMaxRedirects = 2 + formatRouterOS = "routeros" //nolint:misspell +) + +// NewHandler creates RouterOS script generation handler. +func NewHandler(ctx context.Context, log *zap.Logger, cacher cache.Cacher, cfg *config.Config) (http.Handler, error) { + if containsIllegalSymbols(cfg.RouterScript.Comment) { + return nil, errors.New("wrong config: script comment contains illegal symbols") + } + + if cfg.RouterScript.MaxSourcesCount <= 0 { + return nil, errors.New("wrong config: max sources count") + } + + checkRedirectFn := func(req *http.Request, via []*http.Request) error { + if len(via) >= httpClientMaxRedirects { + return errors.New("request: too many (2) redirects") + } + + return nil + } + + var h = &handler{ + ctx: ctx, + log: log, + cacher: cacher, + cfg: cfg, + + httpClient: &http.Client{Timeout: httpClientTimeout, CheckRedirect: checkRedirectFn}, + } + + if ip := net.ParseIP(cfg.RouterScript.Redirect.Address); ip != nil { + h.defaultRedirectIP = ip + } else { + h.defaultRedirectIP = net.IPv4(127, 0, 0, 1) //nolint:gomnd // config contains wrong value + } + + return h, nil +} + +type hostsFileData struct { + url string + records []hostsfile.Record + cacheHit bool + cacheTTL time.Duration + err error +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { //nolint:funlen,gocognit,gocyclo + params := newReqParams(h.defaultRedirectIP) + + if r == nil || r.URL == nil { + w.WriteHeader(http.StatusBadRequest) + h.writeComment(w, "Empty request or query parameters") + + return + } + + if err := params.fromValues(r.URL.Query()); err != nil { + w.WriteHeader(http.StatusBadRequest) + h.writeComment(w, "Query parameters error: "+err.Error()) + + return + } + + if err := params.validate(h.cfg.RouterScript.MaxSourcesCount); err != nil { + w.WriteHeader(http.StatusBadRequest) + h.writeComment(w, "Query parameters validation failed: "+err.Error()) + + return + } + + if format := params.format; format != formatRouterOS { + w.WriteHeader(http.StatusBadRequest) + h.writeComment(w, fmt.Sprintf("Unsupported format [%s] requested", format)) + + return + } + + // write script header + h.writeComment(w, + "Script generated at "+time.Now().Format("2006-01-02 15:04:05"), + "Generator version: "+version.Version(), + fmt.Sprintf("Limit: %d", params.limit), + fmt.Sprintf("Cache lifetime: %s", h.cacher.TTL().Round(time.Second)), + "Format: "+params.format, + "Redirect to: "+params.redirect.String(), + "Sources list:", + ) + + for i := 0; i < len(params.sources); i++ { + h.writeComment(w, fmt.Sprintf(" - <%s>", params.sources[i])) + } + + if len(params.excluded) > 0 { + h.writeComment(w, "Excluded hosts:") + + for i := 0; i < len(params.sources); i++ { + h.writeComment(w, fmt.Sprintf(" - %s", params.excluded[i])) + } + } + + var ( + hostsDataCh = make(chan hostsFileData, len(params.sources)) + hostsRecordsCount uint32 // atomic usage only, used for hosts list pre-allocation + wg sync.WaitGroup + ) + + wg.Add(len(params.sources)) + + // fetch hosts files content and parse them + for i := 0; i < len(params.sources); i++ { + go func(ch chan<- hostsFileData, url string) { + defer wg.Done() + + if hit, data, ttl, err := h.cacher.Get(url); hit && err == nil { + records, parsingErr := hostsfile.Parse(bytes.NewReader(data)) + if parsingErr == nil { + atomic.AddUint32(&hostsRecordsCount, uint32(len(records))) + ch <- hostsFileData{url: url, records: records, cacheHit: hit, cacheTTL: ttl} + + return + } + + ch <- hostsFileData{url: url, cacheHit: hit, err: parsingErr} + + return + } + + data, srcErr := h.fetchRemoteSource(url) + if srcErr != nil { + h.log.Warn("remote source fetching failed", zap.Error(srcErr)) + ch <- hostsFileData{url: url, err: srcErr} + + return + } + + if err := h.cacher.Put(url, data.Bytes()); err != nil { + h.log.Error("cache writing error", zap.Error(err)) + ch <- hostsFileData{url: url, err: err} + + return + } + + if records, err := hostsfile.Parse(data); err == nil { + atomic.AddUint32(&hostsRecordsCount, uint32(len(records))) + ch <- hostsFileData{url: url, records: records, cacheTTL: h.cacher.TTL()} + } else { + ch <- hostsFileData{url: url, err: err} + } + }(hostsDataCh, params.sources[i]) + } + + wg.Wait() + close(hostsDataCh) + + if err := h.ctx.Err(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + h.writeComment(w, "Context error: "+err.Error()) + + return + } + + // burn excludes map for fastest checking + var excludes = make(map[string]struct{}, len(params.excluded)) + for i := 0; i < len(params.excluded); i++ { + excludes[params.excluded[i]] = struct{}{} + } + + // calculate results map size for pre-allocation + var size uint32 + if params.limit > 0 { + size = params.limit + } else { + size = atomic.LoadUint32(&hostsRecordsCount) + } + + var hostNames, limit = make(map[string]struct{}, size), int(size) + + // read parsed hosts files content from channel + for i := 0; i < len(params.sources); i++ { + data := <-hostsDataCh + + if data.err != nil { + h.writeComment(w, fmt.Sprintf("Source <%s> error: %v", data.url, data.err)) + + continue + } + + if data.cacheHit { + h.writeComment(w, fmt.Sprintf("Cache HIT for <%s> (expires after %s)", data.url, data.cacheTTL.Round(time.Second))) + } else { + h.writeComment(w, fmt.Sprintf("Cache miss for <%s>", data.url)) + } + + recordsLoop: + for j := 0; j < len(data.records); j++ { // loop over records inside hosts file + if name := data.records[j].Host; name != "" { + if len(hostNames) >= limit { // hostnames limit has been reached + break recordsLoop + } + + if !containsIllegalSymbols(name) { + if _, ok := excludes[name]; !ok { // is in excludes list? + hostNames[name] = struct{}{} // append + } + } + } + + if len(data.records[j].AdditionalHosts) > 0 { //nolint:nestif + for k := 0; k < len(data.records[j].AdditionalHosts); k++ { // loop over additional hostnames + if len(hostNames) >= limit { // hostnames limit has been reached + break recordsLoop + } + + name := data.records[j].AdditionalHosts[k] + + if _, ok := excludes[name]; ok { // is in excludes list? + continue + } + + if !containsIllegalSymbols(name) { + if _, ok := excludes[name]; !ok { // is in excludes list? + hostNames[name] = struct{}{} // append + } + } + } + } + } + } + + if len(hostNames) == 0 { + h.writeComment(w, "Script generation failed (empty hosts list)") + + return + } + + var result, redirectAddr = make(mikrotik.DNSStaticEntries, 0, len(hostNames)), params.redirect.String() + for hostName := range hostNames { + result = append(result, mikrotik.DNSStaticEntry{ + Address: redirectAddr, + Comment: h.cfg.RouterScript.Comment, + Name: hostName, + }) + } + + // make sorting + sort.Slice(result, func(i, j int) bool { + return result[i].Name < result[j].Name + }) + + _, _ = w.Write([]byte("\n/ip dns static\n")) + _, renderingErr := result.Render(w, mikrotik.RenderingOptions{Prefix: "add"}) + _, _ = w.Write([]byte("\n\n")) + + if renderingErr != nil { + h.writeComment(w, fmt.Sprintf("Script rendering error: %v", renderingErr)) + } + + h.writeComment(w, fmt.Sprintf( + "Records count: %d (%d records ignored)", + len(result), + int(atomic.LoadUint32(&hostsRecordsCount))-len(result), + )) +} + +func containsIllegalSymbols(s string) bool { + return strings.ContainsRune(s, '"') || strings.ContainsRune(s, '\\') +} + +func (h *handler) writeComment(w io.Writer, comments ...string) { + for i := 0; i < len(comments); i++ { + _, _ = w.Write([]byte("## " + comments[i] + "\n")) + } +} + +func (h *handler) fetchRemoteSource(url string) (*bytes.Buffer, error) { + req, err := http.NewRequestWithContext(h.ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return nil, err + } + + resp, err := h.httpClient.Do(req) + if err != nil { + return nil, err + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("wrong response code: %d", resp.StatusCode) + } + + if ct, allowed := resp.Header.Get("Content-Type"), "text/plain"; !strings.HasPrefix(ct, allowed) { + return nil, fmt.Errorf("wrong Content-Type response header [%s] (%s* is required)", ct, allowed) + } + + var buf bytes.Buffer + + const defaultBufCapacity = 64 * 1024 // 64 KiB + + if cl := resp.Header.Get("Content-Length"); cl != "" { //nolint:nestif + value, parsingErr := strconv.Atoi(cl) + if parsingErr != nil { + return nil, errors.New("header Content-Length parsing error: " + parsingErr.Error()) + } + + if max := int(h.cfg.RouterScript.MaxSourceSizeBytes); value >= max { + return nil, fmt.Errorf("header Content-Length value [%d] is too big (max: %d)", value, max) + } + + if value > 0 { + buf.Grow(value) + } else { + buf.Grow(defaultBufCapacity) + } + } else { + buf.Grow(defaultBufCapacity) + } + + if _, readingErr := buf.ReadFrom(resp.Body); readingErr != nil { + return nil, readingErr + } + + return &buf, nil +} + +type reqParams struct { + sources []string + format string + ver string + excluded []string + limit uint32 + redirect net.IP +} + +func newReqParams(redirect net.IP) reqParams { + return reqParams{ + sources: make([]string, 0, 8), + format: formatRouterOS, // default value + excluded: make([]string, 0, 16), + redirect: redirect, + } +} + +func (p *reqParams) fromValues(v url.Values) error { //nolint:funlen,gocognit,gocyclo + if urls, ok := v["sources_urls"]; ok { + m := make(map[string]struct{}, 8) + + for i := 0; i < len(urls); i++ { + for list, j := strings.Split(urls[i], ","), 0; j < len(list); j++ { + if u, err := url.ParseRequestURI(list[j]); err == nil { + m[u.String()] = struct{}{} + } + } + } + + for u := range m { + p.sources = append(p.sources, u) + } + + sort.Strings(p.sources) + } else { + return errors.New("required parameter 'sources_urls' was not found") + } + + if value, ok := v["format"]; ok { // optional + if len(value) > 0 { + p.format = value[0] + } + } + + if value, ok := v["version"]; ok { // optional + if len(value) > 0 { + p.ver = value[0] + } + } + + if hosts, ok := v["excluded_hosts"]; ok { // optional + m := make(map[string]struct{}, 16) + + for i := 0; i < len(hosts); i++ { + for list, j := strings.Split(hosts[i], ","), 0; j < len(list); j++ { + if host := list[j]; host != "" { + m[strings.Trim(host, " '\"\n\r")] = struct{}{} + } + } + } + + for host := range m { + p.excluded = append(p.excluded, host) + } + + sort.Strings(p.excluded) + } + + if value, ok := v["limit"]; ok { // optional + if len(value) > 0 { + if limit, err := strconv.ParseUint(value[0], 10, 32); err == nil && limit > 0 { + p.limit = uint32(limit) + } else { + return errors.New("wrong 'limit' value") + } + } + } + + if value, ok := v["redirect_to"]; ok { // optional + if len(value) > 0 { + ip := net.ParseIP(value[0]) + if ip == nil { + return errors.New("wrong 'redirect_to' value (invalid IP address)") + } + + p.redirect = ip + } + } + + return nil +} + +func (p *reqParams) validate(maxSources uint16) error { + if l := len(p.sources); l == 0 { + return errors.New("empty sources list") + } else if l > int(maxSources) { + return fmt.Errorf("too many sources (only %d is allowed)", maxSources) + } + + if len(p.excluded) > 32 { //nolint:gomnd + return errors.New("too many excluded hosts (more then 32)") + } + + return nil +} diff --git a/internal/pkg/http/handlers/generate/handler_test.go b/internal/pkg/http/handlers/generate/handler_test.go new file mode 100644 index 00000000..1086db6e --- /dev/null +++ b/internal/pkg/http/handlers/generate/handler_test.go @@ -0,0 +1,276 @@ +package generate + +import ( + "bytes" + "context" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cache" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/config" + "go.uber.org/zap" +) + +type fakeHTTPClientFunc func(*http.Request) (*http.Response, error) + +func (f fakeHTTPClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) } + +var httpMock fakeHTTPClientFunc = func(req *http.Request) (*http.Response, error) { //nolint:gochecknoglobals + path, absErr := filepath.Abs(testDataPath + req.URL.RequestURI()) + if absErr != nil { + panic(absErr) + } + + if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() { + raw, readingErr := ioutil.ReadFile(path) + if readingErr != nil { + panic(readingErr) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"text/plain; charset=utf-8"}, + "Content-Length": []string{strconv.FormatInt(info.Size(), 10)}, + }, + Body: ioutil.NopCloser(bytes.NewReader(raw)), + }, nil + } + + return &http.Response{ + StatusCode: http.StatusNotFound, + Header: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Requested file was not found: " + path))), + }, nil +} + +const testDataPath = "../../../../../test/testdata/hosts" + +func createConfig() *config.Config { + cfg := &config.Config{} + cfg.RouterScript.MaxSourcesCount = 10 + cfg.RouterScript.Comment = "foo" + cfg.RouterScript.MaxSourceSizeBytes = 2097152 + + return cfg +} + +func BenchmarkHandler_ServeHTTP(b *testing.B) { + b.ReportAllocs() + + cacher := cache.NewInMemoryCache(time.Minute, time.Second) + defer cacher.Close() + + h, _ := NewHandler(context.Background(), zap.NewNop(), cacher, createConfig()) + + h.(*handler).httpClient = httpMock + + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing?"+ + "format=routeros"+ //nolint:misspell + "&version=v0.0.666@1a0339c"+ + "&redirect_to=127.0.0.5"+ + "&limit=1234"+ + "&sources_urls="+ + "https%3A%2F%2Fmock%2Fad_servers.txt"+ + ",http://mock/hosts_adaway.txt"+ + ",http://non-existing-file.txt"+ + "&excluded_hosts="+ + "d.com"+ + ",c.org"+ + ",localhost"+ + ",localhost.localdomain"+ + ",broadcasthost"+ + ",local", http.NoBody) + rr = httptest.NewRecorder() + ) + + for n := 0; n < b.N; n++ { + h.ServeHTTP(rr, req) + } +} + +func TestHandler_ServeHTTP(t *testing.T) { + cacher := cache.NewInMemoryCache(time.Minute, time.Second) + defer cacher.Close() + + h, err := NewHandler(context.Background(), zap.NewNop(), cacher, createConfig()) + assert.NoError(t, err) + + h.(*handler).httpClient = httpMock + + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing?"+ + "format=routeros"+ //nolint:misspell + "&version=v0.0.666@1a0339c"+ + "&redirect_to=127.0.0.5"+ + "&limit=1234"+ + "&sources_urls="+ + "https%3A%2F%2Fmock%2Fad_servers.txt"+ + ",http://mock/hosts_adaway.txt"+ + ",http://non-existing-file.txt"+ + "&excluded_hosts="+ + "aaa.com"+ + ",bbb.org"+ + ",localhost", http.NoBody) + rr = httptest.NewRecorder() + ) + + h.ServeHTTP(rr, req) // first run + + body := rr.Body.String() + + assert.Regexp(t, `Cache.+miss.+http:\/\/mock\/hosts_adaway\.txt`, body) + assert.Regexp(t, `Cache.+miss.+https:\/\/mock\/ad_servers\.txt`, body) + assert.Regexp(t, `Source.+non-existing-file\.txt.+404`, body) + assert.Regexp(t, `(?sU)Excluded hosts.+aaa\.com.+bbb\.org.+localhost`, rr.Body.String()) + assert.Contains(t, body, "/ip dns static") + assert.Equal(t, strings.Count(body, "add address=127.0.0.5 comment=\"foo\" disabled=no"), 1234) + + // assert non-comments and non-empty lines count + var lineWithoutCommentsRegex = regexp.MustCompile(`(?mU)^([^#\n]+.*)\n`) // + + assert.Equal(t, 1234+1, len(lineWithoutCommentsRegex.FindAllStringIndex(body, -1))) //nolint:wsl + + rr = httptest.NewRecorder() + h.ServeHTTP(rr, req) // second run + + body = rr.Body.String() + + assert.Regexp(t, `Cache.+HIT.+http:\/\/mock\/hosts_adaway\.txt`, body) + assert.Regexp(t, `Cache.+HIT.+https:\/\/mock\/ad_servers\.txt`, body) + assert.Regexp(t, `Source.+non-existing-file\.txt.+404`, body) + + assert.Equal(t, 1234+1, len(lineWithoutCommentsRegex.FindAllStringIndex(body, -1))) +} + +func TestHandler_ServeHTTPHostnamesExcluding(t *testing.T) { + cacher := cache.NewInMemoryCache(time.Minute, time.Second) + defer cacher.Close() + + h, err := NewHandler(context.Background(), zap.NewNop(), cacher, createConfig()) + assert.NoError(t, err) + + var customHTTPMock fakeHTTPClientFunc = func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"text/plain; charset=utf-8"}, + }, + Body: ioutil.NopCloser(bytes.NewReader([]byte(` +4.3.2.1 ___id___.c.mystat-in.net # comment with double tab +1.1.1.1 a.cn b.cn a.cn # "a.cn" is duplicate + +::1 localfoo +2606:4700:4700::1111 cloudflare #[cf] + +broken line format + +0.0.0.1 example.com +0.0.0.1 example.com # duplicate +`))), + }, nil + } + + h.(*handler).httpClient = customHTTPMock + + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing?"+ + "&sources_urls="+ + "https%3A%2F%2Fmock%2Fad_servers.txt"+ + "&excluded_hosts="+ + "a.cn", http.NoBody) + rr = httptest.NewRecorder() + ) + + h.ServeHTTP(rr, req) + + body := rr.Body.String() + + assert.NotContains(t, body, "name=\"a.cn\"") + assert.Contains(t, body, "name=\"___id___.c.mystat-in.net\"") + assert.Contains(t, body, "name=\"b.cn\"") + assert.Contains(t, body, "name=\"localfoo\"") + assert.Contains(t, body, "name=\"cloudflare\"") + assert.Contains(t, body, "name=\"example.com\"") + assert.Contains(t, body, "/ip dns static") + assert.Equal(t, strings.Count(body, "add address=127.0.0.1 comment=\"foo\" disabled=no"), 5) +} + +func TestHandler_ServeHTTPWithoutRequest(t *testing.T) { //nolint:dupl + cacher := cache.NewInMemoryCache(time.Minute, time.Second) + defer cacher.Close() + + h, err := NewHandler(context.Background(), zap.NewNop(), cacher, createConfig()) + assert.NoError(t, err) + + var rr = httptest.NewRecorder() + + h.ServeHTTP(rr, nil) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Equal(t, "## Empty request or query parameters\n", rr.Body.String()) +} + +func TestHandler_ServeHTTPRequestWithoutSourcesURLs(t *testing.T) { //nolint:dupl + cacher := cache.NewInMemoryCache(time.Minute, time.Second) + defer cacher.Close() + + h, err := NewHandler(context.Background(), zap.NewNop(), cacher, createConfig()) + assert.NoError(t, err) + + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing", http.NoBody) + rr = httptest.NewRecorder() + ) + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Regexp(t, `(?mU)## Query parameters error.*sources_urls`, rr.Body.String()) +} + +func TestHandler_ServeHTTPRequestEmptySourcesURLs(t *testing.T) { //nolint:dupl + cacher := cache.NewInMemoryCache(time.Minute, time.Second) + defer cacher.Close() + + h, err := NewHandler(context.Background(), zap.NewNop(), cacher, createConfig()) + assert.NoError(t, err) + + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing?sources_urls=", http.NoBody) + rr = httptest.NewRecorder() + ) + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Regexp(t, `(?mU)## Query parameters.*fail.*empty.*sources`, rr.Body.String()) +} + +func TestHandler_ServeHTTPRequestWrongFormat(t *testing.T) { //nolint:dupl + cacher := cache.NewInMemoryCache(time.Minute, time.Second) + defer cacher.Close() + + h, err := NewHandler(context.Background(), zap.NewNop(), cacher, createConfig()) + assert.NoError(t, err) + + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing?sources_urls=http://foo&format=foobar", http.NoBody) + rr = httptest.NewRecorder() + ) + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Regexp(t, `(?mU)## Unsupported format.*foobar`, rr.Body.String()) +} diff --git a/internal/pkg/http/handlers/healthz/handler.go b/internal/pkg/http/handlers/healthz/handler.go new file mode 100644 index 00000000..825546b1 --- /dev/null +++ b/internal/pkg/http/handlers/healthz/handler.go @@ -0,0 +1,26 @@ +// Package healthz contains healthcheck handler. +package healthz + +import ( + "net/http" +) + +// checker allows to check some service part. +type checker interface { + // Check makes a check and return error only if something is wrong. + Check() error +} + +// NewHandler creates healthcheck handler. +func NewHandler(checker checker) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + if err := checker.Check(); err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(err.Error())) + + return + } + + w.WriteHeader(http.StatusOK) + } +} diff --git a/internal/pkg/http/handlers/healthz/handler_test.go b/internal/pkg/http/handlers/healthz/handler_test.go new file mode 100644 index 00000000..3cb26dc8 --- /dev/null +++ b/internal/pkg/http/handlers/healthz/handler_test.go @@ -0,0 +1,38 @@ +package healthz + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +type fakeChecker struct{ err error } + +func (c *fakeChecker) Check() error { return c.err } + +func TestNewHandlerNoError(t *testing.T) { + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing", http.NoBody) + rr = httptest.NewRecorder() + ) + + NewHandler(&fakeChecker{err: nil})(rr, req) + + assert.Equal(t, rr.Code, http.StatusOK) + assert.Empty(t, rr.Body.Bytes()) +} + +func TestNewHandlerError(t *testing.T) { + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing?foo=bar", http.NoBody) + rr = httptest.NewRecorder() + ) + + NewHandler(&fakeChecker{err: errors.New("foo")})(rr, req) + + assert.Equal(t, rr.Code, http.StatusServiceUnavailable) + assert.Equal(t, "foo", rr.Body.String()) +} diff --git a/internal/pkg/http/middlewares.go b/internal/pkg/http/middlewares.go deleted file mode 100644 index b275ff37..00000000 --- a/internal/pkg/http/middlewares.go +++ /dev/null @@ -1,14 +0,0 @@ -package http - -import "net/http" - -// DisableAPICachingMiddleware is HTTP middleware for disabling response caching. -func DisableAPICachingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - - next.ServeHTTP(w, r) - }) -} diff --git a/internal/pkg/http/middlewares/logreq/logreq.go b/internal/pkg/http/middlewares/logreq/logreq.go new file mode 100644 index 00000000..cdaf47f2 --- /dev/null +++ b/internal/pkg/http/middlewares/logreq/logreq.go @@ -0,0 +1,59 @@ +// Package logreq contains middleware for HTTP requests logging using "zap" package. +package logreq + +import ( + "net" + "net/http" + "strings" + + "github.com/felixge/httpsnoop" + "github.com/gorilla/mux" + "go.uber.org/zap" +) + +// New creates mux.MiddlewareFunc for HTTP requests logging using "zap" package. +func New(log *zap.Logger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + metrics := httpsnoop.CaptureMetrics(next, w, r) + + log.Info("HTTP request processed", + zap.String("remote_addr", getRealClientAddress(r)), + zap.String("useragent", r.UserAgent()), + zap.String("method", r.Method), + zap.String("url", r.URL.String()), + zap.Int("status_code", metrics.Code), + zap.Int64("duration_micro", metrics.Duration.Microseconds()), + ) + }) + } +} + +// we will trust following HTTP headers for the real ip extracting (priority low -> high). +var trustHeaders = [...]string{"X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP"} //nolint:gochecknoglobals + +// getRealClientAddress extracts real client IP address from request. +func getRealClientAddress(r *http.Request) string { + var ip string + + for _, name := range trustHeaders { + if value := r.Header.Get(name); value != "" { + // `X-Forwarded-For` can be `10.0.0.1, 10.0.0.2, 10.0.0.3` + if strings.Contains(value, ",") { + parts := strings.Split(value, ",") + + if len(parts) > 0 { + ip = strings.TrimSpace(parts[0]) + } + } else { + ip = strings.TrimSpace(value) + } + } + } + + if net.ParseIP(ip) != nil { + return ip + } + + return strings.Split(r.RemoteAddr, ":")[0] +} diff --git a/internal/pkg/http/middlewares/logreq/logreq_test.go b/internal/pkg/http/middlewares/logreq/logreq_test.go new file mode 100644 index 00000000..03c768ea --- /dev/null +++ b/internal/pkg/http/middlewares/logreq/logreq_test.go @@ -0,0 +1,117 @@ +package logreq + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/kami-zh/go-capturer" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestMiddleware(t *testing.T) { + cases := []struct { + name string + giveRequest func() *http.Request + giveHandler http.Handler + checkOutputFields func(t *testing.T, in map[string]interface{}) + }{ + { + name: "basic usage", + giveHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + time.Sleep(time.Millisecond) + w.WriteHeader(http.StatusUnsupportedMediaType) + }), + giveRequest: func() (req *http.Request) { + req, _ = http.NewRequest(http.MethodGet, "http://unit/test/?foo=bar&baz", nil) + req.RemoteAddr = "4.3.2.1:567" + req.Header.Set("User-Agent", "Foo Useragent") + + return + }, + checkOutputFields: func(t *testing.T, in map[string]interface{}) { + assert.Equal(t, http.MethodGet, in["method"]) + assert.NotZero(t, in["duration_micro"]) + assert.Equal(t, "info", in["level"]) + assert.Contains(t, in["msg"], "processed") + assert.Equal(t, "4.3.2.1", in["remote_addr"]) + assert.Equal(t, float64(http.StatusUnsupportedMediaType), in["status_code"]) + assert.Equal(t, "http://unit/test/?foo=bar&baz", in["url"]) + assert.Equal(t, "Foo Useragent", in["useragent"]) + }, + }, + { + name: "IP from 'CF-Connecting-IP' header", + giveHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + giveRequest: func() (req *http.Request) { + req, _ = http.NewRequest(http.MethodGet, "http://testing", nil) + req.RemoteAddr = "4.4.4.4:567" + req.Header.Set("X-Forwarded-For", "10.0.0.1, 10.0.0.2, 10.0.0.3") + req.Header.Set("X-Real-IP", "10.0.1.1") + req.Header.Set("CF-Connecting-IP", "10.1.1.1") + + return + }, + checkOutputFields: func(t *testing.T, in map[string]interface{}) { + assert.Equal(t, "10.1.1.1", in["remote_addr"]) + }, + }, + { + name: "IP from 'X-Real-IP' header", + giveHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + giveRequest: func() (req *http.Request) { + req, _ = http.NewRequest(http.MethodGet, "http://testing", nil) + req.RemoteAddr = "8.8.8.8:567" + req.Header.Set("X-Forwarded-For", "10.0.0.1, 10.0.0.2, 10.0.0.3") + req.Header.Set("X-Real-IP", "10.0.1.1") + + return + }, + checkOutputFields: func(t *testing.T, in map[string]interface{}) { + assert.Equal(t, "10.0.1.1", in["remote_addr"]) + }, + }, + { + name: "IP from 'X-Forwarded-For' header", + giveHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + giveRequest: func() (req *http.Request) { + req, _ = http.NewRequest(http.MethodGet, "http://testing", nil) + req.RemoteAddr = "1.2.3.4:567" + req.Header.Set("X-Forwarded-For", "10.0.0.1, 10.0.0.2, 10.0.0.3") + + return + }, + checkOutputFields: func(t *testing.T, in map[string]interface{}) { + assert.Equal(t, "10.0.0.1", in["remote_addr"]) + }, + }, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + var rr = httptest.NewRecorder() + + output := capturer.CaptureStderr(func() { + log, err := zap.NewProduction() + assert.NoError(t, err) + + New(log).Middleware(tt.giveHandler).ServeHTTP(rr, tt.giveRequest()) + }) + + var asJSON map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(output), &asJSON), "logger output must be valid JSON") + + tt.checkOutputFields(t, asJSON) + }) + } +} diff --git a/internal/pkg/http/middlewares/nocache/nocache.go b/internal/pkg/http/middlewares/nocache/nocache.go new file mode 100644 index 00000000..e6f43f81 --- /dev/null +++ b/internal/pkg/http/middlewares/nocache/nocache.go @@ -0,0 +1,21 @@ +// Package nocache contains middleware for HTTP response caching disabling. +package nocache + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +// New creates mux.MiddlewareFunc for HTTP response caching disabling. +func New() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/pkg/http/middlewares/nocache/nocache_test.go b/internal/pkg/http/middlewares/nocache/nocache_test.go new file mode 100644 index 00000000..7ce6ca23 --- /dev/null +++ b/internal/pkg/http/middlewares/nocache/nocache_test.go @@ -0,0 +1,33 @@ +package nocache + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMiddleware(t *testing.T) { + var ( + req, _ = http.NewRequest(http.MethodGet, "http://testing", nil) + rr = httptest.NewRecorder() + handled bool + ) + + assert.Empty(t, rr.Header().Get("Cache-Control")) + assert.Empty(t, rr.Header().Get("Pragma")) + assert.Empty(t, rr.Header().Get("Expires")) + + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "no-cache, no-store, must-revalidate", w.Header().Get("Cache-Control")) + assert.Equal(t, "no-cache", w.Header().Get("Pragma")) + assert.Equal(t, "0", w.Header().Get("Expires")) + + handled = true + }) + + New().Middleware(nextHandler).ServeHTTP(rr, req) + + assert.True(t, handled) +} diff --git a/internal/pkg/http/middlewares/panic/panic.go b/internal/pkg/http/middlewares/panic/panic.go new file mode 100644 index 00000000..b5a30f49 --- /dev/null +++ b/internal/pkg/http/middlewares/panic/panic.go @@ -0,0 +1,67 @@ +// Package panic contains middleware for panics (inside HTTP handlers) logging using "zap" package. +package panic + +import ( + "encoding/json" + "fmt" + "net/http" + "runtime" + + "github.com/gorilla/mux" + "go.uber.org/zap" +) + +type response struct { + Message string `json:"message"` + Code int `json:"code"` +} + +const statusCode = http.StatusInternalServerError + +// New creates mux.MiddlewareFunc for panics (inside HTTP handlers) logging using "zap" package. Also it allows +// to respond with JSON-formatted error string instead empty response. +func New(log *zap.Logger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + // convert panic reason into error + err, ok := rec.(error) + if !ok { + err = fmt.Errorf("%v", rec) + } + + stackBuf := make([]byte, 1024) + // do NOT use `debug.Stack()` here for skipping one unimportant call trace in stacktrace + for { + n := runtime.Stack(stackBuf, false) + if n < len(stackBuf) { + stackBuf = stackBuf[:n] + + break + } + + stackBuf = make([]byte, 2*len(stackBuf)) //nolint:gomnd + } + + // log error with logger + log.Error("HTTP handler panic", zap.Error(err), zap.String("stacktrace", string(stackBuf))) + + resp := response{ + Message: fmt.Sprintf("%s: %s", http.StatusText(statusCode), err.Error()), + Code: statusCode, + } + + w.WriteHeader(statusCode) + + // and respond with JSON (not "empty response") + if e := json.NewEncoder(w).Encode(resp); e != nil { + panic(e) + } + } + }() + + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/pkg/http/middlewares/panic/panic_test.go b/internal/pkg/http/middlewares/panic/panic_test.go new file mode 100644 index 00000000..fa90a707 --- /dev/null +++ b/internal/pkg/http/middlewares/panic/panic_test.go @@ -0,0 +1,86 @@ +package panic + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/kami-zh/go-capturer" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestMiddleware(t *testing.T) { + cases := []struct { + name string + giveHandler http.Handler + giveRequest func() *http.Request + checkResult func(t *testing.T, in map[string]interface{}, rr *httptest.ResponseRecorder) + }{ + { + name: "panic with error", + giveHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + panic(errors.New("foo error")) + }), + giveRequest: func() *http.Request { + rq, _ := http.NewRequest(http.MethodGet, "http://testing/foo/bar", nil) + + return rq + }, + checkResult: func(t *testing.T, in map[string]interface{}, rr *httptest.ResponseRecorder) { + // check log entry + assert.Equal(t, "foo error", in["error"]) + assert.Contains(t, in["stacktrace"], "/panic.go:") + assert.Contains(t, in["stacktrace"], ".ServeHTTP") + + // check HTTP response + wantJSON, err := json.Marshal(struct { + Message string `json:"message"` + Code int `json:"code"` + }{ + Message: "Internal Server Error: foo error", + Code: http.StatusInternalServerError, + }) + assert.NoError(t, err) + + assert.Equal(t, http.StatusInternalServerError, rr.Code) + assert.JSONEq(t, string(wantJSON), rr.Body.String()) + }, + }, + { + name: "panic with string", + giveHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + panic("bar error") + }), + giveRequest: func() *http.Request { + rq, _ := http.NewRequest(http.MethodGet, "http://testing/foo/bar", nil) + + return rq + }, + checkResult: func(t *testing.T, in map[string]interface{}, rr *httptest.ResponseRecorder) { + assert.Equal(t, "bar error", in["error"]) + }, + }, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + var rr = httptest.NewRecorder() + + output := capturer.CaptureStderr(func() { + log, err := zap.NewProduction() + assert.NoError(t, err) + + New(log).Middleware(tt.giveHandler).ServeHTTP(rr, tt.giveRequest()) + }) + + var asJSON map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(output), &asJSON), "logger output must be valid JSON") + + tt.checkResult(t, asJSON, rr) + }) + } +} diff --git a/internal/pkg/http/middlewares_test.go b/internal/pkg/http/middlewares_test.go deleted file mode 100644 index 5dce742e..00000000 --- a/internal/pkg/http/middlewares_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package http - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestDisableAPICachingMiddleware(t *testing.T) { - var handled bool = false - - // create a handler to use as "next" which will verify the request - nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if w.Header().Get("Cache-Control") != "no-cache, no-store, must-revalidate" { - t.Error("Wrong header `Cache-Control` value found") - } - if w.Header().Get("Pragma") != "no-cache" { - t.Error("Wrong header `Pragma` value found") - } - if w.Header().Get("Expires") != "0" { - t.Error("Wrong header `Expires` value found") - } - - handled = true - }) - - middlewareHandler := DisableAPICachingMiddleware(nextHandler) - - var ( - req, _ = http.NewRequest("GET", "http://testing", nil) - rr = httptest.NewRecorder() - ) - - if rr.Header().Get("Cache-Control") != "" { - t.Error("Header `Cache-Control` must be empty before execution") - } - - if rr.Header().Get("Pragma") != "" { - t.Error("Header `Pragma` must be empty before execution") - } - - if rr.Header().Get("Expires") != "" { - t.Error("Header `Expires` must be empty before execution") - } - - middlewareHandler.ServeHTTP(rr, req) - - if handled != true { - t.Error("next handler was not executed") - } -} diff --git a/internal/pkg/http/routes.go b/internal/pkg/http/routes.go index c5344841..9c190693 100644 --- a/internal/pkg/http/routes.go +++ b/internal/pkg/http/routes.go @@ -3,58 +3,76 @@ package http import ( "net/http" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/api" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/checkers" "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/fileserver" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/script" + apiSettings "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/handlers/api/settings" + apiVersion "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/handlers/api/version" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/handlers/generate" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/handlers/healthz" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/middlewares/nocache" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/version" ) -// RegisterHandlers register server http handlers. -func (s *Server) RegisterHandlers() { - s.registerStaticHandlers() - s.registerAPIHandlers() - s.registerFileServerHandler() -} +func (s *Server) registerScriptGeneratorHandlers() error { + h, err := generate.NewHandler(s.ctx, s.log, s.cacher, s.cfg) + if err != nil { + return err + } -// Register static route handlers. -func (s *Server) registerStaticHandlers() { - s.Router. - HandleFunc("/script/source", script.RouterOsScriptSourceGenerationHandlerFunc(s.ServeSettings)). - Methods("GET"). + s.router. + Handle("/script/source", h). + Methods(http.MethodGet). Name("script_generator") + + return nil } -// Register API handlers. func (s *Server) registerAPIHandlers() { - apiRouter := s.Router. + apiRouter := s.router. PathPrefix("/api"). Subrouter() - apiRouter.Use(DisableAPICachingMiddleware) + apiRouter.Use(nocache.New()) apiRouter. - HandleFunc("/settings", api.GetSettingsHandlerFunc(s.ServeSettings)). - Methods("GET"). + HandleFunc("/settings", apiSettings.NewHandler(*s.cfg, s.cacher)). + Methods(http.MethodGet). Name("api_get_settings") apiRouter. - HandleFunc("/version", api.GetVersionHandler). - Methods("GET"). + HandleFunc("/version", apiVersion.NewHandler(version.Version())). + Methods(http.MethodGet). Name("api_get_version") +} - apiRouter. - HandleFunc("/routes", api.GetRoutesHandlerFunc(s.Router)). - Methods("GET"). - Name("api_get_routes") +func (s *Server) registerServiceHandlers() { + s.router. + HandleFunc("/ready", healthz.NewHandler(checkers.NewReadyChecker(s.ctx, s.rdb))). + Methods(http.MethodGet, http.MethodHead). + Name("ready") + + s.router. + HandleFunc("/live", healthz.NewHandler(checkers.NewLiveChecker())). + Methods(http.MethodGet, http.MethodHead). + Name("live") } -// Register file server handler. -func (s *Server) registerFileServerHandler() { - s.Router. +func (s *Server) registerFileServerHandler(resourcesDir string) error { + fs, err := fileserver.NewFileServer(fileserver.Settings{ + FilesRoot: resourcesDir, + IndexFileName: "index.html", + ErrorFileName: "__error__.html", + RedirectIndexFileToRoot: true, + }) + if err != nil { + return err + } + + s.router. PathPrefix("/"). - Handler(&fileserver.FileServer{Settings: fileserver.Settings{ - Root: http.Dir(s.ServeSettings.Resources.DirPath), - IndexFile: s.ServeSettings.Resources.IndexName, - Error404file: s.ServeSettings.Resources.Error404Name, - }}). + Methods(http.MethodGet, http.MethodHead). + Handler(fs). Name("static") + + return nil } diff --git a/internal/pkg/http/routes_test.go b/internal/pkg/http/routes_test.go deleted file mode 100644 index a04058ac..00000000 --- a/internal/pkg/http/routes_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package http - -import ( - "net/http" - "reflect" - "testing" - - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/api" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/fileserver" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/script" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" -) - -func TestServer_RegisterHandlers(t *testing.T) { - compareHandlers := func(h1, h2 interface{}) bool { - t.Helper() - return reflect.ValueOf(h1).Pointer() == reflect.ValueOf(h2).Pointer() - } - - // @link: - getType := func(myvar interface{}) string { - t.Helper() - - if t := reflect.TypeOf(myvar); t.Kind() == reflect.Ptr { - return "*" + t.Elem().Name() - } - - return t.Name() - } - - var s = NewServer(&ServerSettings{}, &serve.Settings{}) - - var cases = []struct { - name string - route string - methods []string - handler func(http.ResponseWriter, *http.Request) - }{ - { - name: "script_generator", - route: "/script/source", - methods: []string{"GET"}, - handler: script.RouterOsScriptSourceGenerationHandlerFunc(s.ServeSettings), - }, - { - name: "api_get_settings", - route: "/api/settings", - methods: []string{"GET"}, - handler: api.GetSettingsHandlerFunc(s.ServeSettings), - }, - { - name: "api_get_version", - route: "/api/version", - methods: []string{"GET"}, - handler: api.GetVersionHandler, - }, - { - name: "api_get_routes", - route: "/api/routes", - methods: []string{"GET"}, - handler: api.GetRoutesHandlerFunc(s.Router), - }, - } - - for _, testCase := range cases { - if s.Router.Get(testCase.name) != nil { - t.Errorf("Handler for route [%s] must be not registered before RegisterHandlers() calling", testCase.name) - } - } - - s.RegisterHandlers() - - for _, testCase := range cases { - if route, _ := s.Router.Get(testCase.name).GetPathTemplate(); route != testCase.route { - t.Errorf("wrong route for [%s] route: want %v, got %v", testCase.name, testCase.route, route) - } - if methods, _ := s.Router.Get(testCase.name).GetMethods(); !reflect.DeepEqual(methods, testCase.methods) { - t.Errorf("wrong method(s) for [%s] route: want %v, got %v", testCase.name, testCase.methods, methods) - } - if !compareHandlers(testCase.handler, s.Router.Get(testCase.name).GetHandler()) { - t.Errorf("wrong handler for [%s] route", testCase.name) - } - } - - // Test static files handler registration - staticRoute := s.Router.Get("static") - - if prefix, _ := staticRoute.GetPathTemplate(); prefix != "/" { - t.Errorf("Wrong prefix for static files handler. Got: %s", prefix) - } - - staticHandler := staticRoute.GetHandler() - - if handlerType := getType(staticHandler); handlerType != "*FileServer" { - t.Errorf("Wrong handler (%s) for static route", handlerType) - } - - if staticHandler.(*fileserver.FileServer).Settings.Root != http.Dir(s.ServeSettings.Resources.DirPath) { - t.Error("Wrong resources root path is set for file server") - } - - if staticHandler.(*fileserver.FileServer).Settings.IndexFile != s.ServeSettings.Resources.IndexName { - t.Error("Wrong resources index file name is set for file server") - } - - if staticHandler.(*fileserver.FileServer).Settings.Error404file != s.ServeSettings.Resources.Error404Name { - t.Error("Wrong resources 404 error file name is set for file server") - } -} diff --git a/internal/pkg/http/script/cache.go b/internal/pkg/http/script/cache.go deleted file mode 100644 index 2976627f..00000000 --- a/internal/pkg/http/script/cache.go +++ /dev/null @@ -1,15 +0,0 @@ -package script - -import ( - "github.com/tarampampam/go-filecache" -) - -// defaultCachePool becomes accessible only after initial function calling -var defaultCachePool filecache.CachePool - -// initDefaultCachePool makes default cache pool initialization -func initDefaultCachePool(cacheDir string, force bool) { - if defaultCachePool == nil || !force { - defaultCachePool = filecache.NewPool(cacheDir) - } -} diff --git a/internal/pkg/http/script/http_client.go b/internal/pkg/http/script/http_client.go deleted file mode 100644 index 1f8a6c31..00000000 --- a/internal/pkg/http/script/http_client.go +++ /dev/null @@ -1,81 +0,0 @@ -package script - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "strings" - "time" -) - -type httpClient struct { - client *http.Client -} - -var defaultHTTPClient = newHTTPClient() - -// newHTTPClient creates new http client for a working with external sources -func newHTTPClient() *httpClient { - return &httpClient{ - client: &http.Client{ - Timeout: time.Second * 10, // Set request timeout - CheckRedirect: func(_ *http.Request, via []*http.Request) error { - if len(via) >= 2 { - return errors.New("request: too many (2) redirects") - } - - return nil - }, - }, - } -} - -// FetchSourceContent sends request to the external source and returns response object only if ALL is ok -func (c *httpClient) FetchSourceContent(uri string, maxLength int) (*http.Response, error) { - // Create HTTP request - httpRequest, requestErr := http.NewRequest("GET", uri, nil) - - // Check request creation - if requestErr != nil { - return nil, requestErr - } - - // Do request - response, responseErr := c.client.Do(httpRequest) - - // Check response getting - if responseErr != nil { - return nil, responseErr - } - - // `Content-Type` header validation - if contentType := response.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "text/plain") { - _ = response.Body.Close() - - return nil, fmt.Errorf("wrong 'Content-Type' header '%s', required is '%s'", contentType, "text/plain*") - } - - // `Content-Length` header validation (if last presents) - if contentLength := response.Header.Get("Content-Length"); contentLength != "" { - value, parseErr := strconv.Atoi(contentLength) - - // Parse value - if parseErr != nil { - _ = response.Body.Close() - - return nil, errors.New("header 'Content-Length' parsing error: " + parseErr.Error()) - } - - // Validate length - if value >= maxLength { - _ = response.Body.Close() - - return nil, fmt.Errorf("'Content-Length' header value is too much (%d, maximum: %d)", value, maxLength) - } - } - - // @todo: add read response body size checking - - return response, nil -} diff --git a/internal/pkg/http/script/query_parameters.go b/internal/pkg/http/script/query_parameters.go deleted file mode 100644 index 18d101ae..00000000 --- a/internal/pkg/http/script/query_parameters.go +++ /dev/null @@ -1,136 +0,0 @@ -package script - -import ( - "errors" - "fmt" - "net" - "net/url" - "strconv" - "strings" -) - -type queryParametersBag struct { - SourceUrls []string - Format string - Version string - ExcludedHosts []string - Limit int - RedirectTo string -} - -// newQueryParametersBag makes query parameters bag using passed url values. -func newQueryParametersBag( //nolint:gocyclo - values url.Values, - defaultRedirectTo string, - maxSources int, -) (*queryParametersBag, error) { - bag := &queryParametersBag{ - // Defaults: - RedirectTo: defaultRedirectTo, - Format: "routeros", - } - - // Extract `sources_urls` values - if sourceUrls, ok := values["sources_urls"]; ok { - // Iterate query values slice - for _, value := range sourceUrls { - // Explode value with URLs list (separated using `,`) into single URLs - for _, sourceURL := range strings.Split(value, ",") { - // Make URL validation, and if all is ok - append it into query parameters bag - if _, err := url.ParseRequestURI(sourceURL); err == nil && len(sourceURL) <= 256 { - bag.SourceUrls = append(bag.SourceUrls, sourceURL) - } - } - } - } else { - return nil, errors.New("required parameter `sources_urls` was not found") - } - - // Validate sources list size - if len(bag.SourceUrls) < 1 { - return nil, errors.New("empty sources list") - } - - // remove duplicated sources - bag.SourceUrls = bag.uniqueStringsSlice(bag.SourceUrls) - - // check for sources count - if len(bag.SourceUrls) > maxSources { - return nil, fmt.Errorf("too much sources (only %d is allowed)", maxSources) - } - - // Extract `format` value - if value, ok := values["format"]; ok { - if len(value) > 0 { - bag.Format = value[0] - } - } - - // Extract `version` value - if value, ok := values["version"]; ok { - if len(value) > 0 { - bag.Version = value[0] - } - } - - // Extract `excluded_hosts` value - if excludedHosts, ok := values["excluded_hosts"]; ok { - // Iterate query values slice - for _, value := range excludedHosts { - // Explode value with host names list (separated using `,`) into single host names - for _, excludedHost := range strings.Split(value, ",") { - // Make basic checking, and if all is ok - append it into query parameters bag - if excludedHost != "" { - bag.ExcludedHosts = append(bag.ExcludedHosts, excludedHost) - } - } - } - // remove duplicated hosts - bag.ExcludedHosts = bag.uniqueStringsSlice(bag.ExcludedHosts) - // Validate excluded hosts list size - if len(bag.ExcludedHosts) > 32 { - return nil, errors.New("too many excluded hosts (more then 32)") - } - } - - // Extract `limit` value - if value, ok := values["limit"]; ok { - if len(value) > 0 { - if value, err := strconv.Atoi(value[0]); err == nil { - if value <= 0 { - return nil, errors.New("wrong `limit` value (cannot be less then 1)") - } - bag.Limit = value - } else { - return nil, errors.New("wrong `limit` value (cannot be converted into integer)") - } - } - } - - // Extract and validate `redirect_to` value - if value, ok := values["redirect_to"]; ok { - if len(value) > 0 { - if net.ParseIP(value[0]) == nil { - return nil, errors.New("wrong `redirect_to` value (invalid IP address)") - } - bag.RedirectTo = value[0] - } - } - - return bag, nil -} - -// uniqueStringsSlice removes duplicated strings from strings slice -func (queryParametersBag) uniqueStringsSlice(in []string) []string { - keys := make(map[string]bool) - out := make([]string, 0) - - for _, entry := range in { - if _, ok := keys[entry]; !ok { - keys[entry] = true - out = append(out, entry) - } - } - - return out -} diff --git a/internal/pkg/http/script/query_parameters_test.go b/internal/pkg/http/script/query_parameters_test.go deleted file mode 100644 index a91e3f2c..00000000 --- a/internal/pkg/http/script/query_parameters_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package script - -import ( - "errors" - "net/url" - "reflect" - "strings" - "testing" -) - -func Test_newQueryParametersBag(t *testing.T) { - var cases = []struct { - name string - giveURLValues url.Values - giveDefaultRedirectTo string - giveMaxSources int - wantError error - wantQueryParametersBag queryParametersBag - }{ - { - name: "Basic usage", - giveURLValues: url.Values{ - "format": []string{"routeros"}, - "version": []string{"foo@bar"}, - "redirect_to": []string{"127.0.0.10"}, - "limit": []string{"50"}, - "sources_urls": []string{"http://foo.com/bar.txt,http://bar.com/baz.asp"}, - "excluded_hosts": []string{"foo.com,bar.com"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt", "http://bar.com/baz.asp"}, - Format: "routeros", - Version: "foo@bar", - ExcludedHosts: []string{"foo.com", "bar.com"}, - Limit: 50, - RedirectTo: "127.0.0.10", - }, - }, - { - name: "Minimal usage", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "routeros", - Limit: 0, - RedirectTo: "127.0.0.2", - }, - }, - { - name: "Wrong URLs skipped", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt,foo,google.com/some.txt,,https:// host.com/file"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "routeros", - RedirectTo: "127.0.0.2", - }, - }, - { - name: "URLs unique (duplicates must be removed)", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt,http://foo.com/bar.txt,http://bar.com/baz.asp"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt", "http://bar.com/baz.asp"}, - Format: "routeros", - RedirectTo: "127.0.0.2", - }, - }, - { - name: "Too long URLs skipped", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt,http://bar.com/baz.asp?x=" + strings.Repeat("x", 232)}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "routeros", - RedirectTo: "127.0.0.2", - }, - }, - { - name: "Error when sources url not passed", - giveURLValues: url.Values{}, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantError: errors.New("required parameter `sources_urls` was not found"), - }, - { - name: "Only wrong values in sources URLs", - giveURLValues: url.Values{ - "sources_urls": []string{ - "http://foo.com/bar.txt?z=" + strings.Repeat("x", 232) + "," + - "http://bar.com/baz.asp?x=" + strings.Repeat("x", 232), - }, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantError: errors.New("empty sources list"), - }, - { - name: "Sources limit exceeded", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt,http://bar.com/baz.asp,http://baz.com/blah"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 2, - wantError: errors.New("too much sources (only 2 is allowed)"), - }, - { - name: "`format` value passing", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "format": []string{"\t s0me Format_yEah! "}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "\t s0me Format_yEah! ", - Limit: 0, - RedirectTo: "127.0.0.2", - }, - }, - { - name: "`version` value passing", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "version": []string{"\t s0me vErSi0n_yEah! "}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "routeros", - Version: "\t s0me vErSi0n_yEah! ", - Limit: 0, - RedirectTo: "127.0.0.2", - }, - }, - { - name: "Excluded host must be unique", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "excluded_hosts": []string{"foo,bar,foo"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "routeros", - ExcludedHosts: []string{"foo", "bar"}, - Limit: 0, - RedirectTo: "127.0.0.2", - }, - }, - { - name: "Empty excluded hosts must be skipped", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "excluded_hosts": []string{",,,foo,,bar,, ,"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "routeros", - ExcludedHosts: []string{"foo", "bar", " "}, - Limit: 0, - RedirectTo: "127.0.0.2", - }, - }, - { - name: "32 excluded host allowed", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "excluded_hosts": []string{"x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20," + - "x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 10, - wantQueryParametersBag: queryParametersBag{ - SourceUrls: []string{"http://foo.com/bar.txt"}, - Format: "routeros", - ExcludedHosts: []string{ - "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", - "x10", "x11", "x12", "x13", "x14", "x15", "x16", "x17", "x18", "x19", - "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "x29", - "x30", "x31", "x32", - }, - Limit: 0, - RedirectTo: "127.0.0.2", - }, - }, - { - name: "Too many excluded hosts", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "excluded_hosts": []string{"x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20," + - "x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 2, - wantError: errors.New("too many excluded hosts (more then 32)"), - }, - { - name: "Negative 'limit' value", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "limit": []string{"-1"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 2, - wantError: errors.New("wrong `limit` value (cannot be less then 1)"), - }, - { - name: "Wrong 'limit' value", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "limit": []string{"foo"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 2, - wantError: errors.New("wrong `limit` value (cannot be converted into integer)"), - }, - { - name: "Wrong 'redirect_to' value", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "redirect_to": []string{"foo"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 2, - wantError: errors.New("wrong `redirect_to` value (invalid IP address)"), - }, - { - name: "Wrong 'redirect_to' value", - giveURLValues: url.Values{ - "sources_urls": []string{"http://foo.com/bar.txt"}, - "redirect_to": []string{"127.0.0.256"}, - }, - giveDefaultRedirectTo: "127.0.0.2", - giveMaxSources: 2, - wantError: errors.New("wrong `redirect_to` value (invalid IP address)"), - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - bag, err := newQueryParametersBag(tt.giveURLValues, tt.giveDefaultRedirectTo, tt.giveMaxSources) - - if tt.wantError != nil && err.Error() != tt.wantError.Error() { - t.Errorf(`Want error "%v", but got "%v"`, tt.wantError, err) - } - - if err != nil && tt.wantError == nil { - t.Errorf(`Error %v returned, but nothing expected`, err) - } - - if tt.wantError == nil && bag != nil { - if !reflect.DeepEqual(&tt.wantQueryParametersBag, bag) { - t.Errorf("Want bag %v, but got %v", tt.wantQueryParametersBag, bag) - } - } - }) - } -} diff --git a/internal/pkg/http/script/source.go b/internal/pkg/http/script/source.go deleted file mode 100644 index 90246abb..00000000 --- a/internal/pkg/http/script/source.go +++ /dev/null @@ -1,236 +0,0 @@ -package script - -import ( - "io" - "net/http" - "sort" - "strconv" - "strings" - "time" - - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" - ver "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/version" - "github.com/tarampampam/mikrotik-hosts-parser/pkg/hostsfile" - hostsParser "github.com/tarampampam/mikrotik-hosts-parser/pkg/hostsfile/parser" - "github.com/tarampampam/mikrotik-hosts-parser/pkg/mikrotik/dns" - - "github.com/tarampampam/go-filecache" -) - -type sourceResponse struct { - URL string - Content io.ReadCloser - Error error - CacheIsHit bool - CacheExpiredAfterSec int -} - -// RouterOsScriptSourceGenerationHandlerFunc generates RouterOS script source and writes it response. -func RouterOsScriptSourceGenerationHandlerFunc( //nolint:funlen,gocyclo - serveSettings *serve.Settings, -) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // initialize default cache pool - initDefaultCachePool(serveSettings.Cache.File.DirPath, false) - - queryParameters, queryErr := newQueryParametersBag( - r.URL.Query(), - serveSettings.RouterScript.Redirect.Address, - serveSettings.RouterScript.MaxSources, - ) - - // Validate query parameters parsing - if queryErr != nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("## Query parameters error: " + queryErr.Error())) - - return - } - - comments := make([]string, 0) // strings slice for storing processing comments (e.g. info messages, errors, etc) - - // append basic information - comments = append(comments, - "Script generated at "+time.Now().Format("2006-01-02 15:04:05"), - "Generator version: "+ver.Version(), - "", - "Sources list: <"+strings.Join(queryParameters.SourceUrls, ">, <")+">", - "Excluded hosts: '"+strings.Join(queryParameters.ExcludedHosts, "', '")+"'", - "Limit: "+strconv.Itoa(queryParameters.Limit), - "Cache lifetime: "+strconv.Itoa(serveSettings.Cache.LifetimeSec)+" seconds", - ) - - sourceResponsesChannel := make(chan *sourceResponse) // channel for source responses - - // fetch sources async and write responses into channel - for _, sourceURL := range queryParameters.SourceUrls { - go writeSourceResponse( - sourceResponsesChannel, - sourceURL, - serveSettings.RouterScript.MaxSourceSize, - serveSettings.Cache.LifetimeSec, - ) - } - - var ( - parser = hostsParser.NewParser() - hostsRecords = make([]*hostsfile.Record, 0) // hosts records stack - ) - - // read source responses and pass it into hosts file parser - for i := 0; i < len(queryParameters.SourceUrls); i++ { - // read message from channel - resp := <-sourceResponsesChannel - if resp.CacheIsHit { - comments = append(comments, "Cache HIT for <"+resp.URL+"> "+ - "(expires after "+strconv.Itoa(resp.CacheExpiredAfterSec)+" sec.)") - } else { - comments = append(comments, "Cache miss for <"+resp.URL+">") - } - // if response contains error - skip it - if resp.Error != nil { - if resp.Content != nil { - _ = resp.Content.Close() - } - comments = append(comments, "Source <"+resp.URL+"> error: "+resp.Error.Error()) - continue - } - // parse response content - records, parseErr := parser.Parse(resp.Content) - _ = resp.Content.Close() - if parseErr != nil { - comments = append(comments, "Source <"+resp.URL+"> error: "+parseErr.Error()) - } - // and append results into hosts records stack - hostsRecords = append(hostsRecords, records...) - } - - // close responses channels after all - close(sourceResponsesChannel) - - // convert hosts records into static mikrotik dns entries - staticEntries := hostsRecordsToStaticEntries( - hostsRecords, - queryParameters.ExcludedHosts, - queryParameters.Limit, - queryParameters.RedirectTo, - serveSettings.RouterScript.Comment, - ) - - // write processing comments - for _, comment := range comments { - buf := make([]byte, 0) - if comment == "" { - buf = append(buf, "\n"...) - } else { - buf = append(buf, "## "+comment+"\n"...) - } - _, _ = w.Write(buf) - } - - // render result script source - if len(staticEntries) > 0 { - _, _ = w.Write([]byte("\n/ip dns static\n")) - _, renderErr := staticEntries.Render(w, &dns.RenderOptions{ - RenderEntryOptions: dns.RenderEntryOptions{ - Prefix: "add", - }, - RenderEmpty: false, - }) - - _, _ = w.Write([]byte("\n\n## Records count: " + strconv.Itoa(len(staticEntries)))) - if renderErr != nil { - _, _ = w.Write([]byte("\n\n## Rendering error: " + renderErr.Error())) - } - } - } -} - -// writeSourceResponse writes source response into channel (content can be fetched from cache) -func writeSourceResponse(channel chan *sourceResponse, sourceURL string, maxLength, cacheLifetimeSec int) { - var ( - result = &sourceResponse{URL: sourceURL} - cacheItem filecache.CacheItem - ) - // if cache missed - if cached := defaultCachePool.GetItem(sourceURL); !cached.IsHit() { - // do request - response, fetchError := defaultHTTPClient.FetchSourceContent(sourceURL, maxLength) - result.Error = fetchError - if response != nil { - // and write response content into cache - cacheItem, _ = defaultCachePool.Put( - sourceURL, - response.Body, - time.Now().Add(time.Second*time.Duration(cacheLifetimeSec)), - ) - _ = response.Body.Close() - } - } else { - result.CacheIsHit = true - } - // extract cached item from cache pool (if was missed previously) - if cacheItem == nil { - cacheItem = defaultCachePool.GetItem(sourceURL) - } - // read from cache item using pipe - var pipeReader, pipeWriter = io.Pipe() - go func() { - defer func() { _ = pipeWriter.Close() }() - _ = cacheItem.Get(pipeWriter) - }() - result.Content = pipeReader - if expiresAt := cacheItem.ExpiresAt(); expiresAt != nil { - result.CacheExpiredAfterSec = int(expiresAt.Unix() - time.Now().Unix()) - } - channel <- result -} - -// hostsRecordsToStaticEntries converts hosts records into static dns entries -func hostsRecordsToStaticEntries( - in []*hostsfile.Record, - excludes []string, - limit int, - redirectTo, - comment string, -) dns.StaticEntries { - var ( - processedHosts = make(map[string]bool) - out = dns.StaticEntries{} - ) - - // put hosts for excluding into processed hosts map for skipping in future - for _, host := range excludes { - processedHosts[host] = true - } - - // loop over all passed hosts file records -records: - for _, record := range in { - // iterate hosts in record - for _, host := range record.Hosts { - // maximal hosts checking - if limit > 0 && len(out) >= limit { - break records - } - // verification that host was not processed previously - if _, ok := processedHosts[host]; !ok { - // set "was processed" flag in hosts map - processedHosts[host] = true - // add new static entry into result - out = append(out, dns.StaticEntry{ - Address: redirectTo, - Comment: comment, - Name: host, - }) - } - } - } - - // make sorting - sort.Slice(out[:], func(i, j int) bool { - return out[i].Name < out[j].Name - }) - - return out -} diff --git a/internal/pkg/http/script/source_test.go b/internal/pkg/http/script/source_test.go deleted file mode 100644 index e17addf3..00000000 --- a/internal/pkg/http/script/source_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package script - -import ( - "bytes" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" -) - -type roundTripFunc func(req *http.Request) *http.Response - -func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { - return f(req), nil -} - -// NewTestClient returns *http.Client with Transport replaced to avoid making real calls. -func NewTestClient(fn roundTripFunc) *http.Client { - return &http.Client{ - Transport: roundTripFunc(fn), //nolint:unconvert - } -} - -func TestRouterOsScriptSourceGenerationHandlerFunc(t *testing.T) { - // Create directory in temporary - createTempDir := func() string { - t.Helper() - if dir, err := ioutil.TempDir("", "test-"); err != nil { - panic(err) - } else { - return dir - } - } - - tmpDir := createTempDir() - - defer func(d string) { - if err := os.RemoveAll(d); err != nil { - panic(err) - } - }(tmpDir) - - var ( - req, _ = http.NewRequest("GET", "http://testing/script/source?"+ - "format=routeros&"+ - "version=v0.0.666@1a0339c&"+ - "redirect_to=127.0.0.5&"+ - "limit=1234&"+ - "sources_urls=https%3A%2F%2Ffoo.com%2Fbar.txt,"+ - "http://bar.com/baz.asp,"+ - "http://baz.com/blah.list"+ - "&excluded_hosts="+ - "d.com,c.org,"+ - "localhost,"+ - "localhost.localdomain,"+ - "broadcasthost,"+ - "local,"+ - "ip6-localhost,"+ - "ip6-loopback,"+ - "ip6-localnet,"+ - "ip6-mcastprefix,"+ - "ip6-allnodes,"+ - "ip6-allrouters,"+ - "ip6-allhosts", nil) - rr = httptest.NewRecorder() - serveSettings = serve.Settings{ - Sources: []serve.Source{{ - URI: "http://goo.gl/hosts.txt", - Name: "Foo name", - Description: "Foo desc", - EnabledByDefault: true, - RecordsCount: 123, - }}, - RouterScript: serve.RouterScript{ - Redirect: serve.Redirect{ - Address: "0.1.1.0", - }, - Exclude: serve.Excludes{ - Hosts: []string{"foo", "bar"}, - }, - MaxSources: 4, - MaxSourceSize: 2097152, - Comment: "AdBlockTest", - }, - Cache: serve.Cache{ - File: serve.CacheFiles{DirPath: tmpDir}, - }, - } - ) - - // mock default http client - defaultHTTPClient = &httpClient{ - client: NewTestClient(func(req *http.Request) *http.Response { - switch req.URL.String() { - case "https://foo.com/bar.txt": - return &http.Response{ - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewBufferString(`127.0.0.1 a.com -127.0.0.1 b.com -127.0.0.1 c.com -127.0.0.1 d.com`)), - Header: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, - } - case "http://bar.com/baz.asp": - return &http.Response{ - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewBufferString( - "\n\n0.0.0.0 a.org\n127.0.0.1 b.org\n127.0.0.1 c.org", - )), - Header: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, - } - } - - return &http.Response{ - StatusCode: 404, - Body: ioutil.NopCloser(bytes.NewBufferString("404 ERROR")), - Header: make(http.Header), - } - }), - } - - RouterOsScriptSourceGenerationHandlerFunc(&serveSettings)(rr, req) - - if rr.Code != http.StatusOK { - t.Errorf("Wrong response HTTP code. Want %d, got %d", http.StatusOK, rr.Code) - } - - body := rr.Body.String() - - for _, substring := range []string{ - `'d.com', 'c.org'`, - `Limit: 1234`, - "/ip dns static", - `add address=127.0.0.5 comment="AdBlockTest" disabled=no name="a.com"`, - `add address=127.0.0.5 comment="AdBlockTest" disabled=no name="b.com"`, - `add address=127.0.0.5 comment="AdBlockTest" disabled=no name="c.com"`, - `add address=127.0.0.5 comment="AdBlockTest" disabled=no name="a.org"`, - `add address=127.0.0.5 comment="AdBlockTest" disabled=no name="b.org"`, - } { - if !strings.Contains(body, substring) { - t.Errorf("Expected substring '%s' was not found in response (%s)", substring, body) - } - } - - for _, substring := range []string{ - `add address=127.0.0.5 comment="AdBlockTest" disabled=no name="d.com"`, - `add address=127.0.0.5 comment="AdBlockTest" disabled=no name="c.org"`, - } { - if strings.Contains(body, substring) { - t.Errorf("Unexpected substring '%s' was found in response (%s)", substring, body) - } - } -} diff --git a/internal/pkg/http/server.go b/internal/pkg/http/server.go index ee5b1018..7122dcd1 100644 --- a/internal/pkg/http/server.go +++ b/internal/pkg/http/server.go @@ -1,84 +1,123 @@ +// Package http contains HTTP server and all required stuff for HTTP server working. package http import ( "context" - "log" "mime" "net/http" - "os" "strconv" "time" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" - - "github.com/gorilla/handlers" + "github.com/go-redis/redis/v8" "github.com/gorilla/mux" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cache" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/config" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/middlewares/logreq" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/http/middlewares/panic" + "go.uber.org/zap" ) type ( - ServerSettings struct { - WriteTimeout time.Duration - ReadTimeout time.Duration - KeepAliveEnabled bool - } - + // Server is HTTP server. Server struct { - Settings *ServerSettings - ServeSettings *serve.Settings - Server *http.Server - Router *mux.Router - stdLog *log.Logger - errLog *log.Logger - startTime time.Time + ctx context.Context + log *zap.Logger + cacher cache.Cacher + resourcesDir string // can be empty + cfg *config.Config + srv *http.Server + router *mux.Router + rdb *redis.Client // optional, can be nil } ) +const ( + defaultWriteTimeout = time.Second * 15 + defaultReadTimeout = time.Second * 15 +) + // NewServer creates new server instance. -func NewServer(settings *ServerSettings, serveSettings *serve.Settings) *Server { +func NewServer( + ctx context.Context, + log *zap.Logger, + cacher cache.Cacher, + resourcesDir string, // can be empty + cfg *config.Config, + rdb *redis.Client, // optional, can be nil +) Server { var ( - router = *mux.NewRouter() - stdLog = log.New(os.Stdout, "", log.Ldate|log.Lmicroseconds) - errLog = log.New(os.Stderr, "[error] ", log.LstdFlags) + router = mux.NewRouter() httpServer = &http.Server{ - Addr: serveSettings.Listen.Address + ":" + strconv.Itoa(serveSettings.Listen.Port), - Handler: handlers.LoggingHandler(os.Stdout, &router), - ErrorLog: errLog, - WriteTimeout: settings.WriteTimeout, - ReadTimeout: settings.ReadTimeout, + Handler: router, + ErrorLog: zap.NewStdLog(log), + WriteTimeout: defaultWriteTimeout, + ReadTimeout: defaultReadTimeout, } ) - httpServer.SetKeepAlivesEnabled(settings.KeepAliveEnabled) - - return &Server{ - Settings: settings, - ServeSettings: serveSettings, - Server: httpServer, - Router: &router, - stdLog: stdLog, - errLog: errLog, + return Server{ + ctx: ctx, + log: log, + cacher: cacher, + resourcesDir: resourcesDir, + cfg: cfg, + srv: httpServer, + router: router, + rdb: rdb, } } -// Start proxy Server. -func (s *Server) Start() error { - s.startTime = time.Now() +// Start server. +func (s *Server) Start(ip string, port uint16) error { + s.srv.Addr = ip + ":" + strconv.Itoa(int(port)) + + return s.srv.ListenAndServe() +} + +// Register server routes, middlewares, etc. +func (s *Server) Register() error { + s.registerGlobalMiddlewares() + + if err := s.registerHandlers(); err != nil { + return err + } + if err := s.registerCustomMimeTypes(); err != nil { - panic(err) + return err } - s.stdLog.Println("Starting Server on " + s.Server.Addr) - return s.Server.ListenAndServe() + return nil } -// Register custom mime types. -func (*Server) registerCustomMimeTypes() error { - return mime.AddExtensionType(".vue", "text/html; charset=utf-8") +func (s *Server) registerGlobalMiddlewares() { + s.router.Use( + logreq.New(s.log), + panic.New(s.log), + ) } -// Stop proxy Server. -func (s *Server) Stop() error { - s.stdLog.Println("Stopping Server") +// registerHandlers register server http handlers. +func (s *Server) registerHandlers() error { + if err := s.registerScriptGeneratorHandlers(); err != nil { + return err + } + + s.registerAPIHandlers() + s.registerServiceHandlers() + + if s.resourcesDir != "" { + if err := s.registerFileServerHandler(s.resourcesDir); err != nil { + return err + } + } + + return nil +} - return s.Server.Shutdown(context.Background()) +// registerCustomMimeTypes registers custom mime types. +func (*Server) registerCustomMimeTypes() error { + return mime.AddExtensionType(".vue", "text/html; charset=utf-8") } + +// Stop server. +func (s *Server) Stop(ctx context.Context) error { return s.srv.Shutdown(ctx) } diff --git a/internal/pkg/http/server_test.go b/internal/pkg/http/server_test.go index 58ddb21f..205bbe9b 100644 --- a/internal/pkg/http/server_test.go +++ b/internal/pkg/http/server_test.go @@ -1,94 +1,146 @@ package http import ( - "log" + "context" + "errors" "mime" - "os" - "reflect" + "net" + "net/http" + "strconv" "testing" "time" - "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/settings/serve" + "github.com/stretchr/testify/assert" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/cache" + "github.com/tarampampam/mikrotik-hosts-parser/internal/pkg/config" + "go.uber.org/zap" ) -func TestNewServer(t *testing.T) { - settings := ServerSettings{ - WriteTimeout: 10 * time.Second, - ReadTimeout: 13 * time.Second, - KeepAliveEnabled: false, - } - - server := NewServer(&settings, &serve.Settings{ - Listen: serve.Listen{Address: "1.2.3.4", Port: 321}, - }) +func getRandomTCPPort(t *testing.T) (int, error) { + t.Helper() - if !reflect.DeepEqual(&settings, server.Settings) { - t.Errorf("Wrong settings set. Expected: %v, got: %v", settings, server.Settings) + // zero port means randomly (os) chosen port + l, err := net.Listen("tcp", ":0") //nolint:gosec + if err != nil { + return 0, err } - if server.stdLog.Writer() != os.Stdout { - t.Error("Wrong 'stdLog' writer set") - } + port := l.Addr().(*net.TCPAddr).Port - if server.stdLog.Flags() != log.Ldate|log.Lmicroseconds { - t.Error("Wrong 'stdLog' flags set") + if closingErr := l.Close(); closingErr != nil { + return 0, closingErr } - if server.errLog.Flags() != log.LstdFlags { - t.Error("Wrong 'errLog' flags set") - } + return port, nil +} - if server.Server.Addr != "1.2.3.4:321" { - t.Errorf("Wrong HTTP server addr set. Want [%s], got [%s]", "1.2.3.4:321", server.Server.Addr) - } +func checkTCPPortIsBusy(t *testing.T, port int) bool { + t.Helper() - if server.Server.WriteTimeout != 10*time.Second { - t.Error("Wrong server write timeout value is set") + l, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + return true } - if server.Server.ReadTimeout != 13*time.Second { - t.Error("Wrong server read timeout value is set") - } + _ = l.Close() + + return false } -func Test_registerCustomMimeTypes(t *testing.T) { - testSliceContainsString := func(t *testing.T, slice []string, expects string) { - t.Helper() +func TestServer_StartAndStop(t *testing.T) { + port, err := getRandomTCPPort(t) + assert.NoError(t, err) - for _, n := range slice { - if expects == n { - return - } - } + cacher := cache.NewInMemoryCache(time.Second, time.Second) + defer cacher.Close() - t.Errorf("Slice %v does not contains %s", slice, expects) - } + srv := NewServer(context.Background(), zap.NewNop(), cacher, ".", &config.Config{}, nil) - testSliceNotContainsString := func(t *testing.T, slice []string, expects string) { - t.Helper() + assert.False(t, checkTCPPortIsBusy(t, port)) + + go func() { + startingErr := srv.Start("", uint16(port)) + + if !errors.Is(startingErr, http.ErrServerClosed) { + assert.NoError(t, startingErr) + } + }() - for _, n := range slice { - if expects == n { - t.Errorf("Slice %v contains %s (but should not)", slice, expects) + for i := 0; ; i++ { + if !checkTCPPortIsBusy(t, port) { + if i > 100 { + t.Error("too many attempts for server start checking") } + + <-time.After(time.Microsecond * 10) + } else { + break } } - types, _ := mime.ExtensionsByType("text/html; charset=utf-8") - testSliceNotContainsString(t, types, ".vue") + assert.True(t, checkTCPPortIsBusy(t, port)) + assert.NoError(t, srv.Stop(context.Background())) + assert.False(t, checkTCPPortIsBusy(t, port)) +} - if err := NewServer(&ServerSettings{}, &serve.Settings{}).registerCustomMimeTypes(); err != nil { - t.Error(err) +func TestServer_Register(t *testing.T) { + var routes = []struct { + name string + route string + methods []string + }{ + {name: "script_generator", route: "/script/source", methods: []string{http.MethodGet}}, + {name: "api_get_settings", route: "/api/settings", methods: []string{http.MethodGet}}, + {name: "api_get_version", route: "/api/version", methods: []string{http.MethodGet}}, + {name: "ready", route: "/ready", methods: []string{http.MethodGet, http.MethodHead}}, + {name: "live", route: "/live", methods: []string{http.MethodGet, http.MethodHead}}, + {name: "static", route: "/", methods: []string{http.MethodGet, http.MethodHead}}, } - types, _ = mime.ExtensionsByType("text/html; charset=utf-8") - testSliceContainsString(t, types, ".vue") -} + cacher := cache.NewInMemoryCache(time.Second, time.Second) + defer cacher.Close() + + cfg := &config.Config{} + cfg.RouterScript.MaxSourcesCount = 1 + + srv := NewServer(context.Background(), zap.NewNop(), cacher, ".", cfg, nil) + router := srv.router // dirty hack, yes, i know + + // state *before* registration + types, err := mime.ExtensionsByType("text/html; charset=utf-8") + assert.NoError(t, err) + assert.NotContains(t, types, ".vue") // mime types registration can be executed only once + + for _, r := range routes { + assert.Nil(t, router.Get(r.name)) + } + + // call register fn + assert.NoError(t, srv.Register()) -func TestServer_Start(t *testing.T) { - t.Skip("Not implemented yet") + // state *after* registration + types, _ = mime.ExtensionsByType("text/html; charset=utf-8") // reload + assert.Contains(t, types, ".vue") + + for _, r := range routes { + route, _ := router.Get(r.name).GetPathTemplate() + assert.Equal(t, r.route, route) + methods, _ := router.Get(r.name).GetMethods() + assert.Equal(t, r.methods, methods) + } } -func TestServer_Stop(t *testing.T) { - t.Skip("Not implemented yet") +func TestServer_RegisterWithoutResourcesDir(t *testing.T) { + c := cache.NewInMemoryCache(time.Second, time.Second) + defer c.Close() + + cfg := &config.Config{} + cfg.RouterScript.MaxSourcesCount = 1 + + srv := NewServer(context.Background(), zap.NewNop(), c, "", cfg, nil) // empty resources dir + router := srv.router // dirty hack, yes, i know + + assert.Nil(t, router.Get("static")) + assert.NoError(t, srv.Register()) + assert.Nil(t, router.Get("static")) } diff --git a/internal/pkg/logger/logger.go b/internal/pkg/logger/logger.go new file mode 100644 index 00000000..b503de80 --- /dev/null +++ b/internal/pkg/logger/logger.go @@ -0,0 +1,38 @@ +// Package logger contains functions for a working with application logging. +package logger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// New creates new "zap" logger with little customization. +func New(verbose, debug, logJSON bool) (*zap.Logger, error) { + var config zap.Config + + if logJSON { + config = zap.NewProductionConfig() + } else { + config = zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder + config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05") + } + + // default configuration for all encoders + config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) + config.Development = false + config.DisableStacktrace = true + config.DisableCaller = true + + if debug { + config.Development = true + config.DisableStacktrace = false + config.DisableCaller = false + } + + if verbose || debug { + config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) + } + + return config.Build() +} diff --git a/internal/pkg/logger/logger_test.go b/internal/pkg/logger/logger_test.go new file mode 100644 index 00000000..d2046890 --- /dev/null +++ b/internal/pkg/logger/logger_test.go @@ -0,0 +1,79 @@ +package logger + +import ( + "regexp" + "strings" + "testing" + "time" + + "github.com/kami-zh/go-capturer" + "github.com/stretchr/testify/assert" +) + +func TestNewNotVerboseDebugJSON(t *testing.T) { + output := capturer.CaptureStderr(func() { + log, err := New(false, false, false) + assert.NoError(t, err) + + log.Info("inf msg") + log.Debug("dbg msg") + log.Error("err msg") + }) + + assert.Contains(t, output, time.Now().Format("15:04:05")) + assert.Regexp(t, `\t.+info.+\tinf msg`, output) + assert.NotContains(t, output, "dbg msg") + assert.Contains(t, output, "err msg") +} + +func TestNewVerboseNotDebugJSON(t *testing.T) { + output := capturer.CaptureStderr(func() { + log, err := New(true, false, false) + assert.NoError(t, err) + + log.Info("inf msg") + log.Debug("dbg msg") + log.Error("err msg") + }) + + assert.Contains(t, output, time.Now().Format("15:04:05")) + assert.Regexp(t, `\t.+info.+\tinf msg`, output) + assert.Contains(t, output, "dbg msg") + assert.Contains(t, output, "err msg") +} + +func TestNewVerboseDebugNotJSON(t *testing.T) { + output := capturer.CaptureStderr(func() { + log, err := New(true, true, false) + assert.NoError(t, err) + + log.Info("inf msg") + log.Debug("dbg msg") + log.Error("err msg") + }) + + assert.Contains(t, output, time.Now().Format("15:04:05")) + assert.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output) + assert.Contains(t, output, "dbg msg") + assert.Contains(t, output, "err msg") +} + +func TestNewNotVerboseDebugButJSON(t *testing.T) { + output := capturer.CaptureStderr(func() { + log, err := New(false, false, true) + assert.NoError(t, err) + + log.Info("inf msg") + log.Debug("dbg msg") + log.Error("err msg") + }) + + // replace timestamp field with fixed value + fakeTimestamp := regexp.MustCompile(`"ts":\d+\.\d+,`) + output = fakeTimestamp.ReplaceAllString(output, `"ts":0.1,`) + + lines := strings.Split(strings.Trim(output, "\n"), "\n") + + assert.JSONEq(t, `{"level":"info","ts":0.1,"msg":"inf msg"}`, lines[0]) + assert.JSONEq(t, `{"level":"error","ts":0.1,"msg":"err msg"}`, lines[1]) +} diff --git a/internal/pkg/settings/serve/settings.go b/internal/pkg/settings/serve/settings.go deleted file mode 100644 index 4b14ee74..00000000 --- a/internal/pkg/settings/serve/settings.go +++ /dev/null @@ -1,123 +0,0 @@ -package serve - -import ( - "fmt" - "io" - "io/ioutil" - "strings" - "text/tabwriter" - - "github.com/a8m/envsubst" - "gopkg.in/yaml.v2" -) - -type ( - Listen struct { - Address string `yaml:"address"` - Port int `yaml:"port"` - } - - Resources struct { - DirPath string `yaml:"dir"` - IndexName string `yaml:"index_name"` - Error404Name string `yaml:"error_404_name"` - } - - Source struct { - URI string `yaml:"uri"` - Name string `yaml:"name"` - Description string `yaml:"description"` - EnabledByDefault bool `yaml:"enabled"` - RecordsCount int `yaml:"count"` // approximate quantity - } - - Redirect struct { - Address string `yaml:"address"` - } - - Excludes struct { - Hosts []string `yaml:"hosts"` - } - - Cache struct { - File CacheFiles `yaml:"files"` - LifetimeSec int `yaml:"lifetime_sec"` - } - - CacheFiles struct { - DirPath string `yaml:"dir"` - } - - RouterScript struct { - Redirect Redirect `yaml:"redirect"` - Exclude Excludes `yaml:"exclude"` - Comment string `yaml:"comment"` - MaxSources int `yaml:"max_sources"` - MaxSourceSize int `yaml:"max_source_size"` // in bytes - } -) - -type Settings struct { - Listen Listen `yaml:"listen"` - Resources Resources `yaml:"resources"` - Sources []Source `yaml:"sources"` - Cache Cache `yaml:"cache"` - RouterScript RouterScript `yaml:"router_script"` -} - -// Creates new settings instance using YAML file. -func FromYamlFile(filename string, expandEnv bool) (*Settings, error) { - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - return FromYaml(bytes, expandEnv) -} - -// Creates new settings instance using YAML-structured content. -func FromYaml(in []byte, expandEnv bool) (*Settings, error) { - settings := &Settings{} - - if expandEnv { - parsed, err := envsubst.Bytes(in) - if err != nil { - return nil, err - } - in = parsed - } - - if err := yaml.Unmarshal(in, settings); err != nil { - return nil, err - } - - return settings, nil -} - -// PrintInfo about most important settings values into writer. -func (s *Settings) PrintInfo(out io.Writer) error { - w := tabwriter.NewWriter(out, 2, 8, 1, '\t', tabwriter.AlignRight) - defer func(w *tabwriter.Writer) { _ = w.Flush() }(w) - - lines := [][]interface{}{ - {"Listen address", s.Listen.Address}, - {"Listen port", s.Listen.Port}, - {"Resources dir", s.Resources.DirPath}, - {"Index file name", s.Resources.IndexName}, - {"Error 404 file name", s.Resources.Error404Name}, - {"Sources count", len(s.Sources)}, - {"Cache lifetime (sec)", s.Cache.LifetimeSec}, - {"Cache files directory", s.Cache.File.DirPath}, - {"Max sources count", s.RouterScript.MaxSources}, - {"Max source response size (bytes)", s.RouterScript.MaxSourceSize}, - } - - for _, line := range lines { - _, err := fmt.Fprintf(w, strings.Repeat("%v\t|\t", len(line))+"\n", line...) - if err != nil { - return err - } - } - - return nil -} diff --git a/internal/pkg/settings/serve/settings_test.go b/internal/pkg/settings/serve/settings_test.go deleted file mode 100644 index 0cf15e68..00000000 --- a/internal/pkg/settings/serve/settings_test.go +++ /dev/null @@ -1,340 +0,0 @@ -package serve - -import ( - "bytes" - "io/ioutil" - "os" - "reflect" - "strings" - "testing" -) - -func TestFromYaml(t *testing.T) { - var cases = []struct { - name string - giveYaml []byte - giveExpandEnv bool - wantSettings *Settings - }{ - { - name: "Using yaml part", - giveExpandEnv: true, - giveYaml: []byte(` -listen: - address: '1.2.3.4' - port: 321 -`), - wantSettings: &Settings{ - Listen: Listen{ - Address: "1.2.3.4", - Port: 321, - }, - }, - }, - { - name: "ENV variables expanding", - giveExpandEnv: true, - giveYaml: []byte(` -listen: - address: ${__TEST_IP_ADDR} - port: ${__TEST_PORT_NUM} -`), - wantSettings: &Settings{ - Listen: Listen{ - Address: "8.7.8.7", - Port: 4567, - }, - }, - }, - { - name: "ENV variables not expanding", - giveExpandEnv: false, - giveYaml: []byte(` -listen: - address: ${__TEST_IP_ADDR} - port: 0 -`), - wantSettings: &Settings{ - Listen: Listen{ - Address: "${__TEST_IP_ADDR}", - Port: 0, - }, - }, - }, - { - name: "Default env values is set", - giveExpandEnv: true, - giveYaml: []byte(` -listen: - address: ${__NON_EXISTING_VALUE_FOR_ADDR__:-3.4.5.6} - port: ${__NON_EXISTING_VALUE_FOR_PORT__:-1234} -`), - wantSettings: &Settings{ - Listen: Listen{ - Address: "3.4.5.6", - Port: 1234, - }, - }, - }, - { - name: "With all possible values", - giveExpandEnv: true, - giveYaml: []byte(` -# Some comment - -listen: - address: '1.2.3.4' - port: 321 - -resources: - dir: /tmp - index_name: idx.html - error_404_name: err404.asp - -sources: - - uri: http://goo.gl/hosts.txt - name: Foo name - description: Foo desc - enabled: true - count: 123 - - uri: http://example.com/txt.stsoh # inline comment - name: Bar name - description: Bar desc - enabled: false - count: 321 - - uri: http://goo.gl/txt.stsoh - count: -2 - -cache: - files: - dir: /foo/bar - lifetime_sec: 10 - -router_script: - redirect: - address: 0.1.1.0 - exclude: - hosts: - - "foo" - - bar - comment: " [ blah ] " - max_sources: 1 - max_source_size: -4 -`), - wantSettings: &Settings{ - Listen: Listen{ - Address: "1.2.3.4", - Port: 321, - }, - Resources: Resources{ - DirPath: "/tmp", - IndexName: "idx.html", - Error404Name: "err404.asp", - }, - Sources: []Source{{ - URI: "http://goo.gl/hosts.txt", - Name: "Foo name", - Description: "Foo desc", - EnabledByDefault: true, - RecordsCount: 123, - }, { - URI: "http://example.com/txt.stsoh", - Name: "Bar name", - Description: "Bar desc", - EnabledByDefault: false, - RecordsCount: 321, - }, { - URI: "http://goo.gl/txt.stsoh", - RecordsCount: -2, - }}, - Cache: Cache{ - File: CacheFiles{ - DirPath: "/foo/bar", - }, - LifetimeSec: 10, - }, - RouterScript: RouterScript{ - Redirect: Redirect{ - Address: "0.1.1.0", - }, - Exclude: Excludes{ - Hosts: []string{"foo", "bar"}, - }, - MaxSources: 1, - MaxSourceSize: -4, - Comment: " [ blah ] ", - }, - }, - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - _ = os.Setenv("__TEST_IP_ADDR", "8.7.8.7") - _ = os.Setenv("__TEST_PORT_NUM", "4567") - - settings, err := FromYaml(tt.giveYaml, tt.giveExpandEnv) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(settings, tt.wantSettings) { - t.Errorf( - `Wrong yaml (as a string) decoding result. Want: %+v, got: %+v`, - tt.wantSettings, - settings, - ) - } - - _ = os.Unsetenv("__TEST_IP_ADDR") - _ = os.Unsetenv("__TEST_PORT_NUM") - }) - } -} - -func TestFromYamlFile(t *testing.T) { - var cases = []struct { - name string - giveYaml []byte - giveExpandEnv bool - wantError bool - wantSettings *Settings - }{ - { - name: "Using correct yaml", - giveExpandEnv: true, - giveYaml: []byte(` -listen: - address: '1.2.3.4' - port: 321 -`), - wantSettings: &Settings{ - Listen: Listen{ - Address: "1.2.3.4", - Port: 321, - }, - }, - }, - { - name: "Using broken file (wrong format)", - giveExpandEnv: true, - giveYaml: []byte(`!foo bar`), - wantError: true, - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - file, _ := ioutil.TempFile("", "unit-test-") - defer func() { - if err := file.Close(); err != nil { - panic(err) - } - if err := os.Remove(file.Name()); err != nil { - panic(err) - } - }() - - if _, err := file.Write(tt.giveYaml); err != nil { - t.Fatal(err) - } - settings, err := FromYamlFile(file.Name(), tt.giveExpandEnv) - - if tt.wantError { - if err == nil { - t.Error(`Expected error not returned`) - } - } else { - if err != nil { - t.Fatal(err) - } - - if tt.wantSettings != nil && !reflect.DeepEqual(settings, tt.wantSettings) { - t.Errorf( - `Wrong yaml (as a string) decoding result. Want: %+v, got: %+v`, - tt.wantSettings, - settings, - ) - } - } - }) - } -} - -func TestSettings_PrintInfo(t *testing.T) { - var cases = []struct { - name string - giveSettings *Settings - wantEntries []string - wantLinesCount byte - }{ - { - name: "Regular use-case", - giveSettings: &Settings{ - Listen: Listen{ - Address: "1.2.3.4", - Port: 112233, - }, - Resources: Resources{ - DirPath: "FooDirPath", - IndexName: "FooIndexName", - Error404Name: "FooError404Name", - }, - Sources: []Source{ - {URI: "source URI", Name: "source name", Description: "source desc", EnabledByDefault: true, RecordsCount: 123}, - }, - Cache: Cache{ - File: CacheFiles{ - DirPath: "/tmp/foo/bar", - }, - LifetimeSec: 321, - }, - RouterScript: RouterScript{ - Redirect: Redirect{ - Address: "", - }, - Exclude: Excludes{ - Hosts: []string{}, - }, - Comment: "", - MaxSources: 222, - MaxSourceSize: 333, - }, - }, - wantEntries: []string{ - "Listen address", "1.2.3.4", - "Listen port", "112233", - "Resources dir", "FooDirPath", - "Index file name", "FooIndexName", - "Error 404 file name", "FooError404Name", - "Sources count", "1", - "Cache lifetime (sec)", "321", - "Cache files directory", "/tmp/foo/bar", - "Max sources count", "222", - "Max source response size (bytes)", "333", - }, - wantLinesCount: 10, - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - var b bytes.Buffer - - err := tt.giveSettings.PrintInfo(&b) - - if err != nil { - t.Fatalf("Got an error: %v", err) - } - - if linesCount := strings.Count(b.String(), "\n"); byte(linesCount) != tt.wantLinesCount { - t.Errorf("Want lines count %d, got: %d", tt.wantLinesCount, linesCount) - } - - for _, line := range tt.wantEntries { - if !strings.Contains(b.String(), line) { - t.Errorf("Result [%s] does not contains required substring: [%s]", b.String(), line) - } - } - }) - } -} diff --git a/internal/pkg/version/version.go b/internal/pkg/version/version.go index 0966ecc1..449123f4 100644 --- a/internal/pkg/version/version.go +++ b/internal/pkg/version/version.go @@ -1,9 +1,12 @@ +// Package version is used as a place, where application version defined. package version -// version value will be set during compilation -var version string = "undefined@undefined" +import "strings" -// Version returns version value. +// version value will be set during compilation. +var version = "v0.0.0@undefined" + +// Version returns version value (without `v` prefix). func Version() string { - return version + return strings.TrimLeft(version, "vV ") } diff --git a/internal/pkg/version/version_test.go b/internal/pkg/version/version_test.go index fc5cddd9..dcad7caf 100644 --- a/internal/pkg/version/version_test.go +++ b/internal/pkg/version/version_test.go @@ -3,7 +3,7 @@ package version import "testing" func TestVersion(t *testing.T) { - if value := Version(); value != "undefined@undefined" { + if value := Version(); value != "0.0.0@undefined" { t.Errorf("Unexpected default version value: %s", value) } } diff --git a/pkg/hostsfile/docs.go b/pkg/hostsfile/docs.go new file mode 100644 index 00000000..ad4f39a8 --- /dev/null +++ b/pkg/hostsfile/docs.go @@ -0,0 +1,2 @@ +// Package hostsfile contains basic functions for the hosts file working. +package hostsfile diff --git a/pkg/hostsfile/hostname_validator.go b/pkg/hostsfile/hostname_validator.go new file mode 100644 index 00000000..59329bc7 --- /dev/null +++ b/pkg/hostsfile/hostname_validator.go @@ -0,0 +1,9799 @@ +// Code generated by re2dfa (https://gitlab.com/opennota/re2dfa). DO NOT EDIT + +package hostsfile + +import "unicode/utf8" + +func validateHostname(s []byte) (end int) { + end = -1 + var r rune + var rlen int + i := 0 + _, _, _ = r, rlen, i + switch { + case i == 0: + goto s2 + } + return +s2: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s427 + case r == 45: + goto s3 + case r >= 48 && r <= 57 || r >= 65 && r <= 87 || r >= 89 && r <= 90 || r >= 97 && r <= 119 || r >= 121 && r <= 122 || r == 383 || r == 8490: + goto s67 + case r == 88 || r == 120: + goto s457 + } + return +s3: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 87 || r >= 89 && r <= 90 || r >= 97 && r <= 119 || r >= 121 && r <= 122 || r == 383 || r == 8490: + goto s4 + case r == 46: + goto s158 + case r == 88 || r == 120: + goto s459 + case r == 95: + goto s544 + } + return +s4: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s5 + case r == 46: + goto s158 + case r == 95: + goto s429 + } + return +s5: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s6 + case r == 46: + goto s158 + case r == 95: + goto s430 + } + return +s6: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s7 + case r == 46: + goto s158 + case r == 95: + goto s431 + } + return +s7: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s8 + case r == 46: + goto s158 + case r == 95: + goto s432 + } + return +s8: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s9 + case r == 46: + goto s158 + case r == 95: + goto s433 + } + return +s9: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s434 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s10 + case r == 46: + goto s158 + } + return +s10: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s11 + case r == 46: + goto s158 + case r == 95: + goto s435 + } + return +s11: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s12 + case r == 46: + goto s158 + case r == 95: + goto s436 + } + return +s12: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s13 + case r == 46: + goto s158 + case r == 95: + goto s437 + } + return +s13: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s14 + case r == 46: + goto s158 + case r == 95: + goto s438 + } + return +s14: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s15 + case r == 46: + goto s158 + case r == 95: + goto s439 + } + return +s15: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s16 + case r == 46: + goto s158 + case r == 95: + goto s440 + } + return +s16: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s17 + case r == 46: + goto s158 + case r == 95: + goto s441 + } + return +s17: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s18 + case r == 46: + goto s158 + case r == 95: + goto s442 + } + return +s18: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s19 + case r == 46: + goto s158 + case r == 95: + goto s443 + } + return +s19: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s20 + case r == 46: + goto s158 + case r == 95: + goto s444 + } + return +s20: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s445 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s21 + } + return +s21: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s22 + case r == 46: + goto s158 + case r == 95: + goto s446 + } + return +s22: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s23 + case r == 46: + goto s158 + case r == 95: + goto s447 + } + return +s23: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s24 + case r == 46: + goto s158 + case r == 95: + goto s448 + } + return +s24: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s25 + case r == 46: + goto s158 + case r == 95: + goto s449 + } + return +s25: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s450 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s26 + } + return +s26: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s27 + case r == 46: + goto s158 + case r == 95: + goto s451 + } + return +s27: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s28 + case r == 46: + goto s158 + case r == 95: + goto s452 + } + return +s28: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s29 + case r == 46: + goto s158 + case r == 95: + goto s453 + } + return +s29: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s30 + case r == 46: + goto s158 + case r == 95: + goto s454 + } + return +s30: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s455 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s31 + } + return +s31: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s456 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s32 + } + return +s32: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s33 + case r == 46: + goto s158 + } + return +s33: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s34 + case r == 46: + goto s66 + } + return +s34: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s35 + case r == 46: + goto s66 + } + return +s35: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s36 + case r == 46: + goto s66 + } + return +s36: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s37 + case r == 46: + goto s66 + } + return +s37: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s38 + case r == 46: + goto s66 + } + return +s38: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s39 + case r == 46: + goto s66 + } + return +s39: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s40 + case r == 46: + goto s66 + } + return +s40: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s41 + } + return +s41: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s42 + case r == 46: + goto s66 + } + return +s42: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s43 + } + return +s43: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s44 + case r == 46: + goto s66 + } + return +s44: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s45 + case r == 46: + goto s66 + } + return +s45: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s46 + case r == 46: + goto s66 + } + return +s46: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s47 + case r == 46: + goto s66 + } + return +s47: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s48 + case r == 46: + goto s66 + } + return +s48: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s49 + case r == 46: + goto s66 + } + return +s49: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s50 + case r == 46: + goto s66 + } + return +s50: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s51 + case r == 46: + goto s66 + } + return +s51: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s52 + case r == 46: + goto s66 + } + return +s52: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s53 + case r == 46: + goto s66 + } + return +s53: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s54 + case r == 46: + goto s66 + } + return +s54: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s55 + case r == 46: + goto s66 + } + return +s55: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s56 + case r == 46: + goto s66 + } + return +s56: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s57 + case r == 46: + goto s66 + } + return +s57: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s58 + case r == 46: + goto s66 + } + return +s58: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s59 + case r == 46: + goto s66 + } + return +s59: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s60 + } + return +s60: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s61 + case r == 46: + goto s66 + } + return +s61: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s62 + case r == 46: + goto s66 + } + return +s62: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s63 + case r == 46: + goto s66 + } + return +s63: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s64 + case r == 46: + goto s66 + } + return +s64: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s65 + case r == 46: + goto s66 + } + return +s65: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + } + return +s66: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45: + goto s3 + case r >= 48 && r <= 57 || r >= 65 && r <= 87 || r >= 89 && r <= 90 || r >= 97 && r <= 119 || r >= 121 && r <= 122 || r == 383 || r == 8490: + goto s67 + case r == 88 || r == 120: + goto s457 + case r == 95: + goto s427 + } + return +s67: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s251 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s69 + } + return +s69: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s70 + case r == 46: + goto s158 + case r == 95: + goto s250 + } + return +s70: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s71 + case r == 46: + goto s158 + case r == 95: + goto s249 + } + return +s71: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s72 + case r == 46: + goto s158 + case r == 95: + goto s248 + } + return +s72: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s73 + case r == 46: + goto s158 + case r == 95: + goto s247 + } + return +s73: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s74 + case r == 46: + goto s158 + case r == 95: + goto s246 + } + return +s74: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s75 + case r == 46: + goto s158 + case r == 95: + goto s245 + } + return +s75: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s76 + case r == 46: + goto s158 + case r == 95: + goto s244 + } + return +s76: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s77 + case r == 46: + goto s158 + case r == 95: + goto s243 + } + return +s77: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s78 + case r == 46: + goto s158 + case r == 95: + goto s242 + } + return +s78: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s79 + case r == 46: + goto s158 + case r == 95: + goto s241 + } + return +s79: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s240 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s80 + case r == 46: + goto s158 + } + return +s80: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s81 + case r == 46: + goto s158 + case r == 95: + goto s239 + } + return +s81: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s82 + case r == 46: + goto s158 + case r == 95: + goto s238 + } + return +s82: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s83 + case r == 46: + goto s158 + case r == 95: + goto s237 + } + return +s83: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s236 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s84 + case r == 46: + goto s158 + } + return +s84: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s235 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s85 + case r == 46: + goto s158 + } + return +s85: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s86 + case r == 46: + goto s158 + case r == 95: + goto s234 + } + return +s86: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s87 + case r == 46: + goto s158 + case r == 95: + goto s233 + } + return +s87: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s88 + case r == 46: + goto s158 + case r == 95: + goto s232 + } + return +s88: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s231 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s89 + case r == 46: + goto s158 + } + return +s89: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s90 + case r == 46: + goto s158 + case r == 95: + goto s230 + } + return +s90: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s91 + case r == 46: + goto s158 + case r == 95: + goto s229 + } + return +s91: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s92 + case r == 46: + goto s158 + case r == 95: + goto s228 + } + return +s92: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s93 + case r == 46: + goto s158 + case r == 95: + goto s227 + } + return +s93: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s94 + case r == 46: + goto s158 + case r == 95: + goto s226 + } + return +s94: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s95 + case r == 46: + goto s158 + case r == 95: + goto s225 + } + return +s95: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s96 + case r == 46: + goto s158 + case r == 95: + goto s224 + } + return +s96: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s97 + case r == 46: + goto s158 + case r == 95: + goto s223 + } + return +s97: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s98 + case r == 46: + goto s158 + case r == 95: + goto s222 + } + return +s98: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s99 + case r == 46: + goto s66 + case r == 95: + goto s157 + } + return +s99: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s100 + case r == 46: + goto s66 + case r == 95: + goto s156 + } + return +s100: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s101 + case r == 46: + goto s66 + case r == 95: + goto s155 + } + return +s101: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s154 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s102 + case r == 46: + goto s66 + } + return +s102: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s103 + case r == 46: + goto s66 + case r == 95: + goto s153 + } + return +s103: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s104 + case r == 46: + goto s66 + case r == 95: + goto s152 + } + return +s104: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s151 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s105 + case r == 46: + goto s66 + } + return +s105: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s106 + case r == 46: + goto s66 + case r == 95: + goto s150 + } + return +s106: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s107 + case r == 46: + goto s66 + case r == 95: + goto s149 + } + return +s107: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s108 + case r == 46: + goto s66 + case r == 95: + goto s148 + } + return +s108: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s109 + case r == 46: + goto s66 + case r == 95: + goto s147 + } + return +s109: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s110 + case r == 46: + goto s66 + case r == 95: + goto s146 + } + return +s110: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s111 + case r == 46: + goto s66 + case r == 95: + goto s145 + } + return +s111: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s144 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s112 + case r == 46: + goto s66 + } + return +s112: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s113 + case r == 46: + goto s66 + case r == 95: + goto s143 + } + return +s113: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s142 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s114 + case r == 46: + goto s66 + } + return +s114: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s141 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s115 + case r == 46: + goto s66 + } + return +s115: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s116 + case r == 46: + goto s66 + case r == 95: + goto s140 + } + return +s116: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s117 + case r == 46: + goto s66 + case r == 95: + goto s139 + } + return +s117: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s118 + case r == 46: + goto s66 + case r == 95: + goto s138 + } + return +s118: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s119 + case r == 46: + goto s66 + case r == 95: + goto s137 + } + return +s119: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s120 + case r == 46: + goto s66 + case r == 95: + goto s136 + } + return +s120: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s121 + case r == 46: + goto s66 + case r == 95: + goto s135 + } + return +s121: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s122 + case r == 46: + goto s66 + case r == 95: + goto s134 + } + return +s122: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s133 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s123 + } + return +s123: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s124 + case r == 46: + goto s66 + case r == 95: + goto s132 + } + return +s124: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s125 + case r == 46: + goto s66 + case r == 95: + goto s131 + } + return +s125: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s126 + case r == 46: + goto s66 + case r == 95: + goto s130 + } + return +s126: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s127 + case r == 46: + goto s66 + case r == 95: + goto s129 + } + return +s127: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s128 + case r == 46: + goto s66 + case r == 95: + goto s64 + } + return +s128: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s65 + case r == 46: + goto s66 + } + return +s129: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s64 + } + return +s130: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s129 + case r == 46: + goto s66 + } + return +s131: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s130 + case r == 46: + goto s66 + } + return +s132: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s131 + case r == 46: + goto s66 + } + return +s133: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s132 + case r == 46: + goto s66 + } + return +s134: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s133 + } + return +s135: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s134 + case r == 46: + goto s66 + } + return +s136: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s135 + case r == 46: + goto s66 + } + return +s137: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s136 + case r == 46: + goto s66 + } + return +s138: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s137 + case r == 46: + goto s66 + } + return +s139: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s138 + case r == 46: + goto s66 + } + return +s140: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s139 + case r == 46: + goto s66 + } + return +s141: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s140 + case r == 46: + goto s66 + } + return +s142: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s141 + case r == 46: + goto s66 + } + return +s143: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s142 + case r == 46: + goto s66 + } + return +s144: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s143 + case r == 46: + goto s66 + } + return +s145: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s144 + case r == 46: + goto s66 + } + return +s146: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s145 + case r == 46: + goto s66 + } + return +s147: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s146 + case r == 46: + goto s66 + } + return +s148: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s147 + case r == 46: + goto s66 + } + return +s149: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s148 + case r == 46: + goto s66 + } + return +s150: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s149 + case r == 46: + goto s66 + } + return +s151: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s150 + } + return +s152: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s151 + case r == 46: + goto s66 + } + return +s153: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s152 + case r == 46: + goto s66 + } + return +s154: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s153 + } + return +s155: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s154 + case r == 46: + goto s66 + } + return +s156: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s155 + case r == 46: + goto s66 + } + return +s157: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s156 + case r == 46: + goto s66 + } + return +s158: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 87 || r >= 89 && r <= 90 || r >= 97 && r <= 119 || r >= 121 && r <= 122 || r == 383 || r == 8490: + goto s159 + case r == 88 || r == 120: + goto s252 + case r == 95: + goto s427 + case r == 45: + goto s3 + case r >= 48 && r <= 57: + goto s67 + } + return +s159: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s69 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s160 + case r == 95: + goto s251 + } + return +s160: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s250 + case r == 45 || r >= 48 && r <= 57: + goto s70 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s161 + } + return +s161: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s71 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s162 + case r == 95: + goto s249 + } + return +s162: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s248 + case r == 45 || r >= 48 && r <= 57: + goto s72 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s163 + } + return +s163: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s247 + case r == 45 || r >= 48 && r <= 57: + goto s73 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s164 + } + return +s164: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s246 + case r == 45 || r >= 48 && r <= 57: + goto s74 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s165 + } + return +s165: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s166 + case r == 95: + goto s245 + case r == 45 || r >= 48 && r <= 57: + goto s75 + } + return +s166: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s167 + case r == 95: + goto s244 + case r == 45 || r >= 48 && r <= 57: + goto s76 + case r == 46: + goto s158 + } + return +s167: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s77 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s168 + case r == 95: + goto s243 + } + return +s168: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s78 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s169 + case r == 95: + goto s242 + } + return +s169: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s170 + case r == 95: + goto s241 + case r == 45 || r >= 48 && r <= 57: + goto s79 + case r == 46: + goto s158 + } + return +s170: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s171 + case r == 95: + goto s240 + case r == 45 || r >= 48 && r <= 57: + goto s80 + } + return +s171: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s81 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s172 + case r == 95: + goto s239 + } + return +s172: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s82 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s173 + case r == 95: + goto s238 + } + return +s173: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s83 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s174 + case r == 95: + goto s237 + } + return +s174: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s236 + case r == 45 || r >= 48 && r <= 57: + goto s84 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s175 + } + return +s175: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s85 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s176 + case r == 95: + goto s235 + } + return +s176: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s177 + case r == 95: + goto s234 + case r == 45 || r >= 48 && r <= 57: + goto s86 + case r == 46: + goto s158 + } + return +s177: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s87 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s178 + case r == 95: + goto s233 + } + return +s178: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s88 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s179 + case r == 95: + goto s232 + } + return +s179: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s89 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s180 + case r == 95: + goto s231 + } + return +s180: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s181 + case r == 95: + goto s230 + case r == 45 || r >= 48 && r <= 57: + goto s90 + case r == 46: + goto s158 + } + return +s181: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s91 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s182 + case r == 95: + goto s229 + } + return +s182: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s92 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s183 + case r == 95: + goto s228 + } + return +s183: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s93 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s184 + case r == 95: + goto s227 + } + return +s184: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s226 + case r == 45 || r >= 48 && r <= 57: + goto s94 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s185 + } + return +s185: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s95 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s186 + case r == 95: + goto s225 + } + return +s186: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s224 + case r == 45 || r >= 48 && r <= 57: + goto s96 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s187 + } + return +s187: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s97 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s188 + case r == 95: + goto s223 + } + return +s188: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s98 + case r == 46: + goto s158 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s189 + case r == 95: + goto s222 + } + return +s189: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s190 + case r == 95: + goto s157 + case r == 45 || r >= 48 && r <= 57: + goto s99 + } + return +s190: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s100 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s191 + case r == 95: + goto s156 + } + return +s191: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s101 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s192 + case r == 95: + goto s155 + } + return +s192: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s102 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s193 + case r == 95: + goto s154 + } + return +s193: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s194 + case r == 95: + goto s153 + case r == 45 || r >= 48 && r <= 57: + goto s103 + } + return +s194: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s152 + case r == 45 || r >= 48 && r <= 57: + goto s104 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s195 + } + return +s195: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s196 + case r == 95: + goto s151 + case r == 45 || r >= 48 && r <= 57: + goto s105 + } + return +s196: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s150 + case r == 45 || r >= 48 && r <= 57: + goto s106 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s197 + } + return +s197: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s107 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s198 + case r == 95: + goto s149 + } + return +s198: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s108 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s199 + case r == 95: + goto s148 + } + return +s199: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s109 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s200 + case r == 95: + goto s147 + } + return +s200: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s201 + case r == 95: + goto s146 + case r == 45 || r >= 48 && r <= 57: + goto s110 + } + return +s201: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s111 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s202 + case r == 95: + goto s145 + } + return +s202: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s112 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s203 + case r == 95: + goto s144 + } + return +s203: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s113 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s204 + case r == 95: + goto s143 + } + return +s204: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s142 + case r == 45 || r >= 48 && r <= 57: + goto s114 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s205 + } + return +s205: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s115 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s206 + case r == 95: + goto s141 + } + return +s206: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s140 + case r == 45 || r >= 48 && r <= 57: + goto s116 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s207 + } + return +s207: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s117 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s208 + case r == 95: + goto s139 + } + return +s208: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s209 + case r == 95: + goto s138 + case r == 45 || r >= 48 && r <= 57: + goto s118 + case r == 46: + goto s66 + } + return +s209: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s119 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s210 + case r == 95: + goto s137 + } + return +s210: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s120 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s211 + case r == 95: + goto s136 + } + return +s211: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s121 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s212 + case r == 95: + goto s135 + } + return +s212: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s213 + case r == 95: + goto s134 + case r == 45 || r >= 48 && r <= 57: + goto s122 + } + return +s213: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s123 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s214 + case r == 95: + goto s133 + } + return +s214: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s215 + case r == 95: + goto s132 + case r == 45 || r >= 48 && r <= 57: + goto s124 + case r == 46: + goto s66 + } + return +s215: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s125 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s216 + case r == 95: + goto s131 + } + return +s216: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s126 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s217 + case r == 95: + goto s130 + } + return +s217: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s129 + case r == 45 || r >= 48 && r <= 57: + goto s127 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s218 + } + return +s218: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s128 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s219 + case r == 95: + goto s64 + } + return +s219: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r == 95: + goto s65 + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s220 + } + return +s220: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s221 + } + return +s221: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s221 + } + return +s222: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s157 + case r == 46: + goto s66 + } + return +s223: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s222 + case r == 46: + goto s66 + } + return +s224: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s223 + case r == 46: + goto s66 + } + return +s225: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s224 + } + return +s226: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s225 + } + return +s227: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s226 + case r == 46: + goto s66 + } + return +s228: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s227 + case r == 46: + goto s66 + } + return +s229: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s228 + } + return +s230: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s229 + case r == 46: + goto s66 + } + return +s231: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s230 + case r == 46: + goto s66 + } + return +s232: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s231 + case r == 46: + goto s66 + } + return +s233: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s232 + case r == 46: + goto s66 + } + return +s234: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s233 + case r == 46: + goto s66 + } + return +s235: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s234 + case r == 46: + goto s66 + } + return +s236: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s235 + case r == 46: + goto s66 + } + return +s237: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s236 + case r == 46: + goto s66 + } + return +s238: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s237 + } + return +s239: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s238 + } + return +s240: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s239 + case r == 46: + goto s66 + } + return +s241: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s240 + case r == 46: + goto s66 + } + return +s242: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s241 + } + return +s243: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s242 + case r == 46: + goto s66 + } + return +s244: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s243 + case r == 46: + goto s66 + } + return +s245: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s244 + } + return +s246: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s245 + case r == 46: + goto s66 + } + return +s247: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s246 + case r == 46: + goto s66 + } + return +s248: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s247 + case r == 46: + goto s66 + } + return +s249: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s248 + case r == 46: + goto s66 + } + return +s250: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s249 + case r == 46: + goto s66 + } + return +s251: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s250 + case r == 46: + goto s66 + } + return +s252: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57: + goto s69 + case r == 46: + goto s158 + case r >= 65 && r <= 77 || r >= 79 && r <= 90 || r >= 97 && r <= 109 || r >= 111 && r <= 122 || r == 383 || r == 8490: + goto s160 + case r == 78 || r == 110: + goto s253 + case r == 95: + goto s251 + } + return +s253: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s161 + case r == 95: + goto s250 + case r == 45: + goto s254 + case r == 46: + goto s158 + case r >= 48 && r <= 57: + goto s70 + } + return +s254: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45: + goto s255 + case r == 46: + goto s158 + case r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s71 + case r == 95: + goto s249 + } + return +s255: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45: + goto s256 + case r == 46: + goto s158 + case r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s369 + case r == 95: + goto s426 + } + return +s256: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s257 + case r == 46: + goto s158 + case r == 95: + goto s368 + } + return +s257: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s367 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s258 + } + return +s258: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s259 + case r == 46: + goto s158 + case r == 95: + goto s366 + } + return +s259: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s260 + case r == 46: + goto s158 + case r == 95: + goto s365 + } + return +s260: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s261 + case r == 46: + goto s158 + case r == 95: + goto s364 + } + return +s261: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s262 + case r == 46: + goto s158 + case r == 95: + goto s363 + } + return +s262: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s263 + case r == 46: + goto s158 + case r == 95: + goto s362 + } + return +s263: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s264 + case r == 46: + goto s158 + case r == 95: + goto s361 + } + return +s264: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s265 + case r == 46: + goto s158 + case r == 95: + goto s360 + } + return +s265: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s266 + case r == 46: + goto s158 + case r == 95: + goto s359 + } + return +s266: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s267 + case r == 46: + goto s158 + case r == 95: + goto s358 + } + return +s267: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s268 + case r == 46: + goto s158 + case r == 95: + goto s357 + } + return +s268: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s269 + case r == 46: + goto s158 + case r == 95: + goto s356 + } + return +s269: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s270 + case r == 46: + goto s158 + case r == 95: + goto s355 + } + return +s270: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s271 + case r == 46: + goto s158 + case r == 95: + goto s354 + } + return +s271: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s353 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s272 + } + return +s272: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s273 + case r == 46: + goto s158 + case r == 95: + goto s352 + } + return +s273: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s351 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s274 + } + return +s274: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s275 + case r == 46: + goto s158 + case r == 95: + goto s350 + } + return +s275: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s276 + case r == 46: + goto s158 + case r == 95: + goto s349 + } + return +s276: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s277 + case r == 46: + goto s158 + case r == 95: + goto s348 + } + return +s277: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s347 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s278 + } + return +s278: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s279 + case r == 46: + goto s158 + case r == 95: + goto s346 + } + return +s279: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s280 + case r == 46: + goto s158 + case r == 95: + goto s345 + } + return +s280: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s281 + case r == 46: + goto s158 + case r == 95: + goto s344 + } + return +s281: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s282 + case r == 46: + goto s158 + case r == 95: + goto s343 + } + return +s282: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s283 + case r == 46: + goto s158 + case r == 95: + goto s342 + } + return +s283: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s341 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s284 + case r == 46: + goto s158 + } + return +s284: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s340 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s285 + } + return +s285: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s286 + case r == 46: + goto s158 + case r == 95: + goto s339 + } + return +s286: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s338 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s287 + case r == 46: + goto s66 + } + return +s287: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s288 + case r == 46: + goto s66 + case r == 95: + goto s337 + } + return +s288: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s336 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s289 + case r == 46: + goto s66 + } + return +s289: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s335 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s290 + } + return +s290: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s291 + case r == 46: + goto s66 + case r == 95: + goto s334 + } + return +s291: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s292 + case r == 46: + goto s66 + case r == 95: + goto s333 + } + return +s292: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s293 + case r == 46: + goto s66 + case r == 95: + goto s332 + } + return +s293: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s331 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s294 + case r == 46: + goto s66 + } + return +s294: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s295 + case r == 46: + goto s66 + case r == 95: + goto s330 + } + return +s295: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s296 + case r == 46: + goto s66 + case r == 95: + goto s329 + } + return +s296: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s297 + case r == 46: + goto s66 + case r == 95: + goto s328 + } + return +s297: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s327 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s298 + case r == 46: + goto s66 + } + return +s298: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s326 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s299 + case r == 46: + goto s66 + } + return +s299: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s300 + case r == 46: + goto s66 + case r == 95: + goto s325 + } + return +s300: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s324 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s301 + case r == 46: + goto s66 + } + return +s301: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s302 + case r == 46: + goto s66 + case r == 95: + goto s323 + } + return +s302: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s303 + case r == 46: + goto s66 + case r == 95: + goto s322 + } + return +s303: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s304 + case r == 46: + goto s66 + case r == 95: + goto s321 + } + return +s304: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s305 + case r == 46: + goto s66 + case r == 95: + goto s320 + } + return +s305: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s319 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s306 + case r == 46: + goto s66 + } + return +s306: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s318 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s307 + } + return +s307: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s317 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s308 + } + return +s308: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s316 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s309 + } + return +s309: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s310 + case r == 46: + goto s66 + case r == 95: + goto s315 + } + return +s310: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s311 + case r == 46: + goto s66 + case r == 95: + goto s314 + } + return +s311: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s312 + case r == 46: + goto s66 + case r == 95: + goto s313 + } + return +s312: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s131 + case r == 46: + goto s66 + } + return +s313: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s131 + } + return +s314: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s313 + } + return +s315: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s314 + case r == 46: + goto s66 + } + return +s316: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s315 + } + return +s317: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s316 + case r == 46: + goto s66 + } + return +s318: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s317 + case r == 46: + goto s66 + } + return +s319: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s318 + } + return +s320: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s319 + case r == 46: + goto s66 + } + return +s321: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s320 + } + return +s322: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s321 + case r == 46: + goto s66 + } + return +s323: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s322 + case r == 46: + goto s66 + } + return +s324: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s323 + case r == 46: + goto s66 + } + return +s325: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s324 + case r == 46: + goto s66 + } + return +s326: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s325 + case r == 46: + goto s66 + } + return +s327: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s326 + case r == 46: + goto s66 + } + return +s328: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s327 + case r == 46: + goto s66 + } + return +s329: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s328 + case r == 46: + goto s66 + } + return +s330: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s329 + case r == 46: + goto s66 + } + return +s331: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s330 + case r == 46: + goto s66 + } + return +s332: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s331 + case r == 46: + goto s66 + } + return +s333: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s332 + } + return +s334: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s333 + case r == 46: + goto s66 + } + return +s335: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s334 + case r == 46: + goto s66 + } + return +s336: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s335 + case r == 46: + goto s66 + } + return +s337: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s336 + case r == 46: + goto s66 + } + return +s338: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s337 + } + return +s339: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s338 + case r == 46: + goto s66 + } + return +s340: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s339 + case r == 46: + goto s66 + } + return +s341: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s340 + case r == 46: + goto s66 + } + return +s342: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s341 + case r == 46: + goto s66 + } + return +s343: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s342 + case r == 46: + goto s66 + } + return +s344: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s343 + case r == 46: + goto s66 + } + return +s345: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s344 + } + return +s346: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s345 + case r == 46: + goto s66 + } + return +s347: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s346 + case r == 46: + goto s66 + } + return +s348: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s347 + case r == 46: + goto s66 + } + return +s349: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s348 + } + return +s350: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s349 + case r == 46: + goto s66 + } + return +s351: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s350 + case r == 46: + goto s66 + } + return +s352: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s351 + case r == 46: + goto s66 + } + return +s353: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s352 + case r == 46: + goto s66 + } + return +s354: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s353 + case r == 46: + goto s66 + } + return +s355: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s354 + case r == 46: + goto s66 + } + return +s356: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s355 + case r == 46: + goto s66 + } + return +s357: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s356 + case r == 46: + goto s66 + } + return +s358: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s357 + case r == 46: + goto s66 + } + return +s359: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s358 + case r == 46: + goto s66 + } + return +s360: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s359 + case r == 46: + goto s66 + } + return +s361: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s360 + case r == 46: + goto s66 + } + return +s362: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s361 + case r == 46: + goto s66 + } + return +s363: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s362 + case r == 46: + goto s66 + } + return +s364: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s363 + case r == 46: + goto s66 + } + return +s365: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s364 + case r == 46: + goto s66 + } + return +s366: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s365 + case r == 46: + goto s66 + } + return +s367: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s366 + } + return +s368: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s367 + case r == 46: + goto s66 + } + return +s369: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s370 + case r == 46: + goto s158 + case r == 95: + goto s368 + } + return +s370: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s367 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s371 + } + return +s371: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s366 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s372 + case r == 46: + goto s158 + } + return +s372: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s373 + case r == 46: + goto s158 + case r == 95: + goto s365 + } + return +s373: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s364 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s374 + case r == 46: + goto s158 + } + return +s374: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s363 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s375 + case r == 46: + goto s158 + } + return +s375: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s362 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s376 + case r == 46: + goto s158 + } + return +s376: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s377 + case r == 46: + goto s158 + case r == 95: + goto s361 + } + return +s377: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s360 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s378 + case r == 46: + goto s158 + } + return +s378: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s359 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s379 + } + return +s379: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s358 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s380 + case r == 46: + goto s158 + } + return +s380: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s357 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s381 + case r == 46: + goto s158 + } + return +s381: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s382 + case r == 46: + goto s158 + case r == 95: + goto s356 + } + return +s382: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s383 + case r == 46: + goto s158 + case r == 95: + goto s355 + } + return +s383: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s384 + case r == 46: + goto s158 + case r == 95: + goto s354 + } + return +s384: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s385 + case r == 46: + goto s158 + case r == 95: + goto s353 + } + return +s385: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s386 + case r == 46: + goto s158 + case r == 95: + goto s352 + } + return +s386: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s387 + case r == 46: + goto s158 + case r == 95: + goto s351 + } + return +s387: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s388 + case r == 46: + goto s158 + case r == 95: + goto s350 + } + return +s388: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s389 + case r == 46: + goto s158 + case r == 95: + goto s349 + } + return +s389: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s390 + case r == 46: + goto s158 + case r == 95: + goto s348 + } + return +s390: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s391 + case r == 46: + goto s158 + case r == 95: + goto s347 + } + return +s391: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s392 + case r == 46: + goto s158 + case r == 95: + goto s346 + } + return +s392: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s393 + case r == 46: + goto s158 + case r == 95: + goto s345 + } + return +s393: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s394 + case r == 46: + goto s158 + case r == 95: + goto s344 + } + return +s394: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s395 + case r == 46: + goto s158 + case r == 95: + goto s343 + } + return +s395: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s342 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s396 + } + return +s396: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s397 + case r == 46: + goto s158 + case r == 95: + goto s341 + } + return +s397: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s340 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s398 + case r == 46: + goto s158 + } + return +s398: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s399 + case r == 46: + goto s158 + case r == 95: + goto s339 + } + return +s399: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s400 + case r == 46: + goto s66 + case r == 95: + goto s338 + } + return +s400: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s401 + case r == 46: + goto s66 + case r == 95: + goto s337 + } + return +s401: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s402 + case r == 46: + goto s66 + case r == 95: + goto s336 + } + return +s402: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s335 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s403 + } + return +s403: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s404 + case r == 46: + goto s66 + case r == 95: + goto s334 + } + return +s404: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s405 + case r == 46: + goto s66 + case r == 95: + goto s333 + } + return +s405: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s406 + case r == 46: + goto s66 + case r == 95: + goto s332 + } + return +s406: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s407 + case r == 46: + goto s66 + case r == 95: + goto s331 + } + return +s407: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s408 + case r == 46: + goto s66 + case r == 95: + goto s330 + } + return +s408: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s409 + case r == 46: + goto s66 + case r == 95: + goto s329 + } + return +s409: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s410 + case r == 46: + goto s66 + case r == 95: + goto s328 + } + return +s410: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s411 + case r == 46: + goto s66 + case r == 95: + goto s327 + } + return +s411: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s412 + case r == 46: + goto s66 + case r == 95: + goto s326 + } + return +s412: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s413 + case r == 46: + goto s66 + case r == 95: + goto s325 + } + return +s413: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s414 + case r == 46: + goto s66 + case r == 95: + goto s324 + } + return +s414: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s415 + case r == 46: + goto s66 + case r == 95: + goto s323 + } + return +s415: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s322 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s416 + case r == 46: + goto s66 + } + return +s416: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s321 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s417 + } + return +s417: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s418 + case r == 46: + goto s66 + case r == 95: + goto s320 + } + return +s418: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s419 + case r == 46: + goto s66 + case r == 95: + goto s319 + } + return +s419: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s420 + case r == 46: + goto s66 + case r == 95: + goto s318 + } + return +s420: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s421 + case r == 46: + goto s66 + case r == 95: + goto s317 + } + return +s421: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s422 + case r == 46: + goto s66 + case r == 95: + goto s316 + } + return +s422: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s423 + case r == 46: + goto s66 + case r == 95: + goto s315 + } + return +s423: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s424 + case r == 46: + goto s66 + case r == 95: + goto s314 + } + return +s424: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 95: + goto s313 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s425 + } + return +s425: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s125 + case r == 46: + goto s66 + case r == 95: + goto s131 + } + return +s426: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s368 + case r == 46: + goto s66 + } + return +s427: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s428 + case r == 46: + goto s66 + } + return +s428: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s429 + case r == 46: + goto s66 + } + return +s429: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s430 + case r == 46: + goto s66 + } + return +s430: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s431 + case r == 46: + goto s66 + } + return +s431: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s432 + case r == 46: + goto s66 + } + return +s432: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s433 + case r == 46: + goto s66 + } + return +s433: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s434 + case r == 46: + goto s66 + } + return +s434: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s435 + case r == 46: + goto s66 + } + return +s435: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s436 + case r == 46: + goto s66 + } + return +s436: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s437 + case r == 46: + goto s66 + } + return +s437: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s438 + case r == 46: + goto s66 + } + return +s438: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s439 + case r == 46: + goto s66 + } + return +s439: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s440 + case r == 46: + goto s66 + } + return +s440: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s441 + case r == 46: + goto s66 + } + return +s441: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s442 + case r == 46: + goto s66 + } + return +s442: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s443 + case r == 46: + goto s66 + } + return +s443: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s444 + case r == 46: + goto s66 + } + return +s444: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s445 + case r == 46: + goto s66 + } + return +s445: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s446 + case r == 46: + goto s66 + } + return +s446: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s447 + case r == 46: + goto s66 + } + return +s447: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s448 + case r == 46: + goto s66 + } + return +s448: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s449 + } + return +s449: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s450 + case r == 46: + goto s66 + } + return +s450: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s451 + } + return +s451: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s452 + case r == 46: + goto s66 + } + return +s452: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s453 + case r == 46: + goto s66 + } + return +s453: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s454 + case r == 46: + goto s66 + } + return +s454: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s455 + } + return +s455: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s456 + case r == 46: + goto s66 + } + return +s456: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s33 + } + return +s457: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 77 || r >= 79 && r <= 90 || r >= 97 && r <= 109 || r >= 111 && r <= 122 || r == 383 || r == 8490: + goto s69 + case r == 46: + goto s158 + case r == 78 || r == 110: + goto s458 + case r == 95: + goto s251 + } + return +s458: + switch { + case i == len(s): + end = i + return + } + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s70 + case r == 95: + goto s250 + case r == 45: + goto s254 + } + return +s459: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 77 || r >= 79 && r <= 90 || r >= 97 && r <= 109 || r >= 111 && r <= 122 || r == 383 || r == 8490: + goto s5 + case r == 46: + goto s158 + case r == 78 || r == 110: + goto s460 + case r == 95: + goto s429 + } + return +s460: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s6 + case r == 95: + goto s430 + case r == 45: + goto s461 + case r == 46: + goto s158 + } + return +s461: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s431 + case r == 45: + goto s462 + case r == 46: + goto s158 + case r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s7 + } + return +s462: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s463 + case r == 46: + goto s158 + case r == 95: + goto s543 + } + return +s463: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s464 + case r == 46: + goto s158 + case r == 95: + goto s542 + } + return +s464: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s465 + case r == 46: + goto s158 + case r == 95: + goto s541 + } + return +s465: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s466 + case r == 46: + goto s158 + case r == 95: + goto s540 + } + return +s466: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s467 + case r == 46: + goto s158 + case r == 95: + goto s539 + } + return +s467: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s468 + case r == 46: + goto s158 + case r == 95: + goto s538 + } + return +s468: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s469 + case r == 46: + goto s158 + case r == 95: + goto s537 + } + return +s469: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s470 + case r == 46: + goto s158 + case r == 95: + goto s536 + } + return +s470: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s471 + case r == 46: + goto s158 + case r == 95: + goto s535 + } + return +s471: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s472 + case r == 46: + goto s158 + case r == 95: + goto s534 + } + return +s472: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s473 + case r == 46: + goto s158 + case r == 95: + goto s533 + } + return +s473: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s474 + case r == 46: + goto s158 + case r == 95: + goto s532 + } + return +s474: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s475 + case r == 46: + goto s158 + case r == 95: + goto s531 + } + return +s475: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s530 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s476 + case r == 46: + goto s158 + } + return +s476: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s477 + case r == 46: + goto s158 + case r == 95: + goto s529 + } + return +s477: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s528 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s478 + } + return +s478: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s158 + case r == 95: + goto s527 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s479 + } + return +s479: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s480 + case r == 46: + goto s158 + case r == 95: + goto s526 + } + return +s480: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s481 + case r == 46: + goto s158 + case r == 95: + goto s525 + } + return +s481: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s482 + case r == 46: + goto s158 + case r == 95: + goto s524 + } + return +s482: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s483 + case r == 46: + goto s158 + case r == 95: + goto s523 + } + return +s483: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s484 + case r == 46: + goto s158 + case r == 95: + goto s522 + } + return +s484: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 95: + goto s521 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s485 + case r == 46: + goto s158 + } + return +s485: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s486 + case r == 46: + goto s158 + case r == 95: + goto s520 + } + return +s486: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s487 + case r == 46: + goto s158 + case r == 95: + goto s519 + } + return +s487: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s488 + case r == 46: + goto s158 + } + return +s488: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s489 + case r == 46: + goto s66 + } + return +s489: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s490 + case r == 46: + goto s66 + } + return +s490: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s491 + } + return +s491: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s492 + case r == 46: + goto s66 + } + return +s492: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s493 + case r == 46: + goto s66 + } + return +s493: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s494 + case r == 46: + goto s66 + } + return +s494: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s495 + case r == 46: + goto s66 + } + return +s495: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s496 + case r == 46: + goto s66 + } + return +s496: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s497 + case r == 46: + goto s66 + } + return +s497: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s498 + case r == 46: + goto s66 + } + return +s498: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s499 + case r == 46: + goto s66 + } + return +s499: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s500 + } + return +s500: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s501 + case r == 46: + goto s66 + } + return +s501: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s502 + case r == 46: + goto s66 + } + return +s502: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s503 + case r == 46: + goto s66 + } + return +s503: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s504 + case r == 46: + goto s66 + } + return +s504: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s505 + case r == 46: + goto s66 + } + return +s505: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s506 + case r == 46: + goto s66 + } + return +s506: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s507 + case r == 46: + goto s66 + } + return +s507: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s508 + case r == 46: + goto s66 + } + return +s508: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s509 + case r == 46: + goto s66 + } + return +s509: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s510 + case r == 46: + goto s66 + } + return +s510: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s511 + case r == 46: + goto s66 + } + return +s511: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s512 + case r == 46: + goto s66 + } + return +s512: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s513 + case r == 46: + goto s66 + } + return +s513: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s514 + case r == 46: + goto s66 + } + return +s514: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s515 + case r == 46: + goto s66 + } + return +s515: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s516 + case r == 46: + goto s66 + } + return +s516: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s517 + case r == 46: + goto s66 + } + return +s517: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s518 + case r == 46: + goto s66 + } + return +s518: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s313 + case r == 46: + goto s66 + } + return +s519: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s488 + case r == 46: + goto s66 + } + return +s520: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s519 + case r == 46: + goto s66 + } + return +s521: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s520 + case r == 46: + goto s66 + } + return +s522: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s521 + case r == 46: + goto s66 + } + return +s523: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s522 + case r == 46: + goto s66 + } + return +s524: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s523 + } + return +s525: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s524 + case r == 46: + goto s66 + } + return +s526: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s525 + } + return +s527: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s526 + case r == 46: + goto s66 + } + return +s528: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s527 + case r == 46: + goto s66 + } + return +s529: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s528 + case r == 46: + goto s66 + } + return +s530: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s529 + case r == 46: + goto s66 + } + return +s531: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s530 + case r == 46: + goto s66 + } + return +s532: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s531 + case r == 46: + goto s66 + } + return +s533: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s532 + case r == 46: + goto s66 + } + return +s534: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s533 + case r == 46: + goto s66 + } + return +s535: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s534 + } + return +s536: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s535 + case r == 46: + goto s66 + } + return +s537: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s536 + case r == 46: + goto s66 + } + return +s538: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s537 + } + return +s539: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s538 + case r == 46: + goto s66 + } + return +s540: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s539 + case r == 46: + goto s66 + } + return +s541: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s540 + case r == 46: + goto s66 + } + return +s542: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s541 + case r == 46: + goto s66 + } + return +s543: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s542 + case r == 46: + goto s66 + } + return +s544: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s545 + case r == 46: + goto s66 + } + return +s545: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s546 + case r == 46: + goto s66 + } + return +s546: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s547 + case r == 46: + goto s66 + } + return +s547: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s548 + case r == 46: + goto s66 + } + return +s548: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s549 + case r == 46: + goto s66 + } + return +s549: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s550 + case r == 46: + goto s66 + } + return +s550: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s551 + case r == 46: + goto s66 + } + return +s551: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s552 + case r == 46: + goto s66 + } + return +s552: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s553 + case r == 46: + goto s66 + } + return +s553: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s554 + case r == 46: + goto s66 + } + return +s554: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s555 + case r == 46: + goto s66 + } + return +s555: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s556 + case r == 46: + goto s66 + } + return +s556: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s557 + case r == 46: + goto s66 + } + return +s557: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s558 + } + return +s558: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s559 + case r == 46: + goto s66 + } + return +s559: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s560 + case r == 46: + goto s66 + } + return +s560: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s561 + case r == 46: + goto s66 + } + return +s561: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s562 + case r == 46: + goto s66 + } + return +s562: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s563 + case r == 46: + goto s66 + } + return +s563: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s564 + case r == 46: + goto s66 + } + return +s564: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s565 + case r == 46: + goto s66 + } + return +s565: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s566 + case r == 46: + goto s66 + } + return +s566: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s567 + } + return +s567: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s568 + case r == 46: + goto s66 + } + return +s568: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s569 + case r == 46: + goto s66 + } + return +s569: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s570 + case r == 46: + goto s66 + } + return +s570: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s571 + case r == 46: + goto s66 + } + return +s571: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s572 + case r == 46: + goto s66 + } + return +s572: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s573 + case r == 46: + goto s66 + } + return +s573: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s574 + case r == 46: + goto s66 + } + return +s574: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s575 + case r == 46: + goto s66 + } + return +s575: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s576 + case r == 46: + goto s66 + } + return +s576: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s577 + case r == 46: + goto s66 + } + return +s577: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s578 + case r == 46: + goto s66 + } + return +s578: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s579 + case r == 46: + goto s66 + } + return +s579: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s580 + case r == 46: + goto s66 + } + return +s580: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s581 + case r == 46: + goto s66 + } + return +s581: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s582 + case r == 46: + goto s66 + } + return +s582: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s583 + case r == 46: + goto s66 + } + return +s583: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s584 + case r == 46: + goto s66 + } + return +s584: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s585 + case r == 46: + goto s66 + } + return +s585: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s586 + } + return +s586: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s587 + case r == 46: + goto s66 + } + return +s587: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s588 + case r == 46: + goto s66 + } + return +s588: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s589 + case r == 46: + goto s66 + } + return +s589: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s590 + case r == 46: + goto s66 + } + return +s590: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s591 + case r == 46: + goto s66 + } + return +s591: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s592 + case r == 46: + goto s66 + } + return +s592: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s593 + } + return +s593: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s594 + case r == 46: + goto s66 + } + return +s594: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s595 + case r == 46: + goto s66 + } + return +s595: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s596 + case r == 46: + goto s66 + } + return +s596: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s597 + } + return +s597: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s598 + case r == 46: + goto s66 + } + return +s598: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s599 + case r == 46: + goto s66 + } + return +s599: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s600 + case r == 46: + goto s66 + } + return +s600: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s601 + case r == 46: + goto s66 + } + return +s601: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 46: + goto s66 + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s602 + } + return +s602: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s603 + case r == 46: + goto s66 + } + return +s603: + r, rlen = utf8.DecodeRune(s[i:]) + if rlen == 0 { + return + } + i += rlen + switch { + case r == 45 || r >= 48 && r <= 57 || r >= 65 && r <= 90 || r == 95 || r >= 97 && r <= 122 || r == 383 || r == 8490: + goto s63 + case r == 46: + goto s66 + } + return +} diff --git a/pkg/hostsfile/hostsfile.go b/pkg/hostsfile/hostsfile.go deleted file mode 100644 index a741007d..00000000 --- a/pkg/hostsfile/hostsfile.go +++ /dev/null @@ -1,11 +0,0 @@ -package hostsfile - -import ( - "net" -) - -// Hosts file record -type Record struct { - IP net.IP - Hosts []string -} diff --git a/pkg/hostsfile/hostsfile_test.go b/pkg/hostsfile/hostsfile_test.go deleted file mode 100644 index e47df4ce..00000000 --- a/pkg/hostsfile/hostsfile_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package hostsfile - -import ( - "net" - "reflect" - "testing" -) - -func TestRecord(t *testing.T) { - r := Record{ - IP: net.IPv4(127, 0, 0, 1), - Hosts: []string{"localhost"}, - } - - if r.IP.String() != "127.0.0.1" { - t.Errorf("Wrong IP addr: %v", r.IP) - } - - if !reflect.DeepEqual(r.Hosts, []string{"localhost"}) { - t.Errorf("Wrong hosts: %v", r.Hosts) - } -} diff --git a/pkg/hostsfile/parser.go b/pkg/hostsfile/parser.go new file mode 100644 index 00000000..24f8f3ef --- /dev/null +++ b/pkg/hostsfile/parser.go @@ -0,0 +1,186 @@ +package hostsfile + +import ( + "bufio" + "bytes" + "io" + "net" +) + +// Hostname validator generation (execute in linux shell) using : +// $ cd ./pkg/hostsfile +// $ docker run --rm -ti -v $(pwd):/rootfs:rw -w /rootfs golang:1.15-buster +// $ go get -u gitlab.com/opennota/re2dfa +// $ re2dfa -o hostname_validator.go \ +// '(?i)^((-?)(xn--|_)?[a-z0-9-_]{0,61}[a-z0-9-_]\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$' \ +// hostsfile.validateHostname []byte +// $ exit +// $ sudo chown "$(id -u):$(id -g)" ./hostname_validator.go + +type wordFlag uint8 + +func (f wordFlag) HasFlag(flag wordFlag) bool { return f&flag != 0 } +func (f *wordFlag) AddFlag(flag wordFlag) { *f |= flag } +func (f *wordFlag) ClearFlag(flag wordFlag) { *f &= ^flag } +func (f *wordFlag) Reset() { *f = wordFlag(0) } + +const ( + wordEnded wordFlag = 1 << iota + wordWithDot + wordWithColon +) + +type word struct { + buf bytes.Buffer + count uint + flag wordFlag + isLast bool +} + +func (w *word) Reset() { + w.count = 0 + w.flag.Reset() + w.isLast = false + w.buf.Reset() +} + +// Parse input and return slice of records. Result order are same as in source. +func Parse(in io.Reader) ([]Record, error) { //nolint:funlen,gocognit,gocyclo + var ( + result = make([]Record, 0, 5) + scan = bufio.NewScanner(in) + w word + hostnames = make([]string, 0, 3) + ip bytes.Buffer + ) + + w.buf.Grow(32) //nolint:gomnd + ip.Grow(7) //nolint:gomnd + +scan: // read content "line by line" + for scan.Scan() { + line := scan.Bytes() + + if len(line) <= 5 { //nolint:gomnd + continue scan // line is too short + } + + if line[0] == '#' { + continue scan // skip any lines, that looks like comments in format: `# Any comment text` + } + + w.Reset() + ip.Reset() + if len(hostnames) > 0 { + hostnames = hostnames[:0] + } + + for i, ll := 0, len(line); i < ll && !w.isLast; i++ { // loop over line runes + if char := line[i]; char != ' ' && char != '\t' { + if char == '.' { + w.flag.AddFlag(wordWithDot) + } else if char == ':' { + w.flag.AddFlag(wordWithColon) + } + + w.buf.WriteByte(char) + w.flag.ClearFlag(wordEnded) + } else { + w.flag.AddFlag(wordEnded) + } + + if w.flag.HasFlag(wordEnded) || i == ll-1 { //nolint:nestif // word filled completely + if w.buf.Len() == 0 { + continue // skip any empty words + } + + w.count++ + + if w.count == 1 && w.buf.Bytes()[0] == '#' { + continue scan // skip if first word starts with comment char + } + + if w.count == 1 { + if (w.flag.HasFlag(wordWithDot) && validateIPv4(w.buf.Bytes())) || + (w.flag.HasFlag(wordWithColon) && net.ParseIP(w.buf.String()) != nil) { + ip.Write(w.buf.Bytes()) + } + } else { + if w.buf.Bytes()[0] == '#' { // comment at the end of line + w.isLast = true + } else if ip.Len() > 0 && validateHostname(w.buf.Bytes()) > 0 { + hostnames = append(hostnames, w.buf.String()) // +1 memory allocation here + } + } + + w.buf.Reset() + w.flag.Reset() + } + } + + if ip.Len() > 0 && len(hostnames) > 0 { + rec := Record{IP: ip.String(), Host: hostnames[0]} // +1 memory allocation here + + if l := len(hostnames); l > 1 { + rec.AdditionalHosts = make([]string, 0, l-1) // +1 memory allocation here (but not for each record) + rec.AdditionalHosts = append(rec.AdditionalHosts, hostnames[1:]...) + } + + result = append(result, rec) + } + } + + if err := scan.Err(); err != nil { + return nil, err + } + + return result, nil +} + +// validateIPv4 address (d.d.d.d). +func validateIPv4(s []byte) bool { + var p [net.IPv4len]byte + + for i := 0; i < net.IPv4len; i++ { + if len(s) == 0 { + return false // missing octets + } + + if i > 0 { + if s[0] != '.' { + return false + } + + s = s[1:] + } + + n, c, ok := dtoi(s) + if !ok || n > 0xFF { + return false + } + + s = s[c:] + p[i] = byte(n) + } + + return len(s) == 0 +} + +// dtoi converts decimal to integer. Returns number, characters consumed, success. +func dtoi(s []byte) (n int, i int, ok bool) { + const big = 0xFFFFFF + + n = 0 + for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { + n = n*10 + int(s[i]-'0') //nolint:gomnd + if n >= big { + return big, i, false + } + } + + if i == 0 { + return 0, 0, false + } + + return n, i, true +} diff --git a/pkg/hostsfile/parser/parser.go b/pkg/hostsfile/parser/parser.go deleted file mode 100644 index e2adabe3..00000000 --- a/pkg/hostsfile/parser/parser.go +++ /dev/null @@ -1,103 +0,0 @@ -package parser - -import ( - "bufio" - "errors" - "io" - "net" - "regexp" - "strings" - - "github.com/tarampampam/mikrotik-hosts-parser/pkg/hostsfile" -) - -// Hosts file parser -type Parser struct { - hostValidate *regexp.Regexp -} - -// NewParser creates new parser -func NewParser() *Parser { - return &Parser{} -} - -// Parse input and return slice of pointers (hosts file entries). -func (p *Parser) Parse(in io.Reader) ([]*hostsfile.Record, error) { - var ( - result []*hostsfile.Record - scan = bufio.NewScanner(in) - ) - - // Read content "line by line" - for scan.Scan() { - if entry, err := p.parseRawLine(scan.Text()); err == nil && entry != nil { - result = append(result, entry) - } - } - - if scanErr := scan.Err(); scanErr != nil { - return result, scanErr - } - - return result, nil -} - -// Validate hostname using regexp. -func (p *Parser) validateHostname(host string) bool { - // Lazy regexp init - if p.hostValidate == nil { - const r string = `(?i)^((-?)(xn--|_)?[a-z0-9-_]{0,61}[a-z0-9-_]\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$` - // @link: https://stackoverflow.com/a/26987741 - p.hostValidate = regexp.MustCompile(r) - } - - return p.hostValidate.Match([]byte(host)) -} - -// Parse raw hosts file line into record. -func (p *Parser) parseRawLine(line string) (*hostsfile.Record, error) { - const delimiter rune = '#' - - // Trim whitespaces - line = strings.TrimSpace(line) - - // Comment format: `# Any comment text` - if p.startsWithRune(line, delimiter) { - return nil, nil - } - - // Format: `IP_address hostname [host_alias]... #some comment` - words := strings.Fields(line) - - if len(words) < 2 { - return nil, errors.New("hosts line parser: wrong line format") - } - - // first word must be an IP address - ip := net.ParseIP(words[0]) - if ip == nil { - return nil, errors.New("hosts line parser: wrong IP address") - } - - var hosts []string - - for _, host := range words[1:] { - if p.startsWithRune(host, delimiter) { - break - } - if p.validateHostname(host) { - hosts = append(hosts, host) - } - } - - if len(hosts) == 0 { - return nil, errors.New("hosts line parser: hosts not found") - } - - return &hostsfile.Record{IP: ip, Hosts: hosts}, nil -} - -// startsWithRune make a check for string starts with passed rune -func (p *Parser) startsWithRune(s string, r rune) bool { - return len(s) >= 1 && []rune(s)[0] == r -} diff --git a/pkg/hostsfile/parser/parser_test.go b/pkg/hostsfile/parser/parser_test.go deleted file mode 100644 index a36966fb..00000000 --- a/pkg/hostsfile/parser/parser_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package parser - -import ( - "errors" - "fmt" - "os" - "reflect" - "testing" -) - -func BenchmarkParser_ParseLargeFile(b *testing.B) { - file, err := os.Open("../../../test/testdata/hosts/ad_servers.txt") - if err != nil { - panic(err) - } - - for n := 0; n < b.N; n++ { - _, _ = (&Parser{}).Parse(file) - } - - if err := file.Close(); err != nil { - panic(err) - } -} - -func TestHostsSourceParser_ParseHostsFileUsingTestData(t *testing.T) { - var cases = []struct { - filepath string - wantRecords int - wantHostNames int - }{ - { - filepath: "../../../test/testdata/hosts/ad_servers.txt", - wantRecords: 45739, - wantHostNames: 45739, - }, - { - filepath: "../../../test/testdata/hosts/block_shit.txt", - wantRecords: 109, - wantHostNames: 109, - }, - { - filepath: "../../../test/testdata/hosts/hosts_adaway.txt", - wantRecords: 411, - wantHostNames: 411, - }, - { - filepath: "../../../test/testdata/hosts/hosts_malwaredomain.txt", - wantRecords: 1106, - wantHostNames: 1106, - }, - { - filepath: "../../../test/testdata/hosts/hosts_someonewhocares.txt", // broken entry `127.0.0.1 secret.ɢoogle.com` - wantRecords: 14308, - wantHostNames: 14309, // ::1 [ip6-localhost ip6-loopback] - }, - { - filepath: "../../../test/testdata/hosts/hosts_winhelp2002.txt", - wantRecords: 11829, - wantHostNames: 11829, - }, - { - filepath: "../../../test/testdata/hosts/serverlist.txt", - wantRecords: 3064, - wantHostNames: 3064, - }, - { - filepath: "../../../test/testdata/hosts/spy.txt", - wantRecords: 367, - wantHostNames: 367, - }, - } - - for _, tt := range cases { - t.Run("Using file "+tt.filepath, func(t *testing.T) { - file, err := os.Open(tt.filepath) - if err != nil { - panic(err) - } - - res, parseErr := (&Parser{}).Parse(file) - - if parseErr != nil { - t.Error(parseErr) - } - - if rLen := len(res); rLen != tt.wantRecords { - t.Errorf("Expected records count is %d, got %d", tt.wantRecords, rLen) - } - - var hostsCount int = 0 - for _, p := range res { - hostsCount += len(p.Hosts) - if len(p.Hosts) > 1 { - fmt.Println(p.IP, p.Hosts) - } - } - - if hostsCount != tt.wantHostNames { - t.Errorf("Expected hosts count is %d, got %d", tt.wantHostNames, hostsCount) - } - - if err := file.Close(); err != nil { - panic(err) - } - }) - } -} - -func TestParser_validateHostname(t *testing.T) { - var cases = []struct { - hostname string - wantResult bool - }{ - { - hostname: "www.google.com", - wantResult: true, - }, - { - hostname: "google.com", - wantResult: true, - }, - { - hostname: "dns.google", - wantResult: true, - }, - { - hostname: "localhost", - wantResult: true, - }, - { - hostname: "x", - wantResult: true, - }, - { - hostname: "i.oh1.me", - wantResult: true, - }, - { - hostname: "localhost", - wantResult: true, - }, - { - hostname: "ip6-loopback", - wantResult: true, - }, - { - hostname: "ad-g.doubleclick.net", - wantResult: true, - }, - { - hostname: "sO.2mdn.net", - wantResult: true, - }, - { - hostname: "adman_test.go2_cloud.org", - wantResult: true, - }, - { - hostname: "r1---sn-vgqsen7z.googlevideo.com", - wantResult: true, - }, - { - hostname: "xn--90a5ai.xn--p1ai", - wantResult: true, - }, - { - hostname: "foo.bar.baz.123.com", - wantResult: true, - }, - { - hostname: "___id___.c.mystat-in.net", - wantResult: true, - }, - { - hostname: "goo gle.com", - wantResult: false, - }, - { - hostname: "\\.com", - wantResult: false, - }, - { - hostname: "/.com", - wantResult: false, - }, - { - hostname: "тест.рф", // must be encoded in `xn--e1aybc.xn--p1ai` - wantResult: false, - }, - } - - parser := &Parser{} - - for _, tt := range cases { - t.Run("Using "+tt.hostname, func(t *testing.T) { - if res := parser.validateHostname(tt.hostname); res != tt.wantResult { - t.Errorf( - `For "%s" must returns "%v", but returns "%v"`, - tt.hostname, - tt.wantResult, - res, - ) - } - }) - } -} - -func TestParser_parseRawLine(t *testing.T) { //nolint:funlen - var cases = []struct { - line string - wantError error - wantResult bool - wantIP string - wantHosts []string - }{ - { - line: "127.0.0.1 google.com dns.google", - wantResult: true, - wantIP: "127.0.0.1", - wantHosts: []string{"google.com", "dns.google"}, - }, - { - line: "0.0.0.0 ___id___.c.mystat-in.net", - wantResult: true, - wantIP: "0.0.0.0", - wantHosts: []string{"___id___.c.mystat-in.net"}, - }, - { - line: " 127.0.0.1 \t\tgoogle.com\tdns.google ", - wantResult: true, - wantIP: "127.0.0.1", - wantHosts: []string{"google.com", "dns.google"}, - }, - { - line: " fe80::74e6:b5f3:fe92:830e \t\tgoogle.com\tdns.google ", - wantResult: true, - wantIP: "fe80::74e6:b5f3:fe92:830e", - wantHosts: []string{"google.com", "dns.google"}, - }, - { - line: "::1 google.com dns.google ", - wantResult: true, - wantIP: "::1", - wantHosts: []string{"google.com", "dns.google"}, - }, - { - line: "foo", - wantError: errors.New("hosts line parser: wrong line format"), - wantResult: false, - }, - { - line: "foo bar", - wantError: errors.New("hosts line parser: wrong IP address"), - wantResult: false, - }, - { - line: "8.8.8.8 ^", - wantError: errors.New("hosts line parser: hosts not found"), - wantResult: false, - }, - { - line: " 1.1.1.257 bar", - wantError: errors.New("hosts line parser: wrong IP address"), - wantResult: false, - }, - { - line: "#127.0.0.1 google.com dns.google", - wantResult: false, - }, - { - line: " #127.0.0.1 google.com dns.google", - wantResult: false, - }, - { - line: "#", - wantResult: false, - }, - { - line: " #", - wantResult: false, - }, - { - line: "# ", - wantResult: false, - }, - { - line: "# Comment line", - wantResult: false, - }, - { - line: "### Comment line", - wantResult: false, - }, - { - line: " ### Comment line", - wantResult: false, - }, - { - line: "127.0.0.1 google.com dns.google # some comment", - wantResult: true, - wantIP: "127.0.0.1", - wantHosts: []string{"google.com", "dns.google"}, - }, - { - line: "127.0.0.1 google.com dns.google #some comment localhost", - wantResult: true, - wantIP: "127.0.0.1", - wantHosts: []string{"google.com", "dns.google"}, - }, - { - line: "0.0.0.0 xn--90a5ai.xn--p1ai\tx \\.com", - wantResult: true, - wantIP: "0.0.0.0", - wantHosts: []string{"xn--90a5ai.xn--p1ai", "x"}, - }, - { - line: "2001:db8:0:1:1:1:1:1 xn--90a5ai.xn--p1ai\tx \\.com", - wantResult: true, - wantIP: "2001:db8:0:1:1:1:1:1", - wantHosts: []string{"xn--90a5ai.xn--p1ai", "x"}, - }, - } - - for _, tt := range cases { - t.Run("Using "+tt.line, func(t *testing.T) { - res, err := (&Parser{}).parseRawLine(tt.line) - - if tt.wantError != nil && err.Error() != tt.wantError.Error() { - t.Errorf(`Want error "%v", but got "%v"`, tt.wantError, err) - } - - if err != nil && tt.wantError == nil { - t.Errorf(`Error %v returned, but nothing expected`, err) - } - - if tt.wantResult && res != nil { - if tt.wantIP != res.IP.String() { - t.Errorf(`Want IP "%s", but got "%s"`, tt.wantIP, res.IP) - } - - if !reflect.DeepEqual(tt.wantHosts, res.Hosts) { - t.Errorf("Want hosts %v, but got %v", tt.wantHosts, res.Hosts) - } - } - - if tt.wantResult && res == nil { - t.Error("Expected non-nil result, but nil") - } - }) - } -} - -func TestParser_startsWithRune(t *testing.T) { - var cases = []struct { - giveString string - giveRune rune - wantResult bool - }{ - { - giveString: "! foo", - giveRune: '!', - wantResult: true, - }, - { - giveString: " ! foo", - giveRune: '!', - wantResult: false, - }, - { - giveString: "abracadabra", - giveRune: 'a', - wantResult: true, - }, - { - giveString: "", - giveRune: 'a', - wantResult: false, - }, - { - giveString: "", - giveRune: ' ', - wantResult: false, - }, - } - - for _, tt := range cases { - t.Run("Using "+tt.giveString, func(t *testing.T) { - res := (&Parser{}).startsWithRune(tt.giveString, tt.giveRune) - - if tt.wantResult != res { - t.Errorf(`Want result "%v", but got "%v"`, tt.wantResult, res) - } - }) - } -} diff --git a/pkg/hostsfile/parser_test.go b/pkg/hostsfile/parser_test.go new file mode 100644 index 00000000..f2043c34 --- /dev/null +++ b/pkg/hostsfile/parser_test.go @@ -0,0 +1,198 @@ +package hostsfile + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +var benchDataset = []struct{ filePath string }{ //nolint:gochecknoglobals + {"../../test/testdata/hosts/foo.txt"}, + {"../../test/testdata/hosts/ad_servers.txt"}, + {"../../test/testdata/hosts/block_shit.txt"}, + {"../../test/testdata/hosts/hosts_adaway.txt"}, + {"../../test/testdata/hosts/serverlist.txt"}, + {"../../test/testdata/hosts/spy.txt"}, +} + +func BenchmarkParse(b *testing.B) { //nolint:dupl + for _, tt := range benchDataset { + tt := tt + + b.Run(filepath.Base(tt.filePath), func(b *testing.B) { + b.ReportAllocs() + + raw, err := ioutil.ReadFile(tt.filePath) + if err != nil { + panic(err) + } + + b.SetBytes(int64(len(raw))) + b.ResetTimer() + for n := 0; n < b.N; n++ { + b.StopTimer() + buf := bytes.NewBuffer(raw) + b.StartTimer() + + _, e := Parse(buf) + + if e != nil { + b.Fatal(e) + } + } + }) + } +} + +func TestParseUsingHostsFileContent(t *testing.T) { + t.Parallel() + + var cases = []struct { + giveFilePath string + wantRecords int + wantHostNames int + }{ + { + giveFilePath: "../../test/testdata/hosts/ad_servers.txt", + wantRecords: 45739, + wantHostNames: 45739, + }, + { + giveFilePath: "../../test/testdata/hosts/block_shit.txt", + wantRecords: 109, + wantHostNames: 109, + }, + { + giveFilePath: "../../test/testdata/hosts/hosts_adaway.txt", + wantRecords: 411, + wantHostNames: 411, + }, + { + giveFilePath: "../../test/testdata/hosts/hosts_malwaredomain.txt", + wantRecords: 1106, + wantHostNames: 1106, + }, + { + giveFilePath: "../../test/testdata/hosts/hosts_someonewhocares.txt", // broken entry `127.0.0.1 secret.ɢoogle.com` + wantRecords: 14308, + wantHostNames: 14309, // ::1 [ip6-localhost ip6-loopback] + }, + { + giveFilePath: "../../test/testdata/hosts/hosts_winhelp2002.txt", + wantRecords: 11829, + wantHostNames: 11829, + }, + { + giveFilePath: "../../test/testdata/hosts/serverlist.txt", + wantRecords: 3064, + wantHostNames: 3064, + }, + { + giveFilePath: "../../test/testdata/hosts/spy.txt", + wantRecords: 367, + wantHostNames: 367, + }, + } + + for _, tt := range cases { + tt := tt // reason: + t.Run("Hosts file: "+tt.giveFilePath, func(t *testing.T) { + t.Parallel() + + file, err := os.Open(tt.giveFilePath) + assert.NoError(t, err) + + records, parseErr := Parse(file) + assert.NoError(t, file.Close()) + assert.NoError(t, parseErr) + + assert.Len(t, records, tt.wantRecords) + + var hostsCount = 0 + + for i := 0; i < len(records); i++ { + if records[i].Host != "" { + hostsCount++ + } + + hostsCount += len(records[i].AdditionalHosts) + } + + assert.Equal(t, tt.wantHostNames, hostsCount) + }) + } +} + +func TestParseUsingCustomInput(t *testing.T) { + buf := bytes.NewBuffer([]byte(` +# This is a sample +#comment without space + #comment with spaces + # comment with tab +1.2.3.4 dns.google #record with comment +4.3.2.1 bar.com # comment with space +4.3.2.1 ___id___.c.mystat-in.net # comment with double tab +1.1.1.1 a.cn b.cn a.cn # "a.cn" is duplicate + +::1 localfoo +2606:4700:4700::1111 cloudflare #[cf] + +broken line format + +0.0.0.1 example.com +0.0.0.1 example.com # duplicate + +3.3.3.3 тест.рф xn--e1aybc.xn--p1ai + +next line with IP only (spaces and tabs after) +0.0.0.0 ' + +the end +`)) + + records, err := Parse(buf) + assert.NoError(t, err) + + assert.Len(t, records, 9) + + assert.Equal(t, "1.2.3.4", records[0].IP) + assert.Equal(t, "dns.google", records[0].Host) + assert.Nil(t, records[0].AdditionalHosts) + + assert.Equal(t, "4.3.2.1", records[1].IP) + assert.Equal(t, "bar.com", records[1].Host) + assert.Nil(t, records[1].AdditionalHosts) + + assert.Equal(t, "4.3.2.1", records[2].IP) + assert.Equal(t, "___id___.c.mystat-in.net", records[2].Host) + assert.Nil(t, records[2].AdditionalHosts) + + assert.Equal(t, "1.1.1.1", records[3].IP) + assert.Equal(t, "a.cn", records[3].Host) + assert.ElementsMatch(t, []string{"b.cn", "a.cn"}, records[3].AdditionalHosts) + + assert.Equal(t, "::1", records[4].IP) + assert.Equal(t, "localfoo", records[4].Host) + assert.Nil(t, records[4].AdditionalHosts) + + assert.Equal(t, "2606:4700:4700::1111", records[5].IP) + assert.Equal(t, "cloudflare", records[5].Host) + assert.Nil(t, records[5].AdditionalHosts) + + assert.Equal(t, "0.0.0.1", records[6].IP) + assert.Equal(t, "example.com", records[6].Host) + assert.Nil(t, records[6].AdditionalHosts) + + assert.Equal(t, "0.0.0.1", records[7].IP) + assert.Equal(t, "example.com", records[7].Host) + assert.Nil(t, records[7].AdditionalHosts) + + // "тест.рф" must be encoded as `xn--e1aybc.xn--p1ai` + assert.Equal(t, "3.3.3.3", records[8].IP) + assert.Equal(t, "xn--e1aybc.xn--p1ai", records[8].Host) + assert.Nil(t, records[8].AdditionalHosts) +} diff --git a/pkg/hostsfile/record.go b/pkg/hostsfile/record.go new file mode 100644 index 00000000..ce7473ce --- /dev/null +++ b/pkg/hostsfile/record.go @@ -0,0 +1,8 @@ +package hostsfile + +// Record is a hosts file record. +type Record struct { + IP string + Host string + AdditionalHosts []string +} diff --git a/pkg/mikrotik/dns/static_entry.go b/pkg/mikrotik/dns/static_entry.go deleted file mode 100644 index 76f3e0e0..00000000 --- a/pkg/mikrotik/dns/static_entry.go +++ /dev/null @@ -1,135 +0,0 @@ -package dns - -import ( - "io" - "reflect" - "strings" -) - -type ( - // Structure that can be "rendered" in RouterOS script format. - StaticEntry struct { - Address string `comment:"IP address" property:"address" examples:"0.0.0.0"` - Comment string `comment:"Short description of the item" property:"comment" examples:"Any text"` - Disabled bool `comment:"Defines whether item is ignored or used" property:"disabled" examples:"yes,no"` - Name string `comment:"Host name" property:"name" examples:"www.example.com"` - Regexp string `property:"regexp" examples:".*\\.example\\.com"` - TTL string `comment:"Time To Live" property:"ttl" examples:"1d"` // @todo: Need more examples - } - - // "Render-able" structure - StaticEntries []StaticEntry -) - -type ( - // Single entry "rendering" options. - RenderEntryOptions struct { - Prefix string - Postfix string - } - - // Summary "rendering" options. - RenderOptions struct { - RenderEntryOptions - RenderEmpty bool - } -) - -// Render mikrotik static dns entry and write it into some writer. Returned values is count of wrote bytes and error, -// if something goes wrong -func (e StaticEntries) Render(to io.Writer, options *RenderOptions) (int, error) { //nolint:gocyclo - var ( - wroteTotal = 0 - ref = reflect.TypeOf(StaticEntry{}) - address = e.getStructPropertyValue(ref, "Address") - comment = e.getStructPropertyValue(ref, "Comment") - disabled = e.getStructPropertyValue(ref, "Disabled") - name = e.getStructPropertyValue(ref, "Name") - regexp = e.getStructPropertyValue(ref, "Regexp") - ttl = e.getStructPropertyValue(ref, "TTL") - ) - - var buf []byte - - for _, entry := range e { - // skip entries without filled address property - if entry.Address == "" { - continue - } - - // add line breaker if bugger is used in previous iteration - if cap(buf) > 0 { - buf = append(buf, "\n"...) - } - - // write entry Prefix - if options.RenderEntryOptions.Prefix != "" { - buf = append(buf, options.RenderEntryOptions.Prefix+" "...) - } - - // write "address" - buf = append(buf, address+"="+entry.Address...) - - // write "comment" - if entry.Comment != "" || options.RenderEmpty { - buf = append(buf, " "+comment+`="`+e.escapeString(entry.Comment)+`"`...) - } - - // write "disabled" - if entry.Disabled { - buf = append(buf, " "+disabled+"=yes"...) - } else { - buf = append(buf, " "+disabled+"=no"...) - } - - // write "name" - if entry.Name != "" || options.RenderEmpty { - buf = append(buf, " "+name+`="`+e.escapeString(entry.Name)+`"`...) - } - - // write "regexp" - if entry.Regexp != "" || options.RenderEmpty { - buf = append(buf, " "+regexp+`="`+entry.Regexp+`"`...) - } - - // write "ttl" - if entry.TTL != "" || options.RenderEmpty { - buf = append(buf, " "+ttl+`="`+e.escapeString(entry.TTL)+`"`...) - } - - // write entry Postfix - if options.RenderEntryOptions.Postfix != "" { - buf = append(buf, " "+options.RenderEntryOptions.Postfix...) - } - - // write buffer - wrote, err := to.Write(buf) - if err != nil { - return wroteTotal, err - } - wroteTotal += wrote - - // make buffer clean (capacity will keep maximum length) - buf = buf[:0] - } - - return wroteTotal, nil -} - -// Escape string value chars for using in rendering. -func (StaticEntries) escapeString(s string) string { - return strings.ReplaceAll(strings.ReplaceAll(s, `\`, ``), `"`, `\"`) -} - -// Small helper for getting structure tag value. -func (StaticEntries) getStructPropertyValue(r reflect.Type, field string) string { - const propertyTag string = "property" - - if field, ok := r.FieldByName(field); ok { - val, _ := field.Tag.Lookup(propertyTag) - - return val - } - - return "" -} diff --git a/pkg/mikrotik/dns/static_entry_internal_test.go b/pkg/mikrotik/dns/static_entry_internal_test.go deleted file mode 100644 index e4b8e160..00000000 --- a/pkg/mikrotik/dns/static_entry_internal_test.go +++ /dev/null @@ -1,253 +0,0 @@ -package dns - -import ( - "bytes" - "reflect" - "testing" -) - -func TestStaticEntry(t *testing.T) { - tests := []struct { - element func() reflect.StructField - wantComment string - wantProperty string - }{ - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(StaticEntry{}).FieldByName("Address") - return field - }, - wantComment: "IP address", - wantProperty: "address", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(StaticEntry{}).FieldByName("Comment") - return field - }, - wantComment: "Short description of the item", - wantProperty: "comment", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(StaticEntry{}).FieldByName("Disabled") - return field - }, - wantComment: "Defines whether item is ignored or used", - wantProperty: "disabled", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(StaticEntry{}).FieldByName("Name") - return field - }, - wantComment: "Host name", - wantProperty: "name", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(StaticEntry{}).FieldByName("Regexp") - return field - }, - wantProperty: "regexp", - }, - { - element: func() reflect.StructField { - field, _ := reflect.TypeOf(StaticEntry{}).FieldByName("TTL") - return field - }, - wantComment: "Time To Live", - wantProperty: "ttl", - }, - } - for _, tt := range tests { - t.Run("property "+tt.wantProperty, func(t *testing.T) { - el := tt.element() - - // required tag - value, _ := el.Tag.Lookup("property") - if value != tt.wantProperty { - t.Errorf("Wrong value for 'property' tag. Want: %v, got: %v", tt.wantProperty, value) - } - - if tt.wantComment != "" { - value, _ := el.Tag.Lookup("comment") - if value != tt.wantComment { - t.Errorf("Wrong value for 'comment' tag. Want: %v, got: %v", tt.wantComment, value) - } - } - }) - } -} - -func TestStaticEntries_Render(t *testing.T) { //nolint:funlen - tests := []struct { - name string - entries *StaticEntries - renderOptions *RenderOptions - wantResult string - wantError error - }{ - { - name: "Empty input", - entries: &StaticEntries{{}}, - renderOptions: &RenderOptions{}, - wantResult: "", - }, - { - name: "Address with comment", - entries: &StaticEntries{{ - Address: "0.0.0.0", - Comment: "foo comment", - }}, - renderOptions: &RenderOptions{}, - wantResult: `address=0.0.0.0 comment="foo comment" disabled=no`, - }, - { - name: "Two entries with addresses", - entries: &StaticEntries{{ - Address: "0.0.0.0", - }, { - Address: "8.8.8.8", - }}, - renderOptions: &RenderOptions{}, - wantResult: "address=0.0.0.0 disabled=no\naddress=8.8.8.8 disabled=no", - }, - { - name: "Two entries (one is empty)", - entries: &StaticEntries{{}, { - Address: "8.8.8.8", - }}, - renderOptions: &RenderOptions{}, - wantResult: "address=8.8.8.8 disabled=no", - wantError: nil, - }, - { - name: "Two entries with Prefix and Postfix", - entries: &StaticEntries{{ - Address: "0.0.0.0", - }, { - Address: "8.8.8.8", - }}, - renderOptions: &RenderOptions{ - RenderEntryOptions: RenderEntryOptions{ - Prefix: "foo", - Postfix: "bar", - }, - }, - wantResult: "foo address=0.0.0.0 disabled=no bar\nfoo address=8.8.8.8 disabled=no bar", - }, - { - name: "Entry with all fields", - entries: &StaticEntries{{ - Address: "1.2.3.4", - Comment: "foo comment", - Disabled: true, - Name: "Bar name", - Regexp: `.*\.example\.com`, - TTL: "1d", - }}, - renderOptions: &RenderOptions{}, - wantResult: `address=1.2.3.4 comment="foo comment" disabled=yes name="Bar name" regexp=".*\.example\.com" ttl="1d"`, - }, - { - name: "Force empty fields render", - entries: &StaticEntries{{ - Address: "1.2.3.4", - }}, - renderOptions: &RenderOptions{ - RenderEmpty: true, - }, - wantResult: `address=1.2.3.4 comment="" disabled=no name="" regexp="" ttl=""`, - }, - { - name: "Regular use-case with address, name and comment", - entries: &StaticEntries{{ - Address: "1.2.3.4", - Comment: "Foo comment", - Name: "Foo entry", - }, { - Address: "4.3.2.1", - Comment: "Bar comment", - Name: "Bar entry", - }}, - renderOptions: &RenderOptions{}, - wantResult: `address=1.2.3.4 comment="Foo comment" disabled=no name="Foo entry"` + "\n" + - `address=4.3.2.1 comment="Bar comment" disabled=no name="Bar entry"`, - }, - { - name: "Entry with all fields with unescaped values", - entries: &StaticEntries{{ - Address: "1.2.3.4", - Comment: `foo \"bar\" "baz"`, - Disabled: true, - Name: " \"'blah", - TTL: "1d", - }}, - renderOptions: &RenderOptions{}, - wantResult: `address=1.2.3.4 comment="foo \"bar\" \"baz\"" disabled=yes name=" \"'blah" ttl="1d"`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := bytes.Buffer{} - l, err := tt.entries.Render(&buf, tt.renderOptions) - - if resLen := len(tt.wantResult); resLen != l { - t.Errorf("Unexpected wrote bytes length: want %d, got %d", resLen, l) - } - - if tt.wantError != nil && tt.wantError.Error() != err.Error() { - t.Errorf("Unexpected error: want %v, got %v", tt.wantError, err) - } - - if res := buf.String(); res != tt.wantResult { - t.Errorf("Unexpected result. Want:\n[%s]\nGot:\n[%s]", tt.wantResult, res) - } - }) - } -} - -func TestStaticEntries_escapeString(t *testing.T) { - tests := []struct { - in, wantOut string - }{ - {in: `foo "bar"`, wantOut: `foo \"bar\"`}, - {in: `foo \"bar\"`, wantOut: `foo \"bar\"`}, - {in: `foo \\"bar\\"`, wantOut: `foo \"bar\"`}, - } - - entries := StaticEntries{} - - for _, tt := range tests { - t.Run(tt.in, func(t *testing.T) { - if res := entries.escapeString(tt.in); res != tt.wantOut { - t.Errorf("Unexpected result. Want: [%s], got: [%s]", tt.wantOut, res) - } - }) - } -} - -func TestStaticEntries_getStructTagValue(t *testing.T) { - type T struct { - A1 string `property:"1"` - A2 string `property:""` - A3 string `bar:""` - } - - entries := StaticEntries{} - ref := reflect.TypeOf(T{}) - - if r := entries.getStructPropertyValue(ref, "A1"); r != "1" { - t.Errorf("Struct tag getter returns %v, but want %v", r, "1") - } - - if r := entries.getStructPropertyValue(ref, "A2"); r != "" { - t.Errorf("Struct tag getter returns %v for blank tag", r) - } - - if r := entries.getStructPropertyValue(ref, "A3"); r != "" { - t.Errorf("Struct tag getter returns %v for non-existing tag", r) - } -} diff --git a/pkg/mikrotik/dns_static_entries.go b/pkg/mikrotik/dns_static_entries.go new file mode 100644 index 00000000..90864526 --- /dev/null +++ b/pkg/mikrotik/dns_static_entries.go @@ -0,0 +1,47 @@ +package mikrotik + +import "io" + +// DNSStaticEntries is static DNS entries set. +type DNSStaticEntries []DNSStaticEntry + +// RenderingOptions describes options for rendering. +type RenderingOptions struct { + Prefix, Postfix string +} + +// Render mikrotik static dns entry and write it into some writer. Returned values is count of wrote bytes and error, +// if something goes wrong. +func (se DNSStaticEntries) Render(to io.Writer, opts ...RenderingOptions) (int, error) { + var ( + buf = make([]byte, 0, 128) // reusable + total int + options RenderingOptions + ) + + if len(opts) > 0 { + options = opts[0] + } + + for i := 0; i < len(se); i++ { + // append line breaker only for non-first entries + if total > 0 { + buf = append(buf, "\n"...) + } + + if formattingErr := se[i].format(&buf, options.Prefix, options.Postfix); formattingErr == nil { + // write buffer + wrote, err := to.Write(buf) + if err != nil { + return total, err + } + + total += wrote + } + + // make buffer clean (capacity will keep maximum length) + buf = buf[:0] + } + + return total, nil +} diff --git a/pkg/mikrotik/dns_static_entries_test.go b/pkg/mikrotik/dns_static_entries_test.go new file mode 100644 index 00000000..c77e13cf --- /dev/null +++ b/pkg/mikrotik/dns_static_entries_test.go @@ -0,0 +1,145 @@ +package mikrotik + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func BenchmarkDNSStaticEntries_Render(b *testing.B) { + b.ReportAllocs() + + var data DNSStaticEntries + + for i := 0; i < 1000; i++ { + data = append(data, DNSStaticEntry{ + Address: "0.0.0.0", + Comment: "Any text", + Disabled: true, + Name: "www.example.com", + Regexp: ".*\\.example\\.com", + TTL: "1d", + }) + } + + var ( + i int + e error + ) + + dest := bytes.NewBuffer([]byte{}) + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + i, e = data.Render(dest) + } + + if e != nil || i <= 0 || dest.Len() <= 0 { + b.Fail() + } +} + +func TestDNSStaticEntries_Render(t *testing.T) { + tests := []struct { + name string + giveEntries DNSStaticEntries + giveOptions RenderingOptions + wantResult string + wantError error + }{ + { + name: "empty input", + giveEntries: DNSStaticEntries{{}}, + wantResult: "", + }, + { + name: "address with comment", + giveEntries: DNSStaticEntries{{ + Address: "0.0.0.0", + Name: "foo.com", + Comment: "foo comment", + }}, + wantResult: `address=0.0.0.0 comment="foo comment" disabled=no name="foo.com"`, + }, + { + name: "two entries with addresses", + giveEntries: DNSStaticEntries{{ + Address: "0.0.0.0", + Name: "foo.com", + }, { + Address: "8.8.8.8", + Name: "bar.com", + }}, + wantResult: "address=0.0.0.0 disabled=no name=\"foo.com\"\naddress=8.8.8.8 disabled=no name=\"bar.com\"", + }, + { + name: "two entries (one is empty)", + giveEntries: DNSStaticEntries{{}, { + Address: "8.8.8.8", + Name: "foo.com", + }}, + wantResult: "address=8.8.8.8 disabled=no name=\"foo.com\"", + }, + { + name: "two entries with Prefix and Postfix", + giveEntries: DNSStaticEntries{{ + Address: "0.0.0.0", + Name: "foo.com", + }, { + Address: "8.8.8.8", + Name: "bar.com", + }}, + giveOptions: RenderingOptions{ + Prefix: "foo", + Postfix: "bar", + }, + wantResult: "foo address=0.0.0.0 disabled=no name=\"foo.com\" bar\nfoo address=8.8.8.8 disabled=no name=\"bar.com\" bar", //nolint:lll + }, + { + name: "entry with all fields", + giveEntries: DNSStaticEntries{{ + Address: "1.2.3.4", + Comment: "foo comment", + Disabled: true, + Name: "Bar name", + Regexp: `.*\.example\.com`, + TTL: "1d", + }}, + wantResult: `address=1.2.3.4 comment="foo comment" disabled=yes name="Bar name" regexp=".*\.example\.com" ttl="1d"`, //nolint:lll + }, + { + name: "regular use-case with address, name and comment", + giveEntries: DNSStaticEntries{{ + Address: "1.2.3.4", + Comment: "Foo comment", + Name: "Foo entry", + }, { + Address: "4.3.2.1", + Comment: "Bar comment", + Name: "Bar entry", + }}, + wantResult: `address=1.2.3.4 comment="Foo comment" disabled=no name="Foo entry"` + "\n" + + `address=4.3.2.1 comment="Bar comment" disabled=no name="Bar entry"`, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + l, err := tt.giveEntries.Render(&buf, tt.giveOptions) + + assert.Equal(t, len(tt.wantResult), l) + + if tt.wantError != nil { + assert.EqualError(t, err, tt.wantError.Error()) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.wantResult, buf.String()) + }) + } +} diff --git a/pkg/mikrotik/dns_static_entry.go b/pkg/mikrotik/dns_static_entry.go new file mode 100644 index 00000000..26eee6c4 --- /dev/null +++ b/pkg/mikrotik/dns_static_entry.go @@ -0,0 +1,75 @@ +package mikrotik + +// DNSStaticEntry is static DNS entry for RouterOS usage. +type DNSStaticEntry struct { + Address string // IP address (net.IP is not used for allocation avoiding reasons (to string), eg.: 0.0.0.0) + Comment string // Short description of the item (eg.: Any text) + Disabled bool // Defines whether item is ignored or used (eg.: yes,no) + Name string // Host name (eg.: www.example.com) + Regexp string // Regular expression (eg.: .*\\.example\\.com) + TTL string // Time To Live (eg.: 1d) +} + +// Format entry as a text in RouterOS script format. +// Important: keep im mind that any unexpected characters will be formatted as-is (without escaping or filtering). +func (s *DNSStaticEntry) Format(prefix, postfix string) ([]byte, error) { + const overSize = 96 // pre-allocation reserve + buf := make([]byte, 0, len(s.Address)+len(s.Comment)+len(s.Name)+len(s.Regexp)+len(s.TTL)+overSize) + err := s.format(&buf, prefix, postfix) + + return buf, err +} + +// format documentation: +// Important: empty values will NOT be printed. Values escaping are not allowed here (reason - allocation avoiding). +func (s *DNSStaticEntry) format(buf *[]byte, prefix, postfix string) error { + if s.Address == "" || (s.Name == "" && s.Regexp == "") { + return ErrEmptyFields + } + + // write prefix + if len(prefix) > 0 { + *buf = append(*buf, prefix+" "...) + } + + // write "address" + *buf = append(*buf, `address=`+s.Address...) // quoting ("..") is needed here? + + // write "comment" + if s.Comment != "" { + *buf = append(*buf, ` comment="`+s.Comment+`"`...) + } + + // write "disabled" + *buf = append(*buf, ` disabled=`+s.boolToString(s.Disabled)...) + + // write "name" + if s.Name != "" { + *buf = append(*buf, ` name="`+s.Name+`"`...) + } + + // write "regexp" + if s.Regexp != "" { + *buf = append(*buf, ` regexp="`+s.Regexp+`"`...) + } + + // write "ttl" + if s.TTL != "" { + *buf = append(*buf, ` ttl="`+s.TTL+`"`...) + } + + // write entry Postfix + if len(postfix) > 0 { + *buf = append(*buf, " "+postfix...) + } + + return nil +} + +func (DNSStaticEntry) boolToString(b bool) string { + if b { + return "yes" + } + + return "no" +} diff --git a/pkg/mikrotik/dns_static_entry_test.go b/pkg/mikrotik/dns_static_entry_test.go new file mode 100644 index 00000000..ab746374 --- /dev/null +++ b/pkg/mikrotik/dns_static_entry_test.go @@ -0,0 +1,113 @@ +package mikrotik + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func BenchmarkDNSStaticEntry_Format(b *testing.B) { + b.ReportAllocs() + + s := DNSStaticEntry{ + Address: "0.0.0.0", + Comment: "Any text", + Disabled: true, + Name: "www.example.com", + Regexp: ".*\\.example\\.com", + TTL: "1d", + } + + for n := 0; n < b.N; n++ { + _, _ = s.Format("foo", "bar") + } +} + +func TestDNSStaticEntry_Format(t *testing.T) { + cases := []struct { + name string + giveEntry DNSStaticEntry + givePrefix string + givePostfix string + wantString string + wantError error + }{ + { + name: "regular usage", + giveEntry: DNSStaticEntry{ + Address: "0.0.0.0", + Comment: "Any text", + Disabled: true, + Name: "www.example.com", + Regexp: `.*\.example\.com`, + TTL: "1d", + }, + givePrefix: "foo", + givePostfix: "bar", + wantString: `foo address=0.0.0.0 comment="Any text" disabled=yes name="www.example.com" regexp=".*\.example\.com" ttl="1d" bar`, //nolint:lll + }, + { + name: "minimal usage", + giveEntry: DNSStaticEntry{ + Address: "0.0.0.0", + Name: "foo.com", + }, + wantString: `address=0.0.0.0 disabled=no name="foo.com"`, + }, + { + name: "without escaping", + giveEntry: DNSStaticEntry{ + Address: "127.0.0.1", + Comment: "Any\\ text", + Name: "www.example\\.com", + Regexp: `.*\.example\.com`, + TTL: "1\\d", + }, + wantString: `address=127.0.0.1 comment="Any\ text" disabled=no name="www.example\.com" regexp=".*\.example\.com" ttl="1\d"`, //nolint:lll + }, + { + name: "empty", + giveEntry: DNSStaticEntry{}, + wantString: "", + wantError: ErrEmptyFields, + }, + { + name: "without address", + giveEntry: DNSStaticEntry{ + Comment: "Any text", + Disabled: true, + Name: "www.example.com", + Regexp: `.*\.example\.com`, + TTL: "1d", + }, + wantString: "", + wantError: ErrEmptyFields, + }, + { + name: "without hostname and regexp", + giveEntry: DNSStaticEntry{ + Address: "127.0.0.1", + Comment: "Any text", + Disabled: true, + TTL: "1d", + }, + wantString: "", + wantError: ErrEmptyFields, + }, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + res, err := tt.giveEntry.Format(tt.givePrefix, tt.givePostfix) + + if tt.wantError == nil { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantError.Error()) + } + + assert.Equal(t, tt.wantString, string(res)) + }) + } +} diff --git a/pkg/mikrotik/docs.go b/pkg/mikrotik/docs.go new file mode 100644 index 00000000..34b2ad49 --- /dev/null +++ b/pkg/mikrotik/docs.go @@ -0,0 +1,2 @@ +// Package mikrotik contains specific for RouterOS structs and functions. +package mikrotik diff --git a/pkg/mikrotik/errors.go b/pkg/mikrotik/errors.go new file mode 100644 index 00000000..3020ec80 --- /dev/null +++ b/pkg/mikrotik/errors.go @@ -0,0 +1,18 @@ +package mikrotik + +// Error is a special type for package-specific errors. +type Error uint8 + +// Error returns error in a string representation. +func (err Error) Error() string { + switch err { + case ErrEmptyFields: + return "required fields does not filled" + + default: + return "unknown error" + } +} + +// ErrEmptyFields means required fields does not filled. +const ErrEmptyFields Error = 1 diff --git a/pkg/mikrotik/errors_test.go b/pkg/mikrotik/errors_test.go new file mode 100644 index 00000000..99e0b1db --- /dev/null +++ b/pkg/mikrotik/errors_test.go @@ -0,0 +1,36 @@ +package mikrotik + +import "testing" + +func TestConstErr_Error(t *testing.T) { + cases := []struct { + name string + giveConst Error + wantString string + }{ + { + name: "ErrEmptyFields", + giveConst: ErrEmptyFields, + wantString: "required fields does not filled", + }, + { + name: "0", + giveConst: Error(0), + wantString: "unknown error", + }, + { + name: "255", + giveConst: Error(255), + wantString: "unknown error", + }, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if got := tt.giveConst.Error(); tt.wantString != got { + t.Errorf(`want: "%s", got: "%s"`, tt.wantString, got) + } + }) + } +} diff --git a/test/testdata/hosts/foo.txt b/test/testdata/hosts/foo.txt new file mode 100644 index 00000000..4a866544 --- /dev/null +++ b/test/testdata/hosts/foo.txt @@ -0,0 +1,23 @@ +# This is a sample +#comment without space + #comment with spaces + # comment with tab +1.2.3.4 dns.google #record with comment +4.3.2.1 bar.com # comment with space +4.3.2.1 ___id___.c.mystat-in.net # comment with double tab +1.1.1.1 a.cn b.cn a.cn # "a.cn" is duplicate + +::1 localfoo +2606:4700:4700::1111 cloudflare #[cf] + +broken line format + +0.0.0.1 example.com +0.0.0.1 example.com # duplicate + +3.3.3.3 тест.рф xn--e1aybc.xn--p1ai + +next line with IP only (spaces and tabs after) +0.0.0.0 ' + +the end diff --git a/web/404.html b/web/404.html deleted file mode 100644 index 11faf837..00000000 --- a/web/404.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - 404 - Page was bot found - - - - - - - - - -
-
-
-

Oops! Page not found

-

404

-
-

please, double check requested url

-
-
- - diff --git a/web/__error__.html b/web/__error__.html new file mode 100644 index 00000000..bb7a6b0f --- /dev/null +++ b/web/__error__.html @@ -0,0 +1,29 @@ + + + + + + + {{ message }} + + + + + +
+
+ {{ code }} +
+
+ {{ message }} +
+
+ + diff --git a/web/components/app.vue b/web/components/app.vue index ff2126ce..00e5833d 100644 --- a/web/components/app.vue +++ b/web/components/app.vue @@ -219,7 +219,7 @@ format: 'routeros', cacheLifetimeSec: NaN, entriesComment: 'ADBlock', - scriptGeneratorPath: 'script/source', + scriptGeneratorPath: '/script/source', useSsl: window.location.protocol === 'https:', } }, @@ -439,18 +439,16 @@ axios .all([ axios.request({method: 'get', url: '/api/version', timeout: 2000}), - axios.request({method: 'get', url: '/api/routes', timeout: 2000}), axios.request({method: 'get', url: '/api/settings', timeout: 2000}), ]) .then(axios.spread( /** * @param {AxiosResponse} versionResponse - * @param {AxiosResponse} routesResponse * @param {AxiosResponse} settingsResponse */ - function (versionResponse, routesResponse, settingsResponse) { + function (versionResponse, settingsResponse) { // Attach `hasOwnNestedProperty` function into each `*Response.data` object - [versionResponse, routesResponse, settingsResponse].forEach(function (response) { + [versionResponse, settingsResponse].forEach(function (response) { // @link: response.data.hasOwnNestedProperty = /** @param {string} path */ function (path) { if (typeof path !== "string" || path.length <= 0) { @@ -519,10 +517,6 @@ if (versionResponse.data.hasOwnNestedProperty('version')) { self.version = versionResponse.data.version; } - - if (routesResponse.data.hasOwnNestedProperty('script_generator.path')) { - self.scriptGeneratorPath = routesResponse.data.script_generator.path; - } } )) .catch(/** @param {Error} error */ function (error) { diff --git a/web/index.html b/web/index.html index 365e7d1b..cb448f30 100644 --- a/web/index.html +++ b/web/index.html @@ -86,24 +86,23 @@ - - - - -