Skip to content

Commit

Permalink
add s3 webhook-server-example
Browse files Browse the repository at this point in the history
  • Loading branch information
hmage authored and Victoria Suslova committed Oct 8, 2020
1 parent c437c8c commit d4345ab
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 0 deletions.
1 change: 1 addition & 0 deletions mcs-s3-webhook-server-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/mcs-s3-webhook-server-example
23 changes: 23 additions & 0 deletions mcs-s3-webhook-server-example/README.md
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
```
13 changes: 13 additions & 0 deletions mcs-s3-webhook-server-example/helpers.go
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)
}
69 changes: 69 additions & 0 deletions mcs-s3-webhook-server-example/hookserver.go
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
}
38 changes: 38 additions & 0 deletions mcs-s3-webhook-server-example/signatures.go
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
}
46 changes: 46 additions & 0 deletions mcs-s3-webhook-server-example/signatures_test.go
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)
}
}
}

0 comments on commit d4345ab

Please sign in to comment.