hswaw/voucherchecker: init
Change-Id: Id79ae9b14f61edf2f4abb3d9a60294edd6074f29
diff --git a/hswaw/voucherchecker/BUILD.bazel b/hswaw/voucherchecker/BUILD.bazel
new file mode 100644
index 0000000..17bd5da
--- /dev/null
+++ b/hswaw/voucherchecker/BUILD.bazel
@@ -0,0 +1,41 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "code.hackerspace.pl/hscloud/hswaw/voucherchecker",
+ visibility = ["//visibility:private"],
+ deps = ["@com_github_golang_glog//:go_default_library"],
+)
+
+go_binary(
+ name = "voucherchecker",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
+
+container_layer(
+ name = "layer_bin",
+ files = [
+ ":voucherchecker",
+ ],
+ directory = "/voucherchecker/",
+)
+
+container_image(
+ name = "runtime",
+ base = "@prodimage-bionic//image",
+ layers = [
+ ":layer_bin",
+ ],
+)
+
+container_push(
+ name = "push",
+ image = ":runtime",
+ format = "Docker",
+ registry = "registry.k0.hswaw.net",
+ repository = "q3k/voucherchecker",
+ tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
+)
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()
+}