github.com/geniusesgroup/libgo@v0.0.0-20220713101832-828057a9d3d4/captcha/phrase.go (about)

     1  /* For license and copyright information please see LEGAL file in repository */
     2  
     3  package captcha
     4  
     5  import (
     6  	"bytes"
     7  	"container/list"
     8  	"image"
     9  	"image/draw"
    10  	"image/jpeg"
    11  	"image/png"
    12  	"math/rand"
    13  	"strconv"
    14  	"time"
    15  	"unsafe"
    16  
    17  	"github.com/golang/freetype"
    18  	"github.com/golang/freetype/truetype"
    19  	"golang.org/x/image/font/gofont/gobolditalic"
    20  
    21  	etime "../earth-time"
    22  	"../log"
    23  	"../uuid"
    24  )
    25  
    26  /*
    27  Usage:
    28  var phraseCaptchas = captcha.NewDefaultPhraseCaptchas()
    29  var pc *captcha.PhraseCaptcha = phraseCaptchas.NewImage(req.Language, req.ImageFormat)
    30  
    31  */
    32  
    33  // PhraseCaptchas store
    34  type PhraseCaptchas struct {
    35  	Len        uint8       // Number of captcha solution. can't be more than 16!
    36  	Difficulty uint8       // 0:very-easy, 1:easy, 2:medium, 3:hard, 4:very-hard, 5:extreme-hard
    37  	Type       uint8       // 0:Number(625896), 1:Word(A19Cat), 2:Math(+ - * /),
    38  	Duration   int64       // The number of seconds indicate expiration time of captchas
    39  	ImageSize  image.Point // Standard width & height of a captcha image.
    40  	Pool       map[[16]byte]*PhraseCaptcha
    41  	idByTime   *list.List
    42  }
    43  
    44  // PhraseCaptcha store
    45  type PhraseCaptcha struct {
    46  	ID       [16]byte
    47  	Answer   string
    48  	ExpireIn int64
    49  	State    state
    50  	Image    []byte // In requested lang & format
    51  	Audio    []byte // In requested lang & format
    52  }
    53  
    54  // NewDefaultPhraseCaptchas use to make new captchas with defaults values!
    55  func NewDefaultPhraseCaptchas() (pcs *PhraseCaptchas) {
    56  	pcs = &PhraseCaptchas{
    57  		Len:        6,
    58  		Difficulty: 2,
    59  		Type:       0,
    60  		Duration:   2 * 60, // 2 Minute
    61  		ImageSize:  image.Point{128, 64},
    62  		Pool:       make(map[[16]byte]*PhraseCaptcha, 1024),
    63  		idByTime:   list.New(),
    64  	}
    65  	// cleaner for expired captchas!
    66  	go pcs.expirationProcessing()
    67  	return
    68  }
    69  
    70  // NewImage make, store and return new captcha!
    71  func (pcs *PhraseCaptchas) NewImage(lang Language, imageformat ImageFormat) (pc *PhraseCaptcha) {
    72  	pc = &PhraseCaptcha{
    73  		ID:       uuid.NewV4(),
    74  		ExpireIn: etime.Now() + pcs.Duration,
    75  		State:    StateCreated,
    76  	}
    77  	switch pcs.Type {
    78  	case 0:
    79  		pc.Answer = pcs.randomDigits()
    80  	case 1:
    81  		pc.Answer = pcs.randomWord(lang)
    82  	case 2:
    83  		pc.Answer = pcs.randomMath()
    84  	}
    85  	pc.Image = pcs.createImage(pc.Answer, imageformat)
    86  
    87  	pcs.Pool[pc.ID] = pc
    88  	return
    89  }
    90  
    91  // GetAudio return exiting captcha with audio generated if exits otherwise returns nil!
    92  func (pcs *PhraseCaptchas) GetAudio(captchaID [16]byte, lang Language, audioFormat AudioFormat) (pc *PhraseCaptcha) {
    93  	pc = pcs.Pool[captchaID]
    94  	if pc == nil {
    95  		return nil
    96  	}
    97  	pc.Audio = pcs.createAudio(pc.Answer, audioFormat)
    98  	return
    99  }
   100  
   101  // Get return exiting captcha if exits otherwise returns nil!
   102  func (pcs *PhraseCaptchas) Get(captchaID [16]byte) (pc *PhraseCaptcha) {
   103  	pc = pcs.Pool[captchaID]
   104  	if pc != nil && pc.ExpireIn < etime.Now() {
   105  		delete(pcs.Pool, captchaID)
   106  		return nil
   107  	}
   108  	return
   109  }
   110  
   111  // Solve check answer and return captcha state!
   112  func (pcs *PhraseCaptchas) Solve(captchaID [16]byte, answer string) error {
   113  	var pc *PhraseCaptcha
   114  	pc = pcs.Pool[captchaID]
   115  	if pc == nil {
   116  		return ErrCaptchaNotFound
   117  	}
   118  	if pc.ExpireIn < etime.Now() {
   119  		delete(pcs.Pool, captchaID)
   120  		return ErrCaptchaExpired
   121  	}
   122  	if pc.Answer != answer {
   123  		pc.State = StateLastAnswerNotValid
   124  		return ErrCaptchaAnswerNotValid
   125  	}
   126  	// Give more time to user to complete any proccess need captcha!
   127  	pc.ExpireIn += pcs.Duration
   128  	pc.State = StateSolved
   129  	return nil
   130  }
   131  
   132  // Check return true if captcha exits and solved otherwise returns false!
   133  func (pcs *PhraseCaptchas) Check(captchaID [16]byte) error {
   134  	var pc *PhraseCaptcha
   135  	pc = pcs.Pool[captchaID]
   136  	if pc == nil {
   137  		return ErrCaptchaNotFound
   138  	}
   139  	if pc.ExpireIn < etime.Now() {
   140  		delete(pcs.Pool, captchaID)
   141  		return ErrCaptchaExpired
   142  	}
   143  	if pc.State != StateSolved {
   144  		return ErrCaptchaNotSolved
   145  	}
   146  	return nil
   147  }
   148  
   149  func (pcs *PhraseCaptchas) randomDigits() string {
   150  	var low, hi int64
   151  	switch pcs.Len {
   152  	case 6:
   153  		low, hi = 100000, 999999
   154  	case 7:
   155  		low, hi = 1000000, 9999999
   156  	case 8:
   157  		low, hi = 10000000, 99999999
   158  	case 9:
   159  		low, hi = 100000000, 999999999
   160  	case 10:
   161  		low, hi = 1000000000, 9999999999
   162  	default:
   163  		low, hi = 1000000000, 9999999999
   164  	}
   165  	var rand = low + rand.Int63n(hi-low)
   166  	return strconv.FormatInt(rand, 10)
   167  }
   168  
   169  var src = rand.NewSource(time.Now().UnixNano())
   170  
   171  const (
   172  	letterIdxBits = 6                    // 6 bits to represent a letter index
   173  	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
   174  )
   175  const englishLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789-"
   176  
   177  func (pcs *PhraseCaptchas) randomWord(lang Language) string {
   178  	var b = make([]byte, pcs.Len)
   179  	var i uint8
   180  	for i = 0; i < pcs.Len; i++ {
   181  		var random = src.Int63()
   182  		if idx := int(random & letterIdxMask); idx < len(englishLetters) {
   183  			b[i] = englishLetters[idx]
   184  		}
   185  		random >>= letterIdxBits
   186  	}
   187  	return *(*string)(unsafe.Pointer(&b))
   188  }
   189  
   190  func (pcs *PhraseCaptchas) randomMath() string {
   191  	// TODO:::
   192  	return ""
   193  }
   194  
   195  var goBoldItalic *truetype.Font
   196  
   197  func init() {
   198  	var err error
   199  	goBoldItalic, err = freetype.ParseFont(gobolditalic.TTF)
   200  	if err != nil {
   201  		// Almost never occur!
   202  		log.Fatal(err)
   203  	}
   204  }
   205  
   206  func (pcs *PhraseCaptchas) createImage(answer string, imageFormat ImageFormat) []byte {
   207  	var img = image.NewRGBA(image.Rect(0, 0, pcs.ImageSize.X, pcs.ImageSize.Y))
   208  	draw.Draw(img, img.Bounds(), image.White, image.ZP, draw.Src)
   209  
   210  	// TODO::: Difficulty??!!
   211  	var c = freetype.NewContext()
   212  	var point = freetype.Pt(10, 10+int(c.PointToFixed(24)>>6))
   213  	c.SetDst(img)
   214  	c.SetSrc(image.Black) //(image.NewUniform(color.RGBA{200, 100, 0, 255}))
   215  	c.SetFont(goBoldItalic)
   216  	c.SetFontSize(24)
   217  	c.SetDPI(72)
   218  	c.SetClip(img.Bounds())
   219  	c.DrawString(answer, point)
   220  
   221  	var buf bytes.Buffer
   222  	switch imageFormat {
   223  	case ImageFormatPNG:
   224  		png.Encode(&buf, img)
   225  	case ImageFormatJPEG:
   226  		jpeg.Encode(&buf, img, &jpeg.Options{Quality: jpeg.DefaultQuality})
   227  	}
   228  	return buf.Bytes()
   229  }
   230  
   231  func (pcs *PhraseCaptchas) createAudio(answer string, audioFormat AudioFormat) []byte {
   232  	return []byte{}
   233  }
   234  
   235  func (pcs *PhraseCaptchas) expirationProcessing() {
   236  	var timer = time.NewTimer(time.Duration(pcs.Duration) * time.Second)
   237  	for {
   238  		select {
   239  		// case shutdownFeedback := <-pcs.shutdownSignal:
   240  		// 	timer.Stop()
   241  		// 	shutdownFeedback <- struct{}{}
   242  		// 	return
   243  		case <-timer.C:
   244  			timer.Reset(time.Duration(pcs.Duration) * time.Second)
   245  
   246  			if len(pcs.Pool) == 0 {
   247  				continue
   248  			}
   249  
   250  			// Usually this proccess is less than one second, so get time once for compare!
   251  			var timeNow = etime.Now()
   252  			for _, captcha := range pcs.Pool {
   253  				if captcha.ExpireIn < timeNow {
   254  					delete(pcs.Pool, captcha.ID)
   255  				}
   256  			}
   257  		}
   258  	}
   259  }
   260  
   261  // https://github.com/search?l=Go&q=captcha&type=Repositories
   262  // https://github.com/dchest/captcha
   263  // https://github.com/afocus/captcha
   264  // https://github.com/steambap/captcha
   265  // https://github.com/lifei6671/gocaptcha
   266  // https://www.hcaptcha.com/