feat: add initial implementation of Go utilities and CI workflows
This commit is contained in:
48
.gitea/workflows/publish.yml
Normal file
48
.gitea/workflows/publish.yml
Normal file
@@ -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"
|
77
.gitea/workflows/tests.yml
Normal file
77
.gitea/workflows/tests.yml
Normal file
@@ -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
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea/
|
26
.golangci.yml
Normal file
26
.golangci.yml
Normal file
@@ -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
|
55
Env/env.go
Executable file
55
Env/env.go
Executable file
@@ -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
|
||||
}
|
69
Env/env_test.go
Normal file
69
Env/env_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
11
Maps/maps.go
Executable file
11
Maps/maps.go
Executable file
@@ -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
|
||||
}
|
26
Maps/maps_test.go
Normal file
26
Maps/maps_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
114
Yubikey/otp.go
Executable file
114
Yubikey/otp.go
Executable file
@@ -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)
|
||||
}
|
44
Yubikey/otp_test.go
Normal file
44
Yubikey/otp_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user