commit c588ade1429ea3143b6836af6a652684873f09c8 Author: Ron Rise Date: Wed Jun 18 16:46:01 2025 -0400 feat: add initial implementation of Go utilities and CI workflows diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..eb10459 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,48 @@ +on: + push: + tags: + - "v*" + +name: ๐Ÿš€ Publish Release Package + +jobs: + publish: + env: + NODE_TLS_REJECT_UNAUTHORIZED: 0 + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›ก๏ธ ๐Ÿ”’ Add Siteworx CA Certificates + run: | + curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt + update-ca-certificates + + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v2 + + - name: Install gopack + run: | + curl -L https://github.com/cloudsmith-io/gopack/releases/download/v0.6.0/gopack_0.6.0_linux_amd64.tar.gz -o /tmp/gopack.tar.gz + tar -xzf /tmp/gopack.tar.gz -C /tmp + mv /tmp/gopack /usr/local/bin/gopack + + - name: ๐Ÿ“ฆ Build Go Package + run: | + gopack ${{ github.ref_name }} . + + - name: ๐Ÿ“ฆ Publish Build Artifacts + uses: christopherhx/gitea-upload-artifact@v4 + with: + name: ${{ github.ref_name }} + path: ${{ github.ref_name }}.zip + retention-days: 1 + + - name: โ˜๏ธ Upload release package + run: | + curl --user ${{ secrets.PACKAGE_PUBLISH_USER }}:${{ secrets.PACKAGE_PUBLISH_TOKEN }} \ + --upload-file ${{ github.ref_name }}.zip \ + ${{ gitea.server_url }}/api/packages/packages/go/upload + if [ $? -ne 0 ]; then + echo "Error uploading release package" + exit 1 + fi + echo "Upload successful" \ No newline at end of file diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..ca23123 --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -0,0 +1,77 @@ +on: + workflow_dispatch: {} + push: + branches: + - "*" + +name: ๐Ÿงช โœจ Tests Workflow + +env: + GO_VERSION: '1.24.3' + +jobs: + lint-go: + name: ๐Ÿ” ๐Ÿน Go Lint + runs-on: ubuntu-latest + steps: + + - name: ๐Ÿ›ก๏ธ ๐Ÿ”’ Add Siteworx CA Certificates + run: | + curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt + update-ca-certificates + + - name: โš™๏ธ ๐Ÿน Set up Go Environment + uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: ๐Ÿ“– ๐Ÿ” Checkout Repository Code + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: ๐Ÿ“ฆ ๐Ÿ“ฅ Install Dependencies + run: | + go mod download + + - name: โœ… ๐Ÿ” Run Go Lint + uses: golangci/golangci-lint-action@v8.0.0 + + test-go: + name: ๐Ÿ” ๐Ÿน Go Tests + runs-on: ubuntu-latest + steps: + + - name: ๐Ÿ›ก๏ธ ๐Ÿ”’ Add Siteworx CA Certificates + run: | + curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt + update-ca-certificates + + - name: โš™๏ธ ๐Ÿน Set up Go Environment + uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: ๐Ÿ“– ๐Ÿ” Checkout Repository Code + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: ๐Ÿ“ฆ ๐Ÿ“ฅ Install Dependencies + run: | + go mod download + + - name: โœ… ๐Ÿ” Run Go Tests + run: | + go test -v ./... -coverprofile=coverage.out + + - name: ๐Ÿ“Š ๐Ÿ“ˆ Upload Coverage Report + env: + NODE_TLS_REJECT_UNAUTHORIZED: 0 + uses: christopherhx/gitea-upload-artifact@v4 + with: + name: coverage-report + path: coverage.out + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..bc0a40e --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,26 @@ +version: "2" +linters: + default: standard + enable: + - whitespace + - tagalign + - reassign + - bodyclose + - contextcheck + - containedctx + - godot + - usestdlibvars + - gochecknoglobals + - tagalign + - sqlclosecheck + - rowserrcheck + - recvcheck + - reassign + - predeclared + - maintidx + - mnd + +formatters: + settings: + gofmt: + simplify: true \ No newline at end of file diff --git a/Env/env.go b/Env/env.go new file mode 100755 index 0000000..6193bca --- /dev/null +++ b/Env/env.go @@ -0,0 +1,55 @@ +package Env + +import ( + "log" + "os" + "strconv" + "strings" +) + +type EnvironmentVariable string + +func (v EnvironmentVariable) GetName() string { + return string(v) +} + +func (v EnvironmentVariable) GetEnvString(fallback string) string { + if value, ok := os.LookupEnv(string(v)); ok { + return value + } + + return fallback +} + +func (v EnvironmentVariable) GetEnvBool(fallback bool) bool { + value, ok := os.LookupEnv(string(v)) + + if !ok { + return fallback + } + + value = strings.ToLower(value) + trueValues := []string{"1", "true"} + + for _, x := range trueValues { + if value == x { + return true + } + } + + return false +} + +func (v EnvironmentVariable) GetEnvInt(key string, fallback int64) int64 { + if value, ok := os.LookupEnv(key); ok { + i, err := strconv.ParseInt(value, 10, 64) + + if err != nil { + log.Fatal(err) + } + + return i + } + + return fallback +} diff --git a/Env/env_test.go b/Env/env_test.go new file mode 100644 index 0000000..415d3e0 --- /dev/null +++ b/Env/env_test.go @@ -0,0 +1,69 @@ +package Env + +import ( + "os" + "testing" +) + +func TestEnvironmentVariable_GetName(t *testing.T) { + v := EnvironmentVariable("TEST_VAR") + if v.GetName() != "TEST_VAR" { + t.Errorf("expected 'TEST_VAR', got '%s'", v.GetName()) + } +} + +func TestEnvironmentVariable_GetEnvString(t *testing.T) { + const envName = "TEST_STRING" + _ = os.Setenv(envName, "value") + defer func() { + _ = os.Unsetenv(envName) + }() + + v := EnvironmentVariable(envName) + got := v.GetEnvString("fallback") + if got != "value" { + t.Errorf("expected 'value', got '%s'", got) + } + + _ = os.Unsetenv(envName) + got = v.GetEnvString("fallback") + if got != "fallback" { + t.Errorf("expected 'fallback', got '%s'", got) + } +} + +func TestEnvironmentVariable_GetEnvBool(t *testing.T) { + const envName = "TEST_BOOL" + v := EnvironmentVariable(envName) + + _ = os.Setenv(envName, "true") + if !v.GetEnvBool(false) { + t.Errorf("expected true for 'true'") + } + _ = os.Setenv(envName, "1") + if !v.GetEnvBool(false) { + t.Errorf("expected true for '1'") + } + _ = os.Setenv(envName, "false") + if v.GetEnvBool(true) { + t.Errorf("expected false for 'false'") + } + _ = os.Unsetenv(envName) + if v.GetEnvBool(true) != true { + t.Errorf("expected fallback true") + } +} + +func TestEnvironmentVariable_GetEnvInt(t *testing.T) { + const envName = "TEST_INT" + v := EnvironmentVariable(envName) + + _ = os.Setenv(envName, "42") + if v.GetEnvInt(envName, 10) != 42 { + t.Errorf("expected 42") + } + _ = os.Unsetenv(envName) + if v.GetEnvInt(envName, 10) != 10 { + t.Errorf("expected fallback 10") + } +} diff --git a/Maps/maps.go b/Maps/maps.go new file mode 100755 index 0000000..4e9965f --- /dev/null +++ b/Maps/maps.go @@ -0,0 +1,11 @@ +package Maps + +//goland:noinspection GoUnusedExportedFunction // library function +func IsElementExist(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + return false +} diff --git a/Maps/maps_test.go b/Maps/maps_test.go new file mode 100644 index 0000000..6fe72b2 --- /dev/null +++ b/Maps/maps_test.go @@ -0,0 +1,26 @@ +package Maps + +import "testing" + +func TestIsElementExist(t *testing.T) { + tests := []struct { + name string + s []string + str string + expected bool + }{ + {"element exists", []string{"a", "b", "c"}, "b", true}, + {"element does not exist", []string{"a", "b", "c"}, "d", false}, + {"empty slice", []string{}, "a", false}, + {"multiple same elements", []string{"a", "b", "a"}, "a", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsElementExist(tt.s, tt.str) + if result != tt.expected { + t.Errorf("IsElementExist(%v, %q) = %v; want %v", tt.s, tt.str, result, tt.expected) + } + }) + } +} diff --git a/README.md b/README.md new file mode 100755 index 0000000..909e9db --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Utilities diff --git a/Yubikey/otp.go b/Yubikey/otp.go new file mode 100755 index 0000000..5063a25 --- /dev/null +++ b/Yubikey/otp.go @@ -0,0 +1,114 @@ +package Yubikey + +import ( + "errors" + "fmt" + "log" + "math/rand" + "net/http" + "strconv" + "strings" + "time" +) + +type Response struct { + h string + t time.Time + otp string + presentedNonce string + nonce string + sl uint8 + timestamp uint32 + sessionCounter uint32 + sessionUse uint32 + status string +} + +const keyLen = 40 + +func (response *Response) Time() time.Time { + return response.t +} + +func (response *Response) TimeStamp() uint32 { + return response.timestamp +} + +func (response *Response) Valid() bool { + return response.status == "OK" && response.nonce == response.presentedNonce +} + +//goland:noinspection GoUnusedExportedFunction // library function +func GetVerification(otp string, clientId string) (*Response, error) { + nonce := randSeq(keyLen) + + url := fmt.Sprintf( + "https://api.yubico.com/wsapi/2.0/verify?id=%s&otp=%s×tamp=1&nonce=%s", + clientId, + otp, + nonce, + ) + + response, _ := http.Get(url) + + defer func() { + _ = response.Body.Close() + }() + + body := make([]byte, response.ContentLength) + _, err := response.Body.Read(body) + if err != nil { + log.Fatal(err) + } + + parts := strings.Split(string(body), "\r\n") + + var status string + for k, part := range parts { + if strings.Contains(part, "status=") { + status = strings.Replace(parts[k], "status=", "", 1) + break + } + } + + if status != "OK" { + return nil, errors.New("invalid response status: " + status) + } + + timeString := strings.Replace(parts[1], "t=", "", 1)[0:20] + + signedTime, err := time.Parse(time.RFC3339, timeString) + if err != nil { + println(err) + } + + sl, _ := strconv.ParseInt(strings.Replace(parts[4], "sl=", "", 1), 10, 8) + timestamp, _ := strconv.ParseInt(strings.Replace(parts[5], "timestamp=", "", 1), 10, 32) + sessionCounter, _ := strconv.ParseInt(strings.Replace(parts[6], "sessioncounter=", "", 1), 10, 32) + sessionUse, _ := strconv.ParseInt(strings.Replace(parts[7], "sessionuse=", "", 1), 10, 32) + + keyResponse := Response{ + h: strings.Replace(parts[0], "h=", "", 1), + t: signedTime, + otp: strings.Replace(parts[2], "otp=", "", 1), + presentedNonce: nonce, + nonce: strings.Replace(parts[3], "nonce=", "", 1), + sl: uint8(sl), + timestamp: uint32(timestamp), + sessionCounter: uint32(sessionCounter), + sessionUse: uint32(sessionUse), + status: status, + } + + return &keyResponse, nil +} + +func randSeq(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} diff --git a/Yubikey/otp_test.go b/Yubikey/otp_test.go new file mode 100644 index 0000000..9c62995 --- /dev/null +++ b/Yubikey/otp_test.go @@ -0,0 +1,44 @@ +package Yubikey + +import ( + "testing" + "time" +) + +// Test the Valid method of Response. +func TestResponse_Valid(t *testing.T) { + resp := &Response{ + status: "OK", + nonce: "abc123", + presentedNonce: "abc123", + } + if !resp.Valid() { + t.Errorf("Expected Valid() to return true") + } + + resp.status = "BAD" + if resp.Valid() { + t.Errorf("Expected Valid() to return false when status is not OK") + } + + resp.status = "OK" + resp.nonce = "xyz" + if resp.Valid() { + t.Errorf("Expected Valid() to return false when nonce does not match presentedNonce") + } +} + +// Test the Time and TimeStamp methods. +func TestResponse_TimeAndTimeStamp(t *testing.T) { + now := time.Now() + resp := &Response{ + t: now, + timestamp: 12345, + } + if !resp.Time().Equal(now) { + t.Errorf("Expected Time() to return the correct time") + } + if resp.TimeStamp() != 12345 { + t.Errorf("Expected TimeStamp() to return 12345") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..012b2f7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.siteworxpro.com/packages/golang-utilities + +go 1.23.4