diff --git a/.github/workflows/lint-and-build-code.yml b/.github/workflows/lint-and-build-code.yml new file mode 100644 index 0000000..c59c0f7 --- /dev/null +++ b/.github/workflows/lint-and-build-code.yml @@ -0,0 +1,98 @@ +# Copyright 2020 Adam Chalkley +# +# https://github.com/atc0005/go-ezproxy +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Validate Codebase + +# Run builds for Pull Requests (new, updated) +# `synchronized` seems to equate to pushing new commits to a linked branch +# (whether force-pushed or not) +on: + pull_request: + types: [opened, synchronize] + +jobs: + lint_and_build_code: + name: Lint and Build codebase + runs-on: ${{ matrix.os }} + # Default: 360 minutes + timeout-minutes: 10 + strategy: + matrix: + # Supported versions of Go + go-version: [1.13.x, 1.14.x] + + # Supported LTS and latest version of Ubuntu Linux + #os: [ubuntu-16.04, ubuntu-18.04, ubuntu-latest] + + # This should be good enough until we learn otherwise + os: [ubuntu-latest] + + steps: + - name: Set up Go + # https://github.com/actions/setup-go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go-version }} + id: go + + # This could prove useful if we need to troubleshoot odd results and + # tie them back to a specific version of Go + - name: Print go version + run: | + go version + + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + + # NOTE: Disabled in favor of top-level `vendor` folder + # + # - name: Get dependencies + # run: | + # go get -v -t -d ./... + + # Force tests to run early as it isn't worth doing much else if the + # tests fail to run properly. + # Note: The `vendor` top-level folder appears to be skipped by default. + - name: Run all tests + run: go test -mod=vendor -v ./... + + - name: Install Go linting tools + run: | + # add executables installed with go get to PATH + # TODO: this will hopefully be fixed by + # https://github.com/actions/setup-go/issues/14 + export PATH=${PATH}:$(go env GOPATH)/bin + make lintinstall + + - name: Install Ubuntu packages + if: contains(matrix.os, 'ubuntu') + run: sudo apt update && sudo apt install -y --no-install-recommends make gcc + + - name: Run Go linting tools using project Makefile + run: | + # add executables installed with go get to PATH + # TODO: this will hopefully be fixed by + # https://github.com/actions/setup-go/issues/14 + export PATH=${PATH}:$(go env GOPATH)/bin + make linting + + - name: Build with (mostly) default options + # Note: We use the `-mod=vendor` flag to explicitly request that our + # top-level vendor folder be used instead of fetching remote packages + run: go build -v -mod=vendor ./... + + - name: Build using project Makefile + run: make all diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml new file mode 100644 index 0000000..a2934c5 --- /dev/null +++ b/.github/workflows/lint-docs.yml @@ -0,0 +1,56 @@ +# Copyright 2020 Adam Chalkley +# +# https://github.com/atc0005/go-ezproxy +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Validate Docs + +# Run Workflow for Pull Requests (new, updated) +# `synchronized` seems to equate to pushing new commits to a linked branch +# (whether force-pushed or not) +on: + pull_request: + types: [opened, synchronize] + +jobs: + lint_markdown: + name: Lint Markdown files + runs-on: "ubuntu-latest" + # Default: 360 minutes + timeout-minutes: 10 + + steps: + - name: Setup Node + # https://github.com/actions/setup-node + uses: actions/setup-node@v1 + with: + node-version: "10.x" + + - name: Install Markdown linting tools + run: | + npm install markdownlint --save-dev + npm install -g markdownlint-cli + + - name: Check out code + uses: actions/checkout@v1 + + - name: Run Markdown linting tools + # The `.markdownlint.yml` file specifies config settings for this + # linter, including which linting rules to ignore. + # + # Note: Explicitly ignoring top-level vendor folder; we do not want + # potential linting issues in bundled documentation to fail linting CI + # runs for *our* documentation + run: | + markdownlint '**/*.md' --ignore node_modules --ignore vendor diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4ae837a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,33 @@ +# Copyright 2020 Adam Chalkley +# +# https://github.com/atc0005/go-ezproxy +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +linters: + enable: + - depguard + - dogsled + - dupl + - goconst + - gocritic + - gofmt + - goimports + - golint + - gosec + - maligned + - misspell + - prealloc + - scopelint + - stylecheck + - unconvert diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..f8bc6e6 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,31 @@ +# Copyright 2020 Adam Chalkley +# +# https://github.com/atc0005/go-ezproxy +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://github.com/igorshubovych/markdownlint-cli#configuration +# https://github.com/DavidAnson/markdownlint#optionsconfig + +# Setting the special default rule to true or false includes/excludes all +# rules by default. +"default": true + +# We know that line lengths will be long in the main README file, so don't +# report those cases. +"MD013": false + +# Don't complain if sub-heading names are duplicated since this is a common +# practice in CHANGELOG.md (e.g., "Fixed"). +"MD024": + "siblings_only": true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b9f2e7d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,78 @@ +# Changelog + +## Overview + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a +Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Please [open an issue][repo-url-issues] for any +deviations that you spot; I'm still learning!. + +## Types of changes + +The following types of changes will be recorded in this file: + +- `Added` for new features. +- `Changed` for changes in existing functionality. +- `Deprecated` for soon-to-be removed features. +- `Removed` for now removed features. +- `Fixed` for any bug fixes. +- `Security` in case of vulnerabilities. + +## [Unreleased] + +- placeholder + +## [v0.1.0] - 2020-06-09 + +Initial release! + +This release provides an early release version of a library intended for use +with the processing of EZproxy related files and sessions. This library was +developed specifically to support the development of an in-progress +application, so the full context may not be entirely clear until that +application is released (currently pending review). + +### Added + +- generate a list of audit records for session-related events + - for all usernames + - for a specific username + +- generate a list of active sessions using audit log + - using entires without a corresponding logout event type + +- generate a list of active sessions using active file + - for all usernames + - for a specific username + +- terminate user sessions + - single user session + - bulk user sessions + +- Go modules support (vs classic `GOPATH` setup) + +### Missing + +- Anything to do with traffic log entries +- Examples + - the in-progress [atc0005/brick][related-brick-project] should serve well + for this once it is released + + + +[Unreleased]: https://github.com/atc0005/go-ezproxy/compare/v0.1.0...HEAD +[v0.1.0]: https://github.com/atc0005/go-ezproxy/releases/tag/v0.1.0 + + + +[repo-url-home]: "This project's GitHub repo" +[repo-url-issues]: "This project's issues list" +[repo-url-release-latest]: "This project's latest release" + +[docs-homepage]: "GoDoc coverage" + +[related-brick-project]: "atc0005/brick project URL" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..044dbbd --- /dev/null +++ b/Makefile @@ -0,0 +1,115 @@ +# Copyright 2020 Adam Chalkley +# +# https://github.com/atc0005/go-ezproxy +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SHELL = /bin/bash + +# https://github.com/golangci/golangci-lint#install +# https://github.com/golangci/golangci-lint/releases/latest +GOLANGCI_LINT_VERSION = v1.27.0 + +BUILDCMD = go build -mod=vendor ./... +GOCLEANCMD = go clean -mod=vendor ./... +GITCLEANCMD = git clean -xfd +CHECKSUMCMD = sha256sum -b + +.DEFAULT_GOAL := help + + ########################################################################## + # Targets will not work properly if a file with the same name is ever + # created in this directory. We explicitly declare our targets to be phony + # by making them a prerequisite of the special target .PHONY + ########################################################################## + +.PHONY: help +## help: prints this help message +help: + @echo "Usage:" + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +.PHONY: lintinstall +## lintinstall: install common linting tools +# https://github.com/golang/go/issues/30515#issuecomment-582044819 +lintinstall: + @echo "Installing linting tools" + + @export PATH="${PATH}:$(go env GOPATH)/bin" + + @echo "Explicitly enabling Go modules mode per command" + (cd; GO111MODULE="on" go get honnef.co/go/tools/cmd/staticcheck) + + @echo Installing golangci-lint ${GOLANGCI_LINT_VERSION} per official binary installation docs ... + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} + golangci-lint --version + + @echo "Finished updating linting tools" + +.PHONY: linting +## linting: runs common linting checks +linting: + @echo "Running linting tools ..." + + @echo "Running go vet ..." + @go vet -mod=vendor $(shell go list -mod=vendor ./... | grep -v /vendor/) + + @echo "Running golangci-lint ..." + @golangci-lint run + + @echo "Running staticcheck ..." + @staticcheck $(shell go list -mod=vendor ./... | grep -v /vendor/) + + @echo "Finished running linting checks" + +.PHONY: gotests +## gotests: runs go test recursively, verbosely +gotests: + @echo "Running go tests ..." + @go test -mod=vendor ./... + @echo "Finished running go tests" + +.PHONY: goclean +## goclean: removes local build artifacts, temporary files, etc +goclean: + @echo "Removing object files and cached files ..." + @$(GOCLEANCMD) + +.PHONY: clean +## clean: alias for goclean +clean: goclean + +.PHONY: gitclean +## gitclean: WARNING - recursively cleans working tree by removing non-versioned files +gitclean: + @echo "Removing non-versioned files ..." + @$(GITCLEANCMD) + +.PHONY: pristine +## pristine: run goclean and gitclean to remove local changes +pristine: goclean gitclean + +.PHONY: all +# https://stackoverflow.com/questions/3267145/makefile-execute-another-target +## all: run all applicable build steps +all: clean build + @echo "Completed build process ..." + +.PHONY: build +## build: ensure that packages build +build: + @echo "Building packages ..." + + $(BUILDCMD) + + @echo "Completed build tasks" diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..f097c13 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,14 @@ +Copyright 2020-Present Adam Chalkley +https://github.com/atc0005/go-ezproxy/blob/master/LICENSE + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/README.md b/README.md index 2489b30..d836abb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,153 @@ + # go-ezproxy -Go library and tooling for working with EZproxy + +Go library and tooling for working with EZproxy. + +[![Latest Release](https://img.shields.io/github/release/atc0005/go-ezproxy.svg?style=flat-square)][repo-url-release-latest] +[![GoDoc](https://godoc.org/github.com/atc0005/go-ezproxy?status.svg)][docs-homepage] +![Validate Codebase](https://github.com/atc0005/go-ezproxy/workflows/Validate%20Codebase/badge.svg) +![Validate Docs](https://github.com/atc0005/go-ezproxy/workflows/Validate%20Docs/badge.svg) + + +## Table of contents + +- [Status](#status) +- [Overview](#overview) +- [Project home](#project-home) +- [Features](#features) + - [Current](#current) + - [Missing](#missing) +- [Changelog](#changelog) +- [Documentation](#documentation) +- [Examples](#examples) +- [License](#license) +- [References](#references) + - [Related projects](#related-projects) + - [Official EZproxy docs](#official-ezproxy-docs) + +## Status + +Alpha; very much getting a feel for how the project will be structured +long-term and what functionality will be offered. + +The existing functionality was added specifically to support the +in-development [atc0005/brick][related-brick-project]. This library is subject +to change in order to better support that project. + +## Overview + +This library is intended to provide common EZproxy-related functionality such +as reporting or terminating active login sessions (either for all usernames or +specific usernames), filtering (or not) audit file entries or traffic patterns +(not implemented yet) for specific usernames or domains. + +**Just to be perfectly clear**: + +- this library is intended to supplement the provided functionality of the + official OCLC-developed/supported `EZproxy` application, not in any way + replace it. +- this library is not in any way associated with OCLC, `EZproxy` or other + services offered by OCLC. + +## Project home + +See [our GitHub repo][repo-url-home] for the latest code, to file an issue or +submit improvements for review and potential inclusion into the project. + +## Features + +### Current + +- generate a list of audit records for session-related events + - for all usernames + - for a specific username + +- generate a list of active sessions using the audit log + - using entires without a corresponding logout event type + +- generate a list of active sessions using the active file + - for all usernames + - for a specific username + +- terminate user sessions + - single user session + - bulk user sessions + +### Missing + +- Anything to do with traffic log entries +- [Examples](examples/README.md) + +## Changelog + +See the [`CHANGELOG.md`](CHANGELOG.md) file for the changes associated with +each release of this application. Changes that have been merged to `master`, +but not yet an official release may also be noted in the file under the +`Unreleased` section. A helpful link to the Git commit history since the last +official release is also provided for further review. + +## Documentation + +Please see our [GoDoc][docs-homepage] coverage. If something doesn't make +sense, please [file an issue][repo-url-issues] and note what is (or was) unclear. + +## Examples + +Please see our [GoDoc][docs-homepage] coverage for general usage and the +[examples](examples/README.md) doc for a list of applications developed using +this module. + +## License + +Taken directly from the [`LICENSE`](LICENSE) and [`NOTICE.txt`](NOTICE.txt) files: + +```License +Copyright 2020-Present Adam Chalkley + +https://github.com/atc0005/go-ezproxy/blob/master/LICENSE + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +``` + +## References + +### Related projects + +- [atc0005/brick][related-brick-project] project + - this project uses this library to provides tools (two as of this writing) + intended to help manage login sessions. + +- + - + - this is the project that proved to me that EZproxy sessions *can* be + terminated programatically. + +### Official EZproxy docs + +- +- +- +- +- +- + + + +[repo-url-home]: "This project's GitHub repo" +[repo-url-issues]: "This project's issues list" +[repo-url-release-latest]: "This project's latest release" + +[docs-homepage]: "GoDoc coverage" + +[related-brick-project]: "atc0005/brick project URL" + + diff --git a/activefile/activefile.go b/activefile/activefile.go new file mode 100644 index 0000000..388374a --- /dev/null +++ b/activefile/activefile.go @@ -0,0 +1,390 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activefile + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/atc0005/go-ezproxy" +) + +const ( + // All lines containing a session ID (among other details) begin with this + // single letter prefix + SessionLinePrefix string = "S" + + // Indicate that this line should be found on even numbered lines + SessionLineEvenNumbered bool = true + + // All lines containing a username begin with this single letter prefix + UsernameLinePrefix string = "L" + + // Indicate that this line should be found on odd numbered lines + UsernameLineEvenNumbered bool = false +) + +// SessionEntry reflects a line in the ezproxy.hst file that contains session +// information. We have to tie this information back to a specific username +// based on line ordering. The session line comes first in the set followed by +// one or more additional lines, one of which contains the username. +type SessionEntry struct { + + // Type is the first field in the file. Observed entries thus far are "P, + // M, H, S, L, g, s". Those lines relevant to our purposes of matching + // session IDs to usernames are "S" for Session and "L" for Login. + Type string + + // SessionID is the second field for a line in the ActiveFile that starts + // with capital letter 'S'. We need to tie this back to a specific + // username in order to reliably terminate active sessions. + SessionID string + + // IPAddress is the seventh field for aline in the ActiveFile that starts + // with capital letter 'S'. We *could* use this value to determine which + // session ID to terminate, though using this value from a remote + // payload/report by itself has a greater chance of terminating the wrong + // user session. + IPAddress string +} + +// activeFileReader represents a file reader specific to the EZProxy active +// users and hosts file. +type activeFileReader struct { + // SearchDelay is the intentional delay between each attempt to open and + // search the specified filename for the specified username. + SearchDelay time.Duration + + // SearchRetries is the number of additional search attempts that will be + // made whenever the initial search attempt returns zero results. Each + // attempt to read the active file is subject to a race condition; EZproxy + // does not immediately write session information to disk when creating or + // terminating sessions, so some amount of delay and a number of retry + // attempts are used in an effort to work around that write delay. + SearchRetries int + + // Username is the name of the user account to search for within the + // specified file. + Username string + + // Filename is the name of the file which will be parsed/searched for the + // specified username. + Filename string +} + +// NewReader creates a new instance of a SessionReader that provides access to +// a collection of user sessions for the specified username. +func NewReader(username string, filename string) (ezproxy.SessionsReader, error) { + + if username == "" { + return nil, errors.New( + "func NewReader: missing username", + ) + } + + if filename == "" { + return nil, errors.New( + "func NewReader: missing filename", + ) + } + + reader := activeFileReader{ + SearchDelay: ezproxy.DefaultSearchDelay, + SearchRetries: ezproxy.DefaultSearchRetries, + Username: username, + Filename: filename, + } + + return &reader, nil +} + +// filterEntries is a helper function that returns all entries from the +// provided active file that have the required line prefix. Other methods +// handle converting these entries to UserSession values. +func (afr activeFileReader) filterEntries(validPrefixes []string) ([]ezproxy.FileEntry, error) { + + ezproxy.Logger.Printf("filterEntries: Attempting to open %q\n", afr.Filename) + + f, err := os.Open(afr.Filename) + if err != nil { + return nil, fmt.Errorf("func filterEntries: error encountered opening file %q: %w", afr.Filename, err) + } + defer f.Close() + + s := bufio.NewScanner(f) + + var lineno int + + var validLines []ezproxy.FileEntry + + // TODO: Does Scan() perform any whitespace manipulation already? + for s.Scan() { + lineno++ + currentLine := s.Text() + //ezproxy.Logger.Printf("Scanned line %d from %q: %q\n", lineno, filename, currentLine) + + currentLine = strings.TrimSpace(currentLine) + // ezproxy.Logger.Printf("Line %d from %q after whitespace removal: %q\n", + // lineno, filename, currentLine) + + if currentLine != "" { + for _, validPrefix := range validPrefixes { + if strings.HasPrefix(currentLine, validPrefix) { + validLines = append(validLines, ezproxy.FileEntry{ + Text: currentLine, + Number: lineno, + }) + } + } + } + } + + ezproxy.Logger.Printf("Exited s.Scan() loop") + + // report any errors encountered while scanning the input file + if err := s.Err(); err != nil { + return nil, fmt.Errorf("func filterEntries: errors encountered while scanning the input file: %w", err) + } + + return validLines, nil +} + +// SetSearchRetries is a helper method for setting the number of additional +// retries allowed when receiving zero search results. +func (afr *activeFileReader) SetSearchRetries(retries int) error { + if retries < 0 { + return fmt.Errorf("func SetSearchRetries: %d is not a valid number of search retries", retries) + } + + afr.SearchRetries = retries + + return nil +} + +// SetSearchDelay is a helper method for setting the delay in seconds between +// search attempts. +func (afr *activeFileReader) SetSearchDelay(delay int) error { + if delay < 0 { + return fmt.Errorf("func SetSearchDelay: %d is not a valid number of seconds for search delay", delay) + } + + afr.SearchDelay = time.Duration(delay) * time.Second + + return nil +} + +// AllUserSessions returns a list of all session IDs along with their associated +// IP Address in the form of a slice of UserSession values. This list of +// session IDs is intended for further processing such as filtering to a +// specific username or aggregating to check thresholds. +func (afr activeFileReader) AllUserSessions() (ezproxy.UserSessions, error) { + + // Lines containing the session entries + validPrefixes := []string{ + SessionLinePrefix, + UsernameLinePrefix, + } + + var allUserSessions ezproxy.UserSessions + + fileEntryDelimiter := " " + + validLines, filterErr := afr.filterEntries(validPrefixes) + if filterErr != nil { + return nil, fmt.Errorf( + "failed to filter active file entries while generating list of user sessions: %w", + filterErr, + ) + } + + // Ensure that the gathered lines consist of pairs, otherwise we are + // likely dealing with an invalid active users file. At this point we + // should bail as continuing would likely mean identifying the wrong user + // session for termination. + if !(len(validLines)%2 == 0) { + errMsg := fmt.Sprintf( + "error: Incomplete data pairs (%d lines) found in file %q while searching for %q user sessions", + len(validLines), + afr.Filename, + afr.Username, + ) + ezproxy.Logger.Println(errMsg) + return nil, errors.New(errMsg) + } + + for idx, currentLine := range validLines { + + activeFileEntry := strings.Split(currentLine.Text, fileEntryDelimiter) + lineno := currentLine.Number + switch activeFileEntry[0] { + case SessionLinePrefix: + // line 1 of 2 (even numbered idx) + // session ID as field 2, IP Address as field 7 + + // if not even numbered line, but the username only occurs on even + // numbered lines + // if !(idx%2 == 0) { + if (idx%2 == 1) && (SessionLineEvenNumbered) { + + // We have found a "S" prefixed line out of expected order. + // This suggests an invalid active users file or a bug in the + // earlier application logic used when generating the list of + // valid file entries. + + errMsg := fmt.Sprintf( + "error: Unexpected data pair ordering encountered at line %d in the active users file %q while searching for %q; "+ + "session line is odd numbered", + lineno, + afr.Filename, + afr.Username, + ) + ezproxy.Logger.Println(errMsg) + return nil, errors.New(errMsg) + } + + allUserSessions = append(allUserSessions, ezproxy.UserSession{ + SessionID: activeFileEntry[1], + IPAddress: activeFileEntry[6], + }) + case UsernameLinePrefix: + // line 2 of 2 (odd numbered idx) + // username as field 2 + + if (idx%2 == 1) && (UsernameLineEvenNumbered) { + + // We have found a "L" prefixed line out of expected order. + // This suggests an invalid active users file or a bug in the + // earlier application logic used when generating the list of + // valid file entries. + + errMsg := fmt.Sprintf( + "error: Unexpected data pair ordering encountered at line %d in the active users file %q while searching for %q; "+ + "session line is odd numbered", + lineno, + afr.Filename, + afr.Username, + ) + ezproxy.Logger.Println(errMsg) + return nil, errors.New(errMsg) + } + + // Use the length of the collected user sessions minus 1 as the + // index into the allUserSessions slice. The intent is to get + // access to the partial ActiveUserSession that was just + // constructed from the previous 'S' line in order to include the + // username alongside the existing Session ID and IP Address + // fields. + prevSessionIdx := len(allUserSessions) - 1 + if prevSessionIdx < 0 { + + ezproxy.Logger.Printf( + "Current text from username line %d: %v", + lineno, + activeFileEntry, + ) + + errMsg := fmt.Sprintf( + "error: unable to update partial ActiveUserSession from line %d; "+ + "unable to reliably determine session ID for %q", + lineno-1, + afr.Username, + ) + ezproxy.Logger.Println(errMsg) + return nil, errors.New(errMsg) + } + allUserSessions[prevSessionIdx].Username = activeFileEntry[1] + default: + continue + } + + } + + ezproxy.Logger.Printf( + "Found %d active sessions\n", + len(allUserSessions), + ) + + return allUserSessions, nil + +} + +// MatchingUserSessions uses the previously provided username to return a list +// of all matching session IDs along with their associated IP Address in the +// form of a slice of UserSession values. +func (afr activeFileReader) MatchingUserSessions() (ezproxy.UserSessions, error) { + + // What we will return to the the caller + requestedUserSessions := make([]ezproxy.UserSession, 0, ezproxy.SessionsLimit) + + searchAttemptsAllowed := afr.SearchRetries + 1 + + // Perform the search up to X times + for searchAttempts := 1; searchAttempts <= searchAttemptsAllowed; searchAttempts++ { + + ezproxy.Logger.Printf( + "Beginning search attempt %d of %d for %q\n", + searchAttempts, + searchAttemptsAllowed, + afr.Username, + ) + + // Intentional delay in an effort to better avoid stale data due to + // potential race condition with EZproxy write delays. + ezproxy.Logger.Printf( + "Intentionally delaying for %v to help avoid race condition due to delayed EZproxy writes\n", + afr.SearchDelay, + ) + time.Sleep(afr.SearchDelay) + + allUserSessions, err := afr.AllUserSessions() + if err != nil { + return nil, fmt.Errorf( + "func UserSessions: failed to retrieve all user sessions in order to filter to specific username: %w", + err, + ) + } + + // filter all user sessions found earlier just to the requested user + for _, session := range allUserSessions { + if strings.EqualFold(afr.Username, session.Username) { + requestedUserSessions = append(requestedUserSessions, session) + } + } + + // skip further attempts to find sessions if we already found some + if len(requestedUserSessions) > 0 { + break + } + + // try again (unless we hit our limit) + continue + + } + + ezproxy.Logger.Printf( + "Found %d active sessions for %q\n", + len(requestedUserSessions), + afr.Username, + ) + + return requestedUserSessions, nil + +} diff --git a/activefile/doc.go b/activefile/doc.go new file mode 100644 index 0000000..4672d34 --- /dev/null +++ b/activefile/doc.go @@ -0,0 +1,19 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package activefile is intended for the processing of EZproxy active users +// and hosts files. +package activefile diff --git a/auditlog/auditlog.go b/auditlog/auditlog.go new file mode 100644 index 0000000..e89c2f4 --- /dev/null +++ b/auditlog/auditlog.go @@ -0,0 +1,464 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auditlog + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/atc0005/go-ezproxy" + "github.com/atc0005/go-ezproxy/internal/textutils" +) + +// These are the events in the audit log applicable to this package. +const ( + + // EventLoginSuccess is recorded for a successful login + EventLoginSuccess string = "Login.Success" + + // EventLoginSuccessRelogin is recorded when a user closes their browser, + // but the session has not dropped so they are logged back in because the + // cookie has not expired yet. Note: this explanation was pulled from a + // cached Google search result, so its accuracy cannot be confirmed. + EventLoginSuccessRelogin string = "Login.Success.Relogin" + + // EventSessionIPChange is recorded when a user establishes a session from + // one IP address, and during that session the IP address changes. + EventSessionIPChange string = "Session.IPChange" + + // EventLogout is recorded when a user logs out of their session. + EventLogout string = "Logout" + + // EventMinFieldLength is the minimum number of fields required + // to represent an audit log entry that we will process. The Logout event + // is 5 fields, Login.Success and Login.Success.Relogin are 6 fields each. + EventMinFieldLength int = 5 +) + +// TimeStampLayout is the layout for timestamps observed in use for the audit +// log. For example, "2020-05-24 00:17:37" is a timestamp found in column 1 of +// a live audit log. +const TimeStampLayout string = "2006-01-02 15:04:05" + +// SessionEntry reflects an entry in a audit/YYYYMMDD.txt file that +// contains fields useful (or required) for working with user sessions. Each +// entry in the audit log is tab separated. Not all event types recorded in +// the audit log will have all fields. For example, the `Logout` event type +// does not have an IP Address and the `System` event does not have IP +// Address, Username or Session ID values. +type SessionEntry struct { + + // Datestamp is recorded as a string in an effort to reduce potential + // friction when ingesting audit log entries. We can convert later + // when/if needed. + Datestamp string + + // Event is the event type associated with an entry in the audit file + Event string + + // IPAddress is an IP Adddress associated with an entry in the audit file + IPAddress string + + // Username is the username associated with an entry in the audit file + Username string + + // SessionID is the session ID associated with an entry in the audit file + SessionID string +} + +// SessionEntries is a collection of SessionEntry values that is intended for +// aggregation before bulk processing of some kind. +type SessionEntries []SessionEntry + +// auditLogReader represents a file reader specific to EZProxy audit logs. +// This type is intended for internal use only and for that reason is not +// exposed for external use. See also the AuditReader type. +type auditLogReader struct { + + // SearchDelay is the intentional delay between each attempt to open and + // search the specified filename for the specified username. + SearchDelay time.Duration + + // SearchRetries is the number of additional search attempts that will be + // made whenever the initial search attempt returns zero results. Each + // attempt to read the active file is subject to a race condition; EZproxy + // does not immediately write session information to disk when creating or + // terminating sessions, so some amount of delay and a number of retry + // attempts are used in an effort to work around that write delay. + SearchRetries int + + // Username is the name of the user account to search for within the + // specified file. + Username string + + // Filename is the name of the file which will be parsed/searched for the + // specified username. + Filename string +} + +// AuditReader is the API for retrieving values from an audit log file +type AuditReader interface { + ezproxy.SessionsReader + + // MatchingSessionEntries uses the previously provided username as a + // search key, the previously provided filename to search through and + // returns a slice of SessionEntry values which reflect entries in the + // specified audit file for that username. + MatchingSessionEntries() (SessionEntries, error) + + // AllSessionEntries uses the previously provided filename to search + // through and return a slice of SessionEntry values which reflect ALL + // session-related events. The SessionEntry values returned are NOT + // filtered to a specific username. + AllSessionEntries() (SessionEntries, error) +} + +// AllSessionEntries uses the previously provided filename to search +// through and return a slice of SessionEntry values which reflect ALL +// session-related events. The SessionEntry values returned are NOT +// filtered to a specific username. +func (alr auditLogReader) AllSessionEntries() (SessionEntries, error) { + + // These are events that contain relevant details for our work + validEvents := []string{ + EventLoginSuccess, + EventLoginSuccessRelogin, + EventSessionIPChange, + + // Used to remove any earlier entries since they're no longer relevant + EventLogout, + } + + ezproxy.Logger.Printf("Attempting to open %q\n", alr.Filename) + + f, err := os.Open(alr.Filename) + if err != nil { + return nil, fmt.Errorf("func AllSessionEntries: error encountered opening file %q: %w", alr.Filename, err) + } + defer f.Close() + + ezproxy.Logger.Printf("Searching for: %q\n", alr.Username) + + s := bufio.NewScanner(f) + var lineno int + + var logoutEvents []SessionEntry + + userSessionIDsIndex := make(map[string]SessionEntry, ezproxy.SessionsLimit) + + // TODO: Does Scan() perform any whitespace manipulation already? + for s.Scan() { + lineno++ + currentLine := s.Text() + ezproxy.Logger.Printf("Scanned line %d from %q: %q\n", + lineno, + alr.Filename, + currentLine, + ) + + currentLine = strings.TrimSpace(currentLine) + ezproxy.Logger.Printf( + "Line %d from %q after whitespace removal: %q\n", + lineno, + alr.Filename, + currentLine, + ) + + auditFileEntry := strings.Split(currentLine, "\t") + if len(auditFileEntry) < EventMinFieldLength { + continue + } + + // Event field is the second field, so 1 in a zero-based array/slice + if !textutils.InList(auditFileEntry[1], validEvents) { + continue + } + + // at this point we are dealing with one of these events: + // + // Logout + // Login.Success + // Login.Success.Relogin + + if strings.EqualFold(auditFileEntry[1], EventLogout) { + + logoutEvents = append(logoutEvents, SessionEntry{ + Datestamp: auditFileEntry[0], + Event: auditFileEntry[1], + IPAddress: "", + Username: auditFileEntry[2], + SessionID: auditFileEntry[3], + }) + + continue + } + + // at this point we're only dealing with these events: + // + // Login.Success + // Login.Success.Relogin + userSessionIDsIndex[auditFileEntry[3]] = SessionEntry{ + Datestamp: auditFileEntry[0], + Event: auditFileEntry[1], + IPAddress: auditFileEntry[2], + Username: auditFileEntry[3], + SessionID: auditFileEntry[4], + } + } + + ezproxy.Logger.Println("Exited s.Scan() loop") + + // report any errors encountered while scanning the input file + if err := s.Err(); err != nil { + return nil, fmt.Errorf("func AllSessionEntries: errors encountered while scanning the input file: %w", err) + } + + // Loop over logoutEvents, remove matching entries from the + // userSessionIDsIndex map + for _, loggedOutSession := range logoutEvents { + delete(userSessionIDsIndex, loggedOutSession.SessionID) + } + + // Convert our userSessionIDsIndex map + userSessions := make(SessionEntries, 0, ezproxy.SessionsLimit) + for _, entry := range userSessionIDsIndex { + userSessions = append(userSessions, entry) + } + + return userSessions, nil + +} + +// MatchingSessionEntries uses the previously provided username as a search +// key and returns a slice of SessionEntry values which reflect entries in the +// specified audit file for that username. +func (alr auditLogReader) MatchingSessionEntries() (SessionEntries, error) { + + ezproxy.Logger.Printf("Searching for: %q\n", alr.Username) + + searchAttemptsAllowed := alr.SearchRetries + 1 + + requestedSessionEntries := make(SessionEntries, 0, ezproxy.SessionsLimit) + + // Perform the search up to X times + for searchAttempts := 1; searchAttempts <= searchAttemptsAllowed; searchAttempts++ { + + ezproxy.Logger.Printf( + "Beginning search attempt %d of %d for %q\n", + searchAttempts, + searchAttemptsAllowed, + alr.Username, + ) + + // Intentional delay in an effort to better avoid stale data due to + // potential race condition with EZproxy write delays. + ezproxy.Logger.Printf( + "Intentionally delaying for %v to help avoid race condition due to delayed EZproxy writes\n", + alr.SearchDelay, + ) + time.Sleep(alr.SearchDelay) + + allSessionEntries, err := alr.AllSessionEntries() + if err != nil { + return nil, fmt.Errorf( + "func SessionEntries: failed to retrieve all session entries in order to filter to specific username: %w", + err, + ) + } + + // Filter ALL session entries in the audit log to the requested username + for _, entry := range allSessionEntries { + if strings.EqualFold(entry.Username, alr.Username) { + requestedSessionEntries = append(requestedSessionEntries, entry) + } + } + + // skip further attempts to find entries if we already found some + if len(requestedSessionEntries) > 0 { + break + } + + continue + + } + + return requestedSessionEntries, nil + +} + +// AllUserSessions returns a list of all session IDs along with their +// associated IP Address in the form of a slice of UserSession values. This +// list of session IDs is intended for further processing such as filtering to +// a specific username or aggregating to check thresholds. +func (alr auditLogReader) AllUserSessions() (ezproxy.UserSessions, error) { + + allUserSessions := make(ezproxy.UserSessions, 0, ezproxy.AllUsersSessionsLimit) + + allSessionEntries, err := alr.AllSessionEntries() + if err != nil { + return nil, fmt.Errorf( + "func AllUserSessions: failed to retrieve all session entries in order to convert to user sessions for all users: %w", + err, + ) + } + + for _, entry := range allSessionEntries { + allUserSessions = append(allUserSessions, ezproxy.UserSession{ + Username: entry.Username, + IPAddress: entry.IPAddress, + SessionID: entry.SessionID, + }) + } + + return allUserSessions, nil + +} + +// MatchingUserSessions uses the previously provided username to return a list +// of all matching session IDs along with their associated IP Address in the +// form of a slice of UserSession values. +func (alr auditLogReader) MatchingUserSessions() (ezproxy.UserSessions, error) { + + ezproxy.Logger.Printf("Searching for: %q\n", alr.Username) + + searchAttemptsAllowed := alr.SearchRetries + 1 + + sessionEntries := make(SessionEntries, 0, ezproxy.SessionsLimit) + + // Perform the search up to X times + for searchAttempts := 1; searchAttempts <= searchAttemptsAllowed; searchAttempts++ { + + ezproxy.Logger.Printf( + "Beginning search attempt %d of %d for %q\n", + searchAttempts, + searchAttemptsAllowed, + alr.Username, + ) + + // Intentional delay in an effort to better avoid stale data due to + // potential race condition with EZproxy write delays. + ezproxy.Logger.Printf( + "Intentionally delaying for %v to help avoid race condition due to delayed EZproxy writes\n", + alr.SearchDelay, + ) + time.Sleep(alr.SearchDelay) + + var err error + sessionEntries, err = alr.MatchingSessionEntries() + if err != nil { + return nil, fmt.Errorf("func UserSessions: unable to convert audit log session entries to user sessions: %w", err) + } + + // skip further attempts to find entries if we already found some + if len(sessionEntries) > 0 { + break + } + + continue + + } + + return sessionEntries.UserSessions(), nil + +} + +// UserSession converts a SessionEntry value to a UserSession value. How the +// SessionEntry value was retrieved determines whether the UserSession value +// is filtered to a specific username or not. +func (se SessionEntry) UserSession() ezproxy.UserSession { + return ezproxy.UserSession{ + SessionID: se.SessionID, + IPAddress: se.IPAddress, + Username: se.Username, + } +} + +// UserSessions converts a collection of SessionEntry values into a collection +// of UserSession values. How the SessionEntry values were retrieved +// determines whether the UserSession values are filtered to a specific +// username or all usernames. +func (se SessionEntries) UserSessions() ezproxy.UserSessions { + + userSessions := make(ezproxy.UserSessions, 0, ezproxy.SessionsLimit) + + for idx := range se { + userSessions = append(userSessions, ezproxy.UserSession{ + SessionID: se[idx].SessionID, + IPAddress: se[idx].IPAddress, + Username: se[idx].Username, + }) + } + + return userSessions + +} + +// NewReader creates a new instance of an AuditReader that provides access to +// collections of user sessions and audit log session entries specific to the +// specified username. +func NewReader(username string, filename string) (AuditReader, error) { + + if username == "" { + return nil, errors.New( + "func NewReader: missing username", + ) + } + + if filename == "" { + return nil, errors.New( + "func NewReader: missing filename", + ) + } + + reader := auditLogReader{ + SearchDelay: ezproxy.DefaultSearchDelay, + SearchRetries: ezproxy.DefaultSearchRetries, + Username: username, + Filename: filename, + } + + return &reader, nil + +} + +// SetSearchRetries is a helper method for setting the number of additional +// retries allowed when receiving zero search results. +func (alr *auditLogReader) SetSearchRetries(retries int) error { + if retries < 0 { + return fmt.Errorf("func SetSearchRetries: %d is not a valid number of search retries", retries) + } + + alr.SearchRetries = retries + + return nil +} + +// SetSearchDelay is a helper method for setting the delay in seconds between +// search attempts. +func (alr *auditLogReader) SetSearchDelay(delay int) error { + if delay < 0 { + return fmt.Errorf("func SetSearchDelay: %d is not a valid number of seconds for search delay", delay) + } + + alr.SearchDelay = time.Duration(delay) * time.Second + + return nil +} diff --git a/auditlog/doc.go b/auditlog/doc.go new file mode 100644 index 0000000..a69ff6a --- /dev/null +++ b/auditlog/doc.go @@ -0,0 +1,18 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package auditlog is intended for the processing of EZproxy audit files. +package auditlog diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..aeaa10a --- /dev/null +++ b/doc.go @@ -0,0 +1,73 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + +Package ezproxy is intended for the processing of EZproxy related files and +sessions. + +PROJECT HOME + +See our GitHub repo (https://github.com/atc0005/go-ezproxy) for the latest +code, to file an issue or submit improvements for review and potential +inclusion into the project. + +PURPOSE + +Process EZproxy related files and sessions. + +FEATURES + +• generate a list of audit records for session-related events for all usernames or just for a specific username + +• generate a list of active sessions using the audit log using entires without a corresponding logout event type + +• generate a list of active sessions using the active file for all usernames or just for a specific username + +• terminate single user session or bulk user sessions + +OVERVIEW + +Ultimately, this package was written in order to support retrieving session +information for a specific username so that the session can be terminated. +Because of this the majority of the functionality is specific to user +sessions. + +General workflow: + +1. Import this package +2. Import one or more of the subpackages +3. Create a new reader for the file type you need to work with +4. Using the new reader, generate a UserSessions collection +5. Use the Terminate method to terminate user sessions + +If using the ezproxy/auditlog package, you can also generate a SessionEntries +collection representing all SessionEntry values from a specified audit log +file or just the values applicable to a specifc user. + +FUTURE + +This package currently provides functionality for working with an active user +or audit log file, but not for EZproxy traffic log files. Having minimal +support for traffic log files could provide a way to die activity for a +specific user account to specifc resources accessed by that user account. This +could prove invaluable where automation is used to automatically terminate +user sessions; after account termination, a report could be generated for the +incident listing at a high-level the providers accessed and general statistics +associated with the access (e.g., PDF downloads, total bandwidth, etc.). + +*/ +package ezproxy diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..2251f84 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,38 @@ + +# go-ezproxy + +Examples for the `go-ezproxy` module. + + +## Table of contents + +- [Examples](#examples) + - [`brick`](#brick) + - [`es`](#es) + - [`ezproxy`](#ezproxy) + +## Examples + +This is a stub page intended to list examples for the usage of the +`go-ezproxy` module. As of this writing it is a stub page only. + +Because the `atc0005/brick` project is close to an initial launch (pre-v1.0), +work for this portion of the module is "on hold". After the release of +`atc0005/brick`, the plan is to return here and update this page to refer +directly to applications that reflect real-world usage of this module. + +### `brick` + +- coming "soon" + +### `es` + +- small CLI application to list (and optionally terminate) matching user + sessions for a specified user account +- coming "soon" + +### `ezproxy` + +- coming "soon" +- a "mock" `ezproxy` binary for testing purposes, not in any way an attempt to + replace the actual/official application diff --git a/ezproxy.go b/ezproxy.go new file mode 100644 index 0000000..7174ab5 --- /dev/null +++ b/ezproxy.go @@ -0,0 +1,179 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ezproxy + +import ( + "io/ioutil" + "log" + "os" + "time" +) + +// Logger is a package logger that can be enabled from client code to allow +// logging output from this package when desired/needed for troubleshooting. +// This variable is exported in order to allow subpackages to use it without +// defining their own. The intent is to make it easier for consumers of the +// package to have one set of methods for enabling or disabling logging output +// for this package and subpackages. +var Logger *log.Logger + +func init() { + + // Disable logging output by default unless client code explicitly + // requests it + Logger = log.New(os.Stderr, "[ezproxy] ", 0) + Logger.SetOutput(ioutil.Discard) + +} + +// EnableLogging enables logging output from this package. Output is muted by +// default unless explicitly requested (by calling this function). +func EnableLogging() { + Logger.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + Logger.SetOutput(os.Stderr) +} + +// DisableLogging reapplies default package-level logging settings of muting +// all logging output. +func DisableLogging() { + Logger.SetFlags(0) + Logger.SetOutput(ioutil.Discard) +} + +// These "SessionsLimit" constants are used as preallocation values for maps +// and slices. +const ( + + // This is intended to approximate the `::Limit=X` value (where X is a + // positive whole number) set within the user.txt EZproxy config file. + // This package uses this value as a preallocation capacity value for maps + // and slices. + SessionsLimit int = 4 + + // This is simply a guess to use as a baseline for preallocating maps and + // slices capacity in regards to ALL user sessions + AllUsersSessionsLimit int = SessionsLimit * 10 +) + +// These are the known/confirmed details regarding Session IDs as of the 6.x +// series. +const ( + SessionIDLength int = 15 + SessionIDRegex string = "[a-zA-Z0-9]{15}" +) + +// These are the known EZproxy binary exit codes and the associated output as +// of the 6.x series. Please open an issue +// (https://github.com/atc0005/go-ezproxy/issues) if you encounter others not +// listed here. +const ( + + // KillSubCmdExitCodeSessionTerminated is the exit code for sessions that + // are successfully terminated via the `ezproxy kill` subcommand. + KillSubCmdExitCodeSessionTerminated int = 0 + + // KillSubCmdExitTextTemplateSessionTerminated is a formatted string + // template for the output shown when a session is successfully + // terminated via the `ezproxy kill` subcommand. + KillSubCmdExitTextTemplateSessionTerminated string = "Session %s terminated" + + // KillSubCmdExitCodeSessionNotSpecified is the exit code for calling + // `ezproxy kill` without specifying a session id. + KillSubCmdExitCodeSessionNotSpecified int = 1 + + // KillSubCmdExitTextSessionNotSpecified is the string returned when + // calling `ezproxy kill` without specifying a session id. + KillSubCmdExitTextSessionNotSpecified string = "Session must be specified" + + // KillSubCmdExitCodeSessionDoesNotExist is the exit code for attempts to + // terminate a session that EZproxy does not believe exists. + KillSubCmdExitCodeSessionDoesNotExist int = 3 + + // KillSubCmdExitTextTemplateSessionDoesNotExist is a formatted string + // template for the output shown when an attempt is made to terminate a + // session that EZproxy does not believe exists. + KillSubCmdExitTextTemplateSessionDoesNotExist string = "Session %s does not exist" +) + +const ( + + // BinaryName is a constant for the name of the EZproxy application binary. + BinaryName string = "ezproxy" + + // SubCmdNameSessionTerminate is the name of the EZproxy application + // subcommand used to terminate user sessions. + SubCmdNameSessionTerminate string = "kill" +) + +const ( + + // DefaultSearchDelay is the delay applied before attempting to read + // either of the Audit File or Active Users File. This intentional delay + // is applied in an effort to account for time between EZproxy noting an + // event and recording it to the file we are reading. + DefaultSearchDelay time.Duration = 1 * time.Second + + // DefaultSearchRetries is the number of retries beyond the first attempt + // that will be made after the first attempt at finding active sessions + // for a specified username yields no results. + DefaultSearchRetries int = 7 +) + +// FileEntry reflects a line of text found in a file and the line number +// associated with it +type FileEntry struct { + Text string + Number int +} + +// A UserSession represents a session for a specific user account. These +// values are returned after processing either an audit file or the active +// file. +type UserSession struct { + // SessionID SessionID + SessionID string + IPAddress string + Username string +} + +// UserSessions is a collection of UserSession values. Intended for +// aggregation before bulk processing of some kind. +type UserSessions []UserSession + +// SessionsReader is an interface used as the API for retrieving user sessions +// from one of the audit log or active users and hosts files. +type SessionsReader interface { + + // AllUserSessions returns a list of all session IDs along with their associated + // IP Address in the form of a slice of UserSession values. This list of + // session IDs is intended for further processing such as filtering to a + // specific username or aggregating to check thresholds. + AllUserSessions() (UserSessions, error) + + // UserSessions uses the previously provided username to return a list of + // all matching session IDs along with their associated IP Address in the + // form of a slice of UserSession values. + MatchingUserSessions() (UserSessions, error) + + // SetSearchRetries is a helper method for setting the number of additional + // retries allowed when receiving zero search results. + SetSearchRetries(retries int) error + + // SetSearchDelay is a helper method for setting the delay in seconds between + // search attempts. + SetSearchDelay(delay int) error +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4bb0f02 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module github.com/atc0005/go-ezproxy + +go 1.13 diff --git a/internal/textutils/doc.go b/internal/textutils/doc.go new file mode 100644 index 0000000..c6124e2 --- /dev/null +++ b/internal/textutils/doc.go @@ -0,0 +1,19 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package textutils is an internal package that contains helper functions for +// working with common text structures. +package textutils diff --git a/internal/textutils/inlist.go b/internal/textutils/inlist.go new file mode 100644 index 0000000..ba0ab15 --- /dev/null +++ b/internal/textutils/inlist.go @@ -0,0 +1,28 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package textutils + +// InList is a helper function to emulate Python's `if "x" +// in list:` functionality +func InList(needle string, haystack []string) bool { + for _, item := range haystack { + if item == needle { + return true + } + } + return false +} diff --git a/terminate.go b/terminate.go new file mode 100644 index 0000000..5e60430 --- /dev/null +++ b/terminate.go @@ -0,0 +1,163 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ezproxy + +import ( + "bytes" + "os/exec" + "strings" +) + +// Terminator is an interface that represents the ability to terminate user +// sessions via the Terminate method. +type Terminator interface { + Terminate() []TerminateUserSessionResult +} + +// TerminateUserSessionResult reflects the result of calling the `kill` +// subcommand of the ezproxy binary to terminate a specific user session. +type TerminateUserSessionResult struct { + // SessionID is the specific ID associated with the session that we + // attempt to terminate + SessionID string + + // ExitCode is what the command called by this application returns + ExitCode int + + // StdOut is the output (if any) sent to stdout by the command called from + // this application + StdOut string + + // StdErr is the output (if any) sent to stderr by the command called from + // this application + StdErr string + + // Error is the error (if any) from the attempt to run the specified + // command + Error error +} + +// TerminateUserSession receives the path to an executable and one or many +// UserSession values, calling the `kill` subcommand of that (presumably +// ezproxy) binary. The result code, stdout, stderr output is captured for +// each subcommand call and returned (along with other details) as a slice of +// `TerminateUserSessionResult`. +func TerminateUserSession(executable string, sessions ...UserSession) []TerminateUserSessionResult { + + results := make([]TerminateUserSessionResult, 0, SessionsLimit) + + for _, session := range sessions { + + result := TerminateUserSessionResult{ + SessionID: session.SessionID, + } + + Logger.Printf( + "Terminating session %q for username %q ... ", + session.SessionID, + session.Username, + ) + + // cmd := exec.Command( + // "echo", + // "hello", + // ) + cmd := exec.Command( + executable, + SubCmdNameSessionTerminate, + session.SessionID, + ) + + printCmdStr := func(cmd *exec.Cmd) string { + return strings.Join(cmd.Args, " ") + } + + Logger.Printf("Executing: %s\n", printCmdStr(cmd)) + + // setup buffer to capture stdout + var cmdStdOut bytes.Buffer + cmd.Stdout = &cmdStdOut + + //setup buffer to capture stderr + var cmdStdErr bytes.Buffer + cmd.Stderr = &cmdStdErr + + cmdErr := cmd.Run() + if cmdErr != nil { + + switch v := cmdErr.(type) { + + // returned by LookPath when it fails to classify a file as an + // executable. + case *exec.Error: + + Logger.Printf( + "An error occurred attempting to run %q: %v\n", + printCmdStr(cmd), + v.Error(), + ) + + // command fail; non-zero (unsuccessful) exit code + case *exec.ExitError: + + if cmd.ProcessState.ExitCode() == -1 { + Logger.Println("-1 returned from ExitCode() method") + + if cmd.ProcessState.Exited() { + Logger.Println("cmd has exited per Exited() method") + } else { + Logger.Println("cmd has NOT exited per Exited() method") + } + } + + default: + + Logger.Printf( + "An unexpected error occurred attempting to run %q: [Type: %T Text: %q]\n", + printCmdStr(cmd), + cmdErr, + cmdErr.Error(), + ) + + } + + } + + Logger.Printf("Exit Code: %d\n", cmd.ProcessState.ExitCode()) + Logger.Printf("Captured stdout: %s\n", cmdStdOut.String()) + Logger.Printf("Captured stderr: %s\n", cmdStdErr.String()) + + result.ExitCode = cmd.ProcessState.ExitCode() + result.StdOut = strings.TrimSpace(cmdStdOut.String()) + result.StdErr = strings.TrimSpace(cmdStdErr.String()) + result.Error = cmdErr + + results = append(results, result) + + } + + return results + +} + +// Terminate attempts to process each UserSession using the provided +// executable, returning the result code, stdout, stderr output as captured +// for each subcommand call (along with other details) as a slice of +// `TerminateUserSessionResult`. +func (us UserSessions) Terminate(executable string) []TerminateUserSessionResult { + return TerminateUserSession(executable, us...) +} diff --git a/trafficlog/doc.go b/trafficlog/doc.go new file mode 100644 index 0000000..c49e94f --- /dev/null +++ b/trafficlog/doc.go @@ -0,0 +1,20 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/go-ezproxy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package trafficlog is intended for the processing of EZproxy traffic log +// files. As of this writing, this package is a placeholder for potential +// future work. +package trafficlog