github.com/ngocphuongnb/tetua@v0.0.7-alpha/app/utils/utils.go (about) 1 package utils 2 3 import ( 4 "bytes" 5 "crypto/aes" 6 "crypto/cipher" 7 "crypto/rand" 8 "crypto/subtle" 9 "encoding/base64" 10 "encoding/hex" 11 "errors" 12 "fmt" 13 "io" 14 "net/url" 15 "reflect" 16 "runtime" 17 "strings" 18 19 h "html" 20 21 "github.com/microcosm-cc/bluemonday" 22 "github.com/ngocphuongnb/tetua/app/config" 23 "github.com/yuin/goldmark" 24 "github.com/yuin/goldmark/extension" 25 "github.com/yuin/goldmark/parser" 26 "github.com/yuin/goldmark/renderer/html" 27 "golang.org/x/crypto/argon2" 28 ) 29 30 type HashConfig struct { 31 Iterations uint32 32 Memory uint32 33 KeyLen uint32 34 Threads uint8 35 } 36 37 // var ugcPolicy = bluemonday.UGCPolicy() 38 var stripTagsPolicy = bluemonday.StripTagsPolicy() 39 var markdownPolicy = bluemonday.StripTagsPolicy() 40 var md goldmark.Markdown 41 42 var iframeAllowHosts = []string{ 43 "www.youtube.com", 44 "codesandbox.io", 45 "gist.github.com", 46 "instagram.com", 47 "twitter.com", 48 "twitch.tv", 49 "vimeo.com", 50 "codepen.io", 51 "glitch.com", 52 "jsbin.com", 53 "jsfiddle.net", 54 "repl.it", 55 "reddit.com", 56 "slideshare.net", 57 "soundcloud.com", 58 "stackblitz.com", 59 } 60 61 func init() { 62 markdownPolicy.AllowIFrames() 63 markdownPolicy.AllowElements("div") 64 markdownPolicy.AllowElements("php") 65 markdownPolicy.AllowElements("?php") 66 markdownPolicy.AllowElements("iframe") 67 markdownPolicy. 68 AllowURLSchemeWithCustomPolicy("https", func(url *url.URL) (allowUrl bool) { 69 return SliceContains(iframeAllowHosts, url.Host) 70 }). 71 AllowAttrs("src", "frameborder", "allowfullscreen", "width", "height", "allow"). 72 OnElements("iframe") 73 74 md = goldmark.New( 75 goldmark.WithExtensions( 76 extension.GFM, 77 ), 78 goldmark.WithParserOptions( 79 parser.WithAutoHeadingID(), 80 ), 81 goldmark.WithRendererOptions( 82 html.WithHardWraps(), 83 html.WithXHTML(), 84 html.WithUnsafe(), 85 ), 86 ) 87 } 88 89 func SanitizePlainText(html string) string { 90 return stripTagsPolicy.Sanitize(html) 91 } 92 93 func SanitizeMarkdown(html string) string { 94 html = strings.ReplaceAll(html, "<?php", "__php_open_tag__") 95 html = h.UnescapeString(markdownPolicy.Sanitize(html)) 96 return strings.ReplaceAll(html, "__php_open_tag__", "<?php") 97 } 98 99 func ExtractContent(content string) (string, string) { 100 content = SanitizeMarkdown(content) 101 lines := strings.Split(content, "\n") 102 name := strings.Trim(strings.Trim(lines[0], "#"), " ") 103 content = strings.Join(lines[1:], "\n") 104 105 return name, content 106 } 107 108 func MarkdownToHtml(content string) (string, error) { 109 var buf bytes.Buffer 110 111 if err := md.Convert([]byte(content), &buf); err != nil { 112 return "", err 113 } 114 115 return buf.String(), nil 116 } 117 118 func GenerateHash(input string) (string, error) { 119 if input == "" { 120 return "", errors.New("hash: input cannot be empty") 121 } 122 123 salt := make([]byte, 16) 124 cfg := &HashConfig{ 125 Iterations: 3, 126 Memory: 64 * 1024, 127 Threads: 4, 128 KeyLen: 32, 129 } 130 131 if _, err := rand.Read(salt); err != nil { 132 return "", err 133 } 134 135 hash := argon2.IDKey([]byte(input), salt, cfg.Iterations, cfg.Memory, cfg.Threads, cfg.KeyLen) 136 b64Salt := base64.RawStdEncoding.EncodeToString(salt) 137 b64Hash := base64.RawStdEncoding.EncodeToString(hash) 138 format := "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s" 139 full := fmt.Sprintf(format, argon2.Version, cfg.Memory, cfg.Iterations, cfg.Threads, b64Salt, b64Hash) 140 141 return full, nil 142 } 143 144 func CheckHash(input, hash string) error { 145 var err error 146 var salt []byte 147 var decodedHash []byte 148 parts := strings.Split(hash, "$") 149 cfg := &HashConfig{} 150 151 if len(parts) != 6 { 152 return errors.New("invalid hash") 153 } 154 155 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &cfg.Memory, &cfg.Iterations, &cfg.Threads); err != nil { 156 return err 157 } 158 159 if salt, err = base64.RawStdEncoding.DecodeString(parts[4]); err != nil { 160 return err 161 } 162 163 if decodedHash, err = base64.RawStdEncoding.DecodeString(parts[5]); err != nil { 164 return err 165 } 166 167 cfg.KeyLen = uint32(len(decodedHash)) 168 comparisonHash := argon2.IDKey([]byte(input), salt, cfg.Iterations, cfg.Memory, cfg.Threads, cfg.KeyLen) 169 valid := subtle.ConstantTimeCompare(decodedHash, comparisonHash) == 1 170 171 if !valid { 172 return errors.New("invalid hash") 173 } 174 175 return nil 176 } 177 178 func Encrypt(stringToEncrypt string, keys ...string) (string, error) { 179 key := []byte(config.APP_KEY) 180 181 if len(keys) > 0 { 182 key = []byte(keys[0]) 183 } 184 185 plaintext := []byte(stringToEncrypt) 186 187 block, err := aes.NewCipher(key) 188 if err != nil { 189 return "", err 190 } 191 192 aesGCM, err := cipher.NewGCM(block) 193 if err != nil { 194 return "", err 195 } 196 197 nonce := make([]byte, aesGCM.NonceSize()) 198 if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 199 return "", err 200 } 201 202 ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) 203 return fmt.Sprintf("%x", ciphertext), nil 204 } 205 206 func Decrypt(encryptedString string, keys ...string) (string, error) { 207 key := []byte(config.APP_KEY) 208 if len(keys) > 0 { 209 key = []byte(keys[0]) 210 } 211 212 enc, err := hex.DecodeString(encryptedString) 213 214 if err != nil { 215 return "", err 216 } 217 218 block, err := aes.NewCipher(key) 219 if err != nil { 220 return "", err 221 } 222 223 aesGCM, err := cipher.NewGCM(block) 224 if err != nil { 225 return "", err 226 } 227 228 if len(enc) < aesGCM.NonceSize() { 229 return "", errors.New("ciphertext too short") 230 } 231 232 nonceSize := aesGCM.NonceSize() 233 nonce, ciphertext := enc[:nonceSize], enc[nonceSize:] 234 235 plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) 236 if err != nil { 237 return "", err 238 } 239 240 return fmt.Sprintf("%s", plaintext), nil 241 } 242 243 func Url(path string) string { 244 appBase := strings.TrimRight(config.Setting("app_base_url"), "/") 245 246 if path == "" { 247 return appBase + "/" 248 } 249 250 path = strings.TrimLeft(path, "/") 251 path = fmt.Sprintf("%s/%s", appBase, path) 252 return path 253 } 254 255 func GetFunctionName(i interface{}) string { 256 return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 257 } 258 259 func SliceContains[T comparable](slice []T, element T) bool { 260 for _, e := range slice { 261 if e == element { 262 return true 263 } 264 } 265 return false 266 } 267 268 func SliceOverlap[T comparable](slice1 []T, slice2 []T) []T { 269 result := make([]T, 0) 270 for _, e1 := range slice1 { 271 if SliceContains(slice2, e1) { 272 result = append(result, e1) 273 } 274 } 275 return result 276 } 277 278 func SliceFilter[T comparable](slice []T, predicate func(T) bool) []T { 279 var result []T 280 for _, e := range slice { 281 if predicate(e) { 282 result = append(result, e) 283 } 284 } 285 return result 286 } 287 288 func SliceMap[T comparable, R comparable](slice []T, mapper func(T) R) []R { 289 var result []R 290 for _, e := range slice { 291 result = append(result, mapper(e)) 292 } 293 return result 294 } 295 296 func Repeat[T comparable](input T, time int) []T { 297 var result []T 298 var i = 0 299 for i < time { 300 result = append(result, input) 301 i++ 302 } 303 return result 304 } 305 306 func SliceAppendIfNotExists[T comparable](slice []T, newItem T, checkExists func(T) bool) []T { 307 for _, s := range slice { 308 if checkExists(s) { 309 return slice 310 } 311 } 312 slice = append(slice, newItem) 313 return slice 314 } 315 316 // GetStructField returns the value of a struct field 317 func GetStructField(entity interface{}, field string) reflect.Value { 318 r := reflect.ValueOf(entity) 319 return reflect.Indirect(r).FieldByName(field) 320 } 321 322 // FirstError return the first error in a list of errors 323 func FirstError(errors ...error) error { 324 for _, err := range errors { 325 if err != nil { 326 return err 327 } 328 } 329 330 return nil 331 }