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 statusCart ) func (v voucherstatus) String() string { switch v { case statusInvalid: return "INVALID" case statusUnused: return "UNUSED" case statusUsed: return "USED" case statusCart: return "INCART" } 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() 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 } if strings.Contains(string(data), "voucher code is currently locked") { return statusCart } 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() }