| 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() |
| } |