From 82fcd41ef47342d77f189189c4e81db4112db364 Mon Sep 17 00:00:00 2001
From: vanDeventer
Date: Tue, 27 May 2025 14:12:40 +0200
Subject: [PATCH 001/191] redo configuration to have configurable services
---
components/{device.go => host.go} | 0
components/husk.go | 2 +-
components/service.go | 11 +-
components/system.go | 54 ++++++++--
components/uasset.go | 5 +
usecases/authentication.go | 57 ++++++----
usecases/configuration.go | 144 ++++++++++++++------------
usecases/docs.go | 24 +++--
usecases/registration.go | 2 +
usecases/serversNhandlers.go | 8 +-
usecases/{packing.go => utilities.go} | 0
11 files changed, 197 insertions(+), 110 deletions(-)
rename components/{device.go => host.go} (100%)
rename usecases/{packing.go => utilities.go} (100%)
diff --git a/components/device.go b/components/host.go
similarity index 100%
rename from components/device.go
rename to components/host.go
diff --git a/components/husk.go b/components/husk.go
index 431562a..0c56363 100644
--- a/components/husk.go
+++ b/components/husk.go
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2024 Synecdoque
+ * Copyright (c) 2025 Synecdoque
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/components/service.go b/components/service.go
index ff8a14c..503a98b 100644
--- a/components/service.go
+++ b/components/service.go
@@ -26,7 +26,7 @@ package components
type Service struct {
ID int `json:"-"` // Id assigned by the Service Registrar
Definition string `json:"definition"` // Service definition or purpose
- SubPath string `json:"-"` // The URL subpath after the resource's
+ SubPath string `json:"subpath"` // The URL subpath after the resource's
Details map[string][]string `json:"details"` // Metadata or details about the service
RegPeriod int `json:"registrationPeriod"` // The period until the registrar is expecting a sign of life
RegTimestamp string `json:"-"` // the creation date in the Service Registry to ensure that reRegistration is with the same record
@@ -113,10 +113,11 @@ func MergeDetails(map1, map2 map[string][]string) map[string][]string {
// A Cervice is a consumed service
type Cervice struct {
- Definition string
- Details map[string][]string
- Nodes map[string][]string
- Protos []string
+ IReferentce string // Internal reference when consuming more than one service of the same type
+ Definition string // Service definition or purpose
+ Details map[string][]string
+ Nodes map[string][]string
+ Protos []string
}
// Cervises is a collection of "Cervice" structs
diff --git a/components/system.go b/components/system.go
index abccc4b..a9ac4cb 100644
--- a/components/system.go
+++ b/components/system.go
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2024 Synecdoque
+ * Copyright (c) 2025 Synecdoque
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -24,14 +24,17 @@ package components
import (
"context"
"fmt"
+ "io"
+ "net/http"
"os"
"os/signal"
+ "strings"
"syscall"
)
-// System struct aggragates an Arrowhead compliant system
+// System struct aggregates an Arrowhead compliant system
type System struct {
- Name string `json:"systemname"`
+ Name string `json:"systemName"`
Host *HostingDevice // the system runs on a device
Husk *Husk // the system aggregates a "husk" (a wrapper or a shell)
UAssets map[string]*UnitAsset // the system aggregates "asset", which is made up of one or more unit-asset
@@ -43,9 +46,8 @@ type System struct {
// CoreSystem struct holds details about the core system included in the configuration file
type CoreSystem struct {
- Name string `json:"coresystem"`
- Url string `json:"url"`
- Certificate string `json:"-"`
+ Name string `json:"coreSystem"`
+ Url string `json:"url"`
}
// NewSystem instantiates the new system and gathers the host information
@@ -61,6 +63,46 @@ func NewSystem(name string, ctx context.Context) System {
return newSystem
}
+// GetRunningCoreSystemURL returns the URL of a running core system based on the provided type.
+// When systemType is "serviceregistrar", it verifies the service is the lead registrar by checking
+// its /status endpoint response. For other core system types, it simply tests that the URL is accessible.
+func GetRunningCoreSystemURL(sys *System, systemType string) (string, error) {
+ for _, core := range sys.CoreS {
+ if core.Name == systemType {
+ // Special logic for the service registrar: check the status endpoint
+ if systemType == "serviceregistrar" {
+ statusURL := core.Url + "/status"
+ resp, err := http.Get(statusURL)
+ if err != nil {
+ fmt.Printf("error checking service registrar status at %s: %v\n", statusURL, err)
+ continue // Try the next core system instance, if any.
+ }
+ bodyBytes, err := io.ReadAll(resp.Body)
+ resp.Body.Close() // Always close the response body when done.
+ if err != nil {
+ fmt.Printf("error reading response from %s: %v\n", statusURL, err)
+ continue
+ }
+ // Verify status response
+ if strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") {
+ fmt.Printf("Lead service registrar found at: %s\n", core.Url)
+ return core.Url, nil
+ }
+ } else {
+ // For other core systems, verify that the service is accessible.
+ resp, err := http.Get(core.Url)
+ if err != nil {
+ fmt.Printf("error checking %s at %s: %v\n", systemType, core.Url, err)
+ continue
+ }
+ resp.Body.Close()
+ return core.Url, nil
+ }
+ }
+ }
+ return "", fmt.Errorf("failed to locate running core system of type %s", systemType)
+}
+
// The following code is used only for issues support on GitHub @sdoque --------------------------
var (
AppName string
diff --git a/components/uasset.go b/components/uasset.go
index 300ba79..65ee1e0 100644
--- a/components/uasset.go
+++ b/components/uasset.go
@@ -33,3 +33,8 @@ type UnitAsset interface {
GetDetails() map[string][]string
Serving(w http.ResponseWriter, r *http.Request, servicePath string)
}
+
+// HasTraits is an interface that defines a method to get traits of a UnitAsset.
+type HasTraits interface {
+ GetTraits() any // or interface{} in older Go
+}
diff --git a/usecases/authentication.go b/usecases/authentication.go
index 134ff25..0e531d2 100644
--- a/usecases/authentication.go
+++ b/usecases/authentication.go
@@ -29,6 +29,7 @@ import (
"encoding/pem"
"fmt"
"log"
+ "net"
"net/http"
"strings"
@@ -40,18 +41,28 @@ func RequestCertificate(sys *components.System) {
// Generate ECDSA Private Key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
- log.Fatalf("Failed to generate private key: %v", err)
+ log.Fatalf("Failed to generate private key: %v\n", err)
}
sys.Husk.Pkey = privateKey
+ dnsNames := []string{"localhost"}
+ var ipAddrs []net.IP
+ for _, ipStr := range sys.Host.IPAddresses {
+ ip := net.ParseIP(ipStr)
+ if ip != nil {
+ ipAddrs = append(ipAddrs, ip)
+ }
+ }
csrTemplate := x509.CertificateRequest{
Subject: sys.Husk.DName,
+ DNSNames: dnsNames, // this is the SAN DNS
+ IPAddresses: ipAddrs, // this is the SAN IPs
SignatureAlgorithm: x509.ECDSAWithSHA256,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey)
if err != nil {
- log.Fatalf("Failed to create CSR: %v", err)
+ log.Fatalf("Failed to create CSR: %v\n", err)
return
}
@@ -61,7 +72,7 @@ func RequestCertificate(sys *components.System) {
// Send the CSR to the CA and receive the certificate in response
response, err := sendCSR(sys, csrPEM)
if err != nil {
- log.Printf("certification failure: %v", err)
+ log.Printf("certification failure: %v\n", err)
return
}
@@ -71,7 +82,7 @@ func RequestCertificate(sys *components.System) {
// Get CA's certificate
caCert, err := getCACertificate(sys)
if err != nil {
- log.Printf("failed to obtain CA's certificate: %v", err)
+ log.Printf("failed to obtain CA's certificate: %v\n", err)
return
}
sys.Husk.CA_cert = caCert
@@ -79,13 +90,13 @@ func RequestCertificate(sys *components.System) {
// Load CA certificate
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM([]byte(caCert)); !ok {
- log.Fatalf("Failed to append CA certificate to pool")
+ log.Fatalf("Failed to append CA certificate to pool\n")
}
// Prepare the client's certificate and key for TLS configuration
clientCert, err := prepareClientCertificate(sys.Husk.Certificate, sys.Husk.Pkey)
if err != nil {
- log.Fatalf("Failed to prepare client certificate: %v", err)
+ log.Fatalf("Failed to prepare client certificate: %v\n", err)
}
// Configure Transport Layer Security (TLS)
@@ -100,7 +111,7 @@ func RequestCertificate(sys *components.System) {
fmt.Printf("System %s's parsed Certificate:\n", sys.Name)
cert, err := x509.ParseCertificate(clientCert.Certificate[0])
if err != nil {
- log.Printf("failed to parse certificate: %v", err)
+ log.Printf("failed to parse certificate: %v\n", err)
return
}
fmt.Printf(" Subject: %s\n", cert.Subject)
@@ -108,6 +119,9 @@ func RequestCertificate(sys *components.System) {
fmt.Printf(" Serial Number: %d\n", cert.SerialNumber)
fmt.Printf(" Not Before: %s\n", cert.NotBefore)
fmt.Printf(" Not After: %s\n", cert.NotAfter)
+ fmt.Printf(" DNS Names: %v\n", cert.DNSNames)
+ fmt.Printf(" IP Addresses: %v\n", cert.IPAddresses)
+
}
func sendCSR(sys *components.System, csrPEM []byte) (string, error) {
@@ -144,18 +158,25 @@ func sendCSR(sys *components.System, csrPEM []byte) (string, error) {
// getCACertificate gets the CA's certificate necessary for the dual server-client authentication in the TLS setup
func getCACertificate(sys *components.System) (string, error) {
- var err error
- coreUAurl := ""
- for _, cSys := range sys.CoreS {
- core := cSys
- if core.Name == "ca" {
- coreUAurl = core.Url
- }
- }
- if coreUAurl == "" {
- return "", fmt.Errorf("failed to locate certificate authority: %w", err)
+ // var err error
+ // coreUAurl := ""
+ // for _, cSys := range sys.CoreS {
+ // core := cSys
+ // if core.Name == "ca" {
+ // coreUAurl = core.Url
+ // }
+ // }
+ // if coreUAurl == "" {
+ // return "", fmt.Errorf("failed to locate certificate authority: %w", err)
+ // }
+
+ // Get the URL of the CA's configuration
+ coreUAurl, err := components.GetRunningCoreSystemURL(sys, "ca") // Assuming the first core system is the CA
+ if err != nil {
+ return "", fmt.Errorf("failed to get CA URL: %w", err)
}
- url := strings.TrimSuffix(coreUAurl, "ification") // the configuration file address to the CA includes the unit asset
+ // Remove the "ification" suffix from the URL to get the CA's address
+ url := strings.TrimSuffix(coreUAurl, "ification")
// Make a GET request to the CA's endpoint
resp, err := http.Get(url)
diff --git a/usecases/configuration.go b/usecases/configuration.go
index 4862200..7a7a5e8 100644
--- a/usecases/configuration.go
+++ b/usecases/configuration.go
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2024 Synecdoque
+ * Copyright (c) 2025 Synecdoque
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,7 +22,6 @@
package usecases
import (
- "crypto/x509/pkix"
"encoding/json"
"fmt"
"os"
@@ -30,14 +29,21 @@ import (
"github.com/sdoque/mbaigo/components"
)
+// configurableAsset is a struct that contains the name of the asset and its
+// configurable details and services
+type ConfigurableAsset struct {
+ Name string `json:"name"`
+ Details map[string][]string `json:"details"`
+ Services []components.Service `json:"services"`
+ Traits []json.RawMessage `json:"traits"`
+}
+
// templateOut is the stuct used to prepare the systemconfig.json file
type templateOut struct {
- CName string `json:"systemname"`
- UAsset []components.UnitAsset `json:"unit_assets"`
- CServices []components.Service `json:"services"`
- Protocols map[string]int `json:"protocolsNports"`
- PKIdetails pkix.Name `json:"distinguishedName"`
- CCoreS []components.CoreSystem `json:"coreSystems"`
+ CName string `json:"systemname"`
+ Assets []ConfigurableAsset `json:"unit_assets"`
+ Protocols map[string]int `json:"protocolsNports"`
+ CCoreS []components.CoreSystem `json:"coreSystems"`
}
// configFileIn is used to extact out the information of the systemconfig.json file
@@ -46,77 +52,95 @@ type templateOut struct {
type configFileIn struct {
CName string `json:"systemname"`
rawResources []json.RawMessage `json:"-"`
- CServices []components.Service `json:"services"`
Protocols map[string]int `json:"protocolsNports"`
- PKIdetails pkix.Name `json:"distinguishedName"`
CCoreS []components.CoreSystem `json:"coreSystems"`
}
-// Configure read the system configuration JSON file to get the deployment details.
+// Configure reads the system configuration JSON file to get the deployment details.
// If the file is missing, it generates a default systemconfig.json file and shuts down the system
-func Configure(sys *components.System) ([]json.RawMessage, []components.Service, error) {
-
- var rawBytes []json.RawMessage // the mbaigo library does not know about the unit asset's structure (defined in the file thing.go and not part of the library)
- var servicesList []components.Service // this is the list of services for each unit asset
+func Configure(sys *components.System) ([]json.RawMessage, error) {
// prepare content of configuration file
var defaultConfig templateOut
+ // var servicesList []components.Service // this is the list of services for each unit asset
+
+ var assetTemplate components.UnitAsset
+ for _, ua := range sys.UAssets {
+ assetTemplate = *ua // this creates a copy (value, not reference)
+ break // stop after the first entry
+ }
+ servicesTemplate := getServicesList(assetTemplate)
+
+ confAsset := ConfigurableAsset{
+ Name: assetTemplate.GetName(),
+ Details: assetTemplate.GetDetails(),
+ Services: servicesTemplate,
+ }
+
+ // If the asset exposes traits, serialize them and store as raw JSON
+ if assetWithTraits, ok := assetTemplate.(components.HasTraits); ok {
+ if traits := assetWithTraits.GetTraits(); traits != nil {
+ traitJSON, err := json.Marshal(traits)
+ if err == nil {
+ confAsset.Traits = []json.RawMessage{traitJSON}
+ } else {
+ fmt.Println("Warning: could not marshal traits:", err)
+ }
+ }
+ }
+
defaultConfig.CName = sys.Name
defaultConfig.Protocols = sys.Husk.ProtoPort
- defaultConfig.UAsset = getFirstAsset(sys.UAssets)
- originalSs := getServicesList(defaultConfig.UAsset[0])
- defaultConfig.CServices = originalSs
-
- defaultConfig.PKIdetails.CommonName = "arrowhead.eu"
- defaultConfig.PKIdetails.Country = []string{"SE"}
- defaultConfig.PKIdetails.Province = []string{"Norrbotten"}
- defaultConfig.PKIdetails.Locality = []string{"Luleaa"}
- defaultConfig.PKIdetails.Organization = []string{"Luleaa University of Technology"}
- defaultConfig.PKIdetails.OrganizationalUnit = []string{"CPS"}
+ defaultConfig.Assets = []ConfigurableAsset{confAsset} // this is a list of unit assets
serReg := components.CoreSystem{
- Name: "serviceregistrar",
- Url: "http://localhost:20102/serviceregistrar/registry",
- Certificate: ".X509pubKey",
+ Name: "serviceregistrar",
+ Url: "http://localhost:20102/serviceregistrar/registry",
}
orches := components.CoreSystem{
- Name: "orchestrator",
- Url: "http://localhost:20103/orchestrator/orchestration",
- Certificate: ".X509pubKey",
+ Name: "orchestrator",
+ Url: "http://localhost:20103/orchestrator/orchestration",
}
ca := components.CoreSystem{
- Name: "ca",
- Url: "http://localhost:20100/ca/certification",
- Certificate: ".X509pubKey",
+ Name: "ca",
+ Url: "http://localhost:20100/ca/certification",
}
- coreSystems := []components.CoreSystem{serReg, orches, ca}
+ maitreD := components.CoreSystem{
+ Name: "maitreD",
+ Url: "http://localhost:20101/maitreD/maitreD",
+ }
+ // add the core systems to the configuration file
+ // the system is part of a local cloud with mandatory core systems
+ coreSystems := []components.CoreSystem{serReg, orches, ca, maitreD}
defaultConfig.CCoreS = coreSystems
+ var rawBytes []json.RawMessage // the mbaigo library does not know about the unit asset's structure (defined in the file thing.go and not part of the library)
+
// open the configuration file or create one with the default content prepared above
systemConfigFile, err := os.Open("systemconfig.json")
if err != nil { // could not find the systemconfig.json so a default one is being created
defaultConfigFile, err := os.Create("systemconfig.json")
if err != nil {
- return rawBytes, servicesList, err
+ return rawBytes, err
}
defer defaultConfigFile.Close()
- systemconfigjson, err := json.MarshalIndent(defaultConfig, "", " ")
+ systemconfigjson, err := json.MarshalIndent(defaultConfig, "", " ")
if err != nil {
- return rawBytes, servicesList, err
+ return rawBytes, err
}
nBytes, err := defaultConfigFile.Write(systemconfigjson)
if err != nil {
- return rawBytes, servicesList, err
+ return rawBytes, err
}
- return rawBytes, servicesList, fmt.Errorf("a new configuration file has been written with %d bytes. Please update it and restart the system", nBytes)
+ return rawBytes, fmt.Errorf("a new configuration file has been written with %d bytes. Please update it and restart the system", nBytes)
}
// the system configuration file could be open, read the configurations and pass them on to the system
defer systemConfigFile.Close()
configBytes, err := os.ReadFile("systemconfig.json")
if err != nil {
- return rawBytes, servicesList, err
+ return rawBytes, err
}
// the challenge is that the definition of the unit asset is unknown to the mbaigo library and only known to the system that invokes the library
@@ -130,13 +154,13 @@ func Configure(sys *components.System) ([]json.RawMessage, []components.Service,
Alias: (*Alias)(&configurationIn),
}
if err := json.Unmarshal(configBytes, aux); err != nil {
- return rawBytes, servicesList, err
+ return rawBytes, err
}
if len(aux.Resources) > 0 {
configurationIn.rawResources = aux.Resources
} else {
var rawMessages []json.RawMessage
- for _, s := range defaultConfig.UAsset {
+ for _, s := range defaultConfig.Assets {
// convert the struct to JSON-encoded byte array
jsonBytes, err := json.Marshal(s)
if err != nil {
@@ -148,34 +172,13 @@ func Configure(sys *components.System) ([]json.RawMessage, []components.Service,
}
sys.Name = configurationIn.CName
- sys.Husk.DName = configurationIn.PKIdetails
sys.Husk.ProtoPort = configurationIn.Protocols
for _, ccore := range configurationIn.CCoreS {
newCore := ccore
sys.CoreS = append(sys.CoreS, &newCore)
}
- // update the services (e.g., re-registration period, costs, or units)
- for i := range configurationIn.CServices {
- for _, originalService := range originalSs {
- if originalService.Definition == configurationIn.CServices[i].Definition {
- configurationIn.CServices[i].Merge(&originalService) // keep the original definition and subpath as the original ones
- }
- }
- }
- servicesList = configurationIn.CServices
-
- return configurationIn.rawResources, servicesList, nil
-}
-
-// getFirstAsset returns the first key-value pair in the Assets map
-func getFirstAsset(assetMap map[string]*components.UnitAsset) []components.UnitAsset {
- var assetList []components.UnitAsset
- for key := range assetMap {
- assetList = append(assetList, *assetMap[key])
- return assetList
- }
- return assetList
+ return configurationIn.rawResources, nil
}
// getServicesList() returns the original list of services
@@ -187,3 +190,14 @@ func getServicesList(uat components.UnitAsset) []components.Service {
}
return serviceList
}
+
+// MakeServiceMap() creates a map of services from a slice of services
+// The map is indexed by the service subpath
+func MakeServiceMap(services []components.Service) map[string]*components.Service {
+ serviceMap := make(map[string]*components.Service)
+ for i := range services {
+ svc := services[i] // take the address of the element in the slice
+ serviceMap[svc.SubPath] = &svc
+ }
+ return serviceMap
+}
diff --git a/usecases/docs.go b/usecases/docs.go
index 412b542..656b149 100644
--- a/usecases/docs.go
+++ b/usecases/docs.go
@@ -60,17 +60,19 @@ func SysHateoas(w http.ResponseWriter, req *http.Request, sys components.System)
w.Write([]byte(resourceURI))
}
- text = " having the following services:"
- w.Write([]byte(text))
- servicesList := getServicesList(getFirstAsset(*assetList)[0])
- for _, service := range servicesList {
- metaservice := ""
- for key, values := range service.Details {
- metaservice += key + ": " + fmt.Sprintf("%v", values) + " "
- }
- serviceURI := "- " + service.Definition + " with details: " + metaservice + "
"
- w.Write([]byte(serviceURI))
- }
+ // This part of the code is commented out because it is not used in the current implementation because the assets on a PLC might have different services
+ // ======================================
+ // text = "
having the following services:"
+ // w.Write([]byte(text))
+ // servicesList := getServicesList(getFirstAsset(*assetList)[0])
+ // for _, service := range servicesList {
+ // metaservice := ""
+ // for key, values := range service.Details {
+ // metaservice += key + ": " + fmt.Sprintf("%v", values) + " "
+ // }
+ // serviceURI := "- " + service.Definition + " with details: " + metaservice + "
"
+ // w.Write([]byte(serviceURI))
+ // }
text = "
The services can be accessed using the following protocols with their respective bound ports:
"
w.Write([]byte(text))
diff --git a/usecases/registration.go b/usecases/registration.go
index 78b7a05..de8dd6c 100755
--- a/usecases/registration.go
+++ b/usecases/registration.go
@@ -49,6 +49,7 @@ func RegisterServices(sys *components.System) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
+ // Check if the leading registrar is already known
if leadingRegistrar != nil {
resp, err := http.Get(leadingRegistrar.Url + "/status")
if err != nil {
@@ -71,6 +72,7 @@ func RegisterServices(sys *components.System) {
log.Println("lost previous leading registrar")
}
} else {
+ // If the leading registrar is not known, check all core systems
for _, cSys := range sys.CoreS {
core := cSys
if core.Name == "serviceregistrar" {
diff --git a/usecases/serversNhandlers.go b/usecases/serversNhandlers.go
index cba86df..31c86a2 100644
--- a/usecases/serversNhandlers.go
+++ b/usecases/serversNhandlers.go
@@ -37,7 +37,7 @@ import (
"github.com/sdoque/mbaigo/forms"
)
-// SetoutServers setup the http and https servers and starts them
+// SetoutServers setups the http and https servers and starts them
func SetoutServers(sys *components.System) (err error) {
// get the servers port number (from configuration file)
httpPort := sys.Husk.ProtoPort["http"]
@@ -166,7 +166,7 @@ func ResourceHandler(sys *components.System, w http.ResponseWriter, r *http.Requ
return
}
- resourceName := parts[2]
+ assetName := parts[2]
servicePath := ""
if len(parts) > 3 {
servicePath = parts[3]
@@ -180,9 +180,9 @@ func ResourceHandler(sys *components.System, w http.ResponseWriter, r *http.Requ
case 3:
handleThreeParts(w, r, parts[2], sys)
case 4:
- handleFourParts(w, r, resourceName, servicePath, sys)
+ handleFourParts(w, r, assetName, servicePath, sys)
case 5:
- handleFiveParts(w, r, resourceName, servicePath, record, sys)
+ handleFiveParts(w, r, assetName, servicePath, record, sys)
default:
http.Error(w, "Invalid request", http.StatusBadRequest)
}
diff --git a/usecases/packing.go b/usecases/utilities.go
similarity index 100%
rename from usecases/packing.go
rename to usecases/utilities.go
From a8c026a747b1abf69af049bbf00ce18dc13f8f63 Mon Sep 17 00:00:00 2001
From: vanDeventer
Date: Tue, 27 May 2025 17:01:00 +0200
Subject: [PATCH 002/191] cleaning up DName and preparing for naming convention
---
components/husk.go | 2 +-
usecases/utilities.go | 49 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/components/husk.go b/components/husk.go
index 0c56363..ddc26ff 100644
--- a/components/husk.go
+++ b/components/husk.go
@@ -35,7 +35,7 @@ type Husk struct {
Certificate string `json:"-"`
CA_cert string `json:"-"`
TlsConfig *tls.Config `json:"-"` // client side mutual TLS configuration
- DName pkix.Name `json:"distinguishedName"`
+ DName pkix.Name `json:"-"`
Details map[string][]string `json:"details"`
ProtoPort map[string]int `json:"protoPort"`
InfoLink string `json:"onlineDocumentation"`
diff --git a/usecases/utilities.go b/usecases/utilities.go
index 6990274..6307265 100644
--- a/usecases/utilities.go
+++ b/usecases/utilities.go
@@ -28,6 +28,7 @@ import (
"log"
"reflect"
"strings"
+ "unicode"
"github.com/sdoque/mbaigo/forms"
)
@@ -121,3 +122,51 @@ func Unpack(data []byte, contentType string) (forms.Form, error) {
return formInstance, nil
}
+
+// ------- Naming Conventions Tools -------
+
+// ToCamel converts PascalCase to camelCase.
+func ToCamel(s string) string {
+ if s == "" {
+ return s
+ }
+ runes := []rune(s)
+ runes[0] = unicode.ToLower(runes[0])
+ return string(runes)
+}
+
+// ToPascal converts camelCase to PascalCase.
+func ToPascal(s string) string {
+ if s == "" {
+ return s
+ }
+ runes := []rune(s)
+ runes[0] = unicode.ToUpper(runes[0])
+ return string(runes)
+}
+
+// IsFirstLetterUpper returns true if the first rune is uppercase.
+func IsFirstLetterUpper(s string) bool {
+ if s == "" {
+ return false
+ }
+ return unicode.IsUpper([]rune(s)[0])
+}
+
+// IsFirstLetterLower returns true if the first rune is lowercase.
+func IsFirstLetterLower(s string) bool {
+ if s == "" {
+ return false
+ }
+ return unicode.IsLower([]rune(s)[0])
+}
+
+// IsPascalCase returns true if the string starts with an uppercase letter.
+func IsPascalCase(s string) bool {
+ return IsFirstLetterUpper(s)
+}
+
+// IsCamelCase returns true if the string starts with a lowercase letter.
+func IsCamelCase(s string) bool {
+ return IsFirstLetterLower(s)
+}
From 7a3bf8fbeee5342acb73218770570ceacf41f8e5 Mon Sep 17 00:00:00 2001
From: Pake
Date: Thu, 5 Jun 2025 14:15:16 +0200
Subject: [PATCH 003/191] Added Makefile, Github Actions and test template
---
.github/workflows/main.yml | 43 ++++++++++++++++++++++++++++++++++++++
Makefile | 36 +++++++++++++++++++++++++++++++
usecases/test_sample.go | 33 +++++++++++++++++++++++++++++
3 files changed, 112 insertions(+)
create mode 100644 .github/workflows/main.yml
create mode 100644 Makefile
create mode 100644 usecases/test_sample.go
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..4cd3683
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,43 @@
+name: Linters, Spellcheck, and Tests
+
+on:
+ push:
+ workflow_dispatch:
+
+jobs:
+ Linters:
+ runs-on: ubuntu-latest
+ timeout-minutes: 2
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup go
+ uses: actions/setup-go@v5
+ with:
+ go-version: 1.23
+ - name: Install dependencies
+ run: make deps
+ - name: Run linters
+ run: make lint
+
+ Spellcheck:
+ runs-on: ubuntu-latest
+ timeout-minutes: 2
+ steps:
+ - uses: actions/checkout@v4
+ - uses: crate-ci/typos@v1.29.7
+
+ Tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 2
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup go
+ uses: actions/setup-go@v5
+ with:
+ go-version: 1.23
+ - name: Install dependencies
+ run: make deps
+ - name: Run tests
+ run: make test
+ - name: Report stats
+ run: make analyse
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d7ce277
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+# Run tests and log the test coverage
+test:
+ go test -v -coverprofile=".cover.out" $$(go list ./... | grep -v /tmp)
+
+# Runs source code linters and catches common errors
+lint:
+ test -z $$(gofmt -l .) || (echo "Code isn't gofmt'ed!" && exit 1)
+ go vet $$(go list ./... | grep -v /tmp)
+ gosec -quiet -fmt=golint -exclude-dir="tmp" ./...
+ pointerinterface ./...
+
+# Runs spellchecker on the code and comments
+# This requires this tool to be installed from https://github.com/crate-ci/typos?tab=readme-ov-file
+# Example installation (if you have rust installed): cargo install typos-cli
+spellcheck:
+ typos .
+
+# Generate pretty coverage report
+analyse:
+ go tool cover -html=".cover.out" -o="cover.html"
+ @echo -e "\nCOVERAGE\n===================="
+ go tool cover -func=.cover.out
+ @echo -e "\nCYCLOMATIC COMPLEXITY\n===================="
+ gocyclo -avg -top 10 -ignore test.go .
+
+# Updates 3rd party packages and tools
+deps:
+ go mod download
+ go install github.com/securego/gosec/v2/cmd/gosec@latest
+ go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
+ go install code.larus.se/lmas/pointerinterface@latest
+
+# Clean up built binary and other temporary files (ignores errors from rm)
+clean:
+ go clean
+ rm .cover.out cover.html
diff --git a/usecases/test_sample.go b/usecases/test_sample.go
new file mode 100644
index 0000000..a7dfecd
--- /dev/null
+++ b/usecases/test_sample.go
@@ -0,0 +1,33 @@
+package usecases
+
+import (
+ "net/http"
+ "testing"
+)
+
+// mockTransport is used for replacing the default network Transport (used by
+// http.DefaultClient) and it will intercept network requests.
+type mockTransport struct {
+ resp *http.Response
+}
+
+func newMockTransport(resp *http.Response) mockTransport {
+ t := mockTransport{
+ resp: resp,
+ }
+ // Hijack the default http client so no actual http requests are sent over the network
+ http.DefaultClient.Transport = t
+ return t
+}
+
+// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient).
+// It prevents the request from being sent over the network and count how many times
+// a domain was requested.
+func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
+ t.resp.Request = req
+ return t.resp, nil
+}
+
+func sample(t *testing.T) {
+ return
+}
From 1a0bf9e3500e4a37ad05a1b31ff556e0522ea654 Mon Sep 17 00:00:00 2001
From: Pake
Date: Thu, 5 Jun 2025 14:17:59 +0200
Subject: [PATCH 004/191] Making sure tests run
---
usecases/test_sample.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/usecases/test_sample.go b/usecases/test_sample.go
index a7dfecd..14ff4fb 100644
--- a/usecases/test_sample.go
+++ b/usecases/test_sample.go
@@ -28,6 +28,6 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er
return t.resp, nil
}
-func sample(t *testing.T) {
+func test_sample(t *testing.T) {
return
}
From 9bac8c374977cf10408bf8045ba60992a07c19c5 Mon Sep 17 00:00:00 2001
From: Pake
Date: Thu, 5 Jun 2025 16:34:30 +0200
Subject: [PATCH 005/191] Fixed linter errors by adding error handlers,
MinVersion for tls and read/write timeouts in server config
---
usecases/authentication.go | 22 ++++++++--
usecases/cost.go | 5 ++-
usecases/docs.go | 81 ++++++++++++++++--------------------
usecases/provision.go | 12 +++++-
usecases/serversNhandlers.go | 25 +++++++----
5 files changed, 87 insertions(+), 58 deletions(-)
diff --git a/usecases/authentication.go b/usecases/authentication.go
index 134ff25..8138fd9 100644
--- a/usecases/authentication.go
+++ b/usecases/authentication.go
@@ -93,6 +93,7 @@ func RequestCertificate(sys *components.System) {
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: false,
+ MinVersion: tls.VersionTLS12,
}
sys.Husk.TlsConfig = tlsConfig
@@ -137,7 +138,11 @@ func sendCSR(sys *components.System, csrPEM []byte) (string, error) {
// Read the response body
buf := new(bytes.Buffer)
- buf.ReadFrom(resp.Body)
+ _, err = buf.ReadFrom(resp.Body)
+ if err != nil {
+ log.Printf("Error while reading body: %v", err)
+ return "", err
+ }
return buf.String(), nil
}
@@ -158,7 +163,14 @@ func getCACertificate(sys *components.System) (string, error) {
url := strings.TrimSuffix(coreUAurl, "ification") // the configuration file address to the CA includes the unit asset
// Make a GET request to the CA's endpoint
- resp, err := http.Get(url)
+ // https://stackoverflow.com/questions/70281883/golang-untaint-url-variable-to-fix-gosec-warning-g107
+ //resp, err := http.Get(url)
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ log.Printf("Error creating NewRequest: %v", err)
+ return "", err
+ }
+ resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request to CA: %w", err)
}
@@ -171,7 +183,11 @@ func getCACertificate(sys *components.System) (string, error) {
// Read the response body
buf := new(bytes.Buffer)
- buf.ReadFrom(resp.Body)
+ _, err = buf.ReadFrom(resp.Body)
+ if err != nil {
+ log.Printf("Error while reading body: %v", err)
+ return "", err
+ }
return buf.String(), nil
}
diff --git a/usecases/cost.go b/usecases/cost.go
index 1a3f1a2..888e8d5 100644
--- a/usecases/cost.go
+++ b/usecases/cost.go
@@ -94,7 +94,10 @@ func ACServices(w http.ResponseWriter, r *http.Request, ua *components.UnitAsset
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
- w.Write(payload)
+ _, err = w.Write(payload)
+ if err != nil {
+ log.Printf("Error while writing to response body for ACServices: %v", err)
+ }
return
case "PUT":
defer r.Body.Close()
diff --git a/usecases/docs.go b/usecases/docs.go
index 412b542..f2644a0 100644
--- a/usecases/docs.go
+++ b/usecases/docs.go
@@ -29,6 +29,7 @@ package usecases
import (
"fmt"
+ "log"
"net/http"
"strconv"
"strings"
@@ -39,102 +40,92 @@ import (
// System Documentation (based on HATEOAS) provides an initial documentation on the system's web server of with hyperlinks to the services for browsers
// HATEOAS is the acronym for Hypermedia as the Engine of Application State, using hyperlinks to navigate the API
func SysHateoas(w http.ResponseWriter, req *http.Request, sys components.System) {
- text := ""
- w.Write([]byte(text))
- text = "System Description
"
- w.Write([]byte(text))
- text = "The system " + sys.Name + " " + sys.Husk.Description + "
"
- w.Write([]byte(text))
- text = "Online Documentation
"
- w.Write([]byte(text))
-
- text = " The resource list is
"
- w.Write([]byte(text))
+ text := "\n"
+ text += "System Description
\n"
+ text += "The system " + sys.Name + " " + sys.Husk.Description + "
\n"
+ text += "Online Documentation\n"
+ text += " The resource list is
\n"
+
assetList := &sys.UAssets
for _, unitasset := range *assetList {
metaservice := ""
for key, values := range (*unitasset).GetDetails() {
metaservice += key + ": " + fmt.Sprintf("%v", values) + " "
}
- resourceURI := "- " + (*unitasset).GetName() + " with details " + metaservice + "
"
- w.Write([]byte(resourceURI))
+ text += "- " + (*unitasset).GetName() + " with details " + metaservice + "
\n"
}
- text = "
having the following services:"
- w.Write([]byte(text))
+ text += "
having the following services:\n"
servicesList := getServicesList(getFirstAsset(*assetList)[0])
for _, service := range servicesList {
metaservice := ""
for key, values := range service.Details {
metaservice += key + ": " + fmt.Sprintf("%v", values) + " "
}
- serviceURI := "- " + service.Definition + " with details: " + metaservice + "
"
- w.Write([]byte(serviceURI))
+ text += "- " + service.Definition + " with details: " + metaservice + "
\n"
}
- text = "
The services can be accessed using the following protocols with their respective bound ports:
"
- w.Write([]byte(text))
+ text += "
The services can be accessed using the following protocols with their respective bound ports:
\n"
for protocol, port := range sys.Husk.ProtoPort {
- protoDoor := "- Protocol " + protocol + " using port " + strconv.Itoa(port) + "
"
- w.Write([]byte(protoDoor))
+ text += "- Protocol " + protocol + " using port " + strconv.Itoa(port) + "
\n"
}
- text = "
of the device whose IP addresses are (upon startup):
"
- w.Write([]byte(text))
+ text += "
of the device whose IP addresses are (upon startup):
\n"
for _, IPAddre := range sys.Host.IPAddresses {
- hostaddresses := "- " + IPAddre + "
"
- w.Write([]byte(hostaddresses))
+ text += "- " + IPAddre + "
\n"
}
- text = "
"
- w.Write([]byte(text))
+ text += "
"
+ _, err := w.Write([]byte(text))
+ if err != nil {
+ log.Printf("Error while writing to response body for SysHateoas: %v", err)
+ }
}
// ResHateoas provides information about the unit asset(s) and each service and is accessed via the system's web server
func ResHateoas(w http.ResponseWriter, req *http.Request, ua components.UnitAsset, sys components.System) {
- text := ""
- w.Write([]byte(text))
-
- text = "Unit Asset Description
"
- w.Write([]byte(text))
+ text := "\n"
+ text += "Unit Asset Description
\n"
uaName := ua.GetName()
metaservice := ""
for key, values := range ua.GetDetails() {
metaservice += key + ": " + fmt.Sprintf("%v", values) + " "
}
- text = "The resource " + uaName + " belongs to system " + sys.Name + " and has the details " + metaservice + " with the following services:" + ""
- w.Write([]byte(text))
+ text += "The resource " + uaName + " belongs to system " + sys.Name + " and has the details " + metaservice + " with the following services:" + "\n"
+
services := ua.GetServices()
for _, service := range services {
metaservice := ""
for key, values := range service.Details {
metaservice += key + ": " + fmt.Sprintf("%v", values) + " "
}
- serviceURI := "- " + service.Definition + " with details: " + metaservice + "
"
- w.Write([]byte(serviceURI))
+ text += "- " + service.Definition + " with details: " + metaservice + "
\n"
}
- text = "
"
- w.Write([]byte(text))
+ text += "