commit 4d168ce8b8aeb2c08f4a5e8e4ff59d34678213ac Author: GRMrGecko Date: Thu Mar 7 11:56:15 2024 -0600 First commit diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8704403 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,29 @@ +on: + release: + types: [created] + +permissions: + contents: write + packages: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v4 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..abab3db --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,30 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 1 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: "{{ .ProjectName }}-{{ .Version }}.{{ .Os }}-{{ .Arch }}" + wrap_in_directory: true + strip_parent_binary_folder: false diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ade9c16 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2024 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3637e2f --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# goreplay-http-logger + +I needed a way to directly capture http traffic for use with [GoReplay](https://goreplay.org/), and there did not seem to be an official method. As such, I wrote my own quick server to do the job. I may as well share it with the world as it has been useful to me. I did not do anything fancy here, just a simple cli argument configuration. + +``` +$ ./goreplay-http-logger --help +http log server + -bind string + HTTP bind address + -log-file string + Log file name with date (default "http-%Y%m%d.log") + -port int + HTTP port (default 8080) +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ee4cdaf --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/grmrgecko/goreplay-http-logger + +go 1.20 diff --git a/main.go b/main.go new file mode 100644 index 0000000..724df09 --- /dev/null +++ b/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// Separator between http requests. +var payloadSeparator = "\nšŸµšŸ™ˆšŸ™‰\n" + +// Generate random hex. +func randByte(len int) []byte { + b := make([]byte, len/2) + rand.Read(b) + + h := make([]byte, len) + hex.Encode(h, b) + + return h +} + +// Generate a uuid for a request. +func uuid() []byte { + return randByte(24) +} + +// Generate a header with type 1 for a request. +func payloadHeader(uuid []byte, timing int64) (header []byte) { + return []byte(fmt.Sprintf("1 %s %d 0\n", uuid, timing)) +} + +// The log file structure for writing logs. +type LogFile struct { + sync.Mutex + file *os.File + currentName string + nextUpdate time.Time +} + +// Write to log file. +func (l *LogFile) write(data []byte) (n int, err error) { + // Lock to prevent multiple writes at the same time. + l.Lock() + defer l.Unlock() + + // Get current time for file name generator. + now := time.Now() + // If no file defined or we are after the next update time, update the tile name. + if l.file == nil || now.After(l.nextUpdate) { + // Set next update to a second later, truncating the nanoseconds. + l.nextUpdate = now.Truncate(time.Second).Add(time.Second) + + // Generate the log file name based on config. + name := config.LogFile + // Year + name = strings.ReplaceAll(name, "%Y", now.Format("2006")) + // Month + name = strings.ReplaceAll(name, "%m", now.Format("01")) + // Day + name = strings.ReplaceAll(name, "%d", now.Format("02")) + // Hour + name = strings.ReplaceAll(name, "%H", now.Format("15")) + // Minute + name = strings.ReplaceAll(name, "%M", now.Format("04")) + // Second + name = strings.ReplaceAll(name, "%S", now.Format("05")) + l.currentName = filepath.Clean(name) + + // If new name generated is different from existing open file or no file is opened, open it. + if l.file == nil || l.currentName != l.file.Name() { + l.file, err = os.OpenFile(l.currentName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660) + + if err != nil { + log.Fatalf("Cannot open file %q. Error: %s", l.currentName, err) + } + } + } + + // Write data. + n, err = l.file.Write(data) + return +} + +// Global log file definition. +var logFile *LogFile + +// Log the http request. +func logRequest(w http.ResponseWriter, r *http.Request) { + log.Printf("Request %s %s", r.Method, r.URL) + + // Generate log entry. + var buff bytes.Buffer + fmt.Fprintf(&buff, "%s", payloadHeader(uuid(), time.Now().UnixNano())) + fmt.Fprintf(&buff, "%s %s %s\n", r.Method, r.URL, r.Proto) + r.Header.Write(&buff) + fmt.Fprint(&buff, "\r\n") + body, _ := io.ReadAll(r.Body) + buff.Write(body) + fmt.Fprint(&buff, payloadSeparator) + + // Write to log file. + logFile.write(buff.Bytes()) +} + +// The config for this run, set in flags. +type Config struct { + HTTPBind string + HTTPPort int + LogFile string +} + +// Global config variable. +var config Config + +// Generate help information. +func usage() { + fmt.Println("http log server") + flag.PrintDefaults() + os.Exit(2) +} + +// The main program. +func main() { + // Parse flags. + flag.Usage = usage + flag.StringVar(&config.HTTPBind, "bind", "", "HTTP bind address") + flag.IntVar(&config.HTTPPort, "port", 8080, "HTTP port") + flag.StringVar(&config.LogFile, "log-file", "http-%Y%m%d.log", "Log file name with date") + flag.Parse() + + // Setup the log file. + logFile = new(LogFile) + + // Handle all http requests with the logRequest handler. + http.HandleFunc("/", logRequest) + + // Start the HTTP server/ + fmt.Printf("Starting server at port %d\n", config.HTTPPort) + if err := http.ListenAndServe(fmt.Sprintf("%s:%d", config.HTTPBind, config.HTTPPort), nil); err != nil { + log.Fatal(err) + } +}