-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
1131 lines (1036 loc) · 31.4 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package main
/*
This is meant to run behind a web server like nginx which handles TLS/capping POST size/rate-limiting/etc.
*/
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
mrand "math/rand"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/awnumar/memguard"
badger "github.com/dgraph-io/badger/v3"
"github.com/mr-tron/base58"
"github.com/vmihailenco/msgpack/v5"
gomail "gopkg.in/gomail.v2"
)
const (
keyPrefixCT = "ct_" // ctRecord
keyPrefixIP = "ip_" // ipRecord
keyPrefixRV = "rv_" // rvRecord
)
var (
config globalConfig
db *badger.DB
ctIDRX *regexp.Regexp
emailRX *regexp.Regexp
randFac chan [16]byte
hmacSecret *memguard.LockedBuffer
hIPSecrets []*memguard.LockedBuffer
gidCounter = randUInt32()
getKeys activeGetKeys
)
// values in config file
type globalConfig struct {
DBPath string
Debug bool
Listen string
HCSiteKey string
HCSecret string
AbuseMail string
Mail string
RealIPHeader string
RTLRRMinRatio float64
RTLRR map[int]map[int]int64 // RTL Range Ratio
RTLRRMinRange int
}
// json response to all api calls
type response struct {
Error string
Result interface{}
}
// limits sGetHandler transactions for the same key to one at a time
type activeGetKeysWaiter struct {
wait chan struct{}
count int
}
type activeGetKeys struct {
sync.Mutex
active map[string]*activeGetKeysWaiter
}
// limits the number of times /contact api endpoint can be used
var contactLimiter struct {
sync.Mutex
i int
}
// stats we store on securely hashed IP address using /api
// this information is only used to track down and stop abuse of /api
// all ipRecords are automatically deleted... just like everything else
type ipRecord struct {
Gets int // total sGet
Sets int // total sSet
Retrievals int // total messages sent by this network that were retrieved
RTLs int // total RTL associated with Sets
TTLs int // total TTL associated with Sets
Complaints int // total abuse/spam complaints specifying messages sent from this ip
Created int64 // created timestamp
Updated int64 // last updated timestamp
}
// encrypted message record
type ctRecord struct {
CT string // ciphertext
RTL int // retrievals-to-live
TTL int // time-to-live
HIP string // hashed IP
Captcha bool // is a captcha required?
NoCopy bool // easy to copy decrypted result?
Created int64 // created timestamp
Updated int64 // last updated timestamp
}
// temporary retrieval record keyed off of ctRecord used to handle spam complaints
// since by the time a user can complain about spam, the ctRecord may already be deleted
// stores the HIP/Created from the ctRecord briefly
type rvRecord struct {
HIP string // hashed IP
Created int64 // created timestamp
}
func init() {
// Make less calls to rand.Read by reading in 256 bytes each time
randFac = make(chan [16]byte, 1000)
var b [256]byte
var count int64
go func(randFac chan [16]byte) {
for {
cur := count & 0xF
count++
if cur == 0 {
_, err := rand.Read(b[:])
if err != nil {
log.Panicln(err)
}
}
start, end := cur*16, (cur+1)*16
var out [16]byte
copy(out[:], b[start:end])
randFac <- out
}
}(randFac)
// HMAC secret updates on restart
hmacSecret = memguard.NewBufferRandom(32)
// init getKeys for use in sGetHandler
getKeys.active = make(map[string]*activeGetKeysWaiter)
// ludicrous amount of bits to hash IPs protected by guard pages
// why more than one? if someone could read process memory, they can get the secrets used to hash IPs
// if they attempt to read a secret and trip a guard page, the process crashes and the remaining
// secrets are hopefully lost - thereby rendering any stored hashed IPs useless
// how much extra protection does this really provide if someone has already gained
// enough access to read process memory? not sure, but at least its something
for i := 0; i < 10; i++ {
hIPSecrets = append(hIPSecrets, memguard.NewBufferRandom(32))
}
// Validation regexes
ctIDRX = regexp.MustCompile(`^[a-zA-Z0-9]{20,24}$`)
emailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
}
// Generate a secure url-friendly global-enough ID
func getGID() string {
// random 16 bytes should be big enough to avoid collisions
randBytes := <-randFac
return string(base58.Encode(randBytes[:]))
}
// A random uint32
func randUInt32() uint32 {
b := make([]byte, 3)
if _, err := rand.Reader.Read(b); err != nil {
log.Panicln(err)
}
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
}
// Create a secure hash of an IP (IPv4 or 6 - all the same to us since it is a string)
// this only yields consistent hashing for the same IP between process restarts
// this is of course annoying but a necessary tradeoff to ensure IP privacy
func hashIP(ip string, secret []byte) string {
for _, hip := range hIPSecrets {
secret = append(secret, hip.Bytes()...)
}
hmac := hmac.New(sha256.New, secret)
_, err := hmac.Write([]byte(ip))
if err != nil {
log.Panicln(err)
}
return fmt.Sprintf("%x", hmac.Sum(nil))
}
// encode a record for DB storage
func encodeRecord(record interface{}) []byte {
b, err := msgpack.Marshal(record)
if err != nil {
log.Panicln(err)
}
return b
}
// decodes an ipRecord
func decodeIPRecord(eipr []byte) *ipRecord {
ipr := &ipRecord{}
err := msgpack.Unmarshal(eipr, ipr)
if err != nil {
log.Panicln(err)
}
return ipr
}
// decodes an ctRecord
func decodeCTRecord(ectr []byte) *ctRecord {
ctr := &ctRecord{}
err := msgpack.Unmarshal(ectr, ctr)
if err != nil {
log.Panicln(err)
}
return ctr
}
// decodes an rvRecord
func decodeRVRecord(ectr []byte) *rvRecord {
ctr := &rvRecord{}
err := msgpack.Unmarshal(ectr, ctr)
if err != nil {
log.Panicln(err)
}
return ctr
}
// Update stats for hashed anonymous IP - also returns the current record (if any)
// IPs are hashed using hashIP() in a way that prevents deriving the IP from the hash
// stats on IPs are only kept to help prevent abuse/spam - most records last for only 5 minutes
func ipStats(txn *badger.Txn, hashedIP string, update map[string]int) (*ipRecord, error) {
// get existing ipRecord if any
var ipr *ipRecord
item, err := txn.Get([]byte(keyPrefixIP + hashedIP))
if err == badger.ErrKeyNotFound || item.IsDeletedOrExpired() {
ipr = &ipRecord{}
} else if err != nil {
return nil, err
} else {
item.Value(func(val []byte) error {
ipr = decodeIPRecord(val)
return nil
})
}
// if there is nothing to update, just return the record
if update == nil {
return ipr, nil
}
now := time.Now()
// update components of ipRecord
for k, v := range update {
switch k {
case "Gets":
ipr.Gets += v
case "Sets":
ipr.Sets += v
case "Retrievals":
ipr.Retrievals += v
case "RTLs":
ipr.RTLs += v
case "TTLs":
ipr.TTLs += v
case "Complaints":
ipr.Complaints += v
}
}
// we cannot store information on all securely hashed IP addresses - we cannot afford those computing resources
// we just want to store information about possible abusers/spammers and quickly delete all other info
// the goal of the below TTL settings is to accomplish this...
// default TTL
ttl := time.Second * time.Duration(300) // 5 minutes for the vast majority of users
if ipr.Created == 0 {
ipr.Created = now.Unix()
} else {
// retrievals / totalrtl ratio provides a good indicator for spaminess once totalrtl is high enough
den := 1
if ipr.RTLs > 0 { // this can be zero in a couple scenarios
den = ipr.RTLs
}
retrievalsToRTLs := float64(ipr.Retrievals) / float64(den)
// conditions which can lengthen the default TTL
if ipr.Complaints > 0 {
// we have received spam complaints
ttl = time.Second * time.Duration(2419200) // 4 weeks
} else if ipr.RTLs >= 2500 && retrievalsToRTLs < .2 {
ttl = time.Second * time.Duration(1209600) // 2 weeks
} else if ipr.RTLs >= 1000 && retrievalsToRTLs < .2 {
ttl = time.Second * time.Duration(604800) // 1 week
} else if ipr.RTLs >= 500 && retrievalsToRTLs < .2 {
ttl = time.Second * time.Duration(86400) // 1 day
} else if ipr.RTLs >= 500 {
ttl = time.Second * time.Duration(3600) // 1 hour
} else if ipr.RTLs >= 250 {
ttl = time.Second * time.Duration(1800) // 30 minutes
} else if ipr.RTLs >= 50 {
ttl = time.Second * time.Duration(600) // 10 minutes
}
}
ipr.Updated = now.Unix()
if config.Debug {
log.Println("iprecord: " + fmt.Sprintf("%+v", ipr))
}
entry := badger.NewEntry([]byte(keyPrefixIP+hashedIP), encodeRecord(ipr)).WithTTL(ttl)
err = txn.SetEntry(entry)
if err != nil {
return nil, err
}
return ipr, nil
}
// retrieves a CT record and increments/decrements associated stats
func sGetHandler(rw http.ResponseWriter, req *http.Request) {
response := &response{}
if req.Method != "POST" {
response.Error = "Only POST accepted"
printResponse(response, rw, http.StatusInternalServerError)
return
}
body, err := ioutil.ReadAll(req.Body) // upstream caps body size for us
if err != nil {
response.Error = "Communication error"
printResponse(response, rw, http.StatusInternalServerError)
return
}
if config.Debug {
log.Println("sGetHandler req: " + string(body))
}
var request struct {
ID string
Token string
}
type getResult struct {
CT string
Captcha bool // set to true to tell the client they need to pass a captcha first
NoCopy bool
}
err = json.Unmarshal(body, &request)
if err != nil {
response.Error = "Malformed json/post sent in request"
printResponse(response, rw, http.StatusOK)
return
} else if !ctIDRX.MatchString(request.ID) {
response.Error = "Invalid ID"
printResponse(response, rw, http.StatusOK)
return
}
if request.Token != "" {
cPass, err := hCaptchaVerify(request.Token)
if err != nil {
log.Println(err)
response.Error = "Problem verifying captcha"
printResponse(response, rw, http.StatusOK)
return
}
if !cPass {
// they must try again
log.Println("Captcha verification failed")
response.Result = getResult{
Captcha: true,
}
printResponse(response, rw, http.StatusOK)
return
}
}
var ctr *ctRecord
cRequired := errors.New("captcha required")
now := time.Now()
// Users need assurance that if a message was set to only ever be retrieved once, that it can indeed
// only be retrieved once. It is possible given badgerdb, although unlikely, that a timing attack could
// be used to retrieve a message more than once if a bunch of gets came in before the transaction was
// committed. We therefore guard against this to ensure that there can only ever be one open transaction
// for the same key.
var waiter *activeGetKeysWaiter
getKey := keyPrefixCT + request.ID
exists := true
for exists {
getKeys.Lock()
waiter, exists = getKeys.active[getKey]
if exists {
waiter.count++
count := waiter.count
getKeys.Unlock()
if count > 3 {
// discourage this sort of thing - should be extremely rare to non-existent outside of it being done on purpose
log.Println("Requests fighting for same key")
response.Error = "Invalid ID"
printResponse(response, rw, http.StatusInternalServerError)
return
}
time.Sleep(1 * time.Second)
<-waiter.wait
} else {
waiter = &activeGetKeysWaiter{wait: make(chan struct{})}
getKeys.active[getKey] = waiter
getKeys.Unlock()
}
}
defer func() {
go func() {
getKeys.Lock()
delete(getKeys.active, getKey)
close(waiter.wait)
getKeys.Unlock()
}()
}()
// NOTE: we issue an update transaction here since excluding the scenario where the key
// does not exist, we have to update/delete the record for RTL anwyays
err = db.Update(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(getKey))
if err != nil {
return err // record either does not exist or we have problems
}
if item.IsDeletedOrExpired() {
// is this even needed?
return badger.ErrKeyNotFound
}
err = item.Value(func(val []byte) error {
ctr = decodeCTRecord(val)
return nil
})
if err != nil {
return err
}
hashedIP := hashIP(req.Header.Get(config.RealIPHeader), stoHIPSecret(req))
_, err = ipStats(txn, hashedIP, map[string]int{"Gets": 1})
if err != nil {
return err
}
_, err = ipStats(txn, ctr.HIP, map[string]int{"Retrievals": 1})
if err != nil {
return err
}
if ctr.Captcha && request.Token == "" {
err = cRequired
} else if ctr.RTL <= 1 {
// we have to delete this record since this is the last allowed read
// but before we do create a rvRecord in case the user reports this message as spam
rvr := &rvRecord{
HIP: ctr.HIP,
Created: ctr.Created,
}
// we give them 10 minutes to decide if it is spam
entry := badger.NewEntry([]byte(getKey), encodeRecord(rvr)).WithTTL(time.Second * time.Duration(600))
err = txn.SetEntry(entry)
if err != nil {
return err
}
// delete CT record
err = txn.Delete([]byte(getKey))
} else {
// we have to decrement rtl
ctr.RTL--
ctr.Updated = now.Unix()
ctrRecord := encodeRecord(ctr)
entry := badger.NewEntry([]byte(getKey), ctrRecord).WithTTL(time.Second * time.Duration(int64(item.ExpiresAt())-now.Unix()))
err = txn.SetEntry(entry)
}
return err
})
if err != nil {
if err == badger.ErrKeyNotFound {
response.Error = "Record not found"
printResponse(response, rw, http.StatusOK)
} else if err == cRequired {
// we cannot serve the CT until they pass a captcha
response.Result = getResult{
Captcha: true,
}
printResponse(response, rw, http.StatusOK)
return
} else {
// we need to know about this type of error
log.Println(err) // should we bail here instead?
response.Error = "Database error"
printResponse(response, rw, http.StatusInternalServerError)
}
return
}
response.Result = getResult{
CT: ctr.CT,
NoCopy: ctr.NoCopy,
}
printResponse(response, rw, http.StatusOK)
}
// creates a new CT record - this stores a new encrypted temporary message
func sSetHandler(rw http.ResponseWriter, req *http.Request) {
response := &response{}
if req.Method != "POST" {
response.Error = "Only POST accepted"
printResponse(response, rw, http.StatusInternalServerError)
return
}
body, err := ioutil.ReadAll(req.Body) // upstream caps body size for us
if err != nil {
response.Error = "Communication error"
printResponse(response, rw, http.StatusInternalServerError)
return
}
if config.Debug {
log.Println("sSetHandler req: " + string(body))
}
var request struct {
CT string // ciphertext to set - only used for set
TTL int // time-to-live - only used for set
RTL int // reads-to-live - only used for set
HMAC string // HMAC verifiy iterations
Deadline int64 // unix time deadline
Captcha bool // require captcha for recipient
NoCopy bool // make copying the plaintext more difficult
}
err = json.Unmarshal(body, &request)
// do we have valid input?
now := time.Now()
if err != nil {
if config.Debug {
log.Println(err)
}
response.Error = "Malformed/invalid json"
printResponse(response, rw, http.StatusOK)
return
} else if request.CT == "" {
response.Error = "CT not specified"
printResponse(response, rw, http.StatusOK)
return
} else if request.HMAC == "" {
response.Error = "HMAC not specified"
printResponse(response, rw, http.StatusOK)
return
} else if request.Deadline == 0 {
response.Error = "Deadline not specified"
printResponse(response, rw, http.StatusOK)
return
} else if request.TTL < 60 || request.TTL > 1209600 {
response.Error = "TTL must be between 60 and 1209600"
printResponse(response, rw, http.StatusOK)
return
} else if request.RTL < 1 || request.RTL > 30 {
response.Error = "RTL must be between 1 and 30"
printResponse(response, rw, http.StatusOK)
return
} else if request.Deadline < now.Unix() {
response.Error = "Deadline has passed"
printResponse(response, rw, http.StatusOK)
return
}
ctParts := strings.Split(request.CT, "~")
if len(ctParts) != 4 {
response.Error = "Invalid CT"
printResponse(response, rw, http.StatusOK)
return
}
hmac := hmac.New(sha256.New, hmacSecret.Bytes())
_, err = hmac.Write([]byte(ctParts[0] + strconv.Itoa(int(request.Deadline))))
if err != nil {
log.Panicln(err)
}
compareNew := []byte(base64.RawURLEncoding.EncodeToString(hmac.Sum(nil)))
compareReq := []byte(request.HMAC)
if subtle.ConstantTimeCompare(compareNew, compareReq) == 0 {
response.Error = "Invalid HMAC/Iter/Deadline"
printResponse(response, rw, http.StatusOK)
return
}
gid := getGID()
err = db.Update(func(txn *badger.Txn) error {
hashedIP := hashIP(req.Header.Get(config.RealIPHeader), stoHIPSecret(req))
_, err := ipStats(txn, hashedIP, map[string]int{
"Sets": 1,
"RTLs": request.RTL,
"TTLs": request.TTL,
})
if err != nil {
return err
}
ctRecord := &ctRecord{
CT: request.CT,
RTL: request.RTL,
TTL: request.TTL,
HIP: hashedIP,
Captcha: request.Captcha,
NoCopy: request.NoCopy,
Created: now.Unix(),
Updated: now.Unix(),
}
entry := badger.NewEntry([]byte(keyPrefixCT+gid), encodeRecord(ctRecord)).WithTTL(time.Second * time.Duration(request.TTL))
err = txn.SetEntry(entry)
return err
})
if err != nil {
log.Println(err) // should we bail here instead?
response.Error = "Database error"
printResponse(response, rw, http.StatusInternalServerError)
return
}
response.Result = struct {
ID string
BURL string
}{
gid,
"https://deletebin.org/s",
}
//response.Error = ""
printResponse(response, rw, http.StatusOK)
}
// handles form submission requests where the user expects to reach a person
func contactHandler(rw http.ResponseWriter, req *http.Request) {
response := &response{}
if req.Method != "POST" {
response.Error = "Only POST accepted"
printResponse(response, rw, http.StatusInternalServerError)
return
}
body, err := ioutil.ReadAll(req.Body) // upstream caps body size for us
if err != nil {
response.Error = "Communication error"
printResponse(response, rw, http.StatusInternalServerError)
return
}
if config.Debug {
log.Println("contactHandler req: " + string(body))
}
var request struct {
Name string
Email string
Message string
Route string
}
err = json.Unmarshal(body, &request)
// do we have valid input?
if err != nil {
if config.Debug {
log.Println(err)
}
response.Error = "Malformed/invalid json"
printResponse(response, rw, http.StatusOK)
return
} else if request.Message == "" {
response.Error = "Comment not specified"
printResponse(response, rw, http.StatusOK)
return
} else if len(request.Message) > 100000 {
response.Error = "Message too big"
printResponse(response, rw, http.StatusOK)
return
}
// avoid a form submission flood
contactLimiter.Lock()
if contactLimiter.i >= 30 {
response.Error = "Too many messages"
printResponse(response, rw, http.StatusOK)
contactLimiter.Unlock()
return
} else {
contactLimiter.i++
contactLimiter.Unlock()
}
// send us an email - these emails are also auto-deleted after a ttl
var to string
var subj string
if request.Route == "abuse" {
to = config.AbuseMail
subj = "DeleteBin Abuse Form Submission"
} else if request.Route == "translate" {
to = config.Mail
subj = "DeleteBin Translate Form Submission"
} else {
to = config.Mail
subj = "DeleteBin Contact Form Submission"
}
m := gomail.NewMessage()
m.SetHeader("To", to)
m.SetHeader("Subject", subj)
m.SetHeader("From", config.Mail)
if len(request.Email) > 3 && len(request.Email) < 254 && emailRX.MatchString(request.Email) {
m.SetHeader("Reply-To", request.Email)
}
messageBody := "Name: " + request.Name + "\n\n" +
"Email: " + request.Email + "\n\n" +
"Message: " + request.Message + "\n\n" +
"Client Details: " + req.Header.Get(config.RealIPHeader) + " / " + req.Header.Get("User-Agent")
m.SetBody("text/plain", messageBody)
// localhost is configured to properly send outbound over encrypted connection
// email sent is encrypted all the way to the end recipient
d := gomail.Dialer{Host: "localhost", Port: 25}
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
if err := d.DialAndSend(m); err != nil {
response.Error = "Error sending"
log.Println(err)
printResponse(response, rw, http.StatusOK)
return
}
response.Error = ""
response.Result = struct {
Sent bool
}{
true,
}
printResponse(response, rw, http.StatusOK)
}
// handles requests that are created when people click the report spam button on message retrieval
func spamHandler(rw http.ResponseWriter, req *http.Request) {
response := &response{}
if req.Method != "POST" {
response.Error = "Only POST accepted"
printResponse(response, rw, http.StatusInternalServerError)
return
}
body, err := ioutil.ReadAll(req.Body) // upstream caps body size for us
if err != nil {
response.Error = "Communication error"
printResponse(response, rw, http.StatusInternalServerError)
return
}
if config.Debug {
log.Println("spamHandler req: " + string(body))
}
var request struct {
ID string
Comments string
}
err = json.Unmarshal(body, &request)
// do we have valid input?
if err != nil {
if config.Debug {
log.Println(err)
}
response.Error = "Malformed/invalid json"
printResponse(response, rw, http.StatusOK)
return
} else if request.ID == "" {
response.Error = "ID not specified"
printResponse(response, rw, http.StatusOK)
return
}
// avoid a form submission flood
contactLimiter.Lock()
if contactLimiter.i >= 30 {
response.Error = "Too many messages"
printResponse(response, rw, http.StatusOK)
contactLimiter.Unlock()
return
} else {
contactLimiter.i++
contactLimiter.Unlock()
}
// update ip stats for sender of the possibly spam message
var hashedIP string
var created int64
var ipr *ipRecord
err = db.Update(func(txn *badger.Txn) error {
// check for CT record first
item, err := txn.Get([]byte(keyPrefixCT + request.ID))
if err != nil {
if err != badger.ErrKeyNotFound {
return err
}
// check for RV record next
item, err = txn.Get([]byte(keyPrefixRV + request.ID))
if err != nil {
return err
} else {
err = item.Value(func(val []byte) error {
rvr := decodeRVRecord(val)
hashedIP = rvr.HIP
created = rvr.Created
return nil
})
if err != nil {
return err
}
}
} else {
// CT record still exists
err = item.Value(func(val []byte) error {
ctr := decodeCTRecord(val)
hashedIP = ctr.HIP
created = ctr.Created
return nil
})
if err != nil {
return err
}
}
ipr, err = ipStats(txn, hashedIP, map[string]int{"Complaints": 1})
return err
})
if err != nil {
if err == badger.ErrKeyNotFound {
response.Error = "Record not found"
printResponse(response, rw, http.StatusOK)
} else {
// we need to know about this type of error
log.Println(err) // should we bail here instead?
response.Error = "Database error"
printResponse(response, rw, http.StatusInternalServerError)
}
return
}
// send us an email - these emails are also auto-deleted after a ttl
m := gomail.NewMessage()
m.SetHeader("To", config.AbuseMail)
m.SetHeader("Subject", "Spam Complaint")
m.SetHeader("From", config.Mail)
senderData := fmt.Sprintf("%+v\n", ipr)
datetime := fmt.Sprint(time.Unix(created, 0))
messageBody := "Comments: " + request.Comments + "\n\n" +
"Message Sent: " + datetime + "\n\n" +
"Message Sender Data: " + senderData + "\n\n" +
"Client Details: " + req.Header.Get(config.RealIPHeader) + " / " + req.Header.Get("User-Agent")
m.SetBody("text/plain", messageBody)
// localhost is configured to properly send outbound over encrypted connection
// email sent is encrypted all the way to the end recipient
d := gomail.Dialer{Host: "localhost", Port: 25}
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
if err := d.DialAndSend(m); err != nil {
response.Error = "Error sending"
log.Println(err)
printResponse(response, rw, http.StatusOK)
return
}
response.Error = ""
response.Result = struct {
Sent bool
}{
true,
}
printResponse(response, rw, http.StatusOK)
}
// Set some rules for the client so they know what they must do to sSet - hands out number of required PBKDF2 iterations and deadline
// this is a precautionary step to offer some options to help prevent and deal with abuse/spam/etc.
// without relying on captchas... use up computer time instead of human time - but of course, this opens us up to
// abuse whereby someone could simply flood us from a "good" network to force real people to run more PBKDF2 iterations
// captchas may be inevitable if abuse ever becomes a major issue... but i hate captchas so we just increase PBKDF2 iterations for now
func sPreSetHandler(rw http.ResponseWriter, req *http.Request) {
response := &response{}
if req.Method != "POST" {
response.Error = "Only POST accepted"
printResponse(response, rw, http.StatusInternalServerError)
return
}
// determine baseline iterations
now := time.Now()
var iter int64
iter = 200000 // lowest possible iter count
mrand.Seed(now.UnixNano())
iter += int64(mrand.Intn(100000-1+1)) + 1
// determine any additional iterations required due to client's previous actions - if any
hashedIP := hashIP(req.Header.Get(config.RealIPHeader), stoHIPSecret(req)) // we set this value upstream
err := db.View(func(txn *badger.Txn) error {
ipr, err := ipStats(txn, hashedIP, nil)
if err != nil {
return err
}
// if this is the first time we are seeing this client, no need to do anything
if ipr.Created == 0 {
return nil
}
// if minimum RTLs have not yet been hit, no need to do anything
if ipr.RTLs < config.RTLRRMinRange {
return nil
}
// retrievals / total RTL ratio provides a good measurement to determine the spaminess of a source
// a real sender would have real recipients and therefore that ratio should approach 1 over time
// we use RTLs instead of Sets since they more accurately measure total possible recipients
retrievalsToRTLs := float64(ipr.Retrievals) / float64(ipr.RTLs)
var biggestRatio float64
for ratio := range config.RTLRR {
rratio := float64(ratio) * .01
if rratio > retrievalsToRTLs && rratio > biggestRatio {
biggestRatio = rratio
}
}
if biggestRatio == 0 {
biggestRatio = config.RTLRRMinRatio
}
biggestRatioI := int(biggestRatio * 100)
var biggestRTLRange int
for totalRTLs := range config.RTLRR[biggestRatioI] {
if totalRTLs > ipr.RTLs && totalRTLs > biggestRTLRange {
biggestRTLRange = totalRTLs
}
}
addIters := config.RTLRR[biggestRatioI][biggestRTLRange]
// TODO: there is probably a better way to incorporate complaint data, but for now, we just
// multiply any extra iterations incurred due to the retrievals / total RTL ratio by the number
// of complaints, if any
if ipr.Complaints > 0 {
addIters = addIters * int64(ipr.Complaints)
}
iter += addIters
return nil
})
if err != nil {
log.Println(err) // should we bail here instead?
response.Error = "Database error"
printResponse(response, rw, http.StatusInternalServerError)
return
}
// a deadline is set to require the client to complete their Set within a reasonable time
deadline := int(now.Unix() + 120) // they have 30 seconds to sSet
hmac := hmac.New(sha256.New, hmacSecret.Bytes())
_, err = hmac.Write([]byte(strconv.FormatInt(iter, 10) + strconv.Itoa(deadline)))
if err != nil {
log.Panicln(err)
}
response.Error = ""
response.Result = struct {
Iter int64
Deadline int
HMAC string
}{
iter,
deadline,
base64.RawURLEncoding.EncodeToString(hmac.Sum(nil)),
}
printResponse(response, rw, http.StatusOK)
}
// run the completion token by hcaptcha to verify authenticity
func hCaptchaVerify(token string) (bool, error) {
var client = &http.Client{
Timeout: time.Second * 10,
}
uv := url.Values{}
uv.Set("secret", config.HCSecret)
uv.Set("sitekey", config.HCSiteKey)
uv.Set("response", token)
payload := uv.Encode()
req, err := http.NewRequest("POST", "https://hcaptcha.com/siteverify", strings.NewReader(payload))
if err != nil {
log.Panicln(err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(payload)))
if config.Debug {
dump, _ := httputil.DumpRequestOut(req, true)
log.Printf("%s\n\n", dump)
}
resp, err := client.Do(req)