hswaw/voucherchecker: init

Change-Id: Id79ae9b14f61edf2f4abb3d9a60294edd6074f29
master
q3k 2019-10-23 20:38:43 +02:00
parent 831a54acd9
commit ed65c190be
2 changed files with 282 additions and 0 deletions

View File

@ -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}",
)

View File

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