hswaw/voucherchecker: init

Change-Id: Id79ae9b14f61edf2f4abb3d9a60294edd6074f29
diff --git a/hswaw/voucherchecker/main.go b/hswaw/voucherchecker/main.go
new file mode 100644
index 0000000..4708add
--- /dev/null
+++ b/hswaw/voucherchecker/main.go
@@ -0,0 +1,241 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/cookiejar"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/golang/glog"
+)
+
+func init() {
+	flag.Set("logtostderr", "true")
+}
+
+var (
+	flagListen string
+
+	reVoucher = regexp.MustCompile("[A-Z0-9]+")
+)
+
+type voucherstatus int
+
+const (
+	statusUnknown voucherstatus = iota
+	statusInvalid
+	statusUnused
+	statusUsed
+)
+
+func (v voucherstatus) String() string {
+	switch v {
+	case statusInvalid:
+		return "INVALID"
+	case statusUnused:
+		return "UNUSED"
+	case statusUsed:
+		return "USED"
+	}
+	return "UNKNOWN"
+}
+
+type vouchercache struct {
+	status  voucherstatus
+	expires time.Time
+}
+
+func (c *vouchercache) fresh() bool {
+	if c.status == statusUsed {
+		return true
+	}
+	if c.expires.Before(time.Now()) {
+		return false
+	}
+	return true
+}
+
+type statusReq struct {
+	voucher string
+	res     chan voucherstatus
+}
+
+type refreshRes struct {
+	voucher string
+	status  voucherstatus
+}
+
+type service struct {
+	statusReq chan *statusReq
+
+	pretixSem chan struct{}
+}
+
+func newService() *service {
+	return &service{
+		statusReq: make(chan *statusReq),
+		pretixSem: make(chan struct{}, 3),
+	}
+}
+
+func (s *service) worker(ctx context.Context) error {
+	cache := make(map[string]*vouchercache)
+	waiters := make(map[string][]chan voucherstatus)
+	refreshes := make(chan *refreshRes)
+
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+
+		// is there a refresh pending?
+		case ref := <-refreshes:
+			glog.Infof("cache feed: %v is %v", ref.voucher, ref.status)
+			expires := 30 * time.Minute
+			if ref.status == statusInvalid {
+				expires = 48 * time.Hour
+			}
+			cache[ref.voucher] = &vouchercache{
+				status:  ref.status,
+				expires: time.Now().Add(expires),
+			}
+			for _, w := range waiters[ref.voucher] {
+				w := w
+				go func() {
+					w <- ref.status
+				}()
+			}
+			delete(waiters, ref.voucher)
+
+		// is there a new request?
+		case req := <-s.statusReq:
+			// return cache if fresh
+			if el, ok := cache[req.voucher]; ok && el.fresh() {
+				go func() {
+					glog.Infof("cache hit: %v is %v", req.voucher, el.status)
+					req.res <- el.status
+				}()
+				continue
+			}
+			// is someone waiting for a refresh already?
+			if _, ok := waiters[req.voucher]; ok {
+				glog.Infof("cache miss, secondary: %v", req.voucher)
+				waiters[req.voucher] = append(waiters[req.voucher], req.res)
+				continue
+			}
+			// request refresh
+			glog.Infof("cache miss, primary: %v", req.voucher)
+			waiters[req.voucher] = []chan voucherstatus{req.res}
+			go func() {
+				s := s.getStatus(ctx, req.voucher)
+				refreshes <- &refreshRes{
+					voucher: req.voucher,
+					status:  s,
+				}
+			}()
+		}
+	}
+}
+
+func (s *service) run() {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/status", s.handlerStatus)
+
+	ctx := context.Background()
+	go s.worker(ctx)
+
+	glog.Infof("Listening on %s...", flagListen)
+	if err := http.ListenAndServe(flagListen, mux); err != nil {
+		glog.Exitf("could not listen: %v", err)
+	}
+}
+
+func (s *service) handlerStatus(w http.ResponseWriter, r *http.Request) {
+	status := statusUnknown
+	defer func() {
+		e := json.NewEncoder(w)
+		e.Encode(struct {
+			Status string
+		}{
+			Status: status.String(),
+		})
+	}()
+
+	voucher := r.URL.Query().Get("voucher")
+	if voucher == "" || !strings.HasPrefix(voucher, "CHAOS") {
+		status = statusInvalid
+		return
+	}
+	if !reVoucher.MatchString(voucher) {
+		status = statusInvalid
+		return
+	}
+
+	resC := make(chan voucherstatus)
+	s.statusReq <- &statusReq{
+		voucher: voucher,
+		res:     resC,
+	}
+	status = <-resC
+}
+
+func (s *service) getStatus(ctx context.Context, voucher string) voucherstatus {
+	s.pretixSem <- struct{}{}
+	defer func() {
+		<-s.pretixSem
+	}()
+
+	cookieJar, _ := cookiejar.New(nil)
+	client := &http.Client{
+		Jar: cookieJar,
+	}
+
+	res, err := client.Get(fmt.Sprintf("https://tickets.events.ccc.de/36c3/redeem/?voucher=%s&subevent=&hello=this-is-q3k-at-hackerspace-pl-we-use-this-for-voucher-distribution", voucher))
+	if err != nil {
+		glog.Errorf("Getting main page: %v", err)
+		return statusUnknown
+	}
+	defer res.Body.Close()
+
+	setcookie := res.Header.Get("Set-Cookie")
+	if !strings.HasPrefix(setcookie, "pretix_session") {
+		glog.Errorf("Main page did not return session")
+		return statusUnknown
+	}
+
+	data, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		glog.Errorf("Reading result page: %v", err)
+		return statusUnknown
+	}
+
+	if strings.Contains(string(data), "not known") {
+		return statusInvalid
+	}
+	if strings.Contains(string(data), "already been") {
+		return statusUsed
+	}
+	if strings.Contains(string(data), "You entered a voucher code that allows you ") {
+		return statusUnused
+	}
+
+	glog.Errorf("Unexpected result for %s", voucher)
+	glog.Infof("%s", data)
+	status := statusUnknown
+
+	return status
+}
+
+func main() {
+	flag.StringVar(&flagListen, "listen", ":8081", "Listen address")
+	flag.Parse()
+
+	s := newService()
+	s.run()
+}