-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Eugene Bujak
authored and
Victoria Suslova
committed
Oct 8, 2020
1 parent
c437c8c
commit 56972a9
Showing
6 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/mcs-s3-webhook-server-example |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Example S3 webhook server for MCS | ||
|
||
## Building | ||
``` | ||
go build | ||
``` | ||
|
||
You'll get the binary named `mcs-s3-webhook-server-example`. | ||
|
||
## Running | ||
|
||
``` | ||
./mcs-s3-webhook-server-example | ||
``` | ||
|
||
It will listen for HTTP requests on TCP port 33345. It assumes https forwarding from nginx. | ||
|
||
## Testing | ||
|
||
To test signature correctness: | ||
``` | ||
go test | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"net/http" | ||
) | ||
|
||
func httpError(w http.ResponseWriter, code int, format string, args ...interface{}) { | ||
text := fmt.Sprintf(format, args...) | ||
log.Println(text) | ||
http.Error(w, text, code) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"net/http/httputil" | ||
"strings" | ||
) | ||
|
||
func main() { | ||
http.HandleFunc("/", ServeHTTP) | ||
log.Fatal(http.ListenAndServe(":33345", nil)) | ||
} | ||
|
||
// general handler for all http requests | ||
func ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
// dump all requests to stdout | ||
dump, err := httputil.DumpRequest(r, true) | ||
if err != nil { | ||
httpError(w, http.StatusInternalServerError, "Failed to dump request") | ||
return | ||
} | ||
fmt.Printf("%s\n", dump) | ||
|
||
// X-Amz-Sns-Message-Type: SubscriptionConfirmation | ||
snsType := strings.ToLower(r.Header.Get("X-Amz-Sns-Message-Type")) | ||
switch snsType { | ||
case "subscriptionconfirmation": | ||
handleSubscriptionConfirmation(w, r) | ||
case "notification": | ||
handleNotification(w, r) | ||
default: | ||
httpError(w, http.StatusBadRequest, "Invalid SNS message type") | ||
} | ||
} | ||
|
||
// handle all subscription confirmations | ||
func handleSubscriptionConfirmation(w http.ResponseWriter, r *http.Request) { | ||
decoded := SubscriptionConfirmation{} | ||
err := json.NewDecoder(r.Body).Decode(&decoded) | ||
if err != nil { | ||
httpError(w, http.StatusBadRequest, "Failed to decode JSON in the body: %s", err) | ||
return | ||
} | ||
|
||
// assume https | ||
fullURL := fmt.Sprintf("https://%s%s", r.Host, r.URL.String()) // r.URL doesn't contain protocol or host | ||
|
||
response := struct { | ||
Signature string `json:"signature"` // base64-encoded signature | ||
}{ | ||
Signature: signSubscriptionHex(decoded, fullURL), | ||
} | ||
w.Header().Set("Content-Type", "application/json") | ||
jsonBody, err := json.Marshal(&response) | ||
if err != nil { | ||
httpError(w, http.StatusInternalServerError, "Failed to marshal json: %s", err) | ||
return | ||
} | ||
|
||
log.Printf("Responding with body: %s", jsonBody) | ||
w.Write(jsonBody) | ||
} | ||
|
||
func handleNotification(w http.ResponseWriter, r *http.Request) { | ||
// do nothing, just 200, we dump the request body in main handler already | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package main | ||
|
||
import ( | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
) | ||
|
||
type SubscriptionConfirmation struct { | ||
Timestamp string // we need to give it to hmac byte-for-byte, "2019-12-26T19:29:12+03:00", | ||
Type string // always "SubscriptionConfirmation", | ||
Message string // always "You have chosen to subscribe to the topic $topic. To confirm the subscription you need to response with calculated signature", | ||
TopicArn string // for feeding into signature, "mcs2883541269|bucketA|s3:ObjectCreated:Put", | ||
SignatureVersion int64 // always 1, | ||
Token string // for feeding into signature, "RPE5UuG94rGgBH6kHXN9FUPugFxj1hs2aUQc99btJp3E49tA" | ||
} | ||
|
||
// signSubscriptionHex returns hex-encoded signature for SNS webhook subscription confirmation | ||
func signSubscriptionHex(decoded SubscriptionConfirmation, fullURL string) string { | ||
return hex.EncodeToString(signSubscription(decoded, fullURL)) | ||
} | ||
|
||
// signSubscription returns array with a signature for SNS webhook subscription confirmation | ||
func signSubscription(decoded SubscriptionConfirmation, fullURL string) []byte { | ||
// get timestamp+token hash first | ||
firstMAC := hmac.New(sha256.New, []byte(decoded.Token)) | ||
firstMAC.Write([]byte(decoded.Timestamp)) | ||
firstHash := firstMAC.Sum(nil) | ||
// then combine that with TopicArn | ||
secondMAC := hmac.New(sha256.New, firstHash) | ||
secondMAC.Write([]byte(decoded.TopicArn)) | ||
secondHash := secondMAC.Sum(nil) | ||
// then combine that with full URL | ||
thirdMAC := hmac.New(sha256.New, secondHash) | ||
thirdMAC.Write([]byte(fullURL)) | ||
thirdHash := thirdMAC.Sum(nil) | ||
return thirdHash | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package main | ||
|
||
import "testing" | ||
|
||
// signature = hmac_sha256_hex(“http://test.com”, | ||
// hmac_sha256(“mcs2883541269|bucketA|s3:ObjectCreated:Put”, | ||
// hmac_sha256(“2019-12-26T19:29:12+03:00”, “RPE5UuG94rGgBH6kHXN9FUPugFxj1hs2aUQc99btJp3E49tA”))) | ||
// results in ea3fce4bb15c6de4fec365d36bcebbc34ccddf54616d5ca12e1972f82b6d37af | ||
func TestSubscriptionConfirmationSignature(t *testing.T) { | ||
fullURL := "http://test.com" | ||
confirmation := SubscriptionConfirmation{ | ||
Timestamp: "2019-12-26T19:29:12+03:00", | ||
Type: "", | ||
Message: "", | ||
TopicArn: "mcs2883541269|bucketA|s3:ObjectCreated:Put", | ||
SignatureVersion: 1, | ||
Token: "RPE5UuG94rGgBH6kHXN9FUPugFxj1hs2aUQc99btJp3E49tA", | ||
} | ||
signature := signSubscriptionHex(confirmation, fullURL) | ||
expected := "ea3fce4bb15c6de4fec365d36bcebbc34ccddf54616d5ca12e1972f82b6d37af" | ||
if signature != expected { | ||
t.Errorf("Wrong signature, expected %s, got %s", expected, signature) | ||
} | ||
} | ||
|
||
func BenchmarkSubscriptionConfirmationSignature(b *testing.B) { | ||
fullURL := "http://test.com" | ||
confirmation := SubscriptionConfirmation{ | ||
Timestamp: "2019-12-26T19:29:12+03:00", | ||
Type: "", | ||
Message: "", | ||
TopicArn: "mcs2883541269|bucketA|s3:ObjectCreated:Put", | ||
SignatureVersion: 1, | ||
Token: "RPE5UuG94rGgBH6kHXN9FUPugFxj1hs2aUQc99btJp3E49tA", | ||
} | ||
expected := "ea3fce4bb15c6de4fec365d36bcebbc34ccddf54616d5ca12e1972f82b6d37af" | ||
bytes := len(confirmation.Timestamp) + len(confirmation.TopicArn) + len(confirmation.Token) + len(fullURL) + len(expected) | ||
b.SetBytes(int64(bytes)) | ||
b.ResetTimer() | ||
for i := 0; i < b.N; i++ { | ||
signature := signSubscriptionHex(confirmation, fullURL) | ||
if signature != expected { | ||
b.Errorf("Wrong signature, expected %s, got %s", expected, signature) | ||
} | ||
} | ||
} |