From 2f8d435b77c282c6f99f39207dabc70f101c0612 Mon Sep 17 00:00:00 2001 From: Joshua Ashton Date: Sat, 31 Aug 2024 13:03:52 -0600 Subject: [PATCH] need to refresh on go modules. implemented rudimentary config and cli arguments. --- .../container/authentication.go | 230 ++++++++++++++++ backend/src/security-layer/container/go.mod | 17 +- backend/src/security-layer/container/go.sum | 34 +++ .../src/security-layer/container/router.go | 256 ++++++++++++------ .../src/security-layer/container/serveAPI.go | 42 +++ .../src/security-layer/container/servePage.go | 31 +++ backend/src/security-layer/container/types.go | 80 ++++++ 7 files changed, 606 insertions(+), 84 deletions(-) create mode 100644 backend/src/security-layer/container/authentication.go create mode 100644 backend/src/security-layer/container/serveAPI.go create mode 100644 backend/src/security-layer/container/servePage.go create mode 100644 backend/src/security-layer/container/types.go diff --git a/backend/src/security-layer/container/authentication.go b/backend/src/security-layer/container/authentication.go new file mode 100644 index 0000000..e2a4381 --- /dev/null +++ b/backend/src/security-layer/container/authentication.go @@ -0,0 +1,230 @@ +package main + +import ( + //"context" + "crypto/aes" + "crypto/cipher" + //"sync" + + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/hex" + //"encoding/json" + "errors" + "fmt" + + "io" + "log" + "net/http" + + //"os" + //"os/signal" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/gorilla/mux" +) + +// TODO: Dynamically get key? Need to research best way to store private keys between two devices. +var key = []byte("passphrasewhichneedstobe32bytes!") + +func createToken(username string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, + jwt.MapClaims{ + "username": username, + "exp": time.Now().Add(time.Hour * 24).Unix(), + }) + + jwtToken, err := token.SignedString(key) + if err != nil { + return "Error creating JWT.", err + } + return jwtToken, nil +} + +func verifyToken(tokenString string) error { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return key, nil + }) + + if err != nil { + return err + } + + if !token.Valid { + return fmt.Errorf("invalid token") + } + + return nil +} + +func hash(text string) string { + hasher := sha1.New() + hasher.Write([]byte(text)) + return base64.URLEncoding.EncodeToString(hasher.Sum(nil)) +} + +func encrypt() { + var newCommunication Communication + + text := []byte(newCommunication.Communication) + + c, err := aes.NewCipher(key) + if err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + var b []byte = gcm.Seal(nonce, nonce, text, nil) + hex, err := convertBytesToHex(b) + if err != nil { + fmt.Println(err) + } + var test string = "{data: " + hex + ", hash: " + hash(string(text)) + "}" + log.Println("encrypted string:" + test) + //gc.IndentedJSON(http.StatusCreated, gin.H{"message": test}) +} + +func convertBytesToHex(b []byte) (string, error) { + // Handle nil pointer case + if b == nil { + return "", errors.New("nil pointer provided for hex string") + } + + // Split the hex string by spaces + var h string = hex.EncodeToString(b) + var builder strings.Builder + for i := 0; i < len(h); i += 2 { + end := i + 2 + if end > len(h) { + end = len(h) + } + chunk := h[i:end] + builder.WriteString(chunk) + if end < len(h) { + builder.WriteString(" ") + } + } + + // Return the slice of uint8 and any errors encountered + return builder.String(), nil +} + +func convertHexToBytes(hexString *string) ([]uint8, error) { + // Handle nil pointer case + if hexString == nil { + return nil, errors.New("nil pointer provided for hex string") + } + + // Split the hex string by spaces + hexBytes := strings.Fields(*hexString) + + // Initialize an empty slice for uint8 + data := make([]uint8, len(hexBytes)) + + // Iterate and convert each hex byte + for i, hexByte := range hexBytes { + // Convert each hex string to a uint8 value (handling errors) + value, err := strconv.ParseUint(hexByte, 16, 8) + if err != nil { + return nil, fmt.Errorf("error parsing hex byte '%s': %w", hexByte, err) + } + + // Assign the converted value to the slice + data[i] = uint8(value) + } + + // Return the slice of uint8 and any errors encountered + return data, nil +} + +func decrypt(input *Communication, output *Chat) bool { + // TODO: Add testing flag for easier manipulation. + //ciphertext, err := ioutil.ReadFile("myfile") + ciphertext, err := convertHexToBytes(&input.Communication) + + // if our program was unable to read the file + // print out the reason why it can't + if err != nil { + fmt.Println(err) + } + + c, err := aes.NewCipher(key) + if err != nil { + fmt.Println(err) + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + fmt.Println(err) + } + + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + fmt.Println(err) + } + + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + fmt.Println(err) + } + s := string(plaintext) + if validateHash(s, input.Hash) { + output.Content = s + return true + } + input.Communication = "DATA CORRUPTED OR TAMPERED" + return false +} + +// Validate there's no tampering with SHA-1 sum. The decrypted hash and the transmitted hash should be identical. +func validateHash(decrypted string, hash string) bool { + // Calculate the SHA256 sum of the decrypted request + hasher := sha1.New() + hasher.Write([]byte(decrypted)) + decryptedHashString := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + // TODO: Add error handling if hash doesn't match. + return decryptedHashString == hash +} + +// Serve the authentication and encryption layer to a provided local port. +// Authentication takes place solely on the backend. +func ServeAuthentication( + router *mux.Router, + port int, +) { + // TODO: Add error handling + authR := router.Host("http://localhost").Subrouter() + authSrv := &http.Server{ + Addr: "0.0.0.0:" + string(port), + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: authR, + } + + go func() { + if err := authSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("Auth server is running on port " + string(port)) +} diff --git a/backend/src/security-layer/container/go.mod b/backend/src/security-layer/container/go.mod index 56cbe7e..49723e0 100644 --- a/backend/src/security-layer/container/go.mod +++ b/backend/src/security-layer/container/go.mod @@ -8,6 +8,7 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -20,21 +21,35 @@ require ( github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.3.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - github.com/quaxlyqueen/services v1.0.0 ) diff --git a/backend/src/security-layer/container/go.sum b/backend/src/security-layer/container/go.sum index c50e37f..3c73b5b 100644 --- a/backend/src/security-layer/container/go.sum +++ b/backend/src/security-layer/container/go.sum @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -37,6 +39,8 @@ github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFz github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -45,8 +49,12 @@ github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -57,6 +65,22 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quaxlyqueen/services v1.0.0 h1:k3VaNSHydxetGVGkv8zKV8H/FlVtLtcDCiENyIAYvjo= github.com/quaxlyqueen/services v1.0.0/go.mod h1:mTP/H7h+u+H/NzcNh3igIP/hkLA4ETYRU9dh9pUfClM= +github.com/quaxlyqueen/services v1.0.1 h1:k6d7j7AKh0QxbzRNFlT0N7ebByTQzYPCGQinvJHH76k= +github.com/quaxlyqueen/services v1.0.1/go.mod h1:DONdzmMLG0uArv3wBqyMRnJpC5/Kbt7jIXsck1ZhYxE= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -68,15 +92,23 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -88,6 +120,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/src/security-layer/container/router.go b/backend/src/security-layer/container/router.go index 61c41b2..d8f3952 100644 --- a/backend/src/security-layer/container/router.go +++ b/backend/src/security-layer/container/router.go @@ -16,39 +16,104 @@ import ( "github.com/gorilla/mux" "github.com/spf13/pflag" "github.com/spf13/viper" - - "github.com/quaxlyqueen/services" ) -type WebpagesStructure struct { - Domain string `mapstructure:"domain"` - WebpageDir string `mapstructure:"webpage_dir"` - WebpagePort string `mapstructure:"webpage_port"` -} - -type ConfigStructure struct { - TextModel string `mapstructure:"text_model"` - ImageModel string `mapstructure:"image_model"` - VideoModel string `mapstructure:"video_model"` - DocModel string `mapstructure:"doc_model"` - ResponseStream string `mapstructure:"response_stream"` - - Domain string `mapstructure:"domain"` - RouterPort string `mapstructure:"router_port"` - API string `mapstructure:"api"` - APIPort string `mapstructure:"api_port"` - Webpages WebpagesStructure `mapstructure:"webpages"` -} - -var CONFIG_DIR string +var CONFIG_FILE string var config ConfigStructure -var chats []services.Chat +var chats []Chat + +// Interfaces with the internal API to encrypt a message and return it in a +// Communication struct (see types.go). This should be called after a +// response has been received from the internal API for the AI, but before +// sending a HTTP response to the client. +func encryptMessage(response string) Communication { + encryptedMessage := Communication{} + msgToEncrypt := Communication{} + msgToEncrypt.Communication = response + msgToEncrypt.Hash = "" + j, err := json.Marshal(msgToEncrypt) + if err != nil { + msgToEncrypt.Communication = "" + return msgToEncrypt + } + buf := strings.NewReader(string(j)) + + resp, err := http.Post("http://localhost:1113/encrypt", "application/json", buf) + if err != nil { + encryptedMessage.Communication = "Error connecting to internal auth API." + return encryptedMessage + } + defer resp.Body.Close() + + // Decode the JSON response into a temporary struct + var responseData struct { + Communication string `json:"communication"` + Hash string `json:"hash"` + } + if err := json.NewDecoder(resp.Body).Decode(&responseData); err != nil { + encryptedMessage.Communication = "Error decoding JSON response from internal auth API." + return encryptedMessage + } + + // Extract the desired fields and assign them to Communication struct + encryptedMessage.Communication = responseData.Communication + encryptedMessage.Hash = responseData.Hash + + return encryptedMessage +} + +// Interfaces with the internal API to decrypt a message and return the prompt +// as a string. +// TODO: Allow for additional fields for the client, ie. streaming, model, etc. +func decryptMessage(comm Communication) Communication { + j, err := json.Marshal(comm) + if err != nil { + return Communication{} + } + buf := strings.NewReader(string(j)) + + resp, err := http.Post("http://localhost:1113/decrypt", "application/json", buf) + if err != nil { + return Communication{} + } + defer resp.Body.Close() + + // Decode the JSON response into a temporary struct + var responseData struct { + Filetype string `json:"filetype"` + Communication string `json:"communication"` + Hash string `json:"hash"` + } + if err := json.NewDecoder(resp.Body).Decode(&responseData); err != nil { + return Communication{} + } + + return responseData +} + +func sendError(w http.ResponseWriter, msg string) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, msg) +} + +func sendResponse(w http.ResponseWriter, encryptedMessage Communication) { + encryptedResponse, err := json.Marshal(encryptedMessage) + if err != nil { + sendError(w, "Error marshalling JSON of encrypted message.") + return + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, string(encryptedResponse)) +} // User HTTP GET request has the prompt decrypted and verified with a SHA-1 checksum. // If there's been no data corruption, re-construct user prompt for ollama // TODO: Eventually, add additional logic that will allow for re-direction and contextual awareness (pre-processing) func chat(w http.ResponseWriter, r *http.Request) { - model := "qwen:0.5b" // qwen:0.5b used for testing while hosting from my laptop. llama3 seems to be the best to use normally. + model := "qwen:0.5b" // qwen:0.5b used for testing while hosting from my laptop. llama3 seems to be the best to use normally. input, err := io.ReadAll(r.Body) if err != nil { @@ -56,61 +121,86 @@ func chat(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - prompt := services.Communication{} + prompt := Communication{} json.Unmarshal(input, &prompt) + prompt = decryptMessage(prompt) - newChat := services.Chat{} + // TODO: Add support for dynamically changing models. + switch prompt.Filetype { + case "txt": + model = "qwen:0.5b" + case "img": + model = "llava" + case "doc": + model = "omniparser" + } + + newChat := Chat{} newChat.Role = true newChat.Content = prompt.Communication - if services.decrypt(&prompt, &newChat) { - chats = append(chats, newChat) - - apiCall := services.PromptWHistory{} - apiCall.Model = model - apiCall.Messages = chats - apiCall.Stream = viper.Get("response_stream") - log.Print("Pre-JSON ification: ") - log.Println(apiCall) - - j, err := json.Marshal(apiCall) - buf := strings.NewReader(string(j)) - log.Print("Post-JSON ification: ") - log.Println(string(j)) - - // Port 11434 corresponds to Ollama. Direct access to Ollama is restricted. - resp, err := http.Post("http://localhost:11434/api/chat", "application/json", buf) - if err != nil { - return - } - - output, err := io.ReadAll(resp.Body) - if err != nil { - return - } - - defer r.Body.Close() - response := services.ResponseWHistory{} - json.Unmarshal(output, &response) - log.Println(response) - + if len(chats) == 0 { + chats = []Chat{newChat} } else { + chats = append(chats, newChat) + } + + apiCall := PromptWHistory{} + apiCall.Model = model + apiCall.Messages = chats + + // TODO: Allow for this as a user setting... + apiCall.Stream = false + + j, err := json.Marshal(apiCall) + buf := strings.NewReader(string(j)) + + // Port 11434 corresponds to Ollama. Direct access to Ollama is restricted. + resp, err := http.Post("http://localhost:11434/api/chat", "application/json", buf) + if err != nil { + sendError(w, "Error connecting to local process.") return } + + output, err := io.ReadAll(resp.Body) + if err != nil { + sendError(w, "Error reading local process communication.") + return + } + defer r.Body.Close() + + response := ResponseWHistory{} + json.Unmarshal(output, &response) + sendResponse(w, encryptMessage(response.Message.Content)) } func generate(w http.ResponseWriter, r *http.Request) { + model := "qwen:0.5b" // qwen:0.5b used for testing while hosting from my laptop. llama3 seems to be the best to use normally. input, err := io.ReadAll(r.Body) if err != nil { + sendError(w, "Error reading request.") return } defer r.Body.Close() - prompt := services.Communication{} - json.Unmarshal(input, &prompt) + prompt := Communication{} + err = json.Unmarshal(input, &prompt) + if err != nil { + sendError(w, "Error unmarshaling JSON.") + return + } + + prompt = decryptMessage(prompt) // TODO: Add support for dynamically changing models. - model := "qwen:0.5b" // qwen:0.5b used for testing while hosting from my laptop. llama3 seems to be the best to use normally. + switch prompt.Filetype { + case "txt": + model = "qwen:0.5b" + case "img": + model = "llava" + case "doc": + model = "omniparser" + } apicall := "{\"model\": \"" apicall = apicall + model @@ -120,25 +210,26 @@ func generate(w http.ResponseWriter, r *http.Request) { buf := strings.NewReader(apicall) - // Port 11434 corresponds to Ollama. Direct access to Ollama is restricted. resp, err := http.Post("http://localhost:11434/api/generate", "application/json", buf) if err != nil { + sendError(w, "Error connecting to local process.") return } output, err := io.ReadAll(resp.Body) if err != nil { + sendError(w, "Error reading local process communication.") return } defer resp.Body.Close() - defer r.Body.Close() - response := services.Response{} - json.Unmarshal(output, &response) - - fmt.Println(response.Response) // Print the body as a string - // TODO: Encrypt response.Response and send back as a Communication JSON object. - //c.IndentedJSON(http.StatusCreated, resp) + response := Response{} + err = json.Unmarshal(output, &response) + if err != nil { + sendError(w, "Error unmarshaling local process JSON.") + return + } + sendResponse(w, encryptMessage(response.Response)) } func serve() { @@ -156,9 +247,12 @@ func serve() { } r := mux.NewRouter() - services.servePage(r, viper.Get("webpage_dir"), "/", viper.Get("webpage_port")) - services.serveApi(r, viper.Get("api"), endpoint, function, viper.Get("api_port")) - addr := "0.0.0.0:", viper.Get("router_port") + //for _, webpage := range config.Webpages { + //} + ServePage(r, config.WebpageDir, "/", config.WebpagePort) + ServeApi(r, config.API, endpoint, function, config.APIPort) + ServeAuthentication(r, config.AuthPort) + addr := "0.0.0.0:" + string(config.RouterPort) srv := &http.Server{ Addr: addr, @@ -178,7 +272,7 @@ func serve() { } }() - log.Println("Router is running on port ", viper.Get("router_port")) + log.Println("Router is running on port ", config.RouterPort) c := make(chan os.Signal, 1) // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+Shift+C) @@ -205,30 +299,26 @@ func parseCLI() { // Define CLI option, shorthand, default value, and description // TODO: Dynamically obtain default config location from environment variables. pflag.StringP("config", "c", "/home/violet/.config/one-ai/default.json", "Configuration file used in initializing One AI.") + //pflag.StringP("help", "h", "false", "Display information about using the One AI CLI.") pflag.Parse() viper.BindPFlags(pflag.CommandLine) // Retrieve CLI argument, either the default value or the user provided value. - CONFIG_DIR = viper.GetString("config") + CONFIG_FILE = viper.GetString("config") } func parseConfig() { viper.SetConfigType("json") - viper.SetConfigFile(CONFIG) + viper.SetConfigFile(CONFIG_FILE) viper.ReadInConfig() - err := viper.Unmarshal(&config) - if err != nil { - fmt.Println("error unmarshalling config file") - return - } else { - fmt.Println(config) - } + // TODO: Handle error + viper.Unmarshal(&config) } func main() { parseCLI() parseConfig() - //serve() + serve() } diff --git a/backend/src/security-layer/container/serveAPI.go b/backend/src/security-layer/container/serveAPI.go new file mode 100644 index 0000000..61ca8e6 --- /dev/null +++ b/backend/src/security-layer/container/serveAPI.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/gorilla/mux" + "log" + "net/http" + "time" +) + +// Serve an API using mux.Router().Host({domain}).Subrouter(). +// Provide the router, domain, an array of endpoints and functions, and +// the port you would like the API accessible to. +func ServeApi( + router *mux.Router, + domain string, + endpoint []string, + function []func(http.ResponseWriter, *http.Request), + port int, +) { + // TODO: Add error handling + apiR := router.Host(domain).Subrouter() + + for i := 0; i < len(endpoint); i++ { + apiR.HandleFunc(endpoint[i], function[i]) + } + + apiSrv := &http.Server{ + Addr: "0.0.0.0:" + string(port), + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: apiR, + } + + go func() { + if err := apiSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("API server is running on port " + string(port)) +} diff --git a/backend/src/security-layer/container/servePage.go b/backend/src/security-layer/container/servePage.go new file mode 100644 index 0000000..cc9dbfb --- /dev/null +++ b/backend/src/security-layer/container/servePage.go @@ -0,0 +1,31 @@ +package main + +import ( + "github.com/gorilla/mux" + "log" + "net/http" + "time" +) + +// Serve a directory to a port. +func ServePage(router *mux.Router, dest string, path string, port int) { + router.PathPrefix(path).Handler(http.FileServer(http.Dir(dest))) + + srv := &http.Server{ + Addr: "0.0.0.0: " + string(port), + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: router, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("Web server is running on port " + string(port)) +} diff --git a/backend/src/security-layer/container/types.go b/backend/src/security-layer/container/types.go new file mode 100644 index 0000000..7f67cac --- /dev/null +++ b/backend/src/security-layer/container/types.go @@ -0,0 +1,80 @@ +package main + +type Communication struct { + Filetype string `json:"filetype"` + Communication string `json:"communication"` + Hash string `json:"hash"` +} + +type Response struct { + Model string `json:"model"` + CreatedAt string `json:"created_at"` + Response string `json:"response"` + Done bool `json:"done"` + DoneReason string `json:"done_reason"` + Context []int `json:"context"` + TotalDuration int `json:"total_duration"` + LoadDuration int `json:"load_duration"` + PromptEC int `json:"prompt_eval_count"` + PromptED int `json:"prompt_eval_duration"` + EvalCount int `json:"eval_count"` + EvalDuration int `json:"eval_duration"` +} + +// Chats are updated once the content is decrypted. Since this is never leaving +// the server once decrypted, and possibly will not be saved (depending on user +// settings), it will remain decrypted until it's time to transform into a +// Communication JSON object, which is the HTTP response that is re-encrypted. + +// TODO: If the user has enabled conversation history, then save the encrypted +// chats to the server's drive. Additionally, only accept incoming additional +// messages from the user, rather than having the client re-send Chats already +// stored on the server. +type Chat struct { + Role bool `json:"role"` + Content string `json:"content"` +} + +type PromptWHistory struct { + Model string `json:"model"` + Messages []Chat `json:"messages"` + Stream bool `json:"stream"` +} + +type ResponseWHistory struct { + Model string `json:"model"` + CreatedAt string `json:"created_at"` + Message Chat `json:"message"` + Done bool `json:"done"` + DoneReason string `json:"done_reason"` + Context []int `json:"context"` + TotalDuration int `json:"total_duration"` + LoadDuration int `json:"load_duration"` + PromptEC int `json:"prompt_eval_count"` + PromptED int `json:"prompt_eval_duration"` + EvalCount int `json:"eval_count"` + EvalDuration int `json:"eval_duration"` +} + +type WebpagesStructure struct { + Domain string `mapstructure:"domain"` + WebpageDir string `mapstructure:"webpage_dir"` + WebpagePort int `mapstructure:"webpage_port"` +} + +type ConfigStructure struct { + TextModel string `mapstructure:"text_model"` + ImageModel string `mapstructure:"image_model"` + VideoModel string `mapstructure:"video_model"` + DocModel string `mapstructure:"doc_model"` + ResponseStream bool `mapstructure:"response_stream"` + + Domain string `mapstructure:"domain"` + RouterPort int `mapstructure:"router_port"` + API string `mapstructure:"api"` + APIPort int `mapstructure:"api_port"` + AuthPort int `mapstructure:"auth_port"` + //Webpages WebpagesStructure `mapstructure:"webpages"` + WebpageDir string `mapstructure:"webpage_dir"` + WebpagePort int `mapstructure:"webpage_port"` +}