-
Notifications
You must be signed in to change notification settings - Fork 18
/
main.go
166 lines (140 loc) · 4.55 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
// Package sns provides helper functions for verifying and processing Amazon AWS SNS HTTP POST payloads.
package sns
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"regexp"
)
// https://github.com/robbiet480/go.sns/issues/2
var hostPattern = regexp.MustCompile(`^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$`)
// Payload contains a single POST from SNS
type Payload struct {
Message string `json:"Message"`
MessageId string `json:"MessageId"`
Signature string `json:"Signature"`
SignatureVersion string `json:"SignatureVersion"`
SigningCertURL string `json:"SigningCertURL"`
SubscribeURL string `json:"SubscribeURL"`
Subject string `json:"Subject"`
Timestamp string `json:"Timestamp"`
Token string `json:"Token"`
TopicArn string `json:"TopicArn"`
Type string `json:"Type"`
UnsubscribeURL string `json:"UnsubscribeURL"`
}
// ConfirmSubscriptionResponse contains the XML response of accessing a SubscribeURL
type ConfirmSubscriptionResponse struct {
XMLName xml.Name `xml:"ConfirmSubscriptionResponse"`
SubscriptionArn string `xml:"ConfirmSubscriptionResult>SubscriptionArn"`
RequestId string `xml:"ResponseMetadata>RequestId"`
}
// UnsubscribeResponse contains the XML response of accessing an UnsubscribeURL
type UnsubscribeResponse struct {
XMLName xml.Name `xml:"UnsubscribeResponse"`
RequestId string `xml:"ResponseMetadata>RequestId"`
}
// BuildSignature returns a byte array containing a signature usable for SNS verification
func (payload *Payload) BuildSignature() []byte {
var builtSignature bytes.Buffer
signableKeys := []string{"Message", "MessageId", "Subject", "SubscribeURL", "Timestamp", "Token", "TopicArn", "Type"}
for _, key := range signableKeys {
reflectedStruct := reflect.ValueOf(payload)
field := reflect.Indirect(reflectedStruct).FieldByName(key)
value := field.String()
if field.IsValid() && value != "" {
builtSignature.WriteString(key + "\n")
builtSignature.WriteString(value + "\n")
}
}
return builtSignature.Bytes()
}
// SignatureAlgorithm returns properly Algorithm for AWS Signature Version.
func (payload *Payload) SignatureAlgorithm() x509.SignatureAlgorithm {
if payload.SignatureVersion == "2" {
return x509.SHA256WithRSA
}
return x509.SHA1WithRSA
}
// VerifyPayload will verify that a payload came from SNS
func (payload *Payload) VerifyPayload() error {
payloadSignature, err := base64.StdEncoding.DecodeString(payload.Signature)
if err != nil {
return err
}
certURL, err := url.Parse(payload.SigningCertURL)
if err != nil {
return err
}
if certURL.Scheme != "https" {
return fmt.Errorf("url should be using https")
}
if !hostPattern.Match([]byte(certURL.Host)) {
return fmt.Errorf("certificate is located on an invalid domain")
}
resp, err := http.Get(payload.SigningCertURL)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
decodedPem, _ := pem.Decode(body)
if decodedPem == nil {
return errors.New("The decoded PEM file was empty!")
}
parsedCertificate, err := x509.ParseCertificate(decodedPem.Bytes)
if err != nil {
return err
}
return parsedCertificate.CheckSignature(payload.SignatureAlgorithm(), payload.BuildSignature(), payloadSignature)
}
// Subscribe will use the SubscribeURL in a payload to confirm a subscription and return a ConfirmSubscriptionResponse
func (payload *Payload) Subscribe() (ConfirmSubscriptionResponse, error) {
var response ConfirmSubscriptionResponse
if payload.SubscribeURL == "" {
return response, errors.New("Payload does not have a SubscribeURL!")
}
resp, err := http.Get(payload.SubscribeURL)
if err != nil {
return response, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return response, err
}
xmlErr := xml.Unmarshal(body, &response)
if xmlErr != nil {
return response, xmlErr
}
return response, nil
}
// Unsubscribe will use the UnsubscribeURL in a payload to confirm a subscription and return a UnsubscribeResponse
func (payload *Payload) Unsubscribe() (UnsubscribeResponse, error) {
var response UnsubscribeResponse
resp, err := http.Get(payload.UnsubscribeURL)
if err != nil {
return response, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return response, err
}
xmlErr := xml.Unmarshal(body, &response)
if xmlErr != nil {
return response, xmlErr
}
return response, nil
}