diff --git a/context.go b/context.go index 30c750b..cc622be 100644 --- a/context.go +++ b/context.go @@ -72,6 +72,15 @@ func (c *Context) GetHeader(key string) string { return c.Request.httpRequest.Header.Get(key) } +func (c *Context) GetCookie() (UserCookie, error) { + + user, err := GetCookie(c.Request.httpRequest) + if err != nil { + return user, err + } + return user, nil +} + func (c *Context) GetUploadedFile(name string) *UploadedFileInfo { file, fileHeader, err := c.Request.httpRequest.FormFile(name) if err != nil { diff --git a/cookies.go b/cookies.go new file mode 100644 index 0000000..87503e7 --- /dev/null +++ b/cookies.go @@ -0,0 +1,238 @@ +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "crypto/aes" + "crypto/cipher" + + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/gob" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +var ( + ErrValueTooLong = errors.New("cookie value too long") + ErrInvalidValue = errors.New("invalid cookie value") +) + +// Declare the User type. +type UserCookie struct { + Email string + Token string +} + +var secretcookie []byte + +func GetCookie(r *http.Request) (UserCookie, error) { + + var err error + // Create a new instance of a User type. + var user UserCookie + + secretcookie, err = hex.DecodeString("13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b") + if err != nil { + return user, err + } + + gobEncodedValue, err := CookieReadEncrypted(r, "goffee", secretcookie) + if err != nil { + return user, err + } + + // Create an strings.Reader containing the gob-encoded value. + reader := strings.NewReader(gobEncodedValue) + + // Decode it into the User type. Notice that we need to pass a *pointer* to + // the Decode() target here? + if err := gob.NewDecoder(reader).Decode(&user); err != nil { + return user, err + } + + return user, nil + +} + +func SetCookie(w http.ResponseWriter, email string, token string) error { + // Initialize a User struct containing the data that we want to store in the + // cookie. + var err error + + secretcookie, err = hex.DecodeString("13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b") + if err != nil { + return err + } + + user := UserCookie{Email: email, Token: token} + + // Initialize a buffer to hold the gob-encoded data. + var buf bytes.Buffer + + // Gob-encode the user data, storing the encoded output in the buffer. + err = gob.NewEncoder(&buf).Encode(&user) + if err != nil { + return err + } + + // Call buf.String() to get the gob-encoded value as a string and set it as + // the cookie value. + cookie := http.Cookie{ + Name: "goffee", + Value: buf.String(), + Path: "/", + MaxAge: 3600, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + } + + // Write an encrypted cookie containing the gob-encoded data as normal. + err = CookieWriteEncrypted(w, cookie, secretcookie) + if err != nil { + return err + } + + fmt.Printf("Cookie set %v\n", email) + + return nil +} + +func CookieWrite(w http.ResponseWriter, cookie http.Cookie) error { + // Encode the cookie value using base64. + cookie.Value = base64.URLEncoding.EncodeToString([]byte(cookie.Value)) + + // Check the total length of the cookie contents. Return the ErrValueTooLong + // error if it's more than 4096 bytes. + if len(cookie.String()) > 4096 { + return ErrValueTooLong + } + + // Write the cookie as normal. + http.SetCookie(w, &cookie) + + return nil +} + +func CookieRead(r *http.Request, name string) (string, error) { + // Read the cookie as normal. + cookie, err := r.Cookie(name) + if err != nil { + return "", err + } + + // Decode the base64-encoded cookie value. If the cookie didn't contain a + // valid base64-encoded value, this operation will fail and we return an + // ErrInvalidValue error. + value, err := base64.URLEncoding.DecodeString(cookie.Value) + if err != nil { + return "", ErrInvalidValue + } + + // Return the decoded cookie value. + return string(value), nil +} + +func CookieWriteEncrypted(w http.ResponseWriter, cookie http.Cookie, secretKey []byte) error { + // Create a new AES cipher block from the secret key. + block, err := aes.NewCipher(secretKey) + if err != nil { + return err + } + + // Wrap the cipher block in Galois Counter Mode. + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return err + } + + // Create a unique nonce containing 12 random bytes. + nonce := make([]byte, aesGCM.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return err + } + + // Prepare the plaintext input for encryption. Because we want to + // authenticate the cookie name as well as the value, we make this plaintext + // in the format "{cookie name}:{cookie value}". We use the : character as a + // separator because it is an invalid character for cookie names and + // therefore shouldn't appear in them. + plaintext := fmt.Sprintf("%s:%s", cookie.Name, cookie.Value) + + // Encrypt the data using aesGCM.Seal(). By passing the nonce as the first + // parameter, the encrypted data will be appended to the nonce — meaning + // that the returned encryptedValue variable will be in the format + // "{nonce}{encrypted plaintext data}". + encryptedValue := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil) + + // Set the cookie value to the encryptedValue. + cookie.Value = string(encryptedValue) + + // Write the cookie as normal. + return CookieWrite(w, cookie) +} + +func CookieReadEncrypted(r *http.Request, name string, secretKey []byte) (string, error) { + // Read the encrypted value from the cookie as normal. + encryptedValue, err := CookieRead(r, name) + if err != nil { + return "", err + } + + // Create a new AES cipher block from the secret key. + block, err := aes.NewCipher(secretKey) + if err != nil { + return "", err + } + + // Wrap the cipher block in Galois Counter Mode. + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + // Get the nonce size. + nonceSize := aesGCM.NonceSize() + + // To avoid a potential 'index out of range' panic in the next step, we + // check that the length of the encrypted value is at least the nonce + // size. + if len(encryptedValue) < nonceSize { + return "", ErrInvalidValue + } + + // Split apart the nonce from the actual encrypted data. + nonce := encryptedValue[:nonceSize] + ciphertext := encryptedValue[nonceSize:] + + // Use aesGCM.Open() to decrypt and authenticate the data. If this fails, + // return a ErrInvalidValue error. + plaintext, err := aesGCM.Open(nil, []byte(nonce), []byte(ciphertext), nil) + if err != nil { + return "", ErrInvalidValue + } + + // The plaintext value is in the format "{cookie name}:{cookie value}". We + // use strings.Cut() to split it on the first ":" character. + expectedName, value, ok := strings.Cut(string(plaintext), ":") + if !ok { + return "", ErrInvalidValue + } + + // Check that the cookie name is the expected one and hasn't been changed. + if expectedName != name { + return "", ErrInvalidValue + } + + // Return the plaintext cookie value. + return value, nil +} diff --git a/core.go b/core.go index ba208d3..59691c6 100644 --- a/core.go +++ b/core.go @@ -98,9 +98,7 @@ func (app *App) Run(router *httprouter.Router) { TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) // if enabled, if TemplateEnable { - // add public path - publicPath := os.Getenv("TEMPLATE_PUBLIC") - router.ServeFiles("/public/*filepath", http.Dir(publicPath)) + router.ServeFiles("/public/*filepath", http.Dir("storage/public")) } useHttpsStr := os.Getenv("App_USE_HTTPS") @@ -183,6 +181,7 @@ func (app *App) RegisterRoutes(routes []Route, router *httprouter.Router) *httpr func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + ctx := &Context{ Request: &Request{ httpRequest: r, @@ -206,11 +205,13 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha GetEventsManager: resolveEventsManager(), GetLogger: resolveLogger(), } + ctx.prepare(ctx) rhs := app.combHandlers(h, ms) app.prepareChain(rhs) app.t = 0 app.chain.execute(ctx) + for _, header := range ctx.Response.headers { w.Header().Add(header.key, header.val) } @@ -223,6 +224,7 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha } else { ct = CONTENT_TYPE_HTML } + w.Header().Add(CONTENT_TYPE, ct) if ctx.Response.statusCode != 0 { w.WriteHeader(ctx.Response.statusCode) diff --git a/template/components/head.html b/template/components/head.html index cecc5ed..68aaca8 100644 --- a/template/components/head.html +++ b/template/components/head.html @@ -1,8 +1,10 @@ {{define "head"}} - - - {{.}} | Goffee - + + + + {{.}} | Goffee + + {{end}} \ No newline at end of file diff --git a/template/components/page.html b/template/components/page.html new file mode 100644 index 0000000..636487b --- /dev/null +++ b/template/components/page.html @@ -0,0 +1,17 @@ + + + + + + + My Website + + + + +
+

Welcome to My Website

+
+ + + \ No newline at end of file