blob: e367887700995e0e7f8c6225a19e54b49a995d4f [file] [log] [blame]
Sergiusz Bazanskied65c192019-10-23 20:38:43 +02001package main
2
3import (
4 "context"
5 "encoding/json"
6 "flag"
7 "fmt"
8 "io/ioutil"
9 "net/http"
10 "net/http/cookiejar"
11 "regexp"
12 "strings"
13 "time"
14
15 "github.com/golang/glog"
16)
17
18func init() {
19 flag.Set("logtostderr", "true")
20}
21
22var (
23 flagListen string
24
25 reVoucher = regexp.MustCompile("[A-Z0-9]+")
26)
27
28type voucherstatus int
29
30const (
31 statusUnknown voucherstatus = iota
32 statusInvalid
33 statusUnused
34 statusUsed
Serge Bazanskida67c6d2019-10-25 19:55:05 +020035 statusCart
Sergiusz Bazanskied65c192019-10-23 20:38:43 +020036)
37
38func (v voucherstatus) String() string {
39 switch v {
40 case statusInvalid:
41 return "INVALID"
42 case statusUnused:
43 return "UNUSED"
44 case statusUsed:
45 return "USED"
Serge Bazanskida67c6d2019-10-25 19:55:05 +020046 case statusCart:
47 return "INCART"
Sergiusz Bazanskied65c192019-10-23 20:38:43 +020048 }
49 return "UNKNOWN"
50}
51
52type vouchercache struct {
53 status voucherstatus
54 expires time.Time
55}
56
57func (c *vouchercache) fresh() bool {
58 if c.status == statusUsed {
59 return true
60 }
61 if c.expires.Before(time.Now()) {
62 return false
63 }
64 return true
65}
66
67type statusReq struct {
68 voucher string
69 res chan voucherstatus
70}
71
72type refreshRes struct {
73 voucher string
74 status voucherstatus
75}
76
77type service struct {
78 statusReq chan *statusReq
79
80 pretixSem chan struct{}
81}
82
83func newService() *service {
84 return &service{
85 statusReq: make(chan *statusReq),
86 pretixSem: make(chan struct{}, 3),
87 }
88}
89
90func (s *service) worker(ctx context.Context) error {
91 cache := make(map[string]*vouchercache)
92 waiters := make(map[string][]chan voucherstatus)
93 refreshes := make(chan *refreshRes)
94
95 for {
96 select {
97 case <-ctx.Done():
98 return ctx.Err()
99
100 // is there a refresh pending?
101 case ref := <-refreshes:
102 glog.Infof("cache feed: %v is %v", ref.voucher, ref.status)
103 expires := 30 * time.Minute
104 if ref.status == statusInvalid {
105 expires = 48 * time.Hour
106 }
107 cache[ref.voucher] = &vouchercache{
108 status: ref.status,
109 expires: time.Now().Add(expires),
110 }
111 for _, w := range waiters[ref.voucher] {
112 w := w
113 go func() {
114 w <- ref.status
115 }()
116 }
117 delete(waiters, ref.voucher)
118
119 // is there a new request?
120 case req := <-s.statusReq:
121 // return cache if fresh
122 if el, ok := cache[req.voucher]; ok && el.fresh() {
123 go func() {
124 glog.Infof("cache hit: %v is %v", req.voucher, el.status)
125 req.res <- el.status
126 }()
127 continue
128 }
129 // is someone waiting for a refresh already?
130 if _, ok := waiters[req.voucher]; ok {
131 glog.Infof("cache miss, secondary: %v", req.voucher)
132 waiters[req.voucher] = append(waiters[req.voucher], req.res)
133 continue
134 }
135 // request refresh
136 glog.Infof("cache miss, primary: %v", req.voucher)
137 waiters[req.voucher] = []chan voucherstatus{req.res}
138 go func() {
139 s := s.getStatus(ctx, req.voucher)
140 refreshes <- &refreshRes{
141 voucher: req.voucher,
142 status: s,
143 }
144 }()
145 }
146 }
147}
148
149func (s *service) run() {
150 mux := http.NewServeMux()
151 mux.HandleFunc("/status", s.handlerStatus)
152
153 ctx := context.Background()
154 go s.worker(ctx)
155
156 glog.Infof("Listening on %s...", flagListen)
157 if err := http.ListenAndServe(flagListen, mux); err != nil {
158 glog.Exitf("could not listen: %v", err)
159 }
160}
161
162func (s *service) handlerStatus(w http.ResponseWriter, r *http.Request) {
163 status := statusUnknown
164 defer func() {
165 e := json.NewEncoder(w)
166 e.Encode(struct {
167 Status string
168 }{
169 Status: status.String(),
170 })
171 }()
172
173 voucher := r.URL.Query().Get("voucher")
174 if voucher == "" || !strings.HasPrefix(voucher, "CHAOS") {
175 status = statusInvalid
176 return
177 }
178 if !reVoucher.MatchString(voucher) {
179 status = statusInvalid
180 return
181 }
182
183 resC := make(chan voucherstatus)
184 s.statusReq <- &statusReq{
185 voucher: voucher,
186 res: resC,
187 }
188 status = <-resC
189}
190
191func (s *service) getStatus(ctx context.Context, voucher string) voucherstatus {
192 s.pretixSem <- struct{}{}
193 defer func() {
194 <-s.pretixSem
195 }()
196
197 cookieJar, _ := cookiejar.New(nil)
198 client := &http.Client{
199 Jar: cookieJar,
200 }
201
202 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))
203 if err != nil {
204 glog.Errorf("Getting main page: %v", err)
205 return statusUnknown
206 }
207 defer res.Body.Close()
208
Sergiusz Bazanskied65c192019-10-23 20:38:43 +0200209 data, err := ioutil.ReadAll(res.Body)
210 if err != nil {
211 glog.Errorf("Reading result page: %v", err)
212 return statusUnknown
213 }
214
215 if strings.Contains(string(data), "not known") {
216 return statusInvalid
217 }
218 if strings.Contains(string(data), "already been") {
219 return statusUsed
220 }
221 if strings.Contains(string(data), "You entered a voucher code that allows you ") {
222 return statusUnused
223 }
Serge Bazanskida67c6d2019-10-25 19:55:05 +0200224 if strings.Contains(string(data), "voucher code is currently locked") {
225 return statusCart
226 }
Sergiusz Bazanskied65c192019-10-23 20:38:43 +0200227
228 glog.Errorf("Unexpected result for %s", voucher)
229 glog.Infof("%s", data)
230 status := statusUnknown
231
232 return status
233}
234
235func main() {
236 flag.StringVar(&flagListen, "listen", ":8081", "Listen address")
237 flag.Parse()
238
239 s := newService()
240 s.run()
241}