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  }