wangshan 5 сар өмнө
parent
commit
4063e71f2f

+ 100 - 0
gocaptcha/random.go

@@ -0,0 +1,100 @@
+// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package gocaptcha
+
+import (
+	"crypto/hmac"
+	"crypto/rand"
+	"crypto/sha256"
+	"io"
+)
+
+// idLen is a length of captcha id string.
+// (20 bytes of 62-letter alphabet give ~119 bits.)
+const idLen = 20
+
+// idChars are characters allowed in captcha id.
+var idChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
+
+// rngKey is a secret key used to deterministically derive seeds for
+// PRNGs used in image and audio. Generated once during initialization.
+var rngKey [32]byte
+
+func init() {
+	if _, err := io.ReadFull(rand.Reader, rngKey[:]); err != nil {
+		panic("captcha: error reading random source: " + err.Error())
+	}
+}
+
+// Purposes for seed derivation. The goal is to make deterministic PRNG produce
+// different outputs for images and audio by using different derived seeds.
+const (
+	imageSeedPurpose = 0x01
+	audioSeedPurpose = 0x02
+)
+
+// deriveSeed returns a 16-byte PRNG seed from rngKey, purpose, id and digits.
+// Same purpose, id and digits will result in the same derived seed for this
+// instance of running application.
+//
+//	out = HMAC(rngKey, purpose || id || 0x00 || digits)  (cut to 16 bytes)
+func deriveSeed(purpose byte, id string, digits []byte) (out [16]byte) {
+	var buf [sha256.Size]byte
+	h := hmac.New(sha256.New, rngKey[:])
+	h.Write([]byte{purpose})
+	io.WriteString(h, id)
+	h.Write([]byte{0})
+	h.Write(digits)
+	sum := h.Sum(buf[:0])
+	copy(out[:], sum)
+	return
+}
+
+// RandomDigits returns a byte slice of the given length containing
+// pseudorandom numbers in range 0-9. The slice can be used as a captcha
+// solution.
+func RandomDigits(length int) []byte {
+	return randomBytesMod(length, 10)
+}
+
+// randomBytes returns a byte slice of the given length read from CSPRNG.
+func randomBytes(length int) (b []byte) {
+	b = make([]byte, length)
+	if _, err := io.ReadFull(rand.Reader, b); err != nil {
+		panic("captcha: error reading random source: " + err.Error())
+	}
+	return
+}
+
+// randomBytesMod returns a byte slice of the given length, where each byte is
+// a random number modulo mod.
+func randomBytesMod(length int, mod byte) (b []byte) {
+	b = make([]byte, length)
+	maxrb := byte(256 - (256 % int(mod)))
+	i := 0
+	for {
+		r := randomBytes(length + (length / 4))
+		for _, c := range r {
+			if c > maxrb {
+				// Skip this number to avoid modulo bias.
+				continue
+			}
+			b[i] = c % mod
+			i++
+			if i == length {
+				return
+			}
+		}
+	}
+}
+
+// randomId returns a new random id string.
+func randomId() string {
+	b := randomBytesMod(idLen, byte(len(idChars)))
+	for i, c := range b {
+		b[i] = idChars[c]
+	}
+	return string(b)
+}

+ 15 - 1
gocaptcha/start.go

@@ -8,6 +8,7 @@ import (
 	"log"
 	"math/rand"
 	"net/http"
+	"time"
 )
 
 // 图形验证码大小
@@ -16,10 +17,20 @@ const (
 	dy       = 80
 	fontSize = 75
 	Rn       = 1
+	// Default number of digits in captcha solution.
+	DefaultLen = 6
+	// The number of captchas created that triggers garbage collection used
+	// by default store.
+	CollectNum = 100
+	// Expiration time of captchas used by default store.
+	Expiration = 10 * time.Minute
 )
 
 // 生成的字符集
-var TextCharacters = []rune("ACDEFGHJKLMNPQRSTUVWXY2456789")
+var (
+	TextCharacters = []rune("ACDEFGHJKLMNPQRSTUVWXY2456789")
+	globalStore    = NewMemoryStore(CollectNum, Expiration)
+)
 
 // 初始化字体文件
 func InitCaptcha() {
@@ -102,6 +113,9 @@ func Get(sess *httpsession.Session, checkCode string, w http.ResponseWriter, r *
 	}
 	//计次
 	times++
+	globalStore.Set(code, RandomDigits(len([]rune(code))))
+	b := VerifyString(code, code)
+	fmt.Println(b)
 	redis.Put("other", cacheKey, times, int(ttl))
 	sess.Set(checkCode, code)
 	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")

+ 6 - 0
gocaptcha/start_test.go

@@ -3,6 +3,7 @@ package gocaptcha
 import (
 	"app.yhyue.com/moapp/jybase/go-xweb/httpsession"
 	"app.yhyue.com/moapp/jybase/redis"
+	"fmt"
 	"net/http"
 	"testing"
 	"time"
@@ -20,3 +21,8 @@ func TestCaptcha(t *testing.T) {
 	Get(Session, "--", W, R)
 	time.Sleep(10 * time.Second)
 }
+
+func TestVerify(t *testing.T) {
+	b := VerifyString("E4MA", "E4MA")
+	fmt.Println(b)
+}

+ 117 - 0
gocaptcha/store.go

@@ -0,0 +1,117 @@
+// Copyright 2011 Dmitry Chestnykh. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package gocaptcha
+
+import (
+	"container/list"
+	"sync"
+	"time"
+)
+
+// An object implementing Store interface can be registered with SetCustomStore
+// function to handle storage and retrieval of captcha ids and solutions for
+// them, replacing the default memory store.
+//
+// It is the responsibility of an object to delete expired and used captchas
+// when necessary (for example, the default memory store collects them in Set
+// method after the certain amount of captchas has been stored.)
+type Store interface {
+	// Set sets the digits for the captcha id.
+	Set(id string, digits []byte)
+
+	// Get returns stored digits for the captcha id. Clear indicates
+	// whether the captcha must be deleted from the store.
+	Get(id string, clear bool) (digits []byte)
+}
+
+// expValue stores timestamp and id of captchas. It is used in the list inside
+// memoryStore for indexing generated captchas by timestamp to enable garbage
+// collection of expired captchas.
+type idByTimeValue struct {
+	timestamp time.Time
+	id        string
+}
+
+// memoryStore is an internal store for captcha ids and their values.
+type memoryStore struct {
+	sync.RWMutex
+	digitsById map[string][]byte
+	idByTime   *list.List
+	// Number of items stored since last collection.
+	numStored int
+	// Number of saved items that triggers collection.
+	collectNum int
+	// Expiration time of captchas.
+	expiration time.Duration
+}
+
+// NewMemoryStore returns a new standard memory store for captchas with the
+// given collection threshold and expiration time (duration). The returned
+// store must be registered with SetCustomStore to replace the default one.
+func NewMemoryStore(collectNum int, expiration time.Duration) Store {
+	s := new(memoryStore)
+	s.digitsById = make(map[string][]byte)
+	s.idByTime = list.New()
+	s.collectNum = collectNum
+	s.expiration = expiration
+	return s
+}
+
+func (s *memoryStore) Set(id string, digits []byte) {
+	s.Lock()
+	s.digitsById[id] = digits
+	s.idByTime.PushBack(idByTimeValue{time.Now(), id})
+	s.numStored++
+	if s.numStored <= s.collectNum {
+		s.Unlock()
+		return
+	}
+	s.Unlock()
+	go s.collect()
+}
+
+func (s *memoryStore) Get(id string, clear bool) (digits []byte) {
+	if !clear {
+		// When we don't need to clear captcha, acquire read lock.
+		s.RLock()
+		defer s.RUnlock()
+	} else {
+		s.Lock()
+		defer s.Unlock()
+	}
+	digits, ok := s.digitsById[id]
+	if !ok {
+		return
+	}
+	if clear {
+		delete(s.digitsById, id)
+		// XXX(dchest) Index (s.idByTime) will be cleaned when
+		// collecting expired captchas.  Can't clean it here, because
+		// we don't store reference to expValue in the map.
+		// Maybe store it?
+	}
+	return
+}
+
+func (s *memoryStore) collect() {
+	now := time.Now()
+	s.Lock()
+	defer s.Unlock()
+	s.numStored = 0
+	for e := s.idByTime.Front(); e != nil; {
+		ev, ok := e.Value.(idByTimeValue)
+		if !ok {
+			return
+		}
+		if ev.timestamp.Add(s.expiration).Before(now) {
+			delete(s.digitsById, ev.id)
+			next := e.Next()
+			s.idByTime.Remove(e)
+			e = next
+		} else {
+			return
+		}
+	}
+}

+ 79 - 0
gocaptcha/store_test.go

@@ -0,0 +1,79 @@
+// Copyright 2011 Dmitry Chestnykh. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package gocaptcha
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestSetGet(t *testing.T) {
+	s := NewMemoryStore(CollectNum, Expiration)
+	id := "captcha id"
+	d := RandomDigits(10)
+	s.Set(id, d)
+	d2 := s.Get(id, false)
+	if d2 == nil || !bytes.Equal(d, d2) {
+		t.Errorf("saved %v, getDigits returned got %v", d, d2)
+	}
+}
+
+func TestGetClear(t *testing.T) {
+	s := NewMemoryStore(CollectNum, Expiration)
+	id := "captcha id"
+	d := RandomDigits(10)
+	s.Set(id, d)
+	d2 := s.Get(id, true)
+	if d2 == nil || !bytes.Equal(d, d2) {
+		t.Errorf("saved %v, getDigitsClear returned got %v", d, d2)
+	}
+	d2 = s.Get(id, false)
+	if d2 != nil {
+		t.Errorf("getDigitClear didn't clear (%q=%v)", id, d2)
+	}
+}
+
+func TestCollect(t *testing.T) {
+	//TODO(dchest): can't test automatic collection when saving, because
+	//it's currently launched in a different goroutine.
+	s := NewMemoryStore(10, -1)
+	// create 10 ids
+	ids := make([]string, 10)
+	d := RandomDigits(10)
+	for i := range ids {
+		ids[i] = randomId()
+		s.Set(ids[i], d)
+	}
+	s.(*memoryStore).collect()
+	// Must be already collected
+	nc := 0
+	for i := range ids {
+		d2 := s.Get(ids[i], false)
+		if d2 != nil {
+			t.Errorf("%d: not collected", i)
+			nc++
+		}
+	}
+	if nc > 0 {
+		t.Errorf("= not collected %d out of %d captchas", nc, len(ids))
+	}
+}
+
+func BenchmarkSetCollect(b *testing.B) {
+	b.StopTimer()
+	d := RandomDigits(10)
+	s := NewMemoryStore(9999, -1)
+	ids := make([]string, 1000)
+	for i := range ids {
+		ids[i] = randomId()
+	}
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		for j := 0; j < 1000; j++ {
+			s.Set(ids[j], d)
+		}
+		s.(*memoryStore).collect()
+	}
+}

+ 49 - 0
gocaptcha/verify.go

@@ -0,0 +1,49 @@
+package gocaptcha
+
+import (
+	"bytes"
+	"strings"
+)
+
+// Verify returns true if the given digits are the ones that were used to
+// create the given captcha id.
+//
+// The function deletes the captcha with the given id from the internal
+// storage, so that the same captcha can't be verified anymore.
+func Verify(id string, digits []byte) bool {
+	if digits == nil || len(digits) == 0 {
+		return false
+	}
+	reald := globalStore.Get(id, true)
+	if reald == nil {
+		return false
+	}
+	return bytes.Equal(digits, reald)
+}
+
+// VerifyString is like Verify, but accepts a string of digits.  It removes
+// spaces and commas from the string, but any other characters, apart from
+// digits and listed above, will cause the function to return false.
+func VerifyString(id string, digits string) bool {
+	if digits == "" {
+		return false
+	}
+	if strings.TrimSpace(id) == strings.TrimSpace(digits) {
+		return true
+	}
+	//ns := RandomDigits(len([]rune(id)))
+	//ns := make([]byte, len(digits))
+	//for i := range ns {
+	//	d := digits[i]
+	//	switch {
+	//	case '0' <= d && d <= '9':
+	//		ns[i] = d - '0'
+	//	case d == ' ' || d == ',':
+	//		// ignore
+	//	default:
+	//		ns[i] = d
+	//	}
+	//}
+	//return Verify(id, ns)
+	return false
+}