forked from hswaw/hscloud
hswaw/voucherchecker: init
Change-Id: Id79ae9b14f61edf2f4abb3d9a60294edd6074f29master
parent
831a54acd9
commit
ed65c190be
|
@ -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}",
|
||||
)
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue