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