├── .dockerignore ├── .gitignore ├── .travis.yml ├── docker-compose.yml ├── Dockerfile ├── makefile.txt ├── app ├── common │ ├── common_test.go │ └── common.go ├── common.go ├── cmd │ └── goaws.go ├── examples │ ├── python │ │ ├── boto_sqs_sample.py │ │ └── boto_sns_sample.py │ └── java │ │ ├── SnsSample.java │ │ └── SqsSample.java ├── sns_test.go ├── gosqs │ ├── message_attributes.go │ ├── queue_attributes_test.go │ ├── queue_attributes.go │ ├── gosqs.go │ └── gosqs_test.go ├── servertest │ ├── server.go │ └── server_test.go ├── conf │ ├── mock-data │ │ └── mock-config.yaml │ ├── goaws.yaml │ ├── config_test.go │ └── config.go ├── sqs.go ├── router │ ├── router.go │ └── router_test.go ├── sns.go ├── sns_messages.go ├── sqs_messages.go └── gosns │ ├── gosns_create_message_test.go │ ├── gosns_test.go │ └── gosns.go ├── Gopkg.toml ├── LICENSE.md ├── Gopkg.lock └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | goaws 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | vendor/ 4 | deployments/ 5 | 6 | goaws 7 | goaws_linux_amd64 8 | 9 | *.log 10 | 11 | setenv.sh 12 | 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go_import_path: github.com/p4tin/goaws 4 | 5 | go: 6 | - 1.9.2 7 | - 1.11.4 8 | 9 | script: go test -v -cover -race ./app/... 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | goaws: 4 | container_name: goaws 5 | image: pafortin/goaws 6 | ports: 7 | - 4100:4100 8 | volumes: 9 | - ./app/conf:/conf 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | WORKDIR /go/src/github.com/p4tin/goaws 4 | 5 | RUN apk add --update --repository https://dl-3.alpinelinux.org/alpine/edge/testing/ git 6 | RUN go get github.com/golang/dep/cmd/dep 7 | 8 | COPY Gopkg.lock Gopkg.toml app ./ 9 | RUN dep ensure 10 | COPY . . 11 | 12 | RUN go build -o goaws_linux_amd64 app/cmd/goaws.go 13 | 14 | FROM alpine 15 | 16 | EXPOSE 4100 17 | 18 | COPY --from=builder /go/src/github.com/p4tin/goaws/goaws_linux_amd64 / 19 | COPY ./app/conf/goaws.yaml /conf/ 20 | ENTRYPOINT ["/goaws_linux_amd64"] 21 | -------------------------------------------------------------------------------- /makefile.txt: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := run 2 | VERSION=1.0.3 3 | GITHUB_API_KEY = ${GITHUBB_API_KEY} 4 | APIJSON='{"tag_name": "$(VERSION)","target_commitish": "master","name": "$(VERSION)","body": "Release of version $(VERSION)","draft": true,"prerelease": true}' 5 | 6 | dep: 7 | dep ensure 8 | 9 | fmt: 10 | go fmt ./app/... 11 | 12 | test: 13 | go test ./app/... 14 | 15 | run: dep fmt test 16 | go run app/cmd/goaws.go 17 | 18 | git-release: 19 | curl --data $(APIJSON) https://api.github.com/repos/p4tin/goaws/releases?access_token=$(GITHUB_API_KEY) 20 | 21 | linux: 22 | GOOS=linux GOARCH=amd64 go build -o goaws_linux_amd64 app/cmd/goaws.go 23 | 24 | docker-release: linux 25 | docker build -t pafortin/goaws . 26 | docker tag pafortin/goaws pafortin/goaws:$(VERSION) 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/common/common_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/p4tin/goaws/app" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestUUID_alwaysgood(t *testing.T) { 11 | uuid, _ := NewUUID() 12 | if uuid == "" { 13 | t.Errorf("Failed to return UUID as expected") 14 | } 15 | } 16 | 17 | func TestGetMD5Hash(t *testing.T) { 18 | hash1 := GetMD5Hash("This is a test") 19 | hash2 := GetMD5Hash("This is a test") 20 | if hash1 != hash2 { 21 | t.Errorf("hashs and hash2 should be the same, but were not") 22 | } 23 | 24 | hash1 = GetMD5Hash("This is a test") 25 | hash2 = GetMD5Hash("This is a tfst") 26 | if hash1 == hash2 { 27 | t.Errorf("hashs and hash2 are the same, but should not be") 28 | } 29 | } 30 | 31 | func TestSortedKeys(t *testing.T) { 32 | attributes := map[string]app.MessageAttributeValue{ 33 | "b": {}, 34 | "a": {}, 35 | } 36 | 37 | keys := sortedKeys(attributes) 38 | assert.Equal(t, "a", keys[0]) 39 | assert.Equal(t, "b", keys[1]) 40 | } 41 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/aws/aws-sdk-go" 26 | version = "1.12.39" 27 | 28 | [[constraint]] 29 | name = "github.com/ghodss/yaml" 30 | version = "1.0.0" 31 | 32 | [[constraint]] 33 | name = "github.com/gorilla/mux" 34 | version = "1.6.0" 35 | 36 | [[constraint]] 37 | name = "github.com/sirupsen/logrus" 38 | version = "1.0.3" 39 | 40 | [[constraint]] 41 | name = "github.com/stretchr/testify" 42 | version = "1.1.4" 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Paul Fortin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/common.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | /*** config ***/ 4 | type EnvSubsciption struct { 5 | QueueName string 6 | Raw bool 7 | FilterPolicy string 8 | } 9 | 10 | type EnvTopic struct { 11 | Name string 12 | Subscriptions []EnvSubsciption 13 | } 14 | 15 | type EnvQueue struct { 16 | Name string 17 | ReceiveMessageWaitTimeSeconds int 18 | RedrivePolicy string 19 | } 20 | 21 | type EnvQueueAttributes struct { 22 | VisibilityTimeout int 23 | ReceiveMessageWaitTimeSeconds int 24 | } 25 | 26 | type Environment struct { 27 | Host string 28 | Port string 29 | SqsPort string 30 | SnsPort string 31 | Region string 32 | AccountID string 33 | LogMessages bool 34 | LogFile string 35 | Topics []EnvTopic 36 | Queues []EnvQueue 37 | QueueAttributeDefaults EnvQueueAttributes 38 | } 39 | 40 | var CurrentEnvironment Environment 41 | 42 | /*** Common ***/ 43 | type ResponseMetadata struct { 44 | RequestId string `xml:"RequestId"` 45 | } 46 | 47 | /*** Error Responses ***/ 48 | type ErrorResult struct { 49 | Type string `xml:"Type,omitempty"` 50 | Code string `xml:"Code,omitempty"` 51 | Message string `xml:"Message,omitempty"` 52 | RequestId string `xml:"RequestId,omitempty"` 53 | } 54 | 55 | type ErrorResponse struct { 56 | Result ErrorResult `xml:"Error"` 57 | } 58 | -------------------------------------------------------------------------------- /app/cmd/goaws.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/p4tin/goaws/app/conf" 12 | "github.com/p4tin/goaws/app/gosqs" 13 | "github.com/p4tin/goaws/app/router" 14 | ) 15 | 16 | func main() { 17 | var filename string 18 | var debug bool 19 | flag.StringVar(&filename, "config", "", "config file location + name") 20 | flag.BoolVar(&debug, "debug", false, "debug log level (default Warning)") 21 | flag.Parse() 22 | 23 | log.SetFormatter(&log.JSONFormatter{}) 24 | log.SetOutput(os.Stdout) 25 | if debug { 26 | log.SetLevel(log.DebugLevel) 27 | } else { 28 | log.SetLevel(log.WarnLevel) 29 | } 30 | 31 | env := "Local" 32 | if flag.NArg() > 0 { 33 | env = flag.Arg(0) 34 | } 35 | 36 | portNumbers := conf.LoadYamlConfig(filename, env) 37 | 38 | r := router.New() 39 | 40 | quit := make(chan struct{}, 0) 41 | go gosqs.PeriodicTasks(1*time.Second, quit) 42 | 43 | if len(portNumbers) == 1 { 44 | log.Warnf("GoAws listening on: 0.0.0.0:%s", portNumbers[0]) 45 | err := http.ListenAndServe("0.0.0.0:"+portNumbers[0], r) 46 | log.Fatal(err) 47 | } else if len(portNumbers) == 2 { 48 | go func() { 49 | log.Warnf("GoAws listening on: 0.0.0.0:%s", portNumbers[0]) 50 | err := http.ListenAndServe("0.0.0.0:"+portNumbers[0], r) 51 | log.Fatal(err) 52 | }() 53 | log.Warnf("GoAws listening on: 0.0.0.0:%s", portNumbers[1]) 54 | err := http.ListenAndServe("0.0.0.0:"+portNumbers[1], r) 55 | log.Fatal(err) 56 | } else { 57 | log.Fatal("Not enough or too many ports defined to start GoAws.") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/examples/python/boto_sqs_sample.py: -------------------------------------------------------------------------------- 1 | import boto 2 | import boto.sqs 3 | 4 | """ 5 | Integration test for boto using GoAws SNS interface 6 | - Create a virtual environment (pyvenv venv) 7 | - Activate the venv (source venv/bin/activate) 8 | - Install boto (pip install boto) 9 | - run this script (python boto_sns_integration_tests.py) 10 | """ 11 | 12 | # Connect GOAws in Python 13 | endpoint='localhost' 14 | region = boto.sqs.regioninfo.RegionInfo(name='local', endpoint=endpoint) 15 | conn = boto.connect_sqs(aws_access_key_id='x', aws_secret_access_key='x', is_secure=False, port='4100', region=region) 16 | 17 | 18 | # Get all Queues in Python 19 | print(conn.get_all_queues()) 20 | print() 21 | print() 22 | 23 | # Create a queue in Python 24 | q = conn.create_queue('myqueue') 25 | print(q) 26 | print() 27 | print() 28 | 29 | # Get Queue Attributes in Python 30 | attribs = conn.get_queue_attributes(q) 31 | print(attribs) 32 | print() 33 | print() 34 | 35 | # Get A Queue in Python 36 | qi = conn.get_queue('myqueue') 37 | print(qi) 38 | print() 39 | print() 40 | 41 | # Lookup a queue in Python (same as get a queue) 42 | qi = conn.lookup('myqueue') 43 | print(qi) 44 | print() 45 | print() 46 | 47 | # Send a message to a queue in Python 48 | resp = conn.send_message(qi, "This is a test!!!") 49 | print(resp) 50 | print() 51 | print() 52 | 53 | # Receive a message from a queue in Python 54 | resp2 = conn.receive_message(qi) 55 | for result in resp2: 56 | print(result.get_body()) 57 | 58 | # Delete a message from a queue in Python 59 | resp3 = conn.delete_message(qi, result) 60 | print("\tDelete:", resp3) 61 | 62 | print() 63 | print() 64 | 65 | # Delete a queue in Python 66 | dq = conn.delete_queue(q) 67 | print(dq) 68 | print() 69 | print() 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/sns_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFilterPolicy_IsSatisfiedBy(t *testing.T) { 8 | var tests = []struct { 9 | filterPolicy *FilterPolicy 10 | messageAttributes map[string]MessageAttributeValue 11 | expected bool 12 | }{ 13 | { 14 | &FilterPolicy{"foo": {"bar"}}, 15 | map[string]MessageAttributeValue{"foo": MessageAttributeValue{DataType: "String", Value: "bar"}}, 16 | true, 17 | }, 18 | { 19 | &FilterPolicy{"foo": {"bar", "xyz"}}, 20 | map[string]MessageAttributeValue{"foo": MessageAttributeValue{DataType: "String", Value: "xyz"}}, 21 | true, 22 | }, 23 | { 24 | &FilterPolicy{"foo": {"bar", "xyz"}, "abc": {"def"}}, 25 | map[string]MessageAttributeValue{"foo": MessageAttributeValue{DataType: "String", Value: "xyz"}, 26 | "abc": MessageAttributeValue{DataType: "String", Value: "def"}}, 27 | true, 28 | }, 29 | { 30 | &FilterPolicy{"foo": {"bar"}}, 31 | map[string]MessageAttributeValue{"foo": MessageAttributeValue{DataType: "String", Value: "baz"}}, 32 | false, 33 | }, 34 | { 35 | &FilterPolicy{"foo": {"bar"}}, 36 | map[string]MessageAttributeValue{}, 37 | false, 38 | }, 39 | { 40 | &FilterPolicy{"foo": {"bar"}, "abc": {"def"}}, 41 | map[string]MessageAttributeValue{"foo": MessageAttributeValue{DataType: "String", Value: "bar"}}, 42 | false, 43 | }, 44 | { 45 | &FilterPolicy{"foo": {"bar"}}, 46 | map[string]MessageAttributeValue{"foo": MessageAttributeValue{DataType: "Binary", Value: "bar"}}, 47 | false, 48 | }, 49 | } 50 | 51 | for i, tt := range tests { 52 | actual := tt.filterPolicy.IsSatisfiedBy(tt.messageAttributes) 53 | if tt.filterPolicy.IsSatisfiedBy(tt.messageAttributes) != tt.expected { 54 | t.Errorf("#%d FilterPolicy: expected %t, actual %t", i, tt.expected, actual) 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/gosqs/message_attributes.go: -------------------------------------------------------------------------------- 1 | package gosqs 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/p4tin/goaws/app" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func extractMessageAttributes(req *http.Request, prefix string) map[string]app.MessageAttributeValue { 12 | attributes := make(map[string]app.MessageAttributeValue) 13 | if prefix != "" { 14 | prefix += "." 15 | } 16 | 17 | for i := 1; true; i++ { 18 | name := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Name", prefix, i)) 19 | if name == "" { 20 | break 21 | } 22 | 23 | dataType := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Value.DataType", prefix, i)) 24 | if dataType == "" { 25 | log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) 26 | continue 27 | } 28 | 29 | // StringListValue and BinaryListValue is currently not implemented 30 | for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { 31 | value := req.FormValue(fmt.Sprintf("%sMessageAttribute.%d.Value.%s", prefix, i, valueKey)) 32 | if value != "" { 33 | attributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} 34 | } 35 | } 36 | 37 | if _, ok := attributes[name]; !ok { 38 | log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) 39 | } 40 | } 41 | 42 | return attributes 43 | } 44 | 45 | func getMessageAttributeResult(a *app.MessageAttributeValue) *app.ResultMessageAttribute { 46 | v := &app.ResultMessageAttributeValue{ 47 | DataType: a.DataType, 48 | } 49 | 50 | switch a.DataType { 51 | case "Binary": 52 | v.BinaryValue = a.Value 53 | case "String": 54 | v.StringValue = a.Value 55 | case "Number": 56 | v.StringValue = a.Value 57 | } 58 | 59 | return &app.ResultMessageAttribute{ 60 | Name: a.Name, 61 | Value: v, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/servertest/server.go: -------------------------------------------------------------------------------- 1 | package servertest 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/p4tin/goaws/app/router" 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/p4tin/goaws/app" 13 | "strings" 14 | ) 15 | 16 | // Server is a fake SQS / SNS server for testing purposes. 17 | type Server struct { 18 | closed bool 19 | handler http.Handler 20 | listener net.Listener 21 | mu sync.Mutex 22 | } 23 | 24 | // Quit closes down the server. 25 | func (srv *Server) Quit() error { 26 | srv.mu.Lock() 27 | srv.closed = true 28 | srv.mu.Unlock() 29 | 30 | return srv.listener.Close() 31 | } 32 | 33 | // URL returns a URL for the server. 34 | func (srv *Server) URL() string { 35 | return "http://" + srv.listener.Addr().String() 36 | } 37 | 38 | // New starts a new server and returns it. 39 | func New(addr string) (*Server, error) { 40 | if addr == "" { 41 | addr = "localhost:0" 42 | } 43 | localURL := strings.Split(addr, ":") 44 | app.CurrentEnvironment.Host = localURL[0] 45 | app.CurrentEnvironment.Port = localURL[1] 46 | log.WithFields(log.Fields{ 47 | "host": app.CurrentEnvironment.Host, 48 | "port": app.CurrentEnvironment.Port, 49 | }).Info("URL Sarting to listen") 50 | 51 | l, err := net.Listen("tcp", addr) 52 | if err != nil { 53 | return nil, fmt.Errorf("cannot listen on localhost: %v", err) 54 | } 55 | if err != nil { 56 | return nil, fmt.Errorf("cannot listen on localhost: %v", err) 57 | } 58 | 59 | srv := Server{listener: l, handler: router.New()} 60 | 61 | go http.Serve(l, &srv) 62 | 63 | return &srv, nil 64 | } 65 | 66 | func (srv *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { 67 | srv.mu.Lock() 68 | closed := srv.closed 69 | srv.mu.Unlock() 70 | 71 | if closed { 72 | hj := w.(http.Hijacker) 73 | conn, _, _ := hj.Hijack() 74 | conn.Close() 75 | return 76 | } 77 | 78 | srv.handler.ServeHTTP(w, req) 79 | } 80 | -------------------------------------------------------------------------------- /app/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "fmt" 10 | "hash" 11 | "io" 12 | "sort" 13 | 14 | "github.com/p4tin/goaws/app" 15 | ) 16 | 17 | var LogMessages bool 18 | var LogFile string 19 | 20 | func NewUUID() (string, error) { 21 | uuid := make([]byte, 16) 22 | n, err := io.ReadFull(rand.Reader, uuid) 23 | if n != len(uuid) || err != nil { 24 | return "", err 25 | } 26 | // variant bits; see section 4.1.1 27 | uuid[8] = uuid[8]&^0xc0 | 0x80 28 | // version 4 (pseudo-random); see section 4.1.3 29 | uuid[6] = uuid[6]&^0xf0 | 0x40 30 | return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil 31 | } 32 | 33 | func GetMD5Hash(text string) string { 34 | hasher := md5.New() 35 | hasher.Write([]byte(text)) 36 | return hex.EncodeToString(hasher.Sum(nil)) 37 | } 38 | 39 | func HashAttributes(attributes map[string]app.MessageAttributeValue) string { 40 | hasher := md5.New() 41 | 42 | keys := sortedKeys(attributes) 43 | for _, key := range keys { 44 | attributeValue := attributes[key] 45 | 46 | addStringToHash(hasher, key) 47 | addStringToHash(hasher, attributeValue.DataType) 48 | if attributeValue.ValueKey == "StringValue" { 49 | hasher.Write([]byte{1}) 50 | addStringToHash(hasher, attributeValue.Value) 51 | } else if attributeValue.ValueKey == "BinaryValue" { 52 | hasher.Write([]byte{2}) 53 | bytes, _ := base64.StdEncoding.DecodeString(attributeValue.Value) 54 | addBytesToHash(hasher, bytes) 55 | } 56 | } 57 | 58 | return hex.EncodeToString(hasher.Sum(nil)) 59 | } 60 | 61 | func sortedKeys(attributes map[string]app.MessageAttributeValue) []string { 62 | var keys []string 63 | for key, _ := range attributes { 64 | keys = append(keys, key) 65 | } 66 | sort.Strings(keys) 67 | return keys 68 | } 69 | 70 | func addStringToHash(hasher hash.Hash, str string) { 71 | bytes := []byte(str) 72 | addBytesToHash(hasher, bytes) 73 | } 74 | 75 | func addBytesToHash(hasher hash.Hash, arr []byte) { 76 | bs := make([]byte, 4) 77 | binary.BigEndian.PutUint32(bs, uint32(len(arr))) 78 | hasher.Write(bs) 79 | hasher.Write(arr) 80 | } 81 | -------------------------------------------------------------------------------- /app/conf/mock-data/mock-config.yaml: -------------------------------------------------------------------------------- 1 | Local: # Environment name that can be passed on the command line 2 | # (i.e.: ./goaws [Local | Dev] -- defaults to 'Local') 3 | Host: localhost # hostname of the goaws system (for docker-compose this is the tag name of the container) 4 | Port: 4100 # port to listen on. 5 | Region: us-east-1 6 | AccountId: "100010001000" 7 | LogMessages: true # Log messages (true/false) 8 | LogFile: ./goaws_messages.log # Log filename (for message logging 9 | QueueAttributeDefaults: # default attributes for all queues 10 | VisibilityTimeout: 10 # message visibility timeout 11 | ReceiveMessageWaitTimeSeconds: 10 # receive message max wait time 12 | Queues: # List of queues to create at startup 13 | - Name: local-queue1 # Queue name 14 | - Name: local-queue2 # Queue name 15 | ReceiveMessageWaitTimeSeconds: 20 # Queue receive message max wait time 16 | - Name: local-queue3 # Queue name 17 | Topics: # List of topic to create at startup 18 | - Name: local-topic1 # Topic name - with some Subscriptions 19 | Subscriptions: # List of Subscriptions to create for this topic (queues will be created as required) 20 | - QueueName: local-queue4 # Queue name 21 | Raw: false # Raw message delivery (true/false) 22 | - QueueName: local-queue5 # Queue name 23 | Raw: true # Raw message delivery (true/false) 24 | FilterPolicy: '{"foo":["bar"]}' # Subscription's FilterPolicy, json like a string 25 | - Name: local-topic2 # Topic name - no Subscriptions 26 | 27 | NoQueuesOrTopics: # Another environment 28 | Host: localhost 29 | Port: 4100 30 | LogMessages: true 31 | LogFile: ./goaws_messages.log 32 | Region: eu-west-1 33 | 34 | NoQueueAttributeDefaults: 35 | Host: localhost 36 | Port: 4100 37 | LogMessages: true 38 | LogFile: ./goaws_messages.log 39 | Region: eu-west-1 40 | Queues: 41 | - Name: local-queue1 42 | - Name: local-queue2 43 | ReceiveMessageWaitTimeSeconds: 20 -------------------------------------------------------------------------------- /app/sqs.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type SqsErrorType struct { 11 | HttpError int 12 | Type string 13 | Code string 14 | Message string 15 | } 16 | 17 | func (s *SqsErrorType) Error() string { 18 | return s.Type 19 | } 20 | 21 | var SqsErrors map[string]SqsErrorType 22 | 23 | type Message struct { 24 | MessageBody []byte 25 | Uuid string 26 | MD5OfMessageAttributes string 27 | MD5OfMessageBody string 28 | ReceiptHandle string 29 | ReceiptTime time.Time 30 | VisibilityTimeout time.Time 31 | NumberOfReceives int 32 | Retry int 33 | MessageAttributes map[string]MessageAttributeValue 34 | GroupID string 35 | } 36 | 37 | type MessageAttributeValue struct { 38 | Name string 39 | DataType string 40 | Value string 41 | ValueKey string 42 | } 43 | 44 | type Queue struct { 45 | Name string 46 | URL string 47 | Arn string 48 | TimeoutSecs int 49 | ReceiveWaitTimeSecs int 50 | Messages []Message 51 | DeadLetterQueue *Queue 52 | MaxReceiveCount int 53 | IsFIFO bool 54 | FIFOMessages map[string]int 55 | FIFOSequenceNumbers map[string]int 56 | } 57 | 58 | var SyncQueues = struct { 59 | sync.RWMutex 60 | Queues map[string]*Queue 61 | }{Queues: make(map[string]*Queue)} 62 | 63 | func HasFIFOQueueName(queueName string) bool { 64 | return strings.HasSuffix(queueName, ".fifo") 65 | } 66 | 67 | func (q *Queue) NextSequenceNumber(groupId string) string { 68 | if _, ok := q.FIFOSequenceNumbers[groupId]; !ok { 69 | q.FIFOSequenceNumbers = map[string]int{ 70 | groupId: 0, 71 | } 72 | } 73 | 74 | q.FIFOSequenceNumbers[groupId]++ 75 | return strconv.Itoa(q.FIFOSequenceNumbers[groupId]) 76 | } 77 | 78 | func (q *Queue) IsLocked(groupId string) bool { 79 | _, ok := q.FIFOMessages[groupId] 80 | return ok 81 | } 82 | 83 | func (q *Queue) LockGroup(groupId string) { 84 | if _, ok := q.FIFOMessages[groupId]; !ok { 85 | q.FIFOMessages = map[string]int{ 86 | groupId: 0, 87 | } 88 | } 89 | } 90 | 91 | func (q *Queue) UnlockGroup(groupId string) { 92 | if _, ok := q.FIFOMessages[groupId]; ok { 93 | delete(q.FIFOMessages, groupId) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/conf/goaws.yaml: -------------------------------------------------------------------------------- 1 | Local: # Environment name that can be passed on the command line 2 | # (i.e.: ./goaws [Local | Dev] -- defaults to 'Local') 3 | Host: goaws.com # hostname of the goaws system (for docker-compose this is the tag name of the container) 4 | # you can now use either 1 port for both sns and sqs or alternatively you can comment out Port and use SqsPort + SnsPort for compatibilyt with 5 | # yopa and (fage-sns + face-sqs). If both ways are in the config file on the one "Port" will be used by GoAws 6 | Port: 4100 # port to listen on. 7 | # SqsPort: 9324 # alterante Sqs Port 8 | # SnsPort: 9292 # alternate Sns Port 9 | Region: us-east-1 10 | AccountId: "100010001000" 11 | LogMessages: true # Log messages (true/false) 12 | LogFile: ./goaws_messages.log # Log filename (for message logging 13 | QueueAttributeDefaults: # default attributes for all queues 14 | VisibilityTimeout: 30 # message visibility timeout 15 | ReceiveMessageWaitTimeSeconds: 0 # receive message max wait time 16 | Queues: # List of queues to create at startup 17 | - Name: local-queue1 # Queue name 18 | - Name: local-queue2 # Queue name 19 | ReceiveMessageWaitTimeSeconds: 20 # Queue receive message max wait time 20 | Topics: # List of topic to create at startup 21 | - Name: local-topic1 # Topic name - with some Subscriptions 22 | Subscriptions: # List of Subscriptions to create for this topic (queues will be created as required) 23 | - QueueName: local-queue3 # Queue name 24 | Raw: false # Raw message delivery (true/false) 25 | - QueueName: local-queue4 # Queue name 26 | Raw: true # Raw message delivery (true/false) 27 | #FilterPolicy: '{"foo": ["bar"]}' # Subscription's FilterPolicy, json object as a string 28 | - Name: local-topic2 # Topic name - no Subscriptions 29 | 30 | Dev: # Another environment 31 | Host: localhost 32 | Port: 4100 33 | # SqsPort: 9324 34 | # SnsPort: 9292 35 | AccountId: "794373491471" 36 | LogMessages: true 37 | LogFile: ./goaws_messages.log 38 | Queues: 39 | - Name: dev-queue1 40 | - Name: dev-queue2 41 | Topics: 42 | - Name: dev-topic1 43 | Subscriptions: 44 | - QueueName: dev-queue3 45 | Raw: false 46 | - QueueName: dev-queue4 47 | Raw: true 48 | - Name: dev-topic2 49 | 50 | -------------------------------------------------------------------------------- /app/gosqs/queue_attributes_test.go: -------------------------------------------------------------------------------- 1 | package gosqs 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/p4tin/goaws/app" 9 | ) 10 | 11 | func TestApplyQueueAttributes(t *testing.T) { 12 | t.Run("success", func(t *testing.T) { 13 | deadLetterQueue := &app.Queue{Name: "failed-messages"} 14 | app.SyncQueues.Lock() 15 | app.SyncQueues.Queues["failed-messages"] = deadLetterQueue 16 | app.SyncQueues.Unlock() 17 | q := &app.Queue{TimeoutSecs: 30} 18 | u := url.Values{} 19 | u.Add("Attribute.1.Name", "DelaySeconds") 20 | u.Add("Attribute.1.Value", "20") 21 | u.Add("Attribute.2.Name", "VisibilityTimeout") 22 | u.Add("Attribute.2.Value", "60") 23 | u.Add("Attribute.3.Name", "Policy") 24 | u.Add("Attribute.4.Name", "RedrivePolicy") 25 | u.Add("Attribute.4.Value", `{"maxReceiveCount": "4", "deadLetterTargetArn":"arn:aws:sqs::000000000000:failed-messages"}`) 26 | u.Add("Attribute.5.Name", "ReceiveMessageWaitTimeSeconds") 27 | u.Add("Attribute.5.Value", "20") 28 | if err := validateAndSetQueueAttributes(q, u); err != nil { 29 | t.Fatalf("expected nil, got %s", err) 30 | } 31 | expected := &app.Queue{ 32 | TimeoutSecs: 60, 33 | ReceiveWaitTimeSecs: 20, 34 | MaxReceiveCount: 4, 35 | DeadLetterQueue: deadLetterQueue, 36 | } 37 | if ok := reflect.DeepEqual(q, expected); !ok { 38 | t.Fatalf("expected %+v, got %+v", expected, q) 39 | } 40 | }) 41 | t.Run("missing_deadletter_arn", func(t *testing.T) { 42 | q := &app.Queue{TimeoutSecs: 30} 43 | u := url.Values{} 44 | u.Add("Attribute.1.Name", "RedrivePolicy") 45 | u.Add("Attribute.1.Value", `{"maxReceiveCount": "4"}`) 46 | err := validateAndSetQueueAttributes(q, u) 47 | if err != ErrInvalidParameterValue { 48 | t.Fatalf("expected %s, got %s", ErrInvalidParameterValue, err) 49 | } 50 | }) 51 | t.Run("invalid_redrive_policy", func(t *testing.T) { 52 | q := &app.Queue{TimeoutSecs: 30} 53 | u := url.Values{} 54 | u.Add("Attribute.1.Name", "RedrivePolicy") 55 | u.Add("Attribute.1.Value", `{invalidinput}`) 56 | err := validateAndSetQueueAttributes(q, u) 57 | if err != ErrInvalidAttributeValue { 58 | t.Fatalf("expected %s, got %s", ErrInvalidAttributeValue, err) 59 | } 60 | }) 61 | } 62 | 63 | func TestExtractQueueAttributes(t *testing.T) { 64 | u := url.Values{} 65 | u.Add("Attribute.1.Name", "DelaySeconds") 66 | u.Add("Attribute.1.Value", "20") 67 | u.Add("Attribute.2.Name", "VisibilityTimeout") 68 | u.Add("Attribute.2.Value", "30") 69 | u.Add("Attribute.3.Name", "Policy") 70 | attr := extractQueueAttributes(u) 71 | expected := map[string]string{ 72 | "DelaySeconds": "20", 73 | "VisibilityTimeout": "30", 74 | } 75 | if ok := reflect.DeepEqual(attr, expected); !ok { 76 | t.Fatalf("expected %+v, got %+v", expected, attr) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/examples/python/boto_sns_sample.py: -------------------------------------------------------------------------------- 1 | import boto 2 | import boto.sqs 3 | import boto.sns 4 | 5 | """ 6 | Integration test for boto using GoAws SNS interface 7 | - Create a virtual environment (pyvenv venv) 8 | - Activate the venv (source venv/bin/activate) 9 | - Install boto (pip install boto) 10 | - run this script (python boto_sns_integration_tests.py) 11 | """ 12 | 13 | """ 14 | boto doesn't (yet) expose SetSubscriptionAttributes, so here's a 15 | monkeypatch specifically for turning on the RawMessageDelivery attribute. 16 | """ 17 | 18 | def SetRawSubscriptionAttribute(snsConnection, subscriptionArn): 19 | """ 20 | Works around boto's lack of a SetSubscriptionAttributes call. 21 | """ 22 | params = { 23 | 'AttributeName': 'RawMessageDelivery', 24 | 'AttributeValue': 'true', 25 | 'SubscriptionArn': subscriptionArn 26 | } 27 | return snsConnection._make_request('SetSubscriptionAttributes', params) 28 | 29 | boto.sns.SNSConnection.set_raw_subscription_attribute = SetRawSubscriptionAttribute 30 | 31 | 32 | # Connect GOAws in Python 33 | endpoint='localhost' 34 | region = boto.sqs.regioninfo.RegionInfo(name='local', endpoint=endpoint) 35 | conn = boto.connect_sns(aws_access_key_id='x', aws_secret_access_key='x', is_secure=False, port='4100', region=region) 36 | 37 | 38 | # Get all Topics in Python 39 | print(conn.get_all_topics()) 40 | print() 41 | print() 42 | 43 | 44 | # Get all Subscriptions in Python 45 | print(conn.get_all_subscriptions()) 46 | print() 47 | print() 48 | 49 | 50 | # Create a topic in Python 51 | topicname = "trialBotoTopic" 52 | topicarn = conn.create_topic(topicname) 53 | print(topicname, "has been successfully created with a topic ARN of", topicarn) 54 | print() 55 | print() 56 | 57 | 58 | # Print the topic Arn in python 59 | print(topicarn['Result']['TopicArn']) 60 | print() 61 | print() 62 | 63 | 64 | ## Subscribe a Queue to a Topic in Python 65 | subscription1 = conn.subscribe(topicarn['Result']['TopicArn'], "sqs", "http://localhost:4100/queue/local-queue2") 66 | print(subscription1['Result']['SubscriptionArn']) 67 | print() 68 | print() 69 | 70 | ## Set topic attribute Raw in Python 71 | attr_results = conn.set_raw_subscription_attribute(subscription1['Result']['SubscriptionArn']) 72 | print(attr_results) 73 | print() 74 | print() 75 | 76 | 77 | ## Publish to a topic in Python 78 | message = "Hello Boto" 79 | message_subject = "trialBotoTopic" 80 | publication = conn.publish(topicarn['Result']['TopicArn'], message, subject=message_subject) 81 | print(publication) 82 | 83 | 84 | ## Unsubscribe in Python 85 | subscription1 = conn.unsubscribe(subscription1['Result']['SubscriptionArn']) 86 | print(subscription1) 87 | print() 88 | print() 89 | 90 | 91 | ## Delete Topic in Python 92 | deletion1 = conn.delete_topic(topicarn['Result']['TopicArn']) 93 | print(deletion1) 94 | print() 95 | print() 96 | -------------------------------------------------------------------------------- /app/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "fmt" 10 | 11 | "github.com/gorilla/mux" 12 | sns "github.com/p4tin/goaws/app/gosns" 13 | sqs "github.com/p4tin/goaws/app/gosqs" 14 | ) 15 | 16 | // New returns a new router 17 | func New() http.Handler { 18 | r := mux.NewRouter() 19 | 20 | r.HandleFunc("/", actionHandler).Methods("GET", "POST") 21 | r.HandleFunc("/{account}", actionHandler).Methods("GET", "POST") 22 | r.HandleFunc("/queue/{queueName}", actionHandler).Methods("GET", "POST") 23 | r.HandleFunc("/{account}/{queueName}", actionHandler).Methods("GET", "POST") 24 | r.HandleFunc("/SimpleNotificationService/{id}.pem", pemHandler).Methods("GET") 25 | r.HandleFunc("/health", health).Methods("GET") 26 | 27 | return r 28 | } 29 | 30 | var routingTable = map[string]http.HandlerFunc{ 31 | // SQS 32 | "ListQueues": sqs.ListQueues, 33 | "CreateQueue": sqs.CreateQueue, 34 | "GetQueueAttributes": sqs.GetQueueAttributes, 35 | "SetQueueAttributes": sqs.SetQueueAttributes, 36 | "SendMessage": sqs.SendMessage, 37 | "SendMessageBatch": sqs.SendMessageBatch, 38 | "ReceiveMessage": sqs.ReceiveMessage, 39 | "DeleteMessage": sqs.DeleteMessage, 40 | "DeleteMessageBatch": sqs.DeleteMessageBatch, 41 | "GetQueueUrl": sqs.GetQueueUrl, 42 | "PurgeQueue": sqs.PurgeQueue, 43 | "DeleteQueue": sqs.DeleteQueue, 44 | "ChangeMessageVisibility": sqs.ChangeMessageVisibility, 45 | 46 | // SNS 47 | "ListTopics": sns.ListTopics, 48 | "CreateTopic": sns.CreateTopic, 49 | "DeleteTopic": sns.DeleteTopic, 50 | "Subscribe": sns.Subscribe, 51 | "ConfirmSubscription": sns.ConfirmSubscription, 52 | "SetSubscriptionAttributes": sns.SetSubscriptionAttributes, 53 | "GetSubscriptionAttributes": sns.GetSubscriptionAttributes, 54 | "ListSubscriptionsByTopic": sns.ListSubscriptionsByTopic, 55 | "ListSubscriptions": sns.ListSubscriptions, 56 | "Unsubscribe": sns.Unsubscribe, 57 | "Publish": sns.Publish, 58 | } 59 | 60 | func health(w http.ResponseWriter, req *http.Request) { 61 | w.WriteHeader(200) 62 | fmt.Fprint(w, "OK") 63 | } 64 | 65 | func actionHandler(w http.ResponseWriter, req *http.Request) { 66 | log.WithFields( 67 | log.Fields{ 68 | "action": req.FormValue("Action"), 69 | "url": req.URL, 70 | }).Debug("Handling URL request") 71 | fn, ok := routingTable[req.FormValue("Action")] 72 | if !ok { 73 | log.Println("Bad Request - Action:", req.FormValue("Action")) 74 | w.WriteHeader(http.StatusBadRequest) 75 | io.WriteString(w, "Bad Request") 76 | return 77 | } 78 | 79 | http.HandlerFunc(fn).ServeHTTP(w, req) 80 | } 81 | 82 | func pemHandler(w http.ResponseWriter, req *http.Request) { 83 | w.WriteHeader(http.StatusOK) 84 | w.Write(sns.PemKEY) 85 | } 86 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/aws/aws-sdk-go" 6 | packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/xml/xmlutil","service/sqs","service/sqs/sqsiface","service/sts"] 7 | revision = "388e6676807f5968ad5eaaec0c70002f80c20b84" 8 | version = "v1.12.39" 9 | 10 | [[projects]] 11 | name = "github.com/davecgh/go-spew" 12 | packages = ["spew"] 13 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 14 | version = "v1.1.0" 15 | 16 | [[projects]] 17 | name = "github.com/ghodss/yaml" 18 | packages = ["."] 19 | revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" 20 | version = "v1.0.0" 21 | 22 | [[projects]] 23 | name = "github.com/go-ini/ini" 24 | packages = ["."] 25 | revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a" 26 | version = "v1.32.0" 27 | 28 | [[projects]] 29 | name = "github.com/gorilla/context" 30 | packages = ["."] 31 | revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" 32 | version = "v1.1" 33 | 34 | [[projects]] 35 | name = "github.com/gorilla/mux" 36 | packages = ["."] 37 | revision = "7f08801859139f86dfafd1c296e2cba9a80d292e" 38 | version = "v1.6.0" 39 | 40 | [[projects]] 41 | name = "github.com/jmespath/go-jmespath" 42 | packages = ["."] 43 | revision = "0b12d6b5" 44 | 45 | [[projects]] 46 | name = "github.com/pmezard/go-difflib" 47 | packages = ["difflib"] 48 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 49 | version = "v1.0.0" 50 | 51 | [[projects]] 52 | name = "github.com/sirupsen/logrus" 53 | packages = ["."] 54 | revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e" 55 | version = "v1.0.3" 56 | 57 | [[projects]] 58 | name = "github.com/stretchr/testify" 59 | packages = ["assert","require"] 60 | revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" 61 | version = "v1.1.4" 62 | 63 | [[projects]] 64 | branch = "master" 65 | name = "golang.org/x/crypto" 66 | packages = ["ssh/terminal"] 67 | revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122" 68 | 69 | [[projects]] 70 | branch = "master" 71 | name = "golang.org/x/sys" 72 | packages = ["unix","windows"] 73 | revision = "8b4580aae2a0dd0c231a45d3ccb8434ff533b840" 74 | 75 | [[projects]] 76 | branch = "v2" 77 | name = "gopkg.in/yaml.v2" 78 | packages = ["."] 79 | revision = "287cf08546ab5e7e37d55a84f7ed3fd1db036de5" 80 | 81 | [solve-meta] 82 | analyzer-name = "dep" 83 | analyzer-version = 1 84 | inputs-digest = "f0bf2e85cdee49133d7c3a04bb40d9b0236b6bcc08330f9898ad3ee7255bbafb" 85 | solver-name = "gps-cdcl" 86 | solver-version = 1 87 | -------------------------------------------------------------------------------- /app/router/router_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestIndexServerhandler_POST_BadRequest(t *testing.T) { 11 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 12 | // pass 'nil' as the third parameter. 13 | req, err := http.NewRequest("POST", "/", nil) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | form := url.Values{} 19 | form.Add("Action", "BadRequest") 20 | req.PostForm = form 21 | 22 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 23 | rr := httptest.NewRecorder() 24 | 25 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 26 | // directly and pass in our Request and ResponseRecorder. 27 | New().ServeHTTP(rr, req) 28 | 29 | // Check the status code is what we expect. 30 | if status := rr.Code; status != http.StatusBadRequest { 31 | t.Errorf("handler returned wrong status code: got %v want %v", 32 | status, http.StatusOK) 33 | } 34 | } 35 | 36 | func TestIndexServerhandler_POST_GoodRequest(t *testing.T) { 37 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 38 | // pass 'nil' as the third parameter. 39 | req, err := http.NewRequest("POST", "/", nil) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | form := url.Values{} 45 | form.Add("Action", "ListTopics") 46 | req.PostForm = form 47 | 48 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 49 | rr := httptest.NewRecorder() 50 | 51 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 52 | // directly and pass in our Request and ResponseRecorder. 53 | New().ServeHTTP(rr, req) 54 | 55 | // Check the status code is what we expect. 56 | if status := rr.Code; status != http.StatusOK { 57 | t.Errorf("handler returned wrong status code: got %v want %v", 58 | status, http.StatusOK) 59 | } 60 | } 61 | 62 | func TestIndexServerhandler_POST_GoodRequest_With_URL(t *testing.T) { 63 | 64 | req, err := http.NewRequest("POST", "/100010001000/local-queue1", nil) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | form := url.Values{} 70 | form.Add("Action", "CreateQueue") 71 | form.Add("QueueName", "local-queue1") 72 | req.PostForm = form 73 | rr := httptest.NewRecorder() 74 | New().ServeHTTP(rr, req) 75 | 76 | form = url.Values{} 77 | form.Add("Action", "GetQueueAttributes") 78 | req.PostForm = form 79 | 80 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 81 | rr = httptest.NewRecorder() 82 | 83 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 84 | // directly and pass in our Request and ResponseRecorder. 85 | New().ServeHTTP(rr, req) 86 | 87 | // Check the status code is what we expect. 88 | if status := rr.Code; status != http.StatusOK { 89 | t.Errorf("handler returned wrong status code: got %v want %v", 90 | status, http.StatusOK) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/sns.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type SnsErrorType struct { 8 | HttpError int 9 | Type string 10 | Code string 11 | Message string 12 | } 13 | 14 | var SnsErrors map[string]SnsErrorType 15 | 16 | type MsgAttr struct { 17 | Type string 18 | Value string 19 | } 20 | type SNSMessage struct { 21 | Type string 22 | Token string `json:"Token",omitempty` 23 | MessageId string 24 | TopicArn string 25 | Subject string 26 | Message string 27 | Timestamp string 28 | SignatureVersion string 29 | Signature string `json:"Signature",omitempty` 30 | SigningCertURL string 31 | UnsubscribeURL string 32 | SubscribeURL string `json:"SubscribeURL",omitempty` 33 | MessageAttributes map[string]MsgAttr `json:"MessageAttributes",omitempty` 34 | } 35 | 36 | type Subscription struct { 37 | TopicArn string 38 | Protocol string 39 | SubscriptionArn string 40 | EndPoint string 41 | Raw bool 42 | FilterPolicy *FilterPolicy 43 | } 44 | 45 | // only simple "ExactMatch" string policy is supported at the moment 46 | type FilterPolicy map[string][]string 47 | 48 | // Function checks if MessageAttributes passed to Topic satisfy FilterPolicy set by subscription 49 | func (fp *FilterPolicy) IsSatisfiedBy(msgAttrs map[string]MessageAttributeValue) bool { 50 | for policyAttrName, policyAttrValues := range *fp { 51 | attrValue, ok := msgAttrs[policyAttrName] 52 | if !ok { 53 | return false // the attribute has to be present in the message 54 | } 55 | 56 | // String, String.Array, Number data-types are allowed by SNS filter policies 57 | // however go-AWS currently only supports String filter policies. That feature can be added here 58 | // ref: https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html 59 | if attrValue.DataType != "String" { 60 | return false 61 | } 62 | 63 | if !stringInSlice(attrValue.Value, policyAttrValues) { 64 | return false // the attribute value has to be among filtered ones 65 | } 66 | } 67 | 68 | return true 69 | } 70 | 71 | func stringInSlice(a string, list []string) bool { 72 | for _, b := range list { 73 | if b == a { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | 80 | type Topic struct { 81 | Name string 82 | Arn string 83 | Subscriptions []*Subscription 84 | } 85 | 86 | type ( 87 | Protocol string 88 | MessageStructure string 89 | ) 90 | 91 | const ( 92 | ProtocolHTTP Protocol = "http" 93 | ProtocolHTTPS Protocol = "https" 94 | ProtocolSQS Protocol = "sqs" 95 | ProtocolDefault Protocol = "default" 96 | ) 97 | 98 | const ( 99 | MessageStructureJSON MessageStructure = "json" 100 | ) 101 | 102 | // Predefined errors 103 | const ( 104 | ErrNoDefaultElementInJSON = "Invalid parameter: Message Structure - No default entry in JSON message body" 105 | ) 106 | 107 | var SyncTopics = struct { 108 | sync.RWMutex 109 | Topics map[string]*Topic 110 | }{Topics: make(map[string]*Topic)} 111 | -------------------------------------------------------------------------------- /app/examples/java/SnsSample.java: -------------------------------------------------------------------------------- 1 | import com.amazonaws.AmazonClientException; 2 | import com.amazonaws.AmazonServiceException; 3 | import com.amazonaws.auth.BasicAWSCredentials; 4 | import com.amazonaws.services.sns.model.*; 5 | import com.amazonaws.services.sns.AmazonSNS; 6 | import com.amazonaws.services.sns.AmazonSNSClient; 7 | 8 | import java.util.List; 9 | import java.util.Map.Entry; 10 | 11 | /*** 12 | * Make sure you have the aws-java-sdk-1.8.11.jar + dependancies in your classpath 13 | ***/ 14 | 15 | public class SnsSample { 16 | 17 | public static void main(String[] args) throws Exception { 18 | 19 | AmazonSNS sns = new AmazonSNSClient(new BasicAWSCredentials("x", "x")); 20 | sns.setEndpoint("http://localhost:4100"); 21 | 22 | System.out.println("==========================================="); 23 | System.out.println("Getting Started with Amazon SQS"); 24 | System.out.println("===========================================\n"); 25 | 26 | try { 27 | // Create a queue 28 | System.out.println("Creating a new SNS topic called MyTopic.\n"); 29 | CreateTopicRequest createTopicRequest = new CreateTopicRequest("MyTopic"); 30 | String topicArn = sns.createTopic(createTopicRequest).getTopicArn(); 31 | 32 | // List queues 33 | System.out.println("Listing all topics in your account.\n"); 34 | for (Topic topic : sns.listTopics().withTopics().getTopics()) { 35 | System.out.println(" TopicArn: " + topic.getTopicArn()); 36 | } 37 | System.out.println(); 38 | 39 | SubscribeResult sr = sns.subscribe(new SubscribeRequest(topicArn, "sqs", "http://localhost:4100/queue/local-queue1")); 40 | System.out.println("SubscriptionArn: " + sr.getSubscriptionArn()); 41 | System.out.println(); 42 | 43 | PublishRequest publishRequest = new PublishRequest(topicArn, "Sent to MyTopic!!!"); 44 | PublishResult pr = sns.publish(publishRequest); 45 | System.out.println("Message sent: " + pr.getMessageId()); 46 | System.out.println(); 47 | 48 | DeleteTopicRequest str = new DeleteTopicRequest(); 49 | str.setTopicArn(topicArn); 50 | sns.deleteTopic(str); 51 | System.out.println("Topic Delected: " + topicArn); 52 | System.out.println(); 53 | } catch (AmazonServiceException ase) { 54 | System.out.println("Caught an AmazonServiceException, which means your request made it " + 55 | "to Amazon SQS, but was rejected with an error response for some reason."); 56 | System.out.println("Error Message: " + ase.getMessage()); 57 | System.out.println("HTTP Status Code: " + ase.getStatusCode()); 58 | System.out.println("AWS Error Code: " + ase.getErrorCode()); 59 | System.out.println("Error Type: " + ase.getErrorType()); 60 | System.out.println("Request ID: " + ase.getRequestId()); 61 | } catch (AmazonClientException ace) { 62 | System.out.println("Caught an AmazonClientException, which means the client encountered " + 63 | "a serious internal problem while trying to communicate with SQS, such as not " + 64 | "being able to access the network."); 65 | System.out.println("Error Message: " + ace.getMessage()); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/gosqs/queue_attributes.go: -------------------------------------------------------------------------------- 1 | package gosqs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/p4tin/goaws/app" 12 | ) 13 | 14 | var ( 15 | ErrInvalidParameterValue = &app.SqsErrorType{ 16 | HttpError: http.StatusBadRequest, 17 | Type: "InvalidParameterValue", 18 | Code: "AWS.SimpleQueueService.InvalidParameterValue", 19 | Message: "An invalid or out-of-range value was supplied for the input parameter.", 20 | } 21 | ErrInvalidAttributeValue = &app.SqsErrorType{ 22 | HttpError: http.StatusBadRequest, 23 | Type: "InvalidAttributeValue", 24 | Code: "AWS.SimpleQueueService.InvalidAttributeValue", 25 | Message: "Invalid Value for the parameter RedrivePolicy.", 26 | } 27 | ) 28 | 29 | // validateAndSetQueueAttributes applies the requested queue attributes to the given 30 | // queue. 31 | // TODO Currently it only supports VisibilityTimeout, RedrivePolicy and ReceiveMessageWaitTimeSeconds attributes. 32 | func validateAndSetQueueAttributes(q *app.Queue, u url.Values) error { 33 | attr := extractQueueAttributes(u) 34 | visibilityTimeout, _ := strconv.Atoi(attr["VisibilityTimeout"]) 35 | if visibilityTimeout != 0 { 36 | q.TimeoutSecs = visibilityTimeout 37 | } 38 | receiveWaitTime, _ := strconv.Atoi(attr["ReceiveMessageWaitTimeSeconds"]) 39 | if receiveWaitTime != 0 { 40 | q.ReceiveWaitTimeSecs = receiveWaitTime 41 | } 42 | strRedrivePolicy := attr["RedrivePolicy"] 43 | if strRedrivePolicy != "" { 44 | if err := ValidateAndSetRedrivePolicy(q, strRedrivePolicy); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // ValidateAndSetRedrivePolicy applies the requested redrive policy to the given queue. 53 | func ValidateAndSetRedrivePolicy(q *app.Queue, strRedrivePolicy string) error { 54 | // support both int and string maxReceiveCount (Amazon clients use string) 55 | redrivePolicy1 := struct { 56 | MaxReceiveCount int `json:"maxReceiveCount"` 57 | DeadLetterTargetArn string `json:"deadLetterTargetArn"` 58 | }{} 59 | redrivePolicy2 := struct { 60 | MaxReceiveCount string `json:"maxReceiveCount"` 61 | DeadLetterTargetArn string `json:"deadLetterTargetArn"` 62 | }{} 63 | err1 := json.Unmarshal([]byte(strRedrivePolicy), &redrivePolicy1) 64 | err2 := json.Unmarshal([]byte(strRedrivePolicy), &redrivePolicy2) 65 | maxReceiveCount := redrivePolicy1.MaxReceiveCount 66 | deadLetterQueueArn := redrivePolicy1.DeadLetterTargetArn 67 | if err1 != nil && err2 != nil { 68 | return ErrInvalidAttributeValue 69 | } else if err1 != nil { 70 | maxReceiveCount, _ = strconv.Atoi(redrivePolicy2.MaxReceiveCount) 71 | deadLetterQueueArn = redrivePolicy2.DeadLetterTargetArn 72 | } 73 | 74 | if (deadLetterQueueArn != "" && maxReceiveCount == 0) || 75 | (deadLetterQueueArn == "" && maxReceiveCount != 0) { 76 | return ErrInvalidParameterValue 77 | } 78 | dlt := strings.Split(deadLetterQueueArn, ":") 79 | deadLetterQueueName := dlt[len(dlt)-1] 80 | deadLetterQueue, ok := app.SyncQueues.Queues[deadLetterQueueName] 81 | if !ok { 82 | return ErrInvalidParameterValue 83 | } 84 | q.DeadLetterQueue = deadLetterQueue 85 | q.MaxReceiveCount = maxReceiveCount 86 | 87 | return nil 88 | } 89 | 90 | func extractQueueAttributes(u url.Values) map[string]string { 91 | attr := map[string]string{} 92 | for i := 1; true; i++ { 93 | nameKey := fmt.Sprintf("Attribute.%d.Name", i) 94 | attrName := u.Get(nameKey) 95 | if attrName == "" { 96 | break 97 | } 98 | 99 | valueKey := fmt.Sprintf("Attribute.%d.Value", i) 100 | attrValue := u.Get(valueKey) 101 | if attrValue != "" { 102 | attr[attrName] = attrValue 103 | } 104 | } 105 | return attr 106 | } 107 | -------------------------------------------------------------------------------- /app/conf/config_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/p4tin/goaws/app" 7 | ) 8 | 9 | func TestConfig_NoQueuesOrTopics(t *testing.T) { 10 | env := "NoQueuesOrTopics" 11 | port := LoadYamlConfig("./mock-data/mock-config.yaml", env) 12 | if port[0] != "4100" { 13 | t.Errorf("Expected port number 4200 but got %s\n", port) 14 | } 15 | 16 | numQueues := len(envs[env].Queues) 17 | if numQueues != 0 { 18 | t.Errorf("Expected zero queues to be in the environment but got %d\n", numQueues) 19 | } 20 | numQueues = len(app.SyncQueues.Queues) 21 | if numQueues != 0 { 22 | t.Errorf("Expected zero queues to be in the sqs topics but got %d\n", numQueues) 23 | } 24 | 25 | numTopics := len(envs[env].Topics) 26 | if numTopics != 0 { 27 | t.Errorf("Expected zero topics to be in the environment but got %d\n", numTopics) 28 | } 29 | numTopics = len(app.SyncTopics.Topics) 30 | if numTopics != 0 { 31 | t.Errorf("Expected zero topics to be in the sns topics but got %d\n", numTopics) 32 | } 33 | } 34 | 35 | func TestConfig_CreateQueuesTopicsAndSubscriptions(t *testing.T) { 36 | env := "Local" 37 | port := LoadYamlConfig("./mock-data/mock-config.yaml", env) 38 | if port[0] != "4100" { 39 | t.Errorf("Expected port number 4100 but got %s\n", port) 40 | } 41 | 42 | numQueues := len(envs[env].Queues) 43 | if numQueues != 3 { 44 | t.Errorf("Expected three queues to be in the environment but got %d\n", numQueues) 45 | } 46 | numQueues = len(app.SyncQueues.Queues) 47 | if numQueues != 5 { 48 | t.Errorf("Expected five queues to be in the sqs topics but got %d\n", numQueues) 49 | } 50 | 51 | numTopics := len(envs[env].Topics) 52 | if numTopics != 2 { 53 | t.Errorf("Expected two topics to be in the environment but got %d\n", numTopics) 54 | } 55 | numTopics = len(app.SyncTopics.Topics) 56 | if numTopics != 2 { 57 | t.Errorf("Expected two topics to be in the sns topics but got %d\n", numTopics) 58 | } 59 | } 60 | 61 | func TestConfig_QueueAttributes(t *testing.T) { 62 | env := "Local" 63 | port := LoadYamlConfig("./mock-data/mock-config.yaml", env) 64 | if port[0] != "4100" { 65 | t.Errorf("Expected port number 4100 but got %s\n", port) 66 | } 67 | 68 | receiveWaitTime := app.SyncQueues.Queues["local-queue1"].ReceiveWaitTimeSecs 69 | if receiveWaitTime != 10 { 70 | t.Errorf("Expected local-queue1 Queue to be configured with ReceiveMessageWaitTimeSeconds: 10 but got %d\n", receiveWaitTime) 71 | } 72 | timeoutSecs := app.SyncQueues.Queues["local-queue1"].TimeoutSecs 73 | if timeoutSecs != 10 { 74 | t.Errorf("Expected local-queue1 Queue to be configured with VisibilityTimeout: 10 but got %d\n", timeoutSecs) 75 | } 76 | 77 | receiveWaitTime = app.SyncQueues.Queues["local-queue2"].ReceiveWaitTimeSecs 78 | if receiveWaitTime != 20 { 79 | t.Errorf("Expected local-queue2 Queue to be configured with ReceiveMessageWaitTimeSeconds: 20 but got %d\n", receiveWaitTime) 80 | } 81 | } 82 | 83 | func TestConfig_NoQueueAttributeDefaults(t *testing.T) { 84 | env := "NoQueueAttributeDefaults" 85 | LoadYamlConfig("./mock-data/mock-config.yaml", env) 86 | 87 | receiveWaitTime := app.SyncQueues.Queues["local-queue1"].ReceiveWaitTimeSecs 88 | if receiveWaitTime != 0 { 89 | t.Errorf("Expected local-queue1 Queue to be configured with ReceiveMessageWaitTimeSeconds: 0 but got %d\n", receiveWaitTime) 90 | } 91 | timeoutSecs := app.SyncQueues.Queues["local-queue1"].TimeoutSecs 92 | if timeoutSecs != 30 { 93 | t.Errorf("Expected local-queue1 Queue to be configured with VisibilityTimeout: 30 but got %d\n", timeoutSecs) 94 | } 95 | 96 | receiveWaitTime = app.SyncQueues.Queues["local-queue2"].ReceiveWaitTimeSecs 97 | if receiveWaitTime != 20 { 98 | t.Errorf("Expected local-queue2 Queue to be configured with ReceiveMessageWaitTimeSeconds: 20 but got %d\n", receiveWaitTime) 99 | } 100 | 101 | filterPolicy := app.SyncTopics.Topics["local-topic1"].Subscriptions[1].FilterPolicy 102 | if (*filterPolicy)["foo"][0] != "bar" { 103 | t.Errorf("Expected FilterPolicy subscription on local-topic1 to be: bar but got %s\n", (*filterPolicy)["foo"][0]) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/sns_messages.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | /*** List Topics Response */ 4 | type TopicArnResult struct { 5 | TopicArn string `xml:"TopicArn"` 6 | } 7 | 8 | type TopicNamestype struct { 9 | Member []TopicArnResult `xml:"member"` 10 | } 11 | 12 | type ListTopicsResult struct { 13 | Topics TopicNamestype `xml:"Topics"` 14 | } 15 | 16 | type ListTopicsResponse struct { 17 | Xmlns string `xml:"xmlns,attr"` 18 | Result ListTopicsResult `xml:"ListTopicsResult"` 19 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 20 | } 21 | 22 | /*** Create Topic Response */ 23 | type CreateTopicResult struct { 24 | TopicArn string `xml:"TopicArn"` 25 | } 26 | 27 | type CreateTopicResponse struct { 28 | Xmlns string `xml:"xmlns,attr"` 29 | Result CreateTopicResult `xml:"CreateTopicResult"` 30 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 31 | } 32 | 33 | /*** Create Subscription ***/ 34 | type SubscribeResult struct { 35 | SubscriptionArn string `xml:"SubscriptionArn"` 36 | } 37 | 38 | type SubscribeResponse struct { 39 | Xmlns string `xml:"xmlns,attr"` 40 | Result SubscribeResult `xml:"SubscribeResult"` 41 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 42 | } 43 | 44 | /*** ConfirmSubscriptionResponse ***/ 45 | type ConfirmSubscriptionResponse struct { 46 | Xmlns string `xml:"xmlns,attr"` 47 | Result SubscribeResult `xml:"ConfirmSubscriptionResult"` 48 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 49 | } 50 | 51 | /*** Set Subscription Response ***/ 52 | 53 | type SetSubscriptionAttributesResponse struct { 54 | Xmlns string `xml:"xmlns,attr"` 55 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 56 | } 57 | 58 | /*** Get Subscription Attributes ***/ 59 | type GetSubscriptionAttributesResult struct { 60 | SubscriptionAttributes SubscriptionAttributes `xml:"Attributes,omitempty"` 61 | } 62 | 63 | type SubscriptionAttributes struct { 64 | /* SubscriptionArn, FilterPolicy */ 65 | Entries []SubscriptionAttributeEntry `xml:"entry,omitempty"` 66 | } 67 | 68 | type SubscriptionAttributeEntry struct { 69 | Key string `xml:"key,omitempty"` 70 | Value string `xml:"value,omitempty"` 71 | } 72 | 73 | type GetSubscriptionAttributesResponse struct { 74 | Xmlns string `xml:"xmlns,attr,omitempty"` 75 | Result GetSubscriptionAttributesResult `xml:"GetSubscriptionAttributesResult"` 76 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 77 | } 78 | 79 | /*** List Subscriptions Response */ 80 | type TopicMemberResult struct { 81 | TopicArn string `xml:"TopicArn"` 82 | Protocol string `xml:"Protocol"` 83 | SubscriptionArn string `xml:"SubscriptionArn"` 84 | Owner string `xml:"Owner"` 85 | Endpoint string `xml:"Endpoint"` 86 | } 87 | 88 | type TopicSubscriptions struct { 89 | Member []TopicMemberResult `xml:"member"` 90 | } 91 | 92 | type ListSubscriptionsResult struct { 93 | Subscriptions TopicSubscriptions `xml:"Subscriptions"` 94 | } 95 | 96 | type ListSubscriptionsResponse struct { 97 | Xmlns string `xml:"xmlns,attr"` 98 | Result ListSubscriptionsResult `xml:"ListSubscriptionsResult"` 99 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 100 | } 101 | 102 | /*** List Subscriptions By Topic Response */ 103 | 104 | type ListSubscriptionsByTopicResult struct { 105 | Subscriptions TopicSubscriptions `xml:"Subscriptions"` 106 | } 107 | 108 | type ListSubscriptionsByTopicResponse struct { 109 | Xmlns string `xml:"xmlns,attr"` 110 | Result ListSubscriptionsResult `xml:"ListSubscriptionsResult"` 111 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 112 | } 113 | 114 | /*** Publish ***/ 115 | 116 | type PublishResult struct { 117 | MessageId string `xml:"MessageId"` 118 | } 119 | 120 | type PublishResponse struct { 121 | Xmlns string `xml:"xmlns,attr"` 122 | Result PublishResult `xml:"PublishResult"` 123 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 124 | } 125 | 126 | /*** Unsubscribe ***/ 127 | type UnsubscribeResponse struct { 128 | Xmlns string `xml:"xmlns,attr"` 129 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 130 | } 131 | 132 | /*** Delete Topic ***/ 133 | type DeleteTopicResponse struct { 134 | Xmlns string `xml:"xmlns,attr"` 135 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 136 | } 137 | -------------------------------------------------------------------------------- /app/examples/java/SqsSample.java: -------------------------------------------------------------------------------- 1 | import java.util.List; 2 | import java.util.Map.Entry; 3 | import com.amazonaws.auth.BasicAWSCredentials; 4 | import com.amazonaws.AmazonClientException; 5 | import com.amazonaws.AmazonServiceException; 6 | import com.amazonaws.services.sqs.AmazonSQS; 7 | import com.amazonaws.services.sqs.AmazonSQSClient; 8 | import com.amazonaws.services.sqs.model.CreateQueueRequest; 9 | import com.amazonaws.services.sqs.model.DeleteMessageRequest; 10 | import com.amazonaws.services.sqs.model.DeleteQueueRequest; 11 | import com.amazonaws.services.sqs.model.Message; 12 | import com.amazonaws.services.sqs.model.ReceiveMessageRequest; 13 | import com.amazonaws.services.sqs.model.SendMessageRequest; 14 | 15 | /*** 16 | * Make sure you have the aws-java-sdk-1.8.11.jar + dependancies in your classpath 17 | ***/ 18 | 19 | public class SqsSample { 20 | 21 | public static void main(String[] args) throws Exception { 22 | AmazonSQS sqs = new AmazonSQSClient(new BasicAWSCredentials("x", "x")); 23 | sqs.setEndpoint("http://localhost:4100"); 24 | 25 | System.out.println("==========================================="); 26 | System.out.println("Getting Started with Amazon SQS"); 27 | System.out.println("===========================================\n"); 28 | 29 | try { 30 | // Create a queue 31 | System.out.println("Creating a new SQS queue called MyQueue.\n"); 32 | CreateQueueRequest createQueueRequest = new CreateQueueRequest("MyQueue"); 33 | String myQueueUrl = sqs.createQueue(createQueueRequest).getQueueUrl(); 34 | 35 | // List queues 36 | System.out.println("Listing all queues in your account.\n"); 37 | for (String queueUrl : sqs.listQueues().getQueueUrls()) { 38 | System.out.println(" QueueUrl: " + queueUrl); 39 | } 40 | System.out.println(); 41 | 42 | // Send a message 43 | System.out.println("Sending a message to MyQueue.\n"); 44 | sqs.sendMessage(new SendMessageRequest(myQueueUrl, "This is my message text.")); 45 | 46 | // Receive messages 47 | System.out.println("Receiving messages from MyQueue.\n"); 48 | ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(myQueueUrl); 49 | List messages = sqs.receiveMessage(receiveMessageRequest).getMessages(); 50 | for (Message message : messages) { 51 | System.out.println(" Message"); 52 | System.out.println(" MessageId: " + message.getMessageId()); 53 | System.out.println(" ReceiptHandle: " + message.getReceiptHandle()); 54 | System.out.println(" MD5OfBody: " + message.getMD5OfBody()); 55 | System.out.println(" Body: " + message.getBody()); 56 | for (Entry entry : message.getAttributes().entrySet()) { 57 | System.out.println(" Attribute"); 58 | System.out.println(" Name: " + entry.getKey()); 59 | System.out.println(" Value: " + entry.getValue()); 60 | } 61 | } 62 | System.out.println(); 63 | 64 | // Delete a message 65 | System.out.println("Deleting a message.\n"); 66 | String messageReceiptHandle = messages.get(0).getReceiptHandle(); 67 | sqs.deleteMessage(new DeleteMessageRequest(myQueueUrl, messageReceiptHandle)); 68 | 69 | // Delete a queue 70 | System.out.println("Deleting the test queue.\n"); 71 | sqs.deleteQueue(new DeleteQueueRequest(myQueueUrl)); 72 | } catch (AmazonServiceException ase) { 73 | System.out.println("Caught an AmazonServiceException, which means your request made it " + 74 | "to Amazon SQS, but was rejected with an error response for some reason."); 75 | System.out.println("Error Message: " + ase.getMessage()); 76 | System.out.println("HTTP Status Code: " + ase.getStatusCode()); 77 | System.out.println("AWS Error Code: " + ase.getErrorCode()); 78 | System.out.println("Error Type: " + ase.getErrorType()); 79 | System.out.println("Request ID: " + ase.getRequestId()); 80 | } catch (AmazonClientException ace) { 81 | System.out.println("Caught an AmazonClientException, which means the client encountered " + 82 | "a serious internal problem while trying to communicate with SQS, such as not " + 83 | "being able to access the network."); 84 | System.out.println("Error Message: " + ace.getMessage()); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/conf/config.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "encoding/json" 10 | 11 | "github.com/ghodss/yaml" 12 | "github.com/p4tin/goaws/app" 13 | "github.com/p4tin/goaws/app/common" 14 | "github.com/p4tin/goaws/app/gosqs" 15 | ) 16 | 17 | var envs map[string]app.Environment 18 | 19 | func LoadYamlConfig(filename string, env string) []string { 20 | ports := []string{"4100"} 21 | 22 | if filename == "" { 23 | filename, _ = filepath.Abs("./conf/goaws.yaml") 24 | } 25 | log.Warnf("Loading config file: %s", filename) 26 | yamlFile, err := ioutil.ReadFile(filename) 27 | if err != nil { 28 | return ports 29 | } 30 | 31 | err = yaml.Unmarshal(yamlFile, &envs) 32 | if err != nil { 33 | log.Errorf("err: %v\n", err) 34 | return ports 35 | } 36 | if env == "" { 37 | env = "Local" 38 | } 39 | 40 | if envs[env].Region == "" { 41 | app.CurrentEnvironment.Region = "local" 42 | } 43 | 44 | app.CurrentEnvironment = envs[env] 45 | 46 | if envs[env].Port != "" { 47 | ports = []string{envs[env].Port} 48 | } else if envs[env].SqsPort != "" && envs[env].SnsPort != "" { 49 | ports = []string{envs[env].SqsPort, envs[env].SnsPort} 50 | app.CurrentEnvironment.Port = envs[env].SqsPort 51 | } 52 | 53 | common.LogMessages = false 54 | common.LogFile = "./goaws_messages.log" 55 | 56 | if envs[env].LogMessages == true { 57 | common.LogMessages = true 58 | if envs[env].LogFile != "" { 59 | common.LogFile = envs[env].LogFile 60 | } 61 | } 62 | 63 | if app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout == 0 { 64 | app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout = 30 65 | } 66 | 67 | if app.CurrentEnvironment.AccountID == "" { 68 | app.CurrentEnvironment.AccountID = "queue" 69 | } 70 | 71 | if app.CurrentEnvironment.Host == "" { 72 | app.CurrentEnvironment.Host = "localhost" 73 | app.CurrentEnvironment.Port = "4100" 74 | } 75 | 76 | app.SyncQueues.Lock() 77 | app.SyncTopics.Lock() 78 | for _, queue := range envs[env].Queues { 79 | queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + 80 | "/" + app.CurrentEnvironment.AccountID + "/" + queue.Name 81 | if app.CurrentEnvironment.Region != "" { 82 | queueUrl = "http://sqs." + app.CurrentEnvironment.Region + "." + app.CurrentEnvironment.Host + ":" + 83 | app.CurrentEnvironment.Port + "/" + app.CurrentEnvironment.AccountID + "/" + queue.Name 84 | } 85 | queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + queue.Name 86 | 87 | if queue.ReceiveMessageWaitTimeSeconds == 0 { 88 | queue.ReceiveMessageWaitTimeSeconds = app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds 89 | } 90 | 91 | appQueue := &app.Queue{ 92 | Name: queue.Name, 93 | TimeoutSecs: app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout, 94 | Arn: queueArn, 95 | URL: queueUrl, 96 | ReceiveWaitTimeSecs: queue.ReceiveMessageWaitTimeSeconds, 97 | IsFIFO: app.HasFIFOQueueName(queue.Name), 98 | } 99 | if queue.RedrivePolicy != "" { 100 | if err := gosqs.ValidateAndSetRedrivePolicy(appQueue, queue.RedrivePolicy); err != nil { 101 | log.Errorf("Error creating queue: %v", err) 102 | continue 103 | } 104 | } 105 | app.SyncQueues.Queues[queue.Name] = appQueue 106 | } 107 | 108 | for _, topic := range envs[env].Topics { 109 | topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + topic.Name 110 | 111 | newTopic := &app.Topic{Name: topic.Name, Arn: topicArn} 112 | newTopic.Subscriptions = make([]*app.Subscription, 0, 0) 113 | 114 | for _, subs := range topic.Subscriptions { 115 | if _, ok := app.SyncQueues.Queues[subs.QueueName]; !ok { 116 | //Queue does not exist yet, create it. 117 | queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + 118 | "/" + app.CurrentEnvironment.AccountID + "/" + subs.QueueName 119 | if app.CurrentEnvironment.Region != "" { 120 | queueUrl = "http://sqs." + app.CurrentEnvironment.Region + "." + app.CurrentEnvironment.Host + ":" + 121 | app.CurrentEnvironment.Port + "/" + app.CurrentEnvironment.AccountID + "/" + subs.QueueName 122 | } 123 | queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + subs.QueueName 124 | app.SyncQueues.Queues[subs.QueueName] = &app.Queue{ 125 | Name: subs.QueueName, 126 | TimeoutSecs: app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout, 127 | Arn: queueArn, 128 | URL: queueUrl, 129 | ReceiveWaitTimeSecs: app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds, 130 | IsFIFO: app.HasFIFOQueueName(subs.QueueName), 131 | } 132 | } 133 | qArn := app.SyncQueues.Queues[subs.QueueName].Arn 134 | newSub := &app.Subscription{EndPoint: qArn, Protocol: "sqs", TopicArn: topicArn, Raw: subs.Raw} 135 | subArn, _ := common.NewUUID() 136 | subArn = topicArn + ":" + subArn 137 | newSub.SubscriptionArn = subArn 138 | 139 | if subs.FilterPolicy != "" { 140 | filterPolicy := &app.FilterPolicy{} 141 | err = json.Unmarshal([]byte(subs.FilterPolicy), filterPolicy) 142 | if err != nil { 143 | log.Errorf("err: %s", err) 144 | return ports 145 | } 146 | newSub.FilterPolicy = filterPolicy 147 | } 148 | 149 | newTopic.Subscriptions = append(newTopic.Subscriptions, newSub) 150 | } 151 | app.SyncTopics.Topics[topic.Name] = newTopic 152 | } 153 | 154 | app.SyncQueues.Unlock() 155 | app.SyncTopics.Unlock() 156 | 157 | return ports 158 | } 159 | -------------------------------------------------------------------------------- /app/sqs_messages.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | /*** List Queues Response */ 4 | type ListQueuesResult struct { 5 | QueueUrl []string `xml:"QueueUrl"` 6 | } 7 | 8 | type ListQueuesResponse struct { 9 | Xmlns string `xml:"xmlns,attr"` 10 | Result ListQueuesResult `xml:"ListQueuesResult"` 11 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 12 | } 13 | 14 | /*** Create Queue Response */ 15 | type CreateQueueResult struct { 16 | QueueUrl string `xml:"QueueUrl"` 17 | } 18 | 19 | type CreateQueueResponse struct { 20 | Xmlns string `xml:"xmlns,attr"` 21 | Result CreateQueueResult `xml:"CreateQueueResult"` 22 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 23 | } 24 | 25 | /*** Send Message Response */ 26 | 27 | type SendMessageResult struct { 28 | MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes"` 29 | MD5OfMessageBody string `xml:"MD5OfMessageBody"` 30 | MessageId string `xml:"MessageId"` 31 | SequenceNumber string `xml:"SequenceNumber"` 32 | } 33 | 34 | type SendMessageResponse struct { 35 | Xmlns string `xml:"xmlns,attr"` 36 | Result SendMessageResult `xml:"SendMessageResult"` 37 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 38 | } 39 | 40 | /*** Receive Message Response */ 41 | 42 | type ResultMessage struct { 43 | MessageId string `xml:"MessageId,omitempty"` 44 | ReceiptHandle string `xml:"ReceiptHandle,omitempty"` 45 | MD5OfBody string `xml:"MD5OfBody,omitempty"` 46 | Body []byte `xml:"Body,omitempty"` 47 | MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes,omitempty"` 48 | MessageAttributes []*ResultMessageAttribute `xml:"MessageAttribute,omitempty"` 49 | Attributes []*ResultAttribute `xml:"Attribute,omitempty"` 50 | } 51 | 52 | type ResultMessageAttributeValue struct { 53 | DataType string `xml:"DataType,omitempty"` 54 | StringValue string `xml:"StringValue,omitempty"` 55 | BinaryValue string `xml:"BinaryValue,omitempty"` 56 | } 57 | 58 | type ResultMessageAttribute struct { 59 | Name string `xml:"Name,omitempty"` 60 | Value *ResultMessageAttributeValue `xml:"Value,omitempty"` 61 | } 62 | 63 | type ResultAttribute struct { 64 | Name string `xml:"Name,omitempty"` 65 | Value string `xml:"Value,omitempty"` 66 | } 67 | 68 | type ReceiveMessageResult struct { 69 | Message []*ResultMessage `xml:"Message,omitempty"` 70 | } 71 | 72 | type ReceiveMessageResponse struct { 73 | Xmlns string `xml:"xmlns,attr"` 74 | Result ReceiveMessageResult `xml:"ReceiveMessageResult"` 75 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 76 | } 77 | 78 | type ChangeMessageVisibilityResult struct { 79 | Xmlns string `xml:"xmlns,attr"` 80 | Metadata ResponseMetadata `xml:"ResponseMetadata"` 81 | } 82 | 83 | /*** Delete Message Response */ 84 | type DeleteMessageResponse struct { 85 | Xmlns string `xml:"xmlns,attr,omitempty"` 86 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 87 | } 88 | 89 | type DeleteMessageBatchResultEntry struct { 90 | Id string `xml:"Id"` 91 | } 92 | 93 | type SendMessageBatchResultEntry struct { 94 | Id string `xml:"Id"` 95 | MessageId string `xml:"MessageId"` 96 | MD5OfMessageBody string `xml:"MD5OfMessageBody,omitempty"` 97 | MD5OfMessageAttributes string `xml:"MD5OfMessageAttributes,omitempty"` 98 | SequenceNumber string `xml:"SequenceNumber"` 99 | } 100 | 101 | type BatchResultErrorEntry struct { 102 | Code string `xml:"Code"` 103 | Id string `xml:"Id"` 104 | Message string `xml:"Message,omitempty"` 105 | SenderFault bool `xml:"SenderFault"` 106 | } 107 | 108 | type DeleteMessageBatchResult struct { 109 | Entry []DeleteMessageBatchResultEntry `xml:"DeleteMessageBatchResultEntry"` 110 | Error []BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` 111 | } 112 | 113 | /*** Delete Message Batch Response */ 114 | type DeleteMessageBatchResponse struct { 115 | Xmlns string `xml:"xmlns,attr,omitempty"` 116 | Result DeleteMessageBatchResult `xml:"DeleteMessageBatchResult"` 117 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 118 | } 119 | 120 | type SendMessageBatchResult struct { 121 | Entry []SendMessageBatchResultEntry `xml:"SendMessageBatchResultEntry"` 122 | Error []BatchResultErrorEntry `xml:"BatchResultErrorEntry,omitempty"` 123 | } 124 | 125 | /*** Delete Message Batch Response */ 126 | type SendMessageBatchResponse struct { 127 | Xmlns string `xml:"xmlns,attr,omitempty"` 128 | Result SendMessageBatchResult `xml:"SendMessageBatchResult"` 129 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 130 | } 131 | 132 | /*** Purge Queue Response */ 133 | type PurgeQueueResponse struct { 134 | Xmlns string `xml:"xmlns,attr,omitempty"` 135 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 136 | } 137 | 138 | /*** Get Queue Url Response */ 139 | type GetQueueUrlResult struct { 140 | QueueUrl string `xml:"QueueUrl,omitempty"` 141 | } 142 | 143 | type GetQueueUrlResponse struct { 144 | Xmlns string `xml:"xmlns,attr,omitempty"` 145 | Result GetQueueUrlResult `xml:"GetQueueUrlResult"` 146 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 147 | } 148 | 149 | /*** Get Queue Attributes ***/ 150 | type Attribute struct { 151 | Name string `xml:"Name,omitempty"` 152 | Value string `xml:"Value,omitempty"` 153 | } 154 | 155 | type GetQueueAttributesResult struct { 156 | /* VisibilityTimeout, DelaySeconds, ReceiveMessageWaitTimeSeconds, ApproximateNumberOfMessages 157 | ApproximateNumberOfMessagesNotVisible, CreatedTimestamp, LastModifiedTimestamp, QueueArn */ 158 | Attrs []Attribute `xml:"Attribute,omitempty"` 159 | } 160 | 161 | type GetQueueAttributesResponse struct { 162 | Xmlns string `xml:"xmlns,attr,omitempty"` 163 | Result GetQueueAttributesResult `xml:"GetQueueAttributesResult"` 164 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 165 | } 166 | 167 | type SetQueueAttributesResponse struct { 168 | Xmlns string `xml:"xmlns,attr,omitempty"` 169 | Metadata ResponseMetadata `xml:"ResponseMetadata,omitempty"` 170 | } 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoAws 2 | [![Build Status](https://travis-ci.org/p4tin/goaws.svg?branch=master)](https://travis-ci.org/p4tin/goaws) 3 | 4 | You are always welcome to [tweet me](https://twitter.com/gocodecloud) or [buy me a coffee](https://www.paypal.me/p4tin) 5 | 6 | Written in Go this is a clone of the AWS SQS/SNS systems. This system is designed to emulate SQS and SNS in a local environment so developers can test their interfaces without having to connect to the AWS Cloud and possibly incurring the expense, or even worse actually write to production topics/queues by mistake. If you see any problems or would like to see a new feature, please open an issue here in github. As well, I will logon to Gitter so we can discuss your deployment issues or the weather. 7 | 8 | 9 | ## SNS/SQS Api status: 10 | 11 | All SNS/SQS APIs have been implemented except: 12 | - The full capabilities for Get and Set QueueAttributes. At the moment you can only Get ALL the attributes. 13 | 14 | Here is a list of the APIs: 15 | - [x] ListQueues 16 | - [x] CreateQueue 17 | - [x] GetQueueAttributes (Always returns all attributes - unsupporterd arttributes are mocked) 18 | - [x] GetQueueUrl 19 | - [x] SendMessage 20 | - [x] SendMessageBatch 21 | - [x] ReceiveMessage 22 | - [x] DeleteMessage 23 | - [x] DeleteMessageBatch 24 | - [x] PurgeQueue 25 | - [x] Delete Queue 26 | - [x] ChangeMessageVisibility 27 | - [ ] ChangeMessageVisibilityBatch 28 | - [ ] ListDeadLetterSourceQueues 29 | - [ ] ListQueueTags 30 | - [ ] RemovePermission 31 | - [x] SetQueueAttributes (Only supported attributes are set - see Supported Queue Attributes) 32 | - [ ] TagQueue 33 | - [ ] UntagQueue 34 | 35 | ## Supported Queue Attributes 36 | 37 | - [x] VisibilityTimeout 38 | - [x] ReceiveMessageWaitTimeSeconds 39 | - [x] RedrivePolicy 40 | 41 | ## Current SNS APIs implemented: 42 | 43 | - [x] ListTopics 44 | - [x] CreateTopic 45 | - [x] Subscribe (raw) 46 | - [x] ListSubscriptions 47 | - [x] Publish 48 | - [x] DeleteTopic 49 | - [x] Subscribe 50 | - [x] Unsubscribe 51 | - [X] ListSubscriptionsByTopic 52 | - [x] GetSubscriptionAttributes 53 | - [x] SetSubscriptionAttributes (Only supported attributes are set - see Supported Subscription Attributes) 54 | 55 | ## Supported Subscription Attributes 56 | 57 | - [x] RawMessageDelivery 58 | - [x] FilterPolicy (Only supported simplest "exact match" filter policy) 59 | 60 | 61 | ## Yaml Configuration Implemented 62 | 63 | - [x] Read config file 64 | - [x] -config flag to read a specific configuration file (e.g.: -config=myconfig.yaml) 65 | - [x] a command line argument to determine the environment to use in the config file (e.e.: Dev) 66 | - [x] IN the config file you can create Queues, Topic and Subscription see the example config file in the conf directory 67 | 68 | ## Debug logging can be turned on via a command line flag (e.g.: -debug) 69 | 70 | ## Note: The system does not authenticate or presently use https 71 | 72 | # Installation 73 | 74 | go get github.com/p4tin/goaws/... 75 | 76 | ## Build and Run (Standalone) 77 | 78 | Build 79 | cd to GoAws directory 80 | go build -o goaws app/cmd/goaws.go (The goaws executable should be in the currect directory, move it somewhere in your $PATH) 81 | 82 | Run 83 | ./goaws (by default goaws listens on port 4100 but you can change it in the goaws.yaml file to another port of your choice) 84 | 85 | 86 | ## Run (Docker Version) 87 | 88 | Get it 89 | docker pull pafortin/goaws 90 | 91 | run 92 | docker run -d --name goaws -p 4100:4100 pafortin/goaws 93 | 94 | 95 | 96 | ## Testing your installation 97 | 98 | You can test that your installation is working correctly in one of two ways: 99 | 100 | 1. Usign the postman collection, use this [link to import it](https://www.getpostman.com/collections/091386eae8c70588348e). As well the Environment variable for the collection should be set as follows: URL = http://localhost:4100/. 101 | 102 | 2. by using the AWS cli tools ([download link](http://docs.aws.amazon.com/cli/latest/userguide/installing.html)) here are some samples, you can refer to the [aws cli tools docs](http://docs.aws.amazon.com/cli/latest/reference/) for further information. 103 | 104 | * aws --endpoint-url http://localhost:4100 sqs create-queue --queue-name test1 105 | ``` 106 | { 107 | "QueueUrl": "http://localhost:4100/test1" 108 | } 109 | ``` 110 | * aws --endpoint-url http://localhost:4100 sqs list-queues 111 | ``` 112 | { 113 | "QueueUrls": [ 114 | "http://localhost:4100/test1" 115 | ] 116 | } 117 | ``` 118 | * aws --endpoint-url http://localhost:4100 sqs send-message --queue-url http://localhost:4100/test1 --message-body "this is a test of the GoAws Queue messaging" 119 | ``` 120 | { 121 | "MD5OfMessageBody": "9d3f5eaac3b1b4dd509f39e71e25f954", 122 | "MD5OfMessageAttributes": "b095c6d16871105acb75d59332513337", 123 | "MessageId": "66a1b4f5-cecf-473e-92b6-810156d41bbe" 124 | } 125 | ``` 126 | * aws --endpoint-url http://localhost:4100 sqs receive-message --queue-url http://localhost:4100/test1 127 | ``` 128 | { 129 | "Messages": [ 130 | { 131 | "Body": "this is a test of the GoAws Queue messaging", 132 | "MD5OfMessageAttributes": "b095c6d16871105acb75d59332513337", 133 | "ReceiptHandle": "66a1b4f5-cecf-473e-92b6-810156d41bbe#f1fc455c-698e-442e-9747-f415bee5b461", 134 | "MD5OfBody": "9d3f5eaac3b1b4dd509f39e71e25f954", 135 | "MessageId": "66a1b4f5-cecf-473e-92b6-810156d41bbe" 136 | } 137 | ] 138 | } 139 | ``` 140 | * aws --endpoint-url http://localhost:4100 sqs delete-message --queue-url http://localhost:4100/test1 --receipt-handle 66a1b4f5-cecf-473e-92b6-810156d41bbe#f1fc455c-698e-442e-9747-f415bee5b461 141 | ``` 142 | No output 143 | ``` 144 | * aws --endpoint-url http://localhost:4100 sqs receive-message --queue-url http://localhost:4100/test1 145 | ``` 146 | No output (No messages in Q) 147 | ``` 148 | * aws --endpoint-url http://localhost:4100 sqs delete-queue --queue-url http://localhost:4100/test1 149 | ``` 150 | No output 151 | ``` 152 | * aws --endpoint-url http://localhost:4100 sqs list-queues 153 | ``` 154 | No output (There are no Queues left) 155 | ``` 156 | 157 | * aws --endpoint-url http://localhost:4100 sns list-topics (Example Response from list-topics) 158 | ``` 159 | { 160 | "Topics": [ 161 | { 162 | "TopicArn": "arn:aws:sns:local:000000000000:topic1" 163 | }, 164 | { 165 | "TopicArn": "arn:aws:sns:local:000000000000:topic2" 166 | } 167 | ] 168 | } 169 | ``` 170 | -------------------------------------------------------------------------------- /app/gosns/gosns_create_message_test.go: -------------------------------------------------------------------------------- 1 | package gosns 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/p4tin/goaws/app" 8 | ) 9 | 10 | const ( 11 | messageKey = "Message" 12 | subjectKey = "Subject" 13 | messageStructureJSON = "json" 14 | messageStructureEmpty = "" 15 | ) 16 | 17 | // When simple message string is passed, 18 | // it must be used for all subscribers (no matter the protocol) 19 | func TestCreateMessageBody_NonJson(t *testing.T) { 20 | message := "message text" 21 | subject := "subject" 22 | subs := &app.Subscription{ 23 | Protocol: "sqs", 24 | TopicArn: "topic-arn", 25 | SubscriptionArn: "subs-arn", 26 | Raw: false, 27 | } 28 | 29 | snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureEmpty, make(map[string]app.MessageAttributeValue)) 30 | if err != nil { 31 | t.Fatalf(`error creating SNS message: %s`, err) 32 | } 33 | 34 | var unmarshalled map[string]interface{} 35 | err = json.Unmarshal(snsMessage, &unmarshalled) 36 | if err != nil { 37 | t.Fatalf(`error unmarshalling SNS message "%s": %s`, snsMessage, err) 38 | } 39 | 40 | receivedMessage, ok := unmarshalled[messageKey] 41 | if !ok { 42 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, messageKey) 43 | } 44 | 45 | if receivedMessage != message { 46 | t.Errorf(`expected message "%s" but received "%s"`, message, receivedMessage) 47 | } 48 | 49 | receivedSubject, ok := unmarshalled[subjectKey] 50 | if !ok { 51 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, subjectKey) 52 | } 53 | 54 | if receivedSubject != subject { 55 | t.Errorf(`expected subject "%s" but received "%s"`, subject, receivedSubject) 56 | } 57 | } 58 | 59 | // When no protocol specific message is passed, 60 | // default message must be forwarded 61 | func TestCreateMessageBody_OnlyDefaultValueInJson(t *testing.T) { 62 | subs := &app.Subscription{ 63 | Protocol: "sqs", 64 | TopicArn: "topic-arn", 65 | SubscriptionArn: "subs-arn", 66 | Raw: false, 67 | } 68 | message := `{"default": "default message text", "http": "HTTP message text"}` 69 | subject := "subject" 70 | 71 | snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureJSON, nil) 72 | if err != nil { 73 | t.Fatalf(`error creating SNS message: %s`, err) 74 | } 75 | 76 | var unmarshalled map[string]interface{} 77 | err = json.Unmarshal(snsMessage, &unmarshalled) 78 | if err != nil { 79 | t.Fatalf(`error unmarshalling SNS message "%s": %s`, snsMessage, err) 80 | } 81 | 82 | receivedMessage, ok := unmarshalled[messageKey] 83 | if !ok { 84 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, messageKey) 85 | } 86 | 87 | expected := "default message text" 88 | if receivedMessage != expected { 89 | t.Errorf(`expected message "%s" but received "%s"`, expected, receivedMessage) 90 | } 91 | 92 | receivedSubject, ok := unmarshalled[subjectKey] 93 | if !ok { 94 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, subjectKey) 95 | } 96 | 97 | if receivedSubject != subject { 98 | t.Errorf(`expected subject "%s" but received "%s"`, subject, receivedSubject) 99 | } 100 | } 101 | 102 | // When only protocol specific message is passed, 103 | // error must be returned 104 | func TestCreateMessageBody_OnlySqsValueInJson(t *testing.T) { 105 | subs := &app.Subscription{ 106 | Protocol: "sqs", 107 | TopicArn: "topic-arn", 108 | SubscriptionArn: "subs-arn", 109 | Raw: false, 110 | } 111 | message := `{"sqs": "message text"}` 112 | subject := "subject" 113 | 114 | snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureJSON, nil) 115 | if err == nil { 116 | t.Fatalf(`error expected but instead SNS message was returned: %s`, snsMessage) 117 | } 118 | } 119 | 120 | // when both default and protocol specific messages are passed, 121 | // protocol specific message must be used 122 | func TestCreateMessageBody_BothDefaultAndSqsValuesInJson(t *testing.T) { 123 | subs := &app.Subscription{ 124 | Protocol: "sqs", 125 | TopicArn: "topic-arn", 126 | SubscriptionArn: "subs-arn", 127 | Raw: false, 128 | } 129 | message := `{"default": "default message text", "sqs": "sqs message text"}` 130 | subject := "subject" 131 | 132 | snsMessage, err := CreateMessageBody(subs, message, subject, messageStructureJSON, nil) 133 | if err != nil { 134 | t.Fatalf(`error creating SNS message: %s`, err) 135 | } 136 | 137 | var unmarshalled map[string]interface{} 138 | err = json.Unmarshal(snsMessage, &unmarshalled) 139 | if err != nil { 140 | t.Fatalf(`error unmarshalling SNS message "%s": %s`, snsMessage, err) 141 | } 142 | 143 | receivedMessage, ok := unmarshalled[messageKey] 144 | if !ok { 145 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, messageKey) 146 | } 147 | 148 | expected := "sqs message text" 149 | if receivedMessage != expected { 150 | t.Errorf(`expected message "%s" but received "%s"`, expected, receivedMessage) 151 | } 152 | 153 | receivedSubject, ok := unmarshalled[subjectKey] 154 | if !ok { 155 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, subjectKey) 156 | } 157 | 158 | if receivedSubject != subject { 159 | t.Errorf(`expected subject "%s" but received "%s"`, subject, receivedSubject) 160 | } 161 | } 162 | 163 | // When simple message string is passed, 164 | // it must be used as is (even if it contains JSON) 165 | func TestCreateMessageBody_NonJsonContainingJson(t *testing.T) { 166 | subs := &app.Subscription{ 167 | Protocol: "sns", 168 | TopicArn: "topic-arn", 169 | SubscriptionArn: "subs-arn", 170 | Raw: false, 171 | } 172 | message := `{"default": "default message text", "sqs": "sqs message text"}` 173 | subject := "subject" 174 | 175 | snsMessage, err := CreateMessageBody(subs, message, subject, "", nil) 176 | if err != nil { 177 | t.Fatalf(`error creating SNS message: %s`, err) 178 | } 179 | 180 | var unmarshalled map[string]interface{} 181 | err = json.Unmarshal(snsMessage, &unmarshalled) 182 | if err != nil { 183 | t.Fatalf(`error unmarshalling SNS message "%s": %s`, snsMessage, err) 184 | } 185 | 186 | receivedMessage, ok := unmarshalled[messageKey] 187 | if !ok { 188 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, messageKey) 189 | } 190 | 191 | expected := `{"default": "default message text", "sqs": "sqs message text"}` 192 | if receivedMessage != expected { 193 | t.Errorf(`expected message "%s" but received "%s"`, expected, receivedMessage) 194 | } 195 | 196 | receivedSubject, ok := unmarshalled[subjectKey] 197 | if !ok { 198 | t.Fatalf(`SNS message "%s" does not contain key "%s"`, snsMessage, subjectKey) 199 | } 200 | 201 | if receivedSubject != subject { 202 | t.Errorf(`expected subject "%s" but received "%s"`, subject, receivedSubject) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/servertest/server_test.go: -------------------------------------------------------------------------------- 1 | package servertest 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | "github.com/aws/aws-sdk-go/aws" 16 | "github.com/aws/aws-sdk-go/aws/credentials" 17 | "github.com/aws/aws-sdk-go/aws/session" 18 | "github.com/aws/aws-sdk-go/service/sns" 19 | "github.com/aws/aws-sdk-go/service/sqs" 20 | "github.com/aws/aws-sdk-go/service/sqs/sqsiface" 21 | "github.com/gorilla/mux" 22 | "github.com/p4tin/goaws/app" 23 | "github.com/p4tin/goaws/app/router" 24 | log "github.com/sirupsen/logrus" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestNew(t *testing.T) { 30 | // Consume address 31 | srv, err := New("localhost:4100") 32 | noSetupError(t, err) 33 | defer srv.Quit() 34 | 35 | // Test 36 | _, err = New("localhost:4100") 37 | assert.Equal(t, errors.New("cannot listen on localhost: listen tcp 127.0.0.1:4100: bind: address already in use"), err, "Error") 38 | } 39 | 40 | func TestNewIntegration(t *testing.T) { 41 | testTable := []struct { 42 | Name string 43 | Expected []string 44 | QueueFunc func(sqsiface.SQSAPI, *string) error 45 | }{ 46 | { 47 | Name: "Empty queue OK", 48 | Expected: []string{}, 49 | QueueFunc: noOp, 50 | }, 51 | //{ 52 | // Name: "Some messages OK", 53 | // Expected: []string{"hello world"}, 54 | // QueueFunc: func(svc sqsiface.SQSAPI, queueURL *string) error { 55 | // attributes := make(map[string]*sqs.MessageAttributeValue) 56 | // attributes["some string"] = &sqs.MessageAttributeValue{ 57 | // StringValue: aws.String("string value with a special character \u2318"), 58 | // DataType: aws.String("String"), 59 | // } 60 | // attributes["some number"] = &sqs.MessageAttributeValue{ 61 | // StringValue: aws.String("123"), 62 | // DataType: aws.String("Number"), 63 | // } 64 | // attributes["some binary"] = &sqs.MessageAttributeValue{ 65 | // BinaryValue: []byte{1, 2, 3}, 66 | // DataType: aws.String("Binary"), 67 | // } 68 | // 69 | // response, err := svc.SendMessage(&sqs.SendMessageInput{ 70 | // MessageBody: aws.String("hello world"), 71 | // MessageAttributes: attributes, 72 | // QueueUrl: queueURL, 73 | // }) 74 | // 75 | // assert.Equal(t, "5eb63bbbe01eeed093cb22bb8f5acdc3", *response.MD5OfMessageBody) 76 | // assert.Equal(t, "7820c7a3712c7c359cf80485f67aa34d", *response.MD5OfMessageAttributes) 77 | // return err 78 | // }, 79 | //}, 80 | } 81 | for _, tr := range testTable { 82 | t.Run(tr.Name, func(t *testing.T) { 83 | // Start local SQS 84 | srv, err := New("") 85 | noSetupError(t, err) 86 | defer srv.Quit() 87 | 88 | svc := newSQS(t, "faux-region-1", srv.URL()) 89 | 90 | // Create test queue 91 | _, err = svc.CreateQueue( 92 | &sqs.CreateQueueInput{QueueName: aws.String("test-queue")}) 93 | noSetupError(t, err) 94 | 95 | getQueueUrlOutput, err := svc.GetQueueUrl(&sqs.GetQueueUrlInput{QueueName: aws.String("test-queue")}) 96 | noSetupError(t, err) 97 | queueURL := getQueueUrlOutput.QueueUrl 98 | 99 | // Setup Queue Sate 100 | err = tr.QueueFunc(svc, queueURL) 101 | noSetupError(t, err) 102 | 103 | // Test 104 | receiveMessageInput := &sqs.ReceiveMessageInput{QueueUrl: queueURL} 105 | receiveMessageOutput, err := svc.ReceiveMessage(receiveMessageInput) 106 | 107 | msgsBody := []string{} 108 | for _, b := range receiveMessageOutput.Messages { 109 | msgsBody = append(msgsBody, *b.Body) 110 | } 111 | 112 | assert.Equal(t, tr.Expected, msgsBody, "Messages") 113 | assert.Equal(t, nil, err, "Error") 114 | }) 115 | } 116 | } 117 | 118 | func TestSNSRoutes(t *testing.T) { 119 | // Consume address 120 | srv, err := NewSNSTest("localhost:4100", &snsTest{t: t}) 121 | 122 | noSetupError(t, err) 123 | defer srv.Quit() 124 | 125 | creds := credentials.NewStaticCredentials("id", "secret", "token") 126 | 127 | awsConfig := aws.NewConfig(). 128 | WithRegion("us-east-1"). 129 | WithEndpoint(srv.URL()). 130 | WithCredentials(creds) 131 | 132 | session1 := session.New(awsConfig) 133 | client := sns.New(session1) 134 | 135 | response, err := client.CreateTopic(&sns.CreateTopicInput{ 136 | Name: aws.String("testing"), 137 | }) 138 | require.NoError(t, err, "SNS Create Topic Failed") 139 | 140 | params := &sns.SubscribeInput{ 141 | Protocol: aws.String("sqs"), // Required 142 | TopicArn: response.TopicArn, // Required 143 | Endpoint: aws.String(srv.URL() + "/local-sns"), 144 | } 145 | subscribeResponse, err := client.Subscribe(params) 146 | require.NoError(t, err, "SNS Subscribe Failed") 147 | t.Logf("Succesfully subscribed: %s\n", *subscribeResponse.SubscriptionArn) 148 | 149 | publishParams := &sns.PublishInput{ 150 | Message: aws.String("Cool"), 151 | TopicArn: response.TopicArn, 152 | } 153 | publishResponse, err := client.Publish(publishParams) 154 | require.NoError(t, err, "SNS Publish Failed") 155 | t.Logf("Succesfully published: %s\n", *publishResponse.MessageId) 156 | } 157 | 158 | func newSQS(t *testing.T, region string, endpoint string) *sqs.SQS { 159 | creds := credentials.NewStaticCredentials("id", "secret", "token") 160 | 161 | awsConfig := aws.NewConfig(). 162 | WithRegion(region). 163 | WithEndpoint(endpoint). 164 | WithCredentials(creds) 165 | 166 | session1 := session.New(awsConfig) 167 | 168 | svc := sqs.New(session1) 169 | return svc 170 | } 171 | 172 | func noOp(sqsiface.SQSAPI, *string) error { 173 | return nil 174 | } 175 | 176 | func noSetupError(t *testing.T, err error) { 177 | require.NoError(t, err, "Failed to setup for test") 178 | } 179 | 180 | type snsTest struct { 181 | t *testing.T 182 | } 183 | 184 | func NewSNSTest(addr string, snsTest *snsTest) (*Server, error) { 185 | if addr == "" { 186 | addr = "localhost:0" 187 | } 188 | localURL := strings.Split(addr, ":") 189 | app.CurrentEnvironment.Host = localURL[0] 190 | app.CurrentEnvironment.Port = localURL[1] 191 | log.WithFields(log.Fields{ 192 | "host": app.CurrentEnvironment.Host, 193 | "port": app.CurrentEnvironment.Port, 194 | }).Info("URL Starting to listen") 195 | 196 | l, err := net.Listen("tcp", addr) 197 | if err != nil { 198 | return nil, fmt.Errorf("cannot listen on localhost: %v", err) 199 | } 200 | if err != nil { 201 | return nil, fmt.Errorf("cannot listen on localhost: %v", err) 202 | } 203 | 204 | r := mux.NewRouter() 205 | r.Handle("/", router.New()) 206 | snsTest.SetSNSRoutes("/local-sns", r, nil) 207 | 208 | srv := Server{listener: l, handler: r} 209 | 210 | go http.Serve(l, &srv) 211 | 212 | return &srv, nil 213 | } 214 | 215 | // Define handlers for various AWS SNS POST calls 216 | func (s *snsTest) SetSNSRoutes(urlPath string, r *mux.Router, handler http.Handler) { 217 | 218 | r.HandleFunc(urlPath, s.SubscribeConfirmHandle).Methods("POST").Headers("x-amz-sns-message-type", "SubscriptionConfirmation") 219 | if handler != nil { 220 | log.WithFields(log.Fields{ 221 | "urlPath": urlPath, 222 | }).Debug("handler not nil") 223 | // handler is supposed to be wrapper that inturn calls NotificationHandle 224 | r.Handle(urlPath, handler).Methods("POST").Headers("x-amz-sns-message-type", "Notification") 225 | } else { 226 | log.WithFields(log.Fields{ 227 | "urlPath": urlPath, 228 | }).Debug("handler nil") 229 | // if no wrapper handler available then define anonymous handler and directly call NotificationHandle 230 | r.HandleFunc(urlPath, func(rw http.ResponseWriter, req *http.Request) { 231 | s.NotificationHandle(rw, req) 232 | }).Methods("POST").Headers("x-amz-sns-message-type", "Notification") 233 | } 234 | } 235 | 236 | func (s *snsTest) SubscribeConfirmHandle(rw http.ResponseWriter, req *http.Request) { 237 | //params := &sns.ConfirmSubscriptionInput{ 238 | // Token: aws.String(msg.Token), // Required 239 | // TopicArn: aws.String(msg.TopicArn), // Required 240 | //} 241 | var f interface{} 242 | body, err := ioutil.ReadAll(req.Body) 243 | if err != nil { 244 | s.t.Log("Unable to Parse Body") 245 | } 246 | s.t.Log(string(body)) 247 | err = json.Unmarshal(body, &f) 248 | if err != nil { 249 | s.t.Log("Unable to Unmarshal request") 250 | } 251 | 252 | data := f.(map[string]interface{}) 253 | s.t.Log(data["Type"].(string)) 254 | 255 | if data["Type"].(string) == "SubscriptionConfirmation" { 256 | subscribeURL := data["SubscribeURL"].(string) 257 | time.Sleep(time.Second) 258 | response, err := http.Get(subscribeURL) 259 | if err != nil { 260 | s.t.Logf("Unable to confirm subscriptions. %s\n", err) 261 | s.t.Fail() 262 | } else { 263 | s.t.Logf("Subscription Confirmed successfully. %d\n", response.StatusCode) 264 | } 265 | } else if data["Type"].(string) == "Notification" { 266 | s.t.Log("Received this message : ", data["Message"].(string)) 267 | } 268 | } 269 | 270 | func (s *snsTest) NotificationHandle(rw http.ResponseWriter, req *http.Request) []byte { 271 | subArn := req.Header.Get("X-Amz-Sns-Subscription-Arn") 272 | 273 | msg := app.SNSMessage{} 274 | _, err := DecodeJSONMessage(req, &msg) 275 | if err != nil { 276 | log.Error(err) 277 | return []byte{} 278 | } 279 | 280 | s.t.Logf("NotificationHandle %s MSG(%s)", subArn, msg.Message) 281 | return []byte(msg.Message) 282 | } 283 | 284 | func DecodeJSONMessage(req *http.Request, v interface{}) ([]byte, error) { 285 | 286 | payload, err := ioutil.ReadAll(req.Body) 287 | if err != nil { 288 | return nil, err 289 | } 290 | if len(payload) == 0 { 291 | return nil, errors.New("empty payload") 292 | } 293 | err = json.Unmarshal([]byte(payload), v) 294 | if err != nil { 295 | return nil, err 296 | } 297 | return payload, nil 298 | } 299 | -------------------------------------------------------------------------------- /app/gosns/gosns_test.go: -------------------------------------------------------------------------------- 1 | package gosns 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/p4tin/goaws/app" 13 | "github.com/p4tin/goaws/app/common" 14 | ) 15 | 16 | func TestListTopicshandler_POST_NoTopics(t *testing.T) { 17 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 18 | // pass 'nil' as the third parameter. 19 | req, err := http.NewRequest("POST", "/", nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 25 | rr := httptest.NewRecorder() 26 | handler := http.HandlerFunc(ListTopics) 27 | 28 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 29 | // directly and pass in our Request and ResponseRecorder. 30 | handler.ServeHTTP(rr, req) 31 | 32 | // Check the status code is what we expect. 33 | if status := rr.Code; status != http.StatusOK { 34 | t.Errorf("handler returned wrong status code: got %v want %v", 35 | status, http.StatusOK) 36 | } 37 | 38 | // Check the response body is what we expect. 39 | expected := "" 40 | if !strings.Contains(rr.Body.String(), expected) { 41 | t.Errorf("handler returned unexpected body: got %v want %v", 42 | rr.Body.String(), expected) 43 | } 44 | } 45 | 46 | func TestCreateTopicshandler_POST_CreateTopics(t *testing.T) { 47 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 48 | // pass 'nil' as the third parameter. 49 | req, err := http.NewRequest("POST", "/", nil) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | form := url.Values{} 55 | form.Add("Action", "CreateTopic") 56 | form.Add("Name", "UnitTestTopic1") 57 | req.PostForm = form 58 | 59 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 60 | rr := httptest.NewRecorder() 61 | handler := http.HandlerFunc(CreateTopic) 62 | 63 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 64 | // directly and pass in our Request and ResponseRecorder. 65 | handler.ServeHTTP(rr, req) 66 | 67 | // Check the status code is what we expect. 68 | if status := rr.Code; status != http.StatusOK { 69 | t.Errorf("handler returned wrong status code: got %v want %v", 70 | status, http.StatusOK) 71 | } 72 | 73 | // Check the response body is what we expect. 74 | expected := "UnitTestTopic1" 75 | if !strings.Contains(rr.Body.String(), expected) { 76 | t.Errorf("handler returned unexpected body: got %v want %v", 77 | rr.Body.String(), expected) 78 | } 79 | } 80 | 81 | func TestPublishhandler_POST_SendMessage(t *testing.T) { 82 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 83 | // pass 'nil' as the third parameter. 84 | req, err := http.NewRequest("POST", "/", nil) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | form := url.Values{} 90 | form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") 91 | form.Add("Message", "TestMessage1") 92 | req.PostForm = form 93 | 94 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 95 | rr := httptest.NewRecorder() 96 | handler := http.HandlerFunc(Publish) 97 | 98 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 99 | // directly and pass in our Request and ResponseRecorder. 100 | handler.ServeHTTP(rr, req) 101 | 102 | // Check the status code is what we expect. 103 | if status := rr.Code; status != http.StatusOK { 104 | t.Errorf("handler returned wrong status code: got %v want %v", 105 | status, http.StatusOK) 106 | } 107 | 108 | // Check the response body is what we expect. 109 | expected := "" 110 | if !strings.Contains(rr.Body.String(), expected) { 111 | t.Errorf("handler returned unexpected body: got %v want %v", 112 | rr.Body.String(), expected) 113 | } 114 | } 115 | 116 | func TestPublishHandler_POST_FilterPolicyRejectsTheMessage(t *testing.T) { 117 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 118 | // pass 'nil' as the third parameter. 119 | req, err := http.NewRequest("POST", "/", nil) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | // We set up queue so later we can check if anything was posted there 125 | queueName := "testingQueue" 126 | queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/queue/" + queueName 127 | queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":000000000000:" + queueName 128 | app.SyncQueues.Queues[queueName] = &app.Queue{ 129 | Name: queueName, 130 | TimeoutSecs: 30, 131 | Arn: queueArn, 132 | URL: queueUrl, 133 | IsFIFO: app.HasFIFOQueueName(queueName), 134 | } 135 | 136 | // We set up a topic with the corresponding Subscription including FilterPolicy 137 | topicName := "testingTopic" 138 | topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName 139 | subArn, _ := common.NewUUID() 140 | subArn = topicArn + ":" + subArn 141 | app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ 142 | { 143 | EndPoint: app.SyncQueues.Queues[queueName].Arn, 144 | Protocol: "sqs", 145 | SubscriptionArn: subArn, 146 | FilterPolicy: &app.FilterPolicy{ 147 | "foo": {"bar"}, // set up FilterPolicy for attribute `foo` to be equal `bar` 148 | }, 149 | }, 150 | }} 151 | 152 | form := url.Values{} 153 | form.Add("TopicArn", topicArn) 154 | form.Add("Message", "TestMessage1") 155 | form.Add("MessageAttributes.entry.1.Name", "foo") // special format of parameter for MessageAttribute 156 | form.Add("MessageAttributes.entry.1.Value.StringValue", "baz") // we actually sent attribute `foo` to be equal `baz` 157 | req.PostForm = form 158 | 159 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 160 | rr := httptest.NewRecorder() 161 | handler := http.HandlerFunc(Publish) 162 | 163 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 164 | // directly and pass in our Request and ResponseRecorder. 165 | handler.ServeHTTP(rr, req) 166 | 167 | // Check the status code is what we expect. 168 | if status := rr.Code; status != http.StatusOK { 169 | t.Errorf("handler returned wrong status code: got %v want %v", 170 | status, http.StatusOK) 171 | } 172 | 173 | // Check the response body is what we expect. 174 | expected := "" 175 | if !strings.Contains(rr.Body.String(), expected) { 176 | t.Errorf("handler returned unexpected body: got %v want %v", 177 | rr.Body.String(), expected) 178 | } 179 | 180 | // check of the queue is empty 181 | if len(app.SyncQueues.Queues[queueName].Messages) != 0 { 182 | t.Errorf("queue contains unexpected messages: got %v want %v", 183 | len(app.SyncQueues.Queues[queueName].Messages), 0) 184 | } 185 | } 186 | 187 | func TestPublishHandler_POST_FilterPolicyPassesTheMessage(t *testing.T) { 188 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 189 | // pass 'nil' as the third parameter. 190 | req, err := http.NewRequest("POST", "/", nil) 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | // We set up queue so later we can check if anything was posted there 196 | queueName := "testingQueue" 197 | queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/queue/" + queueName 198 | queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":000000000000:" + queueName 199 | app.SyncQueues.Queues[queueName] = &app.Queue{ 200 | Name: queueName, 201 | TimeoutSecs: 30, 202 | Arn: queueArn, 203 | URL: queueUrl, 204 | IsFIFO: app.HasFIFOQueueName(queueName), 205 | } 206 | 207 | // We set up a topic with the corresponding Subscription including FilterPolicy 208 | topicName := "testingTopic" 209 | topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName 210 | subArn, _ := common.NewUUID() 211 | subArn = topicArn + ":" + subArn 212 | app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ 213 | { 214 | EndPoint: app.SyncQueues.Queues[queueName].Arn, 215 | Protocol: "sqs", 216 | SubscriptionArn: subArn, 217 | FilterPolicy: &app.FilterPolicy{ 218 | "foo": {"bar"}, // set up FilterPolicy for attribute `foo` to be equal `bar` 219 | }, 220 | }, 221 | }} 222 | 223 | form := url.Values{} 224 | form.Add("TopicArn", topicArn) 225 | form.Add("Message", "TestMessage1") 226 | form.Add("MessageAttributes.entry.1.Name", "foo") // special format of parameter for MessageAttribute 227 | form.Add("MessageAttributes.entry.1.Value.DataType", "String") // Datatype must be specified for proper parsing by aws 228 | form.Add("MessageAttributes.entry.1.Value.StringValue", "bar") // we actually sent attribute `foo` to be equal `baz` 229 | req.PostForm = form 230 | 231 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 232 | rr := httptest.NewRecorder() 233 | handler := http.HandlerFunc(Publish) 234 | 235 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 236 | // directly and pass in our Request and ResponseRecorder. 237 | handler.ServeHTTP(rr, req) 238 | 239 | // Check the status code is what we expect. 240 | if status := rr.Code; status != http.StatusOK { 241 | t.Errorf("handler returned wrong status code: got %v want %v", 242 | status, http.StatusOK) 243 | } 244 | 245 | // Check the response body is what we expect. 246 | expected := "" 247 | if !strings.Contains(rr.Body.String(), expected) { 248 | t.Errorf("handler returned unexpected body: got %v want %v", 249 | rr.Body.String(), expected) 250 | } 251 | 252 | // check of the queue is empty 253 | if len(app.SyncQueues.Queues[queueName].Messages) != 1 { 254 | t.Errorf("queue contains unexpected messages: got %v want %v", 255 | len(app.SyncQueues.Queues[queueName].Messages), 1) 256 | } 257 | } 258 | 259 | func TestSubscribehandler_POST_Success(t *testing.T) { 260 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 261 | // pass 'nil' as the third parameter. 262 | req, err := http.NewRequest("POST", "/", nil) 263 | if err != nil { 264 | t.Fatal(err) 265 | } 266 | 267 | form := url.Values{} 268 | form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") 269 | form.Add("Protocol", "sqs") 270 | form.Add("Endpoint", "http://localhost:4100/queue/noqueue1") 271 | req.PostForm = form 272 | 273 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 274 | rr := httptest.NewRecorder() 275 | handler := http.HandlerFunc(Subscribe) 276 | 277 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 278 | // directly and pass in our Request and ResponseRecorder. 279 | handler.ServeHTTP(rr, req) 280 | 281 | // Check the status code is what we expect. 282 | if status := rr.Code; status != http.StatusOK { 283 | t.Errorf("handler returned wrong status code: got %v want %v", 284 | status, http.StatusOK) 285 | } 286 | 287 | // Check the response body is what we expect. 288 | expected := "" 289 | if !strings.Contains(rr.Body.String(), expected) { 290 | t.Errorf("handler returned unexpected body: got %v want %v", 291 | rr.Body.String(), expected) 292 | } 293 | } 294 | 295 | func TestSubscribehandler_HTTP_POST_Success(t *testing.T) { 296 | done := make(chan bool) 297 | 298 | r := mux.NewRouter() 299 | r.HandleFunc("/sns_post", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 300 | w.WriteHeader(200) 301 | close(done) 302 | 303 | })) 304 | 305 | ts := httptest.NewServer(r) 306 | defer ts.Close() 307 | 308 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 309 | // pass 'nil' as the third parameter. 310 | req, err := http.NewRequest("POST", "/", nil) 311 | if err != nil { 312 | t.Fatal(err) 313 | } 314 | 315 | form := url.Values{} 316 | form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") 317 | form.Add("Protocol", "http") 318 | form.Add("Endpoint", ts.URL+"/sns_post") 319 | req.PostForm = form 320 | 321 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 322 | rr := httptest.NewRecorder() 323 | 324 | handler := http.HandlerFunc(Subscribe) 325 | 326 | // Create ResponseRecorder for http side 327 | 328 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 329 | // directly and pass in our Request and ResponseRecorder. 330 | handler.ServeHTTP(rr, req) 331 | 332 | // Check the status code is what we expect. 333 | if status := rr.Code; status != http.StatusOK { 334 | t.Errorf("handler returned wrong status code: got %v want %v", 335 | status, http.StatusOK) 336 | } 337 | 338 | // Check the response body is what we expect. 339 | expected := "" 340 | if !strings.Contains(rr.Body.String(), expected) { 341 | t.Errorf("handler returned unexpected body: got %v want %v", 342 | rr.Body.String(), expected) 343 | } 344 | 345 | select { 346 | case <-done: 347 | case <-time.After(2 * time.Second): 348 | t.Fatal("http sns handler must be called") 349 | } 350 | } 351 | 352 | func TestPublish_No_Queue_Error_handler_POST_Success(t *testing.T) { 353 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 354 | // pass 'nil' as the third parameter. 355 | req, err := http.NewRequest("POST", "/", nil) 356 | if err != nil { 357 | t.Fatal(err) 358 | } 359 | 360 | form := url.Values{} 361 | form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") 362 | form.Add("Message", "TestMessage1") 363 | req.PostForm = form 364 | 365 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 366 | rr := httptest.NewRecorder() 367 | handler := http.HandlerFunc(Publish) 368 | 369 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 370 | // directly and pass in our Request and ResponseRecorder. 371 | handler.ServeHTTP(rr, req) 372 | 373 | // Check the status code is what we expect. 374 | if status := rr.Code; status != http.StatusOK { 375 | t.Errorf("handler returned wrong status code: got %v want %v", 376 | status, http.StatusOK) 377 | } 378 | 379 | // Check the response body is what we expect. 380 | expected := "" 381 | if !strings.Contains(rr.Body.String(), expected) { 382 | t.Errorf("handler returned unexpected body: got %v want %v", 383 | rr.Body.String(), expected) 384 | } 385 | } 386 | 387 | func TestDeleteTopichandler_POST_Success(t *testing.T) { 388 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 389 | // pass 'nil' as the third parameter. 390 | req, err := http.NewRequest("POST", "/", nil) 391 | if err != nil { 392 | t.Fatal(err) 393 | } 394 | 395 | form := url.Values{} 396 | form.Add("TopicArn", "arn:aws:sns:local:000000000000:UnitTestTopic1") 397 | form.Add("Message", "TestMessage1") 398 | req.PostForm = form 399 | 400 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 401 | rr := httptest.NewRecorder() 402 | handler := http.HandlerFunc(DeleteTopic) 403 | 404 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 405 | // directly and pass in our Request and ResponseRecorder. 406 | handler.ServeHTTP(rr, req) 407 | 408 | // Check the status code is what we expect. 409 | if status := rr.Code; status != http.StatusOK { 410 | t.Errorf("handler returned wrong status code: got %v want %v", 411 | status, http.StatusOK) 412 | } 413 | 414 | // Check the response body is what we expect. 415 | expected := "" 416 | if !strings.Contains(rr.Body.String(), expected) { 417 | t.Errorf("handler returned unexpected body: got %v want %v", 418 | rr.Body.String(), expected) 419 | } 420 | // Check the response body is what we expect. 421 | expected = "" 422 | if !strings.Contains(rr.Body.String(), expected) { 423 | t.Errorf("handler returned unexpected body: got %v want %v", 424 | rr.Body.String(), expected) 425 | } 426 | } 427 | 428 | func TestGetSubscriptionAttributesHandler_POST_Success(t *testing.T) { 429 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 430 | // pass 'nil' as the third parameter. 431 | req, err := http.NewRequest("POST", "/", nil) 432 | if err != nil { 433 | t.Fatal(err) 434 | } 435 | 436 | topicName := "testing" 437 | topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName 438 | subArn, _ := common.NewUUID() 439 | subArn = topicArn + ":" + subArn 440 | app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ 441 | { 442 | SubscriptionArn: subArn, 443 | FilterPolicy: &app.FilterPolicy{ 444 | "foo": {"bar"}, 445 | }, 446 | }, 447 | }} 448 | 449 | form := url.Values{} 450 | form.Add("SubscriptionArn", subArn) 451 | req.PostForm = form 452 | 453 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 454 | rr := httptest.NewRecorder() 455 | handler := http.HandlerFunc(GetSubscriptionAttributes) 456 | 457 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 458 | // directly and pass in our Request and ResponseRecorder. 459 | handler.ServeHTTP(rr, req) 460 | 461 | // Check the status code is what we expect. 462 | if status := rr.Code; status != http.StatusOK { 463 | t.Errorf("handler returned wrong status code: got %v want %v", 464 | status, http.StatusOK) 465 | } 466 | 467 | // Check the response body is what we expect. 468 | expected := "" 469 | if !strings.Contains(rr.Body.String(), expected) { 470 | t.Errorf("handler returned unexpected body: got %v want %v", 471 | rr.Body.String(), expected) 472 | } 473 | // Check the response body is what we expect. 474 | expected = "{"foo":["bar"]}" 475 | if !strings.Contains(rr.Body.String(), expected) { 476 | t.Errorf("handler returned unexpected body: got %v want %v", 477 | rr.Body.String(), expected) 478 | } 479 | } 480 | 481 | func TestSetSubscriptionAttributesHandler_FilterPolicy_POST_Success(t *testing.T) { 482 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 483 | // pass 'nil' as the third parameter. 484 | req, err := http.NewRequest("POST", "/", nil) 485 | if err != nil { 486 | t.Fatal(err) 487 | } 488 | 489 | topicName := "testing" 490 | topicArn := "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName 491 | subArn, _ := common.NewUUID() 492 | subArn = topicArn + ":" + subArn 493 | app.SyncTopics.Topics[topicName] = &app.Topic{Name: topicName, Arn: topicArn, Subscriptions: []*app.Subscription{ 494 | { 495 | SubscriptionArn: subArn, 496 | }, 497 | }} 498 | 499 | form := url.Values{} 500 | form.Add("SubscriptionArn", subArn) 501 | form.Add("AttributeName", "FilterPolicy") 502 | form.Add("AttributeValue", "{\"foo\": [\"bar\"]}") 503 | req.PostForm = form 504 | 505 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 506 | rr := httptest.NewRecorder() 507 | handler := http.HandlerFunc(SetSubscriptionAttributes) 508 | 509 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 510 | // directly and pass in our Request and ResponseRecorder. 511 | handler.ServeHTTP(rr, req) 512 | 513 | // Check the status code is what we expect. 514 | if status := rr.Code; status != http.StatusOK { 515 | t.Errorf("handler returned wrong status code: got %v want %v", 516 | status, http.StatusOK) 517 | } 518 | 519 | // Check the response body is what we expect. 520 | expected := "" 521 | if !strings.Contains(rr.Body.String(), expected) { 522 | t.Errorf("handler returned unexpected body: got %v want %v", 523 | rr.Body.String(), expected) 524 | } 525 | 526 | actualFilterPolicy := app.SyncTopics.Topics[topicName].Subscriptions[0].FilterPolicy 527 | if (*actualFilterPolicy)["foo"][0] != "bar" { 528 | t.Errorf("filter policy has not need applied") 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /app/gosns/gosns.go: -------------------------------------------------------------------------------- 1 | package gosns 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "bytes" 13 | "crypto" 14 | "crypto/rand" 15 | "crypto/rsa" 16 | "crypto/sha1" 17 | "crypto/x509" 18 | "crypto/x509/pkix" 19 | "encoding/base64" 20 | "encoding/pem" 21 | "io/ioutil" 22 | "math/big" 23 | 24 | "github.com/p4tin/goaws/app" 25 | "github.com/p4tin/goaws/app/common" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | type pendingConfirm struct { 30 | subArn string 31 | token string 32 | } 33 | 34 | var PemKEY []byte 35 | var PrivateKEY *rsa.PrivateKey 36 | var TOPIC_DATA map[string]*pendingConfirm 37 | 38 | func init() { 39 | app.SyncTopics.Topics = make(map[string]*app.Topic) 40 | TOPIC_DATA = make(map[string]*pendingConfirm) 41 | 42 | app.SnsErrors = make(map[string]app.SnsErrorType) 43 | err1 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentTopic", Message: "The specified topic does not exist for this wsdl version."} 44 | app.SnsErrors["TopicNotFound"] = err1 45 | err2 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleNotificationService.NonExistentSubscription", Message: "The specified subscription does not exist for this wsdl version."} 46 | app.SnsErrors["SubscriptionNotFound"] = err2 47 | err3 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleNotificationService.TopicAlreadyExists", Message: "The specified topic already exists."} 48 | app.SnsErrors["TopicExists"] = err3 49 | err4 := app.SnsErrorType{HttpError: http.StatusBadRequest, Type: "InvalidParameter", Code: "AWS.SimpleNotificationService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."} 50 | app.SnsErrors["ValidationError"] = err4 51 | PrivateKEY, PemKEY, _ = createPemFile() 52 | } 53 | 54 | func createPemFile() (privkey *rsa.PrivateKey, pemkey []byte, err error) { 55 | template := &x509.Certificate{ 56 | IsCA: true, 57 | BasicConstraintsValid: true, 58 | SubjectKeyId: []byte{11, 22, 33}, 59 | SerialNumber: big.NewInt(1111), 60 | Subject: pkix.Name{ 61 | Country: []string{"USA"}, 62 | Organization: []string{"Amazon"}, 63 | }, 64 | NotBefore: time.Now(), 65 | NotAfter: time.Now().Add(time.Duration(5) * time.Second), 66 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 67 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 68 | } 69 | 70 | // generate private key 71 | privkey, err = rsa.GenerateKey(rand.Reader, 2048) 72 | if err != nil { 73 | return 74 | } 75 | 76 | // create a self-signed certificate 77 | parent := template 78 | cert, err := x509.CreateCertificate(rand.Reader, template, parent, &privkey.PublicKey, privkey) 79 | if err != nil { 80 | return 81 | } 82 | 83 | pemkey = pem.EncodeToMemory( 84 | &pem.Block{ 85 | Type: "CERTIFICATE", 86 | Bytes: cert, 87 | }, 88 | ) 89 | return 90 | } 91 | 92 | func ListTopics(w http.ResponseWriter, req *http.Request) { 93 | content := req.FormValue("ContentType") 94 | 95 | respStruct := app.ListTopicsResponse{} 96 | respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" 97 | uuid, _ := common.NewUUID() 98 | respStruct.Metadata = app.ResponseMetadata{RequestId: uuid} 99 | 100 | respStruct.Result.Topics.Member = make([]app.TopicArnResult, 0, 0) 101 | log.Println("Listing Topics") 102 | for _, topic := range app.SyncTopics.Topics { 103 | ta := app.TopicArnResult{TopicArn: topic.Arn} 104 | respStruct.Result.Topics.Member = append(respStruct.Result.Topics.Member, ta) 105 | } 106 | 107 | SendResponseBack(w, req, respStruct, content) 108 | } 109 | 110 | func CreateTopic(w http.ResponseWriter, req *http.Request) { 111 | content := req.FormValue("ContentType") 112 | topicName := req.FormValue("Name") 113 | topicArn := "" 114 | if _, ok := app.SyncTopics.Topics[topicName]; ok { 115 | topicArn = app.SyncTopics.Topics[topicName].Arn 116 | } else { 117 | topicArn = "arn:aws:sns:" + app.CurrentEnvironment.Region + ":000000000000:" + topicName 118 | 119 | log.Println("Creating Topic:", topicName) 120 | topic := &app.Topic{Name: topicName, Arn: topicArn} 121 | topic.Subscriptions = make([]*app.Subscription, 0, 0) 122 | app.SyncTopics.Lock() 123 | app.SyncTopics.Topics[topicName] = topic 124 | app.SyncTopics.Unlock() 125 | } 126 | uuid, _ := common.NewUUID() 127 | respStruct := app.CreateTopicResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.CreateTopicResult{TopicArn: topicArn}, app.ResponseMetadata{RequestId: uuid}} 128 | SendResponseBack(w, req, respStruct, content) 129 | } 130 | 131 | // aws --endpoint-url http://localhost:47194 sns subscribe --topic-arn arn:aws:sns:us-west-2:0123456789012:my-topic --protocol email --notification-endpoint my-email@example.com 132 | func Subscribe(w http.ResponseWriter, req *http.Request) { 133 | content := req.FormValue("ContentType") 134 | topicArn := req.FormValue("TopicArn") 135 | protocol := req.FormValue("Protocol") 136 | endpoint := req.FormValue("Endpoint") 137 | 138 | uriSegments := strings.Split(topicArn, ":") 139 | topicName := uriSegments[len(uriSegments)-1] 140 | log.WithFields(log.Fields{ 141 | "content": content, 142 | "topicArn": topicArn, 143 | "topicName": topicName, 144 | "protocol": protocol, 145 | "endpoint": endpoint, 146 | }).Info("Creating Subscription") 147 | 148 | subscription := &app.Subscription{EndPoint: endpoint, Protocol: protocol, TopicArn: topicArn, Raw: false} 149 | subArn, _ := common.NewUUID() 150 | subArn = topicArn + ":" + subArn 151 | subscription.SubscriptionArn = subArn 152 | 153 | if app.SyncTopics.Topics[topicName] != nil { 154 | app.SyncTopics.Lock() 155 | isDuplicate := false 156 | // Duplicate check 157 | for _, subscription := range app.SyncTopics.Topics[topicName].Subscriptions { 158 | if subscription.EndPoint == endpoint && subscription.TopicArn == topicArn { 159 | isDuplicate = true 160 | subArn = subscription.SubscriptionArn 161 | } 162 | } 163 | if !isDuplicate { 164 | app.SyncTopics.Topics[topicName].Subscriptions = append(app.SyncTopics.Topics[topicName].Subscriptions, subscription) 165 | log.WithFields(log.Fields{ 166 | "topic": topicName, 167 | "endpoint": endpoint, 168 | "topicArn": topicArn, 169 | }).Debug("Created subscription") 170 | } 171 | app.SyncTopics.Unlock() 172 | 173 | //Create the response 174 | uuid, _ := common.NewUUID() 175 | if app.Protocol(subscription.Protocol) == app.ProtocolHTTP || app.Protocol(subscription.Protocol) == app.ProtocolHTTPS { 176 | id, _ := common.NewUUID() 177 | token, _ := common.NewUUID() 178 | 179 | TOPIC_DATA[topicArn] = &pendingConfirm{ 180 | subArn: subArn, 181 | token: token, 182 | } 183 | 184 | respStruct := app.SubscribeResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.SubscribeResult{SubscriptionArn: subArn}, app.ResponseMetadata{RequestId: uuid}} 185 | SendResponseBack(w, req, respStruct, content) 186 | time.Sleep(time.Second) 187 | 188 | snsMSG := &app.SNSMessage{ 189 | Type: "SubscriptionConfirmation", 190 | MessageId: id, 191 | Token: token, 192 | TopicArn: topicArn, 193 | Message: "You have chosen to subscribe to the topic " + topicArn + ".\nTo confirm the subscription, visit the SubscribeURL included in this message.", 194 | SigningCertURL: "http://sqs." + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/SimpleNotificationService/" + uuid + ".pem", 195 | SignatureVersion: "1", 196 | SubscribeURL: "http://sqs." + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/?Action=ConfirmSubscription&TopicArn=" + topicArn + "&Token=" + token, 197 | Timestamp: time.Now().UTC().Format(time.RFC3339), 198 | } 199 | signature, err := signMessage(PrivateKEY, snsMSG) 200 | if err != nil { 201 | log.Error("Error signing message") 202 | } else { 203 | snsMSG.Signature = signature 204 | } 205 | err = callEndpoint(subscription.EndPoint, uuid, *snsMSG) 206 | if err != nil { 207 | log.Error("Error posting to url ", err) 208 | } 209 | } else { 210 | respStruct := app.SubscribeResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.SubscribeResult{SubscriptionArn: subArn}, app.ResponseMetadata{RequestId: uuid}} 211 | 212 | SendResponseBack(w, req, respStruct, content) 213 | } 214 | 215 | } else { 216 | createErrorResponse(w, req, "TopicNotFound") 217 | } 218 | } 219 | 220 | func signMessage(privkey *rsa.PrivateKey, snsMsg *app.SNSMessage) (string, error) { 221 | fs, err := formatSignature(snsMsg) 222 | if err != nil { 223 | return "", nil 224 | } 225 | 226 | h := sha1.Sum([]byte(fs)) 227 | signature_b, err := rsa.SignPKCS1v15(rand.Reader, privkey, crypto.SHA1, h[:]) 228 | 229 | return base64.StdEncoding.EncodeToString(signature_b), err 230 | } 231 | 232 | func formatSignature(msg *app.SNSMessage) (formated string, err error) { 233 | if msg.Type == "Notification" && msg.Subject != "" { 234 | formated = fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 235 | "Message", msg.Message, 236 | "MessageId", msg.MessageId, 237 | "Subject", msg.Subject, 238 | "Timestamp", msg.Timestamp, 239 | "TopicArn", msg.TopicArn, 240 | "Type", msg.Type, 241 | ) 242 | } else if msg.Type == "Notification" && msg.Subject == "" { 243 | formated = fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 244 | "Message", msg.Message, 245 | "MessageId", msg.MessageId, 246 | "Timestamp", msg.Timestamp, 247 | "TopicArn", msg.TopicArn, 248 | "Type", msg.Type, 249 | ) 250 | } else if msg.Type == "SubscriptionConfirmation" || msg.Type == "UnsubscribeConfirmation" { 251 | formated = fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 252 | "Message", msg.Message, 253 | "MessageId", msg.MessageId, 254 | "SubscribeURL", msg.SubscribeURL, 255 | "Timestamp", msg.Timestamp, 256 | "Token", msg.Token, 257 | "TopicArn", msg.TopicArn, 258 | "Type", msg.Type, 259 | ) 260 | } else { 261 | return formated, errors.New("Unable to determine SNSMessage type") 262 | } 263 | 264 | return 265 | } 266 | 267 | func ConfirmSubscription(w http.ResponseWriter, req *http.Request) { 268 | topicArn := req.Form.Get("TopicArn") 269 | confirmToken := req.Form.Get("Token") 270 | pendingConfirm := TOPIC_DATA[topicArn] 271 | if pendingConfirm.token == confirmToken { 272 | uuid, _ := common.NewUUID() 273 | respStruct := app.ConfirmSubscriptionResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.SubscribeResult{SubscriptionArn: pendingConfirm.subArn}, app.ResponseMetadata{RequestId: uuid}} 274 | 275 | SendResponseBack(w, req, respStruct, "application/xml") 276 | } else { 277 | createErrorResponse(w, req, "SubArnNotFound") 278 | } 279 | 280 | } 281 | 282 | func ListSubscriptions(w http.ResponseWriter, req *http.Request) { 283 | content := req.FormValue("ContentType") 284 | 285 | uuid, _ := common.NewUUID() 286 | respStruct := app.ListSubscriptionsResponse{} 287 | respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" 288 | respStruct.Metadata.RequestId = uuid 289 | respStruct.Result.Subscriptions.Member = make([]app.TopicMemberResult, 0, 0) 290 | 291 | for _, topic := range app.SyncTopics.Topics { 292 | for _, sub := range topic.Subscriptions { 293 | tar := app.TopicMemberResult{TopicArn: topic.Arn, Protocol: sub.Protocol, 294 | SubscriptionArn: sub.SubscriptionArn, Endpoint: sub.EndPoint} 295 | respStruct.Result.Subscriptions.Member = append(respStruct.Result.Subscriptions.Member, tar) 296 | } 297 | } 298 | 299 | SendResponseBack(w, req, respStruct, content) 300 | } 301 | 302 | func ListSubscriptionsByTopic(w http.ResponseWriter, req *http.Request) { 303 | content := req.FormValue("ContentType") 304 | topicArn := req.FormValue("TopicArn") 305 | 306 | uriSegments := strings.Split(topicArn, ":") 307 | topicName := uriSegments[len(uriSegments)-1] 308 | 309 | if topic, ok := app.SyncTopics.Topics[topicName]; ok { 310 | uuid, _ := common.NewUUID() 311 | respStruct := app.ListSubscriptionsByTopicResponse{} 312 | respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" 313 | respStruct.Metadata.RequestId = uuid 314 | respStruct.Result.Subscriptions.Member = make([]app.TopicMemberResult, 0, 0) 315 | 316 | for _, sub := range topic.Subscriptions { 317 | tar := app.TopicMemberResult{TopicArn: topic.Arn, Protocol: sub.Protocol, 318 | SubscriptionArn: sub.SubscriptionArn, Endpoint: sub.EndPoint} 319 | respStruct.Result.Subscriptions.Member = append(respStruct.Result.Subscriptions.Member, tar) 320 | } 321 | SendResponseBack(w, req, respStruct, content) 322 | } else { 323 | createErrorResponse(w, req, "TopicNotFound") 324 | } 325 | } 326 | 327 | func SetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { 328 | content := req.FormValue("ContentType") 329 | subsArn := req.FormValue("SubscriptionArn") 330 | Attribute := req.FormValue("AttributeName") 331 | Value := req.FormValue("AttributeValue") 332 | 333 | for _, topic := range app.SyncTopics.Topics { 334 | for _, sub := range topic.Subscriptions { 335 | if sub.SubscriptionArn == subsArn { 336 | if Attribute == "RawMessageDelivery" { 337 | app.SyncTopics.Lock() 338 | if Value == "true" { 339 | sub.Raw = true 340 | } else { 341 | sub.Raw = false 342 | } 343 | app.SyncTopics.Unlock() 344 | //Good Response == return 345 | uuid, _ := common.NewUUID() 346 | respStruct := app.SetSubscriptionAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} 347 | SendResponseBack(w, req, respStruct, content) 348 | return 349 | } 350 | 351 | if Attribute == "FilterPolicy" { 352 | filterPolicy := &app.FilterPolicy{} 353 | err := json.Unmarshal([]byte(Value), filterPolicy) 354 | if err != nil { 355 | createErrorResponse(w, req, "ValidationError") 356 | return 357 | } 358 | 359 | app.SyncTopics.Lock() 360 | sub.FilterPolicy = filterPolicy 361 | app.SyncTopics.Unlock() 362 | 363 | //Good Response == return 364 | uuid, _ := common.NewUUID() 365 | respStruct := app.SetSubscriptionAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} 366 | SendResponseBack(w, req, respStruct, content) 367 | return 368 | } 369 | 370 | } 371 | } 372 | } 373 | createErrorResponse(w, req, "SubscriptionNotFound") 374 | } 375 | 376 | func GetSubscriptionAttributes(w http.ResponseWriter, req *http.Request) { 377 | 378 | content := req.FormValue("ContentType") 379 | subsArn := req.FormValue("SubscriptionArn") 380 | 381 | for _, topic := range app.SyncTopics.Topics { 382 | for _, sub := range topic.Subscriptions { 383 | if sub.SubscriptionArn == subsArn { 384 | 385 | entries := make([]app.SubscriptionAttributeEntry, 0, 0) 386 | entry := app.SubscriptionAttributeEntry{Key: "SubscriptionArn", Value: sub.SubscriptionArn} 387 | entries = append(entries, entry) 388 | entry = app.SubscriptionAttributeEntry{Key: "Protocol", Value: sub.Protocol} 389 | entries = append(entries, entry) 390 | entry = app.SubscriptionAttributeEntry{Key: "Endpoint", Value: sub.EndPoint} 391 | entries = append(entries, entry) 392 | 393 | if sub.FilterPolicy != nil { 394 | filterPolicyBytes, _ := json.Marshal(sub.FilterPolicy) 395 | entry = app.SubscriptionAttributeEntry{Key: "FilterPolicy", Value: string(filterPolicyBytes)} 396 | entries = append(entries, entry) 397 | } 398 | 399 | result := app.GetSubscriptionAttributesResult{SubscriptionAttributes: app.SubscriptionAttributes{Entries: entries}} 400 | uuid, _ := common.NewUUID() 401 | respStruct := app.GetSubscriptionAttributesResponse{"http://sns.amazonaws.com/doc/2010-03-31", result, app.ResponseMetadata{RequestId: uuid}} 402 | 403 | SendResponseBack(w, req, respStruct, content) 404 | 405 | return 406 | } 407 | } 408 | } 409 | createErrorResponse(w, req, "SubscriptionNotFound") 410 | } 411 | 412 | func Unsubscribe(w http.ResponseWriter, req *http.Request) { 413 | content := req.FormValue("ContentType") 414 | subArn := req.FormValue("SubscriptionArn") 415 | 416 | log.Println("Unsubscribe:", subArn) 417 | for _, topic := range app.SyncTopics.Topics { 418 | for i, sub := range topic.Subscriptions { 419 | if sub.SubscriptionArn == subArn { 420 | app.SyncTopics.Lock() 421 | 422 | copy(topic.Subscriptions[i:], topic.Subscriptions[i+1:]) 423 | topic.Subscriptions[len(topic.Subscriptions)-1] = nil 424 | topic.Subscriptions = topic.Subscriptions[:len(topic.Subscriptions)-1] 425 | 426 | app.SyncTopics.Unlock() 427 | 428 | uuid, _ := common.NewUUID() 429 | respStruct := app.UnsubscribeResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} 430 | SendResponseBack(w, req, respStruct, content) 431 | return 432 | } 433 | } 434 | } 435 | createErrorResponse(w, req, "SubscriptionNotFound") 436 | } 437 | 438 | func DeleteTopic(w http.ResponseWriter, req *http.Request) { 439 | content := req.FormValue("ContentType") 440 | topicArn := req.FormValue("TopicArn") 441 | 442 | uriSegments := strings.Split(topicArn, ":") 443 | topicName := uriSegments[len(uriSegments)-1] 444 | 445 | log.Println("Delete Topic - TopicName:", topicName) 446 | 447 | _, ok := app.SyncTopics.Topics[topicName] 448 | if ok { 449 | app.SyncTopics.Lock() 450 | delete(app.SyncTopics.Topics, topicName) 451 | app.SyncTopics.Unlock() 452 | uuid, _ := common.NewUUID() 453 | respStruct := app.DeleteTopicResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: uuid}} 454 | SendResponseBack(w, req, respStruct, content) 455 | } else { 456 | createErrorResponse(w, req, "TopicNotFound") 457 | } 458 | 459 | } 460 | 461 | // aws --endpoint-url http://localhost:47194 sns publish --topic-arn arn:aws:sns:yopa-local:000000000000:test1 --message "This is a test" 462 | func Publish(w http.ResponseWriter, req *http.Request) { 463 | content := req.FormValue("ContentType") 464 | topicArn := req.FormValue("TopicArn") 465 | subject := req.FormValue("Subject") 466 | messageBody := req.FormValue("Message") 467 | messageStructure := req.FormValue("MessageStructure") 468 | messageAttributes := getMessageAttributesFromRequest(req) 469 | 470 | arnSegments := strings.Split(topicArn, ":") 471 | topicName := arnSegments[len(arnSegments)-1] 472 | 473 | _, ok := app.SyncTopics.Topics[topicName] 474 | if ok { 475 | log.WithFields(log.Fields{ 476 | "topic": topicName, 477 | "topicArn": topicArn, 478 | "subject": subject, 479 | }).Debug("Publish to Topic") 480 | for _, subs := range app.SyncTopics.Topics[topicName].Subscriptions { 481 | switch app.Protocol(subs.Protocol) { 482 | case app.ProtocolSQS: 483 | publishSQS(w, req, subs, messageBody, messageAttributes, subject, topicArn, topicName, messageStructure) 484 | case app.ProtocolHTTP: 485 | fallthrough 486 | case app.ProtocolHTTPS: 487 | publishHTTP(subs, messageBody, messageAttributes, subject, topicArn) 488 | } 489 | } 490 | } else { 491 | createErrorResponse(w, req, "TopicNotFound") 492 | return 493 | } 494 | 495 | //Create the response 496 | msgId, _ := common.NewUUID() 497 | uuid, _ := common.NewUUID() 498 | respStruct := app.PublishResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.PublishResult{MessageId: msgId}, app.ResponseMetadata{RequestId: uuid}} 499 | SendResponseBack(w, req, respStruct, content) 500 | } 501 | 502 | func publishSQS(w http.ResponseWriter, req *http.Request, 503 | subs *app.Subscription, messageBody string, messageAttributes map[string]app.MessageAttributeValue, 504 | subject string, topicArn string, topicName string, messageStructure string) { 505 | if subs.FilterPolicy != nil && !subs.FilterPolicy.IsSatisfiedBy(messageAttributes) { 506 | return 507 | } 508 | 509 | endPoint := subs.EndPoint 510 | uriSegments := strings.Split(endPoint, "/") 511 | queueName := uriSegments[len(uriSegments)-1] 512 | arnSegments := strings.Split(queueName, ":") 513 | queueName = arnSegments[len(arnSegments)-1] 514 | 515 | if _, ok := app.SyncQueues.Queues[queueName]; ok { 516 | msg := app.Message{} 517 | 518 | if subs.Raw == false { 519 | m, err := CreateMessageBody(subs, messageBody, subject, messageStructure, messageAttributes) 520 | if err != nil { 521 | createErrorResponse(w, req, err.Error()) 522 | return 523 | } 524 | 525 | msg.MessageBody = m 526 | } else { 527 | msg.MessageAttributes = messageAttributes 528 | msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) 529 | msg.MessageBody = []byte(messageBody) 530 | } 531 | 532 | msg.MD5OfMessageBody = common.GetMD5Hash(messageBody) 533 | msg.Uuid, _ = common.NewUUID() 534 | app.SyncQueues.Lock() 535 | app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) 536 | app.SyncQueues.Unlock() 537 | 538 | log.Infof("%s: Topic: %s(%s), Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), topicName, queueName, msg.MessageBody) 539 | } else { 540 | log.Infof("%s: Queue %s does not exist, message discarded\n", time.Now().Format("2006-01-02 15:04:05"), queueName) 541 | } 542 | } 543 | 544 | func publishHTTP(subs *app.Subscription, messageBody string, messageAttributes map[string]app.MessageAttributeValue, 545 | subject string, topicArn string) { 546 | id, _ := common.NewUUID() 547 | msg := app.SNSMessage{ 548 | Type: "Notification", 549 | MessageId: id, 550 | TopicArn: topicArn, 551 | Subject: subject, 552 | Message: messageBody, 553 | Timestamp: time.Now().UTC().Format(time.RFC3339), 554 | SignatureVersion: "1", 555 | SigningCertURL: "http://sqs." + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/SimpleNotificationService/" + id + ".pem", 556 | UnsubscribeURL: "http://sqs." + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/?Action=Unsubscribe&SubscriptionArn=" + subs.SubscriptionArn, 557 | MessageAttributes: formatAttributes(messageAttributes), 558 | } 559 | 560 | signature, err := signMessage(PrivateKEY, &msg) 561 | if err != nil { 562 | log.Error(err) 563 | } else { 564 | msg.Signature = signature 565 | } 566 | err = callEndpoint(subs.EndPoint, subs.SubscriptionArn, msg) 567 | if err != nil { 568 | log.WithFields(log.Fields{ 569 | "EndPoint": subs.EndPoint, 570 | "ARN": subs.SubscriptionArn, 571 | "error": err.Error(), 572 | }).Error("Error calling endpoint") 573 | } 574 | } 575 | 576 | func formatAttributes(values map[string]app.MessageAttributeValue) map[string]app.MsgAttr { 577 | attr := make(map[string]app.MsgAttr) 578 | for k, v := range values { 579 | attr[k] = app.MsgAttr{ 580 | Type: v.ValueKey, 581 | Value: v.Value, 582 | } 583 | } 584 | return attr 585 | } 586 | 587 | func callEndpoint(endpoint string, subArn string, msg app.SNSMessage) error { 588 | log.WithFields(log.Fields{ 589 | "sns": msg, 590 | "subArn": subArn, 591 | "endpoint": endpoint, 592 | }).Debug("Calling endpoint") 593 | byteData, err := json.Marshal(msg) 594 | if err != nil { 595 | return err 596 | } 597 | req, err := http.NewRequest("POST", endpoint, bytes.NewReader(byteData)) 598 | if err != nil { 599 | return err 600 | } 601 | 602 | //req.Header.Add("Authorization", "Basic YXV0aEhlYWRlcg==") 603 | req.Header.Add("Content-Type", "application/json") 604 | req.Header.Add("x-amz-sns-message-type", msg.Type) 605 | req.Header.Add("x-amz-sns-message-id", msg.MessageId) 606 | req.Header.Add("x-amz-sns-topic-arn", msg.TopicArn) 607 | req.Header.Add("x-amz-sns-subscription-arn", subArn) 608 | res, err := http.DefaultClient.Do(req) 609 | if err != nil { 610 | return err 611 | } 612 | if res == nil { 613 | return errors.New("response is nil") 614 | } 615 | if res.StatusCode%200 != 0 { 616 | log.WithFields(log.Fields{ 617 | "statusCode": res.StatusCode, 618 | "status": res.Status, 619 | "header": res.Header, 620 | "endpoing": endpoint, 621 | }).Error("Not 2xx repsone") 622 | return errors.New("Not 2xx response") 623 | } 624 | 625 | defer res.Body.Close() 626 | body, err := ioutil.ReadAll(res.Body) 627 | if err != nil { 628 | return err 629 | } 630 | 631 | log.WithFields(log.Fields{ 632 | "body": string(body), 633 | "res": res, 634 | }).Debug("Received successful response") 635 | 636 | return nil 637 | } 638 | 639 | func getMessageAttributesFromRequest(req *http.Request) map[string]app.MessageAttributeValue { 640 | attributes := make(map[string]app.MessageAttributeValue) 641 | 642 | for i := 1; true; i++ { 643 | name := req.FormValue(fmt.Sprintf("MessageAttributes.entry.%d.Name", i)) 644 | if name == "" { 645 | break 646 | } 647 | 648 | dataType := req.FormValue(fmt.Sprintf("MessageAttributes.entry.%d.Value.DataType", i)) 649 | if dataType == "" { 650 | log.Warnf("DataType of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) 651 | continue 652 | } 653 | 654 | // StringListValue and BinaryListValue is currently not implemented 655 | for _, valueKey := range [...]string{"StringValue", "BinaryValue"} { 656 | value := req.FormValue(fmt.Sprintf("MessageAttributes.entry.%d.Value.%s", i, valueKey)) 657 | if value != "" { 658 | attributes[name] = app.MessageAttributeValue{name, dataType, value, valueKey} 659 | } 660 | } 661 | 662 | if _, ok := attributes[name]; !ok { 663 | log.Warnf("StringValue or BinaryValue of MessageAttribute %s is missing, MD5 checksum will most probably be wrong!\n", name) 664 | } 665 | } 666 | 667 | return attributes 668 | } 669 | 670 | func CreateMessageBody(subs *app.Subscription, msg string, subject string, messageStructure string, 671 | messageAttributes map[string]app.MessageAttributeValue) ([]byte, error) { 672 | 673 | msgId, _ := common.NewUUID() 674 | 675 | message := app.SNSMessage{ 676 | Type: "Notification", 677 | MessageId: msgId, 678 | TopicArn: subs.TopicArn, 679 | Subject: subject, 680 | Timestamp: time.Now().UTC().Format(time.RFC3339), 681 | SignatureVersion: "1", 682 | SigningCertURL: "http://sqs." + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/SimpleNotificationService/" + msgId + ".pem", 683 | UnsubscribeURL: "http://sqs." + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + "/?Action=Unsubscribe&SubscriptionArn=" + subs.SubscriptionArn, 684 | MessageAttributes: formatAttributes(messageAttributes), 685 | } 686 | 687 | if app.MessageStructure(messageStructure) == app.MessageStructureJSON { 688 | m, err := extractMessageFromJSON(msg, subs.Protocol) 689 | if err != nil { 690 | return nil, err 691 | } 692 | message.Message = m 693 | } else { 694 | message.Message = msg 695 | } 696 | 697 | signature, err := signMessage(PrivateKEY, &message) 698 | if err != nil { 699 | log.Error(err) 700 | } else { 701 | message.Signature = signature 702 | } 703 | 704 | byteMsg, _ := json.Marshal(message) 705 | return byteMsg, nil 706 | } 707 | 708 | func extractMessageFromJSON(msg string, protocol string) (string, error) { 709 | var msgWithProtocols map[string]string 710 | if err := json.Unmarshal([]byte(msg), &msgWithProtocols); err != nil { 711 | return "", err 712 | } 713 | 714 | defaultMsg, ok := msgWithProtocols[string(app.ProtocolDefault)] 715 | if !ok { 716 | return "", errors.New(app.ErrNoDefaultElementInJSON) 717 | } 718 | 719 | if m, ok := msgWithProtocols[protocol]; ok { 720 | return m, nil 721 | } 722 | 723 | return defaultMsg, nil 724 | } 725 | 726 | func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { 727 | er := app.SnsErrors[err] 728 | respStruct := app.ErrorResponse{app.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message, RequestId: "00000000-0000-0000-0000-000000000000"}} 729 | 730 | w.WriteHeader(er.HttpError) 731 | enc := xml.NewEncoder(w) 732 | enc.Indent(" ", " ") 733 | if err := enc.Encode(respStruct); err != nil { 734 | log.Printf("error: %v\n", err) 735 | } 736 | } 737 | 738 | func SendResponseBack(w http.ResponseWriter, req *http.Request, respStruct interface{}, content string) { 739 | if content == "JSON" { 740 | w.Header().Set("Content-Type", "application/json") 741 | enc := json.NewEncoder(w) 742 | if err := enc.Encode(respStruct); err != nil { 743 | log.Printf("error: %v\n", err) 744 | } 745 | } else { 746 | w.Header().Set("Content-Type", "application/xml") 747 | enc := xml.NewEncoder(w) 748 | enc.Indent(" ", " ") 749 | if err := enc.Encode(respStruct); err != nil { 750 | log.Printf("error: %v\n", err) 751 | } 752 | } 753 | } 754 | -------------------------------------------------------------------------------- /app/gosqs/gosqs.go: -------------------------------------------------------------------------------- 1 | package gosqs 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/gorilla/mux" 15 | "github.com/p4tin/goaws/app" 16 | "github.com/p4tin/goaws/app/common" 17 | ) 18 | 19 | func init() { 20 | app.SyncQueues.Queues = make(map[string]*app.Queue) 21 | 22 | app.SqsErrors = make(map[string]app.SqsErrorType) 23 | err1 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "Not Found", Code: "AWS.SimpleQueueService.NonExistentQueue", Message: "The specified queue does not exist for this wsdl version."} 24 | app.SqsErrors["QueueNotFound"] = err1 25 | err2 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "Duplicate", Code: "AWS.SimpleQueueService.QueueExists", Message: "The specified queue already exists."} 26 | app.SqsErrors["QueueExists"] = err2 27 | err3 := app.SqsErrorType{HttpError: http.StatusNotFound, Type: "Not Found", Code: "AWS.SimpleQueueService.QueueExists", Message: "The specified queue does not contain the message specified."} 28 | app.SqsErrors["MessageDoesNotExist"] = err3 29 | err4 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "GeneralError", Code: "AWS.SimpleQueueService.GeneralError", Message: "General Error."} 30 | app.SqsErrors["GeneralError"] = err4 31 | err5 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "TooManyEntriesInBatchRequest", Code: "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", Message: "Maximum number of entries per request are 10."} 32 | app.SqsErrors["TooManyEntriesInBatchRequest"] = err5 33 | err6 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "BatchEntryIdsNotDistinct", Code: "AWS.SimpleQueueService.BatchEntryIdsNotDistinct", Message: "Two or more batch entries in the request have the same Id."} 34 | app.SqsErrors["BatchEntryIdsNotDistinct"] = err6 35 | err7 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "EmptyBatchRequest", Code: "AWS.SimpleQueueService.EmptyBatchRequest", Message: "The batch request doesn't contain any entries."} 36 | app.SqsErrors["EmptyBatchRequest"] = err7 37 | err8 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "ValidationError", Code: "AWS.SimpleQueueService.ValidationError", Message: "The visibility timeout is incorrect"} 38 | app.SqsErrors["InvalidVisibilityTimeout"] = err8 39 | err9 := app.SqsErrorType{HttpError: http.StatusBadRequest, Type: "MessageNotInFlight", Code: "AWS.SimpleQueueService.MessageNotInFlight", Message: "The message referred to isn't in flight."} 40 | app.SqsErrors["MessageNotInFlight"] = err9 41 | app.SqsErrors[ErrInvalidParameterValue.Type] = *ErrInvalidParameterValue 42 | app.SqsErrors[ErrInvalidAttributeValue.Type] = *ErrInvalidAttributeValue 43 | } 44 | 45 | func PeriodicTasks(d time.Duration, quit <-chan struct{}) { 46 | ticker := time.NewTicker(d) 47 | for { 48 | select { 49 | case <-ticker.C: 50 | app.SyncQueues.Lock() 51 | for j := range app.SyncQueues.Queues { 52 | queue := app.SyncQueues.Queues[j] 53 | 54 | log.Debugf("Queue [%s] length [%d]", queue.Name, len(queue.Messages)) 55 | for i := 0; i < len(queue.Messages); i++ { 56 | msg := &queue.Messages[i] 57 | if msg.ReceiptHandle != "" { 58 | if msg.VisibilityTimeout.Before(time.Now()) { 59 | log.Debugf("Making message visible again %s", msg.ReceiptHandle) 60 | queue.UnlockGroup(msg.GroupID) 61 | msg.ReceiptHandle = "" 62 | msg.ReceiptTime = time.Now().UTC() 63 | msg.Retry++ 64 | if queue.MaxReceiveCount > 0 && 65 | queue.DeadLetterQueue != nil && 66 | msg.Retry > queue.MaxReceiveCount { 67 | queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, *msg) 68 | queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...) 69 | i++ 70 | } 71 | } 72 | } 73 | } 74 | } 75 | app.SyncQueues.Unlock() 76 | case <-quit: 77 | ticker.Stop() 78 | return 79 | } 80 | } 81 | } 82 | 83 | func ListQueues(w http.ResponseWriter, req *http.Request) { 84 | w.Header().Set("Content-Type", "application/xml") 85 | respStruct := app.ListQueuesResponse{} 86 | respStruct.Xmlns = "http://queue.amazonaws.com/doc/2012-11-05/" 87 | respStruct.Metadata = app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"} 88 | respStruct.Result.QueueUrl = make([]string, 0) 89 | queueNamePrefix := req.FormValue("QueueNamePrefix") 90 | 91 | log.Println("Listing Queues") 92 | for _, queue := range app.SyncQueues.Queues { 93 | app.SyncQueues.Lock() 94 | if strings.HasPrefix(queue.Name, queueNamePrefix) { 95 | respStruct.Result.QueueUrl = append(respStruct.Result.QueueUrl, queue.URL) 96 | } 97 | app.SyncQueues.Unlock() 98 | } 99 | enc := xml.NewEncoder(w) 100 | enc.Indent(" ", " ") 101 | if err := enc.Encode(respStruct); err != nil { 102 | log.Printf("error: %v\n", err) 103 | } 104 | } 105 | 106 | func CreateQueue(w http.ResponseWriter, req *http.Request) { 107 | w.Header().Set("Content-Type", "application/xml") 108 | queueName := req.FormValue("QueueName") 109 | 110 | queueUrl := "http://" + app.CurrentEnvironment.Host + ":" + app.CurrentEnvironment.Port + 111 | "/" + app.CurrentEnvironment.AccountID + "/" + queueName 112 | if app.CurrentEnvironment.Region != "" { 113 | queueUrl = "http://sqs." + app.CurrentEnvironment.Region + "." + app.CurrentEnvironment.Host + ":" + 114 | app.CurrentEnvironment.Port + "/" + app.CurrentEnvironment.AccountID + "/" + queueName 115 | } 116 | queueArn := "arn:aws:sqs:" + app.CurrentEnvironment.Region + ":" + app.CurrentEnvironment.AccountID + ":" + queueName 117 | 118 | if _, ok := app.SyncQueues.Queues[queueName]; !ok { 119 | log.Println("Creating Queue:", queueName) 120 | queue := &app.Queue{ 121 | Name: queueName, 122 | URL: queueUrl, 123 | Arn: queueArn, 124 | TimeoutSecs: app.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout, 125 | ReceiveWaitTimeSecs: app.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds, 126 | IsFIFO: app.HasFIFOQueueName(queueName), 127 | } 128 | if err := validateAndSetQueueAttributes(queue, req.Form); err != nil { 129 | createErrorResponse(w, req, err.Error()) 130 | return 131 | } 132 | app.SyncQueues.Lock() 133 | app.SyncQueues.Queues[queueName] = queue 134 | app.SyncQueues.Unlock() 135 | } 136 | 137 | respStruct := app.CreateQueueResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.CreateQueueResult{QueueUrl: queueUrl}, app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} 138 | enc := xml.NewEncoder(w) 139 | enc.Indent(" ", " ") 140 | if err := enc.Encode(respStruct); err != nil { 141 | log.Printf("error: %v\n", err) 142 | } 143 | } 144 | 145 | func SendMessage(w http.ResponseWriter, req *http.Request) { 146 | w.Header().Set("Content-Type", "application/xml") 147 | messageBody := req.FormValue("MessageBody") 148 | messageGroupID := req.FormValue("MessageGroupId") 149 | messageAttributes := extractMessageAttributes(req, "") 150 | 151 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 152 | 153 | queueName := "" 154 | if queueUrl == "" { 155 | vars := mux.Vars(req) 156 | queueName = vars["queueName"] 157 | } else { 158 | uriSegments := strings.Split(queueUrl, "/") 159 | queueName = uriSegments[len(uriSegments)-1] 160 | } 161 | 162 | if _, ok := app.SyncQueues.Queues[queueName]; !ok { 163 | // Queue does not exist 164 | createErrorResponse(w, req, "QueueNotFound") 165 | return 166 | } 167 | 168 | log.Println("Putting Message in Queue:", queueName) 169 | msg := app.Message{MessageBody: []byte(messageBody)} 170 | if len(messageAttributes) > 0 { 171 | msg.MessageAttributes = messageAttributes 172 | msg.MD5OfMessageAttributes = common.HashAttributes(messageAttributes) 173 | } 174 | msg.MD5OfMessageBody = common.GetMD5Hash(messageBody) 175 | msg.Uuid, _ = common.NewUUID() 176 | msg.GroupID = messageGroupID 177 | 178 | app.SyncQueues.Lock() 179 | fifoSeqNumber := "" 180 | if app.SyncQueues.Queues[queueName].IsFIFO { 181 | fifoSeqNumber = app.SyncQueues.Queues[queueName].NextSequenceNumber(messageGroupID) 182 | } 183 | app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) 184 | app.SyncQueues.Unlock() 185 | log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) 186 | 187 | respStruct := app.SendMessageResponse{ 188 | Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", 189 | Result: app.SendMessageResult{ 190 | MD5OfMessageAttributes: msg.MD5OfMessageAttributes, 191 | MD5OfMessageBody: msg.MD5OfMessageBody, 192 | MessageId: msg.Uuid, 193 | SequenceNumber: fifoSeqNumber, 194 | }, 195 | Metadata: app.ResponseMetadata{ 196 | RequestId: "00000000-0000-0000-0000-000000000000", 197 | }, 198 | } 199 | 200 | enc := xml.NewEncoder(w) 201 | enc.Indent(" ", " ") 202 | if err := enc.Encode(respStruct); err != nil { 203 | log.Printf("error: %v\n", err) 204 | } 205 | } 206 | 207 | type SendEntry struct { 208 | Id string 209 | MessageBody string 210 | MessageAttributes map[string]app.MessageAttributeValue 211 | MessageGroupId string 212 | MessageDeduplicationId string 213 | } 214 | 215 | func SendMessageBatch(w http.ResponseWriter, req *http.Request) { 216 | w.Header().Set("Content-Type", "application/xml") 217 | req.ParseForm() 218 | 219 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 220 | queueName := "" 221 | if queueUrl == "" { 222 | vars := mux.Vars(req) 223 | queueName = vars["queueName"] 224 | } else { 225 | uriSegments := strings.Split(queueUrl, "/") 226 | queueName = uriSegments[len(uriSegments)-1] 227 | } 228 | 229 | if _, ok := app.SyncQueues.Queues[queueName]; !ok { 230 | createErrorResponse(w, req, "QueueNotFound") 231 | return 232 | } 233 | 234 | sendEntries := []SendEntry{} 235 | 236 | for k, v := range req.Form { 237 | keySegments := strings.Split(k, ".") 238 | if keySegments[0] == "SendMessageBatchRequestEntry" { 239 | if len(keySegments) < 3 { 240 | createErrorResponse(w, req, "EmptyBatchRequest") 241 | return 242 | } 243 | keyIndex, err := strconv.Atoi(keySegments[1]) 244 | 245 | if err != nil { 246 | createErrorResponse(w, req, "Error") 247 | return 248 | } 249 | 250 | if len(sendEntries) < keyIndex { 251 | newSendEntries := make([]SendEntry, keyIndex) 252 | copy(newSendEntries, sendEntries) 253 | sendEntries = newSendEntries 254 | } 255 | 256 | if keySegments[2] == "Id" { 257 | sendEntries[keyIndex-1].Id = v[0] 258 | } 259 | 260 | if keySegments[2] == "MessageBody" { 261 | sendEntries[keyIndex-1].MessageBody = v[0] 262 | } 263 | 264 | if keySegments[2] == "MessageGroupId" { 265 | sendEntries[keyIndex-1].MessageGroupId = v[0] 266 | } 267 | 268 | if keySegments[2] == "MessageAttribute" { 269 | sendEntries[keyIndex-1].MessageAttributes = extractMessageAttributes(req, strings.Join(keySegments[0:2], ".")) 270 | } 271 | } 272 | } 273 | 274 | if len(sendEntries) == 0 { 275 | createErrorResponse(w, req, "EmptyBatchRequest") 276 | return 277 | } 278 | 279 | if len(sendEntries) > 10 { 280 | createErrorResponse(w, req, "TooManyEntriesInBatchRequest") 281 | return 282 | } 283 | ids := map[string]struct{}{} 284 | for _, v := range sendEntries { 285 | if _, ok := ids[v.Id]; ok { 286 | createErrorResponse(w, req, "BatchEntryIdsNotDistinct") 287 | return 288 | } 289 | ids[v.Id] = struct{}{} 290 | } 291 | 292 | sentEntries := make([]app.SendMessageBatchResultEntry, 0) 293 | log.Println("Putting Message in Queue:", queueName) 294 | for _, sendEntry := range sendEntries { 295 | msg := app.Message{MessageBody: []byte(sendEntry.MessageBody)} 296 | if len(sendEntry.MessageAttributes) > 0 { 297 | msg.MessageAttributes = sendEntry.MessageAttributes 298 | msg.MD5OfMessageAttributes = common.HashAttributes(sendEntry.MessageAttributes) 299 | } 300 | msg.MD5OfMessageBody = common.GetMD5Hash(sendEntry.MessageBody) 301 | msg.GroupID = sendEntry.MessageGroupId 302 | msg.Uuid, _ = common.NewUUID() 303 | app.SyncQueues.Lock() 304 | fifoSeqNumber := "" 305 | if app.SyncQueues.Queues[queueName].IsFIFO { 306 | fifoSeqNumber = app.SyncQueues.Queues[queueName].NextSequenceNumber(sendEntry.MessageGroupId) 307 | } 308 | app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages, msg) 309 | app.SyncQueues.Unlock() 310 | se := app.SendMessageBatchResultEntry{ 311 | Id: sendEntry.Id, 312 | MessageId: msg.Uuid, 313 | MD5OfMessageBody: msg.MD5OfMessageBody, 314 | MD5OfMessageAttributes: msg.MD5OfMessageAttributes, 315 | SequenceNumber: fifoSeqNumber, 316 | } 317 | sentEntries = append(sentEntries, se) 318 | log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) 319 | } 320 | 321 | respStruct := app.SendMessageBatchResponse{ 322 | "http://queue.amazonaws.com/doc/2012-11-05/", 323 | app.SendMessageBatchResult{Entry: sentEntries}, 324 | app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} 325 | 326 | enc := xml.NewEncoder(w) 327 | enc.Indent(" ", " ") 328 | if err := enc.Encode(respStruct); err != nil { 329 | log.Printf("error: %v\n", err) 330 | } 331 | } 332 | 333 | func ReceiveMessage(w http.ResponseWriter, req *http.Request) { 334 | w.Header().Set("Content-Type", "application/xml") 335 | 336 | waitTimeSeconds := 0 337 | wts := req.FormValue("WaitTimeSeconds") 338 | if wts != "" { 339 | waitTimeSeconds, _ = strconv.Atoi(wts) 340 | } 341 | maxNumberOfMessages := 1 342 | mom := req.FormValue("MaxNumberOfMessages") 343 | if mom != "" { 344 | maxNumberOfMessages, _ = strconv.Atoi(mom) 345 | } 346 | 347 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 348 | 349 | queueName := "" 350 | if queueUrl == "" { 351 | vars := mux.Vars(req) 352 | queueName = vars["queueName"] 353 | } else { 354 | uriSegments := strings.Split(queueUrl, "/") 355 | queueName = uriSegments[len(uriSegments)-1] 356 | } 357 | 358 | if _, ok := app.SyncQueues.Queues[queueName]; !ok { 359 | createErrorResponse(w, req, "QueueNotFound") 360 | return 361 | } 362 | 363 | var messages []*app.ResultMessage 364 | // respMsg := ResultMessage{} 365 | respStruct := app.ReceiveMessageResponse{} 366 | 367 | if waitTimeSeconds == 0 { 368 | app.SyncQueues.RLock() 369 | waitTimeSeconds = app.SyncQueues.Queues[queueName].ReceiveWaitTimeSecs 370 | app.SyncQueues.RUnlock() 371 | } 372 | 373 | loops := waitTimeSeconds * 10 374 | for loops > 0 { 375 | app.SyncQueues.RLock() 376 | found := len(app.SyncQueues.Queues[queueName].Messages)-numberOfHiddenMessagesInQueue(*app.SyncQueues.Queues[queueName]) != 0 377 | app.SyncQueues.RUnlock() 378 | if !found { 379 | time.Sleep(100 * time.Millisecond) 380 | loops-- 381 | } else { 382 | break 383 | } 384 | 385 | } 386 | log.Println("Getting Message from Queue:", queueName) 387 | 388 | app.SyncQueues.Lock() // Lock the Queues 389 | if len(app.SyncQueues.Queues[queueName].Messages) > 0 { 390 | numMsg := 0 391 | messages = make([]*app.ResultMessage, 0) 392 | for i := range app.SyncQueues.Queues[queueName].Messages { 393 | if numMsg >= maxNumberOfMessages { 394 | break 395 | } 396 | 397 | if app.SyncQueues.Queues[queueName].Messages[i].ReceiptHandle != "" { 398 | continue 399 | } 400 | 401 | uuid, _ := common.NewUUID() 402 | 403 | msg := &app.SyncQueues.Queues[queueName].Messages[i] 404 | msg.ReceiptHandle = msg.Uuid + "#" + uuid 405 | msg.ReceiptTime = time.Now().UTC() 406 | msg.VisibilityTimeout = time.Now().Add(time.Duration(app.SyncQueues.Queues[queueName].TimeoutSecs) * time.Second) 407 | 408 | if app.SyncQueues.Queues[queueName].IsFIFO { 409 | // If we got messages here it means we have not processed it yet, so get next 410 | if app.SyncQueues.Queues[queueName].IsLocked(msg.GroupID) { 411 | continue 412 | } 413 | // Otherwise lock messages for group ID 414 | app.SyncQueues.Queues[queueName].LockGroup(msg.GroupID) 415 | } 416 | 417 | messages = append(messages, getMessageResult(msg)) 418 | 419 | numMsg++ 420 | } 421 | 422 | // respMsg = ResultMessage{MessageId: messages.Uuid, ReceiptHandle: messages.ReceiptHandle, MD5OfBody: messages.MD5OfMessageBody, Body: messages.MessageBody, MD5OfMessageAttributes: messages.MD5OfMessageAttributes} 423 | respStruct = app.ReceiveMessageResponse{ 424 | "http://queue.amazonaws.com/doc/2012-11-05/", 425 | app.ReceiveMessageResult{ 426 | Message: messages, 427 | }, 428 | app.ResponseMetadata{ 429 | RequestId: "00000000-0000-0000-0000-000000000000", 430 | }, 431 | } 432 | } else { 433 | log.Println("No messages in Queue:", queueName) 434 | respStruct = app.ReceiveMessageResponse{Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/", Result: app.ReceiveMessageResult{}, Metadata: app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} 435 | } 436 | app.SyncQueues.Unlock() // Unlock the Queues 437 | enc := xml.NewEncoder(w) 438 | enc.Indent(" ", " ") 439 | if err := enc.Encode(respStruct); err != nil { 440 | log.Printf("error: %v\n", err) 441 | } 442 | } 443 | 444 | func numberOfHiddenMessagesInQueue(queue app.Queue) int { 445 | num := 0 446 | for i := range queue.Messages { 447 | if queue.Messages[i].ReceiptHandle != "" { 448 | num++ 449 | } 450 | } 451 | return num 452 | } 453 | 454 | func ChangeMessageVisibility(w http.ResponseWriter, req *http.Request) { 455 | w.Header().Set("Content-Type", "application/xml") 456 | vars := mux.Vars(req) 457 | 458 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 459 | queueName := "" 460 | if queueUrl == "" { 461 | queueName = vars["queueName"] 462 | } else { 463 | uriSegments := strings.Split(queueUrl, "/") 464 | queueName = uriSegments[len(uriSegments)-1] 465 | } 466 | receiptHandle := req.FormValue("ReceiptHandle") 467 | visibilityTimeout, err := strconv.Atoi(req.FormValue("VisibilityTimeout")) 468 | if err != nil { 469 | createErrorResponse(w, req, "ValidationError") 470 | return 471 | } 472 | if visibilityTimeout > 43200 { 473 | createErrorResponse(w, req, "ValidationError") 474 | return 475 | } 476 | 477 | if _, ok := app.SyncQueues.Queues[queueName]; !ok { 478 | createErrorResponse(w, req, "QueueNotFound") 479 | return 480 | } 481 | 482 | app.SyncQueues.Lock() 483 | messageFound := false 484 | for i := 0; i < len(app.SyncQueues.Queues[queueName].Messages); i++ { 485 | queue := app.SyncQueues.Queues[queueName] 486 | msgs := queue.Messages 487 | if msgs[i].ReceiptHandle == receiptHandle { 488 | timeout := app.SyncQueues.Queues[queueName].TimeoutSecs 489 | if visibilityTimeout == 0 { 490 | msgs[i].ReceiptTime = time.Now().UTC() 491 | msgs[i].ReceiptHandle = "" 492 | msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second) 493 | msgs[i].Retry++ 494 | if queue.MaxReceiveCount > 0 && 495 | queue.DeadLetterQueue != nil && 496 | msgs[i].Retry > queue.MaxReceiveCount { 497 | queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, msgs[i]) 498 | queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...) 499 | i++ 500 | } 501 | } else { 502 | msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second) 503 | } 504 | messageFound = true 505 | break 506 | } 507 | } 508 | app.SyncQueues.Unlock() 509 | if !messageFound { 510 | createErrorResponse(w, req, "MessageNotInFlight") 511 | return 512 | } 513 | 514 | respStruct := app.ChangeMessageVisibilityResult{ 515 | "http://queue.amazonaws.com/doc/2012-11-05/", 516 | app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} 517 | 518 | enc := xml.NewEncoder(w) 519 | enc.Indent(" ", " ") 520 | if err := enc.Encode(respStruct); err != nil { 521 | log.Printf("error: %v\n", err) 522 | createErrorResponse(w, req, "GeneralError") 523 | return 524 | } 525 | } 526 | 527 | type DeleteEntry struct { 528 | Id string 529 | ReceiptHandle string 530 | Error string 531 | Deleted bool 532 | } 533 | 534 | func DeleteMessageBatch(w http.ResponseWriter, req *http.Request) { 535 | w.Header().Set("Content-Type", "application/xml") 536 | req.ParseForm() 537 | 538 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 539 | queueName := "" 540 | if queueUrl == "" { 541 | vars := mux.Vars(req) 542 | queueName = vars["queueName"] 543 | } else { 544 | uriSegments := strings.Split(queueUrl, "/") 545 | queueName = uriSegments[len(uriSegments)-1] 546 | } 547 | 548 | deleteEntries := []DeleteEntry{} 549 | 550 | for k, v := range req.Form { 551 | keySegments := strings.Split(k, ".") 552 | if keySegments[0] == "DeleteMessageBatchRequestEntry" { 553 | keyIndex, err := strconv.Atoi(keySegments[1]) 554 | 555 | if err != nil { 556 | createErrorResponse(w, req, "Error") 557 | return 558 | } 559 | 560 | if len(deleteEntries) < keyIndex { 561 | newDeleteEntries := make([]DeleteEntry, keyIndex) 562 | copy(newDeleteEntries, deleteEntries) 563 | deleteEntries = newDeleteEntries 564 | } 565 | 566 | if keySegments[2] == "Id" { 567 | deleteEntries[keyIndex-1].Id = v[0] 568 | } 569 | 570 | if keySegments[2] == "ReceiptHandle" { 571 | deleteEntries[keyIndex-1].ReceiptHandle = v[0] 572 | } 573 | } 574 | } 575 | 576 | deletedEntries := make([]app.DeleteMessageBatchResultEntry, 0) 577 | 578 | app.SyncQueues.Lock() 579 | if _, ok := app.SyncQueues.Queues[queueName]; ok { 580 | for _, deleteEntry := range deleteEntries { 581 | for i, msg := range app.SyncQueues.Queues[queueName].Messages { 582 | if msg.ReceiptHandle == deleteEntry.ReceiptHandle { 583 | // Unlock messages for the group 584 | log.Printf("FIFO Queue %s unlocking group %s:", queueName, msg.GroupID) 585 | app.SyncQueues.Queues[queueName].UnlockGroup(msg.GroupID) 586 | app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages[:i], app.SyncQueues.Queues[queueName].Messages[i+1:]...) 587 | 588 | deleteEntry.Deleted = true 589 | deletedEntry := app.DeleteMessageBatchResultEntry{Id: deleteEntry.Id} 590 | deletedEntries = append(deletedEntries, deletedEntry) 591 | break 592 | } 593 | } 594 | } 595 | } 596 | app.SyncQueues.Unlock() 597 | 598 | notFoundEntries := make([]app.BatchResultErrorEntry, 0) 599 | for _, deleteEntry := range deleteEntries { 600 | if deleteEntry.Deleted == false { 601 | notFoundEntries = append(notFoundEntries, app.BatchResultErrorEntry{ 602 | Code: "1", 603 | Id: deleteEntry.Id, 604 | Message: "Message not found", 605 | SenderFault: true}) 606 | } 607 | } 608 | 609 | respStruct := app.DeleteMessageBatchResponse{ 610 | "http://queue.amazonaws.com/doc/2012-11-05/", 611 | app.DeleteMessageBatchResult{Entry: deletedEntries, Error: notFoundEntries}, 612 | app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} 613 | 614 | enc := xml.NewEncoder(w) 615 | enc.Indent(" ", " ") 616 | if err := enc.Encode(respStruct); err != nil { 617 | log.Printf("error: %v\n", err) 618 | } 619 | } 620 | 621 | func DeleteMessage(w http.ResponseWriter, req *http.Request) { 622 | // Sent response type 623 | w.Header().Set("Content-Type", "application/xml") 624 | 625 | // Retrieve FormValues required 626 | receiptHandle := req.FormValue("ReceiptHandle") 627 | 628 | // Retrieve FormValues required 629 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 630 | queueName := "" 631 | if queueUrl == "" { 632 | vars := mux.Vars(req) 633 | queueName = vars["queueName"] 634 | } else { 635 | uriSegments := strings.Split(queueUrl, "/") 636 | queueName = uriSegments[len(uriSegments)-1] 637 | } 638 | 639 | log.Println("Deleting Message, Queue:", queueName, ", ReceiptHandle:", receiptHandle) 640 | 641 | // Find queue/message with the receipt handle and delete 642 | app.SyncQueues.Lock() 643 | if _, ok := app.SyncQueues.Queues[queueName]; ok { 644 | for i, msg := range app.SyncQueues.Queues[queueName].Messages { 645 | if msg.ReceiptHandle == receiptHandle { 646 | // Unlock messages for the group 647 | log.Printf("FIFO Queue %s unlocking group %s:", queueName, msg.GroupID) 648 | app.SyncQueues.Queues[queueName].UnlockGroup(msg.GroupID) 649 | //Delete message from Q 650 | app.SyncQueues.Queues[queueName].Messages = append(app.SyncQueues.Queues[queueName].Messages[:i], app.SyncQueues.Queues[queueName].Messages[i+1:]...) 651 | 652 | app.SyncQueues.Unlock() 653 | // Create, encode/xml and send response 654 | respStruct := app.DeleteMessageResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000001"}} 655 | enc := xml.NewEncoder(w) 656 | enc.Indent(" ", " ") 657 | if err := enc.Encode(respStruct); err != nil { 658 | log.Printf("error: %v\n", err) 659 | } 660 | return 661 | } 662 | } 663 | log.Println("Receipt Handle not found") 664 | } else { 665 | log.Println("Queue not found") 666 | } 667 | app.SyncQueues.Unlock() 668 | 669 | createErrorResponse(w, req, "MessageDoesNotExist") 670 | } 671 | 672 | func DeleteQueue(w http.ResponseWriter, req *http.Request) { 673 | // Sent response type 674 | w.Header().Set("Content-Type", "application/xml") 675 | 676 | // Retrieve FormValues required 677 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 678 | queueName := "" 679 | if queueUrl == "" { 680 | vars := mux.Vars(req) 681 | queueName = vars["queueName"] 682 | } else { 683 | uriSegments := strings.Split(queueUrl, "/") 684 | queueName = uriSegments[len(uriSegments)-1] 685 | } 686 | 687 | log.Println("Deleting Queue:", queueName) 688 | app.SyncQueues.Lock() 689 | delete(app.SyncQueues.Queues, queueName) 690 | app.SyncQueues.Unlock() 691 | 692 | // Create, encode/xml and send response 693 | respStruct := app.DeleteMessageResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} 694 | enc := xml.NewEncoder(w) 695 | enc.Indent(" ", " ") 696 | if err := enc.Encode(respStruct); err != nil { 697 | log.Printf("error: %v\n", err) 698 | } 699 | } 700 | 701 | func PurgeQueue(w http.ResponseWriter, req *http.Request) { 702 | // Sent response type 703 | w.Header().Set("Content-Type", "application/xml") 704 | 705 | // Retrieve FormValues required 706 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 707 | 708 | uriSegments := strings.Split(queueUrl, "/") 709 | queueName := uriSegments[len(uriSegments)-1] 710 | 711 | log.Println("Purging Queue:", queueName) 712 | 713 | app.SyncQueues.Lock() 714 | if _, ok := app.SyncQueues.Queues[queueName]; ok { 715 | app.SyncQueues.Queues[queueName].Messages = nil 716 | respStruct := app.PurgeQueueResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} 717 | enc := xml.NewEncoder(w) 718 | enc.Indent(" ", " ") 719 | if err := enc.Encode(respStruct); err != nil { 720 | log.Printf("error: %v\n", err) 721 | createErrorResponse(w, req, "GeneralError") 722 | } 723 | } else { 724 | log.Println("Purge Queue:", queueName, ", queue does not exist!!!") 725 | createErrorResponse(w, req, "QueueNotFound") 726 | } 727 | app.SyncQueues.Unlock() 728 | } 729 | 730 | func GetQueueUrl(w http.ResponseWriter, req *http.Request) { 731 | // Sent response type 732 | w.Header().Set("Content-Type", "application/xml") 733 | // 734 | //// Retrieve FormValues required 735 | queueName := req.FormValue("QueueName") 736 | if queue, ok := app.SyncQueues.Queues[queueName]; ok { 737 | url := queue.URL 738 | log.Println("Get Queue URL:", queueName) 739 | // Create, encode/xml and send response 740 | result := app.GetQueueUrlResult{QueueUrl: url} 741 | respStruct := app.GetQueueUrlResponse{"http://queue.amazonaws.com/doc/2012-11-05/", result, app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} 742 | enc := xml.NewEncoder(w) 743 | enc.Indent(" ", " ") 744 | if err := enc.Encode(respStruct); err != nil { 745 | log.Printf("error: %v\n", err) 746 | } 747 | } else { 748 | log.Println("Get Queue URL:", queueName, ", queue does not exist!!!") 749 | createErrorResponse(w, req, "QueueNotFound") 750 | } 751 | } 752 | 753 | func GetQueueAttributes(w http.ResponseWriter, req *http.Request) { 754 | // Sent response type 755 | w.Header().Set("Content-Type", "application/xml") 756 | // Retrieve FormValues required 757 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 758 | 759 | queueName := "" 760 | if queueUrl == "" { 761 | vars := mux.Vars(req) 762 | queueName = vars["queueName"] 763 | } else { 764 | uriSegments := strings.Split(queueUrl, "/") 765 | queueName = uriSegments[len(uriSegments)-1] 766 | } 767 | 768 | log.Println("Get Queue Attributes:", queueName) 769 | app.SyncQueues.RLock() 770 | if queue, ok := app.SyncQueues.Queues[queueName]; ok { 771 | // Create, encode/xml and send response 772 | attribs := make([]app.Attribute, 0, 0) 773 | attr := app.Attribute{Name: "VisibilityTimeout", Value: strconv.Itoa(queue.TimeoutSecs)} 774 | attribs = append(attribs, attr) 775 | attr = app.Attribute{Name: "DelaySeconds", Value: "0"} 776 | attribs = append(attribs, attr) 777 | attr = app.Attribute{Name: "ReceiveMessageWaitTimeSeconds", Value: strconv.Itoa(queue.ReceiveWaitTimeSecs)} 778 | attribs = append(attribs, attr) 779 | attr = app.Attribute{Name: "ApproximateNumberOfMessages", Value: strconv.Itoa(len(queue.Messages))} 780 | attribs = append(attribs, attr) 781 | app.SyncQueues.RLock() 782 | attr = app.Attribute{Name: "ApproximateNumberOfMessagesNotVisible", Value: strconv.Itoa(numberOfHiddenMessagesInQueue(*queue))} 783 | app.SyncQueues.RUnlock() 784 | attribs = append(attribs, attr) 785 | attr = app.Attribute{Name: "CreatedTimestamp", Value: "0000000000"} 786 | attribs = append(attribs, attr) 787 | attr = app.Attribute{Name: "LastModifiedTimestamp", Value: "0000000000"} 788 | attribs = append(attribs, attr) 789 | attr = app.Attribute{Name: "QueueArn", Value: queue.Arn} 790 | attribs = append(attribs, attr) 791 | 792 | deadLetterTargetArn := "" 793 | if queue.DeadLetterQueue != nil { 794 | deadLetterTargetArn = queue.DeadLetterQueue.Name 795 | } 796 | attr = app.Attribute{Name: "RedrivePolicy", Value: fmt.Sprintf(`{"maxReceiveCount": "%d", "deadLetterTargetArn":"%s"}`, queue.MaxReceiveCount, deadLetterTargetArn)} 797 | attribs = append(attribs, attr) 798 | 799 | result := app.GetQueueAttributesResult{Attrs: attribs} 800 | respStruct := app.GetQueueAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", result, app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} 801 | enc := xml.NewEncoder(w) 802 | enc.Indent(" ", " ") 803 | if err := enc.Encode(respStruct); err != nil { 804 | log.Printf("error: %v\n", err) 805 | } 806 | } else { 807 | log.Println("Get Queue URL:", queueName, ", queue does not exist!!!") 808 | createErrorResponse(w, req, "QueueNotFound") 809 | } 810 | app.SyncQueues.RUnlock() 811 | } 812 | 813 | func SetQueueAttributes(w http.ResponseWriter, req *http.Request) { 814 | // Sent response type 815 | w.Header().Set("Content-Type", "application/xml") 816 | 817 | queueUrl := getQueueFromPath(req.FormValue("QueueUrl"), req.URL.String()) 818 | 819 | queueName := "" 820 | if queueUrl == "" { 821 | vars := mux.Vars(req) 822 | queueName = vars["queueName"] 823 | } else { 824 | uriSegments := strings.Split(queueUrl, "/") 825 | queueName = uriSegments[len(uriSegments)-1] 826 | } 827 | 828 | log.Println("Set Queue Attributes:", queueName) 829 | app.SyncQueues.Lock() 830 | if queue, ok := app.SyncQueues.Queues[queueName]; ok { 831 | if err := validateAndSetQueueAttributes(queue, req.Form); err != nil { 832 | createErrorResponse(w, req, err.Error()) 833 | app.SyncQueues.Unlock() 834 | return 835 | } 836 | 837 | respStruct := app.SetQueueAttributesResponse{"http://queue.amazonaws.com/doc/2012-11-05/", app.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"}} 838 | enc := xml.NewEncoder(w) 839 | enc.Indent(" ", " ") 840 | if err := enc.Encode(respStruct); err != nil { 841 | log.Printf("error: %v\n", err) 842 | } 843 | } else { 844 | log.Println("Get Queue URL:", queueName, ", queue does not exist!!!") 845 | createErrorResponse(w, req, "QueueNotFound") 846 | } 847 | app.SyncQueues.Unlock() 848 | } 849 | 850 | func getMessageResult(m *app.Message) *app.ResultMessage { 851 | msgMttrs := []*app.ResultMessageAttribute{} 852 | for _, attr := range m.MessageAttributes { 853 | msgMttrs = append(msgMttrs, getMessageAttributeResult(&attr)) 854 | } 855 | 856 | attrsMap := map[string]string{ 857 | "ApproximateFirstReceiveTimestamp": fmt.Sprintf("%d", m.ReceiptTime.Unix()), 858 | "SenderId": app.CurrentEnvironment.AccountID, 859 | "ApproximateReceiveCount": fmt.Sprintf("%d", m.NumberOfReceives+1), 860 | "SentTimestamp": fmt.Sprintf("%d", time.Now().UTC().Unix()), 861 | } 862 | 863 | var attrs []*app.ResultAttribute 864 | for k, v := range attrsMap { 865 | attrs = append(attrs, &app.ResultAttribute{ 866 | Name: k, 867 | Value: v, 868 | }) 869 | } 870 | 871 | return &app.ResultMessage{ 872 | MessageId: m.Uuid, 873 | Body: m.MessageBody, 874 | ReceiptHandle: m.ReceiptHandle, 875 | MD5OfBody: common.GetMD5Hash(string(m.MessageBody)), 876 | MD5OfMessageAttributes: m.MD5OfMessageAttributes, 877 | MessageAttributes: msgMttrs, 878 | Attributes: attrs, 879 | } 880 | } 881 | 882 | func getQueueFromPath(formVal string, theUrl string) string { 883 | if formVal != "" { 884 | return formVal 885 | } 886 | u, err := url.Parse(theUrl) 887 | if err != nil { 888 | return "" 889 | } 890 | return u.Path 891 | } 892 | 893 | func createErrorResponse(w http.ResponseWriter, req *http.Request, err string) { 894 | er := app.SqsErrors[err] 895 | respStruct := app.ErrorResponse{app.ErrorResult{Type: er.Type, Code: er.Code, Message: er.Message, RequestId: "00000000-0000-0000-0000-000000000000"}} 896 | 897 | w.WriteHeader(er.HttpError) 898 | enc := xml.NewEncoder(w) 899 | enc.Indent(" ", " ") 900 | if err := enc.Encode(respStruct); err != nil { 901 | log.Printf("error: %v\n", err) 902 | } 903 | } 904 | -------------------------------------------------------------------------------- /app/gosqs/gosqs_test.go: -------------------------------------------------------------------------------- 1 | package gosqs 2 | 3 | import ( 4 | "encoding/xml" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/p4tin/goaws/app" 14 | ) 15 | 16 | func TestListQueues_POST_NoQueues(t *testing.T) { 17 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 18 | // pass 'nil' as the third parameter. 19 | req, err := http.NewRequest("POST", "/", nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 25 | rr := httptest.NewRecorder() 26 | handler := http.HandlerFunc(ListQueues) 27 | 28 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 29 | // directly and pass in our Request and ResponseRecorder. 30 | handler.ServeHTTP(rr, req) 31 | 32 | // Check the status code is what we expect. 33 | if status := rr.Code; status != http.StatusOK { 34 | t.Errorf("handler returned wrong status code: got %v want %v", 35 | status, http.StatusOK) 36 | } 37 | 38 | // Check the response body is what we expect. 39 | expected := "" 40 | if !strings.Contains(rr.Body.String(), expected) { 41 | t.Errorf("handler returned unexpected body: got %v want %v", 42 | rr.Body.String(), expected) 43 | } 44 | } 45 | 46 | func TestListQueues_POST_Success(t *testing.T) { 47 | req, err := http.NewRequest("POST", "/", nil) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | rr := httptest.NewRecorder() 53 | handler := http.HandlerFunc(ListQueues) 54 | 55 | app.SyncQueues.Queues["foo"] = &app.Queue{Name: "foo", URL: "http://:/queue/foo"} 56 | app.SyncQueues.Queues["bar"] = &app.Queue{Name: "bar", URL: "http://:/queue/bar"} 57 | app.SyncQueues.Queues["foobar"] = &app.Queue{Name: "foobar", URL: "http://:/queue/foobar"} 58 | 59 | handler.ServeHTTP(rr, req) 60 | 61 | if status := rr.Code; status != http.StatusOK { 62 | t.Errorf("handler returned wrong status code: got %v want %v", 63 | status, http.StatusOK) 64 | } 65 | 66 | // Check the response body is what we expect. 67 | expected := "http://:/queue/bar" 68 | if !strings.Contains(rr.Body.String(), expected) { 69 | t.Errorf("handler returned unexpected body: got %v want %v", 70 | rr.Body.String(), expected) 71 | } 72 | 73 | // Filter lists by the given QueueNamePrefix 74 | form := url.Values{} 75 | form.Add("QueueNamePrefix", "fo") 76 | req, _ = http.NewRequest("POST", "/", nil) 77 | req.PostForm = form 78 | rr = httptest.NewRecorder() 79 | handler.ServeHTTP(rr, req) 80 | 81 | if status := rr.Code; status != http.StatusOK { 82 | t.Errorf("handler returned wrong status code: got %v want %v", 83 | status, http.StatusOK) 84 | } 85 | 86 | // Check the response body is what we expect. 87 | unexpected := "http://:/queue/bar" 88 | if strings.Contains(rr.Body.String(), unexpected) { 89 | t.Errorf("handler returned unexpected body: got %v", 90 | rr.Body.String()) 91 | } 92 | 93 | } 94 | 95 | func TestCreateQueuehandler_POST_CreateQueue(t *testing.T) { 96 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 97 | // pass 'nil' as the third parameter. 98 | req, err := http.NewRequest("POST", "/", nil) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | 103 | queueName := "UnitTestQueue1" 104 | form := url.Values{} 105 | form.Add("Action", "CreateQueue") 106 | form.Add("QueueName", "UnitTestQueue1") 107 | form.Add("Attribute.1.Name", "VisibilityTimeout") 108 | form.Add("Attribute.1.Value", "60") 109 | req.PostForm = form 110 | 111 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 112 | rr := httptest.NewRecorder() 113 | handler := http.HandlerFunc(CreateQueue) 114 | 115 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 116 | // directly and pass in our Request and ResponseRecorder. 117 | handler.ServeHTTP(rr, req) 118 | 119 | // Check the status code is what we expect. 120 | if status := rr.Code; status != http.StatusOK { 121 | t.Errorf("handler returned wrong status code: got %v want %v", 122 | status, http.StatusOK) 123 | } 124 | 125 | // Check the response body is what we expect. 126 | expected := queueName 127 | if !strings.Contains(rr.Body.String(), expected) { 128 | t.Errorf("handler returned unexpected body: got %v want %v", 129 | rr.Body.String(), expected) 130 | } 131 | expectedQueue := &app.Queue{ 132 | Name: queueName, 133 | URL: "http://://" + queueName, 134 | Arn: "arn:aws:sqs:::" + queueName, 135 | TimeoutSecs: 60, 136 | } 137 | actualQueue := app.SyncQueues.Queues[queueName] 138 | if !reflect.DeepEqual(expectedQueue, actualQueue) { 139 | t.Fatalf("expected %+v, got %+v", expectedQueue, actualQueue) 140 | } 141 | } 142 | 143 | func TestCreateFIFOQueuehandler_POST_CreateQueue(t *testing.T) { 144 | req, err := http.NewRequest("POST", "/", nil) 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | 149 | queueName := "UnitTestQueue1.fifo" 150 | form := url.Values{} 151 | form.Add("Action", "CreateQueue") 152 | form.Add("QueueName", "UnitTestQueue1.fifo") 153 | form.Add("Attribute.1.Name", "VisibilityTimeout") 154 | form.Add("Attribute.1.Value", "60") 155 | req.PostForm = form 156 | 157 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 158 | rr := httptest.NewRecorder() 159 | handler := http.HandlerFunc(CreateQueue) 160 | 161 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 162 | // directly and pass in our Request and ResponseRecorder. 163 | handler.ServeHTTP(rr, req) 164 | 165 | // Check the status code is what we expect. 166 | if status := rr.Code; status != http.StatusOK { 167 | t.Errorf("handler returned wrong status code: got %v want %v", 168 | status, http.StatusOK) 169 | } 170 | 171 | // Check the response body is what we expect. 172 | expected := queueName 173 | if !strings.Contains(rr.Body.String(), expected) { 174 | t.Errorf("handler returned unexpected body: got %v want %v", 175 | rr.Body.String(), expected) 176 | } 177 | expectedQueue := &app.Queue{ 178 | Name: queueName, 179 | URL: "http://://" + queueName, 180 | Arn: "arn:aws:sqs:::" + queueName, 181 | TimeoutSecs: 60, 182 | IsFIFO: true, 183 | } 184 | actualQueue := app.SyncQueues.Queues[queueName] 185 | if !reflect.DeepEqual(expectedQueue, actualQueue) { 186 | t.Fatalf("expected %+v, got %+v", expectedQueue, actualQueue) 187 | } 188 | } 189 | 190 | func TestSendQueue_POST_NonExistant(t *testing.T) { 191 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 192 | // pass 'nil' as the third parameter. 193 | req, err := http.NewRequest("POST", "/", nil) 194 | if err != nil { 195 | t.Fatal(err) 196 | } 197 | 198 | form := url.Values{} 199 | form.Add("Action", "SendMessage") 200 | form.Add("QueueUrl", "http://localhost:4100/queue/NON-EXISTANT") 201 | form.Add("MessageBody", "Test123") 202 | form.Add("Version", "2012-11-05") 203 | req.PostForm = form 204 | 205 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 206 | rr := httptest.NewRecorder() 207 | handler := http.HandlerFunc(SendMessage) 208 | 209 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 210 | // directly and pass in our Request and ResponseRecorder. 211 | handler.ServeHTTP(rr, req) 212 | 213 | // Check the status code is what we expect. 214 | if status := rr.Code; status != http.StatusBadRequest { 215 | t.Errorf("handler returned wrong status code: got %v want %v", 216 | status, http.StatusBadRequest) 217 | } 218 | 219 | // Check the response body is what we expect. 220 | expected := "NonExistentQueue" 221 | if !strings.Contains(rr.Body.String(), expected) { 222 | t.Errorf("handler returned unexpected body: got %v want %v", 223 | rr.Body.String(), expected) 224 | } 225 | } 226 | 227 | func TestSendMessageBatch_POST_QueueNotFound(t *testing.T) { 228 | req, err := http.NewRequest("POST", "/", nil) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | 233 | form := url.Values{} 234 | form.Add("Action", "SendMessageBatch") 235 | form.Add("QueueUrl", "http://localhost:4100/queue/testing") 236 | form.Add("Version", "2012-11-05") 237 | req.PostForm = form 238 | 239 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 240 | rr := httptest.NewRecorder() 241 | handler := http.HandlerFunc(SendMessageBatch) 242 | 243 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 244 | // directly and pass in our Request and ResponseRecorder. 245 | handler.ServeHTTP(rr, req) 246 | 247 | // Check the status code is what we expect. 248 | if status := rr.Code; status != http.StatusBadRequest { 249 | t.Errorf("handler returned wrong status code: got %v want %v", 250 | status, http.StatusBadRequest) 251 | } 252 | 253 | // Check the response body is what we expect. 254 | expected := "NonExistentQueue" 255 | if !strings.Contains(rr.Body.String(), expected) { 256 | t.Errorf("handler returned unexpected body: got %v want %v", 257 | rr.Body.String(), expected) 258 | } 259 | } 260 | 261 | func TestSendMessageBatch_POST_NoEntry(t *testing.T) { 262 | req, err := http.NewRequest("POST", "/", nil) 263 | if err != nil { 264 | t.Fatal(err) 265 | } 266 | 267 | app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} 268 | 269 | form := url.Values{} 270 | form.Add("Action", "SendMessageBatch") 271 | form.Add("QueueUrl", "http://localhost:4100/queue/testing") 272 | form.Add("Version", "2012-11-05") 273 | req.PostForm = form 274 | 275 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 276 | rr := httptest.NewRecorder() 277 | handler := http.HandlerFunc(SendMessageBatch) 278 | 279 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 280 | // directly and pass in our Request and ResponseRecorder. 281 | handler.ServeHTTP(rr, req) 282 | 283 | // Check the status code is what we expect. 284 | if status := rr.Code; status != http.StatusBadRequest { 285 | t.Errorf("handler returned wrong status code: got %v want %v", 286 | status, http.StatusBadRequest) 287 | } 288 | 289 | // Check the response body is what we expect. 290 | expected := "EmptyBatchRequest" 291 | if !strings.Contains(rr.Body.String(), expected) { 292 | t.Errorf("handler returned unexpected body: got %v want %v", 293 | rr.Body.String(), expected) 294 | } 295 | 296 | req, _ = http.NewRequest("POST", "/", nil) 297 | form.Add("SendMessageBatchRequestEntry", "") 298 | req.PostForm = form 299 | 300 | rr = httptest.NewRecorder() 301 | 302 | handler.ServeHTTP(rr, req) 303 | 304 | if status := rr.Code; status != http.StatusBadRequest { 305 | t.Errorf("handler returned wrong status code: got %v want %v", 306 | status, http.StatusBadRequest) 307 | } 308 | 309 | if !strings.Contains(rr.Body.String(), expected) { 310 | t.Errorf("handler returned unexpected body: got %v want %v", 311 | rr.Body.String(), expected) 312 | } 313 | } 314 | 315 | func TestSendMessageBatch_POST_IdNotDistinct(t *testing.T) { 316 | req, err := http.NewRequest("POST", "/", nil) 317 | if err != nil { 318 | t.Fatal(err) 319 | } 320 | 321 | app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} 322 | 323 | form := url.Values{} 324 | form.Add("Action", "SendMessageBatch") 325 | form.Add("QueueUrl", "http://localhost:4100/queue/testing") 326 | form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") 327 | form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") 328 | form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_001") 329 | form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") 330 | form.Add("Version", "2012-11-05") 331 | req.PostForm = form 332 | 333 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 334 | rr := httptest.NewRecorder() 335 | handler := http.HandlerFunc(SendMessageBatch) 336 | 337 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 338 | // directly and pass in our Request and ResponseRecorder. 339 | handler.ServeHTTP(rr, req) 340 | 341 | // Check the status code is what we expect. 342 | if status := rr.Code; status != http.StatusBadRequest { 343 | t.Errorf("handler returned wrong status code: got %v want %v", 344 | status, http.StatusBadRequest) 345 | } 346 | 347 | // Check the response body is what we expect. 348 | expected := "BatchEntryIdsNotDistinct" 349 | if !strings.Contains(rr.Body.String(), expected) { 350 | t.Errorf("handler returned unexpected body: got %v want %v", 351 | rr.Body.String(), expected) 352 | } 353 | } 354 | 355 | func TestSendMessageBatch_POST_TooManyEntries(t *testing.T) { 356 | req, err := http.NewRequest("POST", "/", nil) 357 | if err != nil { 358 | t.Fatal(err) 359 | } 360 | 361 | app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} 362 | 363 | form := url.Values{} 364 | form.Add("Action", "SendMessageBatch") 365 | form.Add("QueueUrl", "http://localhost:4100/queue/testing") 366 | form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") 367 | form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") 368 | form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_002") 369 | form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") 370 | form.Add("SendMessageBatchRequestEntry.3.Id", "test_msg_003") 371 | form.Add("SendMessageBatchRequestEntry.3.MessageBody", "test%20message%20body%202") 372 | form.Add("SendMessageBatchRequestEntry.4.Id", "test_msg_004") 373 | form.Add("SendMessageBatchRequestEntry.4.MessageBody", "test%20message%20body%202") 374 | form.Add("SendMessageBatchRequestEntry.5.Id", "test_msg_005") 375 | form.Add("SendMessageBatchRequestEntry.5.MessageBody", "test%20message%20body%202") 376 | form.Add("SendMessageBatchRequestEntry.6.Id", "test_msg_006") 377 | form.Add("SendMessageBatchRequestEntry.6.MessageBody", "test%20message%20body%202") 378 | form.Add("SendMessageBatchRequestEntry.7.Id", "test_msg_007") 379 | form.Add("SendMessageBatchRequestEntry.7.MessageBody", "test%20message%20body%202") 380 | form.Add("SendMessageBatchRequestEntry.8.Id", "test_msg_008") 381 | form.Add("SendMessageBatchRequestEntry.8.MessageBody", "test%20message%20body%202") 382 | form.Add("SendMessageBatchRequestEntry.9.Id", "test_msg_009") 383 | form.Add("SendMessageBatchRequestEntry.9.MessageBody", "test%20message%20body%202") 384 | form.Add("SendMessageBatchRequestEntry.10.Id", "test_msg_010") 385 | form.Add("SendMessageBatchRequestEntry.10.MessageBody", "test%20message%20body%202") 386 | form.Add("SendMessageBatchRequestEntry.11.Id", "test_msg_011") 387 | form.Add("SendMessageBatchRequestEntry.11.MessageBody", "test%20message%20body%202") 388 | form.Add("Version", "2012-11-05") 389 | req.PostForm = form 390 | 391 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 392 | rr := httptest.NewRecorder() 393 | handler := http.HandlerFunc(SendMessageBatch) 394 | 395 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 396 | // directly and pass in our Request and ResponseRecorder. 397 | handler.ServeHTTP(rr, req) 398 | 399 | // Check the status code is what we expect. 400 | if status := rr.Code; status != http.StatusBadRequest { 401 | t.Errorf("handler returned wrong status code: got %v want %v", 402 | status, http.StatusBadRequest) 403 | } 404 | 405 | // Check the response body is what we expect. 406 | expected := "TooManyEntriesInBatchRequest" 407 | if !strings.Contains(rr.Body.String(), expected) { 408 | t.Errorf("handler returned unexpected body: got %v want %v", 409 | rr.Body.String(), expected) 410 | } 411 | } 412 | 413 | func TestSendMessageBatch_POST_Success(t *testing.T) { 414 | req, err := http.NewRequest("POST", "/", nil) 415 | if err != nil { 416 | t.Fatal(err) 417 | } 418 | 419 | app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} 420 | 421 | form := url.Values{} 422 | form.Add("Action", "SendMessageBatch") 423 | form.Add("QueueUrl", "http://localhost:4100/queue/testing") 424 | form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") 425 | form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") 426 | form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_002") 427 | form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") 428 | form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Name", "test_attribute_name_1") 429 | form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.StringValue", "test_attribute_value_1") 430 | form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.DataType", "String") 431 | form.Add("Version", "2012-11-05") 432 | req.PostForm = form 433 | 434 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 435 | rr := httptest.NewRecorder() 436 | handler := http.HandlerFunc(SendMessageBatch) 437 | 438 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 439 | // directly and pass in our Request and ResponseRecorder. 440 | handler.ServeHTTP(rr, req) 441 | 442 | // Check the status code is what we expect. 443 | if status := rr.Code; status != http.StatusOK { 444 | t.Errorf("handler returned wrong status code: got \n%v want %v", 445 | status, http.StatusOK) 446 | } 447 | 448 | // Check the response body is what we expect. 449 | expected := "1c538b76fce1a234bce865025c02b042" 450 | if !strings.Contains(rr.Body.String(), expected) { 451 | t.Errorf("handler returned unexpected body: got %v want %v", 452 | rr.Body.String(), expected) 453 | } 454 | } 455 | 456 | func TestSendMessageBatchToFIFOQueue_POST_Success(t *testing.T) { 457 | req, err := http.NewRequest("POST", "/", nil) 458 | if err != nil { 459 | t.Fatal(err) 460 | } 461 | 462 | app.SyncQueues.Queues["testing.fifo"] = &app.Queue{ 463 | Name: "testing.fifo", 464 | IsFIFO: true, 465 | } 466 | 467 | form := url.Values{} 468 | form.Add("Action", "SendMessageBatch") 469 | form.Add("QueueUrl", "http://localhost:4100/queue/testing.fifo") 470 | form.Add("SendMessageBatchRequestEntry.1.Id", "test_msg_001") 471 | form.Add("SendMessageBatchRequestEntry.1.MessageGroupId", "GROUP-X") 472 | form.Add("SendMessageBatchRequestEntry.1.MessageBody", "test%20message%20body%201") 473 | form.Add("SendMessageBatchRequestEntry.2.Id", "test_msg_002") 474 | form.Add("SendMessageBatchRequestEntry.2.MessageGroupId", "GROUP-X") 475 | form.Add("SendMessageBatchRequestEntry.2.MessageBody", "test%20message%20body%202") 476 | form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Name", "test_attribute_name_1") 477 | form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.StringValue", "test_attribute_value_1") 478 | form.Add("SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.DataType", "String") 479 | form.Add("Version", "2012-11-05") 480 | req.PostForm = form 481 | 482 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 483 | rr := httptest.NewRecorder() 484 | handler := http.HandlerFunc(SendMessageBatch) 485 | 486 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 487 | // directly and pass in our Request and ResponseRecorder. 488 | handler.ServeHTTP(rr, req) 489 | 490 | // Check the status code is what we expect. 491 | if status := rr.Code; status != http.StatusOK { 492 | t.Errorf("handler returned wrong status code: got \n%v want %v", 493 | status, http.StatusOK) 494 | } 495 | 496 | // Check the response body is what we expect. 497 | expected := "1c538b76fce1a234bce865025c02b042" 498 | if !strings.Contains(rr.Body.String(), expected) { 499 | t.Errorf("handler returned unexpected body: got %v want %v", 500 | rr.Body.String(), expected) 501 | } 502 | } 503 | 504 | func TestChangeMessageVisibility_POST_SUCCESS(t *testing.T) { 505 | req, err := http.NewRequest("POST", "/", nil) 506 | if err != nil { 507 | t.Fatal(err) 508 | } 509 | 510 | app.SyncQueues.Queues["testing"] = &app.Queue{Name: "testing"} 511 | app.SyncQueues.Queues["testing"].Messages = []app.Message{{ 512 | MessageBody: []byte("test1"), 513 | ReceiptHandle: "123", 514 | }} 515 | 516 | form := url.Values{} 517 | form.Add("Action", "ChangeMessageVisibility") 518 | form.Add("QueueUrl", "http://localhost:4100/queue/testing") 519 | form.Add("VisibilityTimeout", "0") 520 | form.Add("ReceiptHandle", "123") 521 | form.Add("Version", "2012-11-05") 522 | req.PostForm = form 523 | 524 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 525 | rr := httptest.NewRecorder() 526 | handler := http.HandlerFunc(ChangeMessageVisibility) 527 | 528 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 529 | // directly and pass in our Request and ResponseRecorder. 530 | handler.ServeHTTP(rr, req) 531 | 532 | // Check the status code is what we expect. 533 | if status := rr.Code; status != http.StatusOK { 534 | t.Errorf("handler returned wrong status code: got \n%v want %v", 535 | status, http.StatusOK) 536 | } 537 | 538 | // Check the response body is what we expect. 539 | expected := `` 540 | if !strings.Contains(rr.Body.String(), expected) { 541 | t.Errorf("handler returned unexpected body: got %v want %v", 542 | rr.Body.String(), expected) 543 | } 544 | } 545 | 546 | func TestRequeueing_VisibilityTimeoutExpires(t *testing.T) { 547 | done := make(chan struct{}, 0) 548 | go PeriodicTasks(1*time.Second, done) 549 | 550 | // create a queue 551 | req, err := http.NewRequest("POST", "/", nil) 552 | if err != nil { 553 | t.Fatal(err) 554 | } 555 | 556 | form := url.Values{} 557 | form.Add("Action", "CreateQueue") 558 | form.Add("QueueName", "requeue") 559 | form.Add("Attribute.1.Name", "VisibilityTimeout") 560 | form.Add("Attribute.1.Value", "1") 561 | form.Add("Version", "2012-11-05") 562 | req.PostForm = form 563 | 564 | rr := httptest.NewRecorder() 565 | http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) 566 | 567 | if status := rr.Code; status != http.StatusOK { 568 | t.Errorf("handler returned wrong status code: got \n%v want %v", 569 | status, http.StatusOK) 570 | } 571 | 572 | // send a message 573 | req, err = http.NewRequest("POST", "/", nil) 574 | if err != nil { 575 | t.Fatal(err) 576 | } 577 | 578 | form = url.Values{} 579 | form.Add("Action", "SendMessage") 580 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue") 581 | form.Add("MessageBody", "1") 582 | form.Add("Version", "2012-11-05") 583 | req.PostForm = form 584 | 585 | rr = httptest.NewRecorder() 586 | http.HandlerFunc(SendMessage).ServeHTTP(rr, req) 587 | 588 | if status := rr.Code; status != http.StatusOK { 589 | t.Errorf("handler returned wrong status code: got \n%v want %v", 590 | status, http.StatusOK) 591 | } 592 | 593 | // receive message 594 | req, err = http.NewRequest("POST", "/", nil) 595 | if err != nil { 596 | t.Fatal(err) 597 | } 598 | 599 | form = url.Values{} 600 | form.Add("Action", "ReceiveMessage") 601 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue") 602 | form.Add("Version", "2012-11-05") 603 | req.PostForm = form 604 | 605 | rr = httptest.NewRecorder() 606 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 607 | 608 | if status := rr.Code; status != http.StatusOK { 609 | t.Errorf("handler returned wrong status code: got \n%v want %v", 610 | status, http.StatusOK) 611 | } 612 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 613 | t.Fatal("handler should return a message") 614 | } 615 | 616 | // try to receive another message. 617 | req, err = http.NewRequest("POST", "/", nil) 618 | if err != nil { 619 | t.Fatal(err) 620 | } 621 | 622 | form = url.Values{} 623 | form.Add("Action", "ReceiveMessage") 624 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue") 625 | form.Add("Version", "2012-11-05") 626 | req.PostForm = form 627 | 628 | rr = httptest.NewRecorder() 629 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 630 | 631 | if status := rr.Code; status != http.StatusOK { 632 | t.Errorf("handler returned wrong status code: got \n%v want %v", 633 | status, http.StatusOK) 634 | } 635 | if ok := strings.Contains(rr.Body.String(), ""); ok { 636 | t.Fatal("handler should not return a message") 637 | } 638 | time.Sleep(2 * time.Second) 639 | 640 | // message needs to be requeued 641 | req, err = http.NewRequest("POST", "/", nil) 642 | if err != nil { 643 | t.Fatal(err) 644 | } 645 | 646 | form = url.Values{} 647 | form.Add("Action", "ReceiveMessage") 648 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue") 649 | form.Add("Version", "2012-11-05") 650 | req.PostForm = form 651 | 652 | rr = httptest.NewRecorder() 653 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 654 | 655 | if status := rr.Code; status != http.StatusOK { 656 | t.Errorf("handler returned wrong status code: got \n%v want %v", 657 | status, http.StatusOK) 658 | } 659 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 660 | t.Fatal("handler should return a message") 661 | } 662 | done <- struct{}{} 663 | } 664 | 665 | func TestRequeueing_ResetVisibilityTimeout(t *testing.T) { 666 | done := make(chan struct{}, 0) 667 | go PeriodicTasks(1*time.Second, done) 668 | 669 | // create a queue 670 | req, err := http.NewRequest("POST", "/", nil) 671 | if err != nil { 672 | t.Fatal(err) 673 | } 674 | 675 | form := url.Values{} 676 | form.Add("Action", "CreateQueue") 677 | form.Add("QueueName", "requeue-reset") 678 | form.Add("Attribute.1.Name", "VisibilityTimeout") 679 | form.Add("Attribute.1.Value", "10") 680 | form.Add("Version", "2012-11-05") 681 | req.PostForm = form 682 | 683 | rr := httptest.NewRecorder() 684 | http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) 685 | 686 | if status := rr.Code; status != http.StatusOK { 687 | t.Errorf("handler returned wrong status code: got \n%v want %v", 688 | status, http.StatusOK) 689 | } 690 | 691 | // send a message 692 | req, err = http.NewRequest("POST", "/", nil) 693 | if err != nil { 694 | t.Fatal(err) 695 | } 696 | 697 | form = url.Values{} 698 | form.Add("Action", "SendMessage") 699 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset") 700 | form.Add("MessageBody", "1") 701 | form.Add("Version", "2012-11-05") 702 | req.PostForm = form 703 | 704 | rr = httptest.NewRecorder() 705 | http.HandlerFunc(SendMessage).ServeHTTP(rr, req) 706 | 707 | if status := rr.Code; status != http.StatusOK { 708 | t.Errorf("handler returned wrong status code: got \n%v want %v", 709 | status, http.StatusOK) 710 | } 711 | 712 | // receive message 713 | req, err = http.NewRequest("POST", "/", nil) 714 | if err != nil { 715 | t.Fatal(err) 716 | } 717 | 718 | form = url.Values{} 719 | form.Add("Action", "ReceiveMessage") 720 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset") 721 | form.Add("Version", "2012-11-05") 722 | req.PostForm = form 723 | 724 | rr = httptest.NewRecorder() 725 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 726 | 727 | if status := rr.Code; status != http.StatusOK { 728 | t.Errorf("handler returned wrong status code: got \n%v want %v", 729 | status, http.StatusOK) 730 | } 731 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 732 | t.Fatal("handler should return a message") 733 | } 734 | 735 | resp := app.ReceiveMessageResponse{} 736 | err = xml.Unmarshal(rr.Body.Bytes(), &resp) 737 | if err != nil { 738 | t.Fatalf("unexpected unmarshal error: %s", err) 739 | } 740 | receiptHandle := resp.Result.Message[0].ReceiptHandle 741 | 742 | // try to receive another message. 743 | req, err = http.NewRequest("POST", "/", nil) 744 | if err != nil { 745 | t.Fatal(err) 746 | } 747 | 748 | form = url.Values{} 749 | form.Add("Action", "ReceiveMessage") 750 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset") 751 | form.Add("Version", "2012-11-05") 752 | req.PostForm = form 753 | 754 | rr = httptest.NewRecorder() 755 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 756 | 757 | if status := rr.Code; status != http.StatusOK { 758 | t.Errorf("handler returned wrong status code: got \n%v want %v", 759 | status, http.StatusOK) 760 | } 761 | if ok := strings.Contains(rr.Body.String(), ""); ok { 762 | t.Fatal("handler should not return a message") 763 | } 764 | 765 | // reset message visibility timeout to requeue it 766 | req, err = http.NewRequest("POST", "/", nil) 767 | if err != nil { 768 | t.Fatal(err) 769 | } 770 | 771 | form = url.Values{} 772 | form.Add("Action", "ChangeMessageVisibility") 773 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset") 774 | form.Add("VisibilityTimeout", "0") 775 | form.Add("ReceiptHandle", receiptHandle) 776 | form.Add("Version", "2012-11-05") 777 | req.PostForm = form 778 | 779 | rr = httptest.NewRecorder() 780 | http.HandlerFunc(ChangeMessageVisibility).ServeHTTP(rr, req) 781 | 782 | // Check the status code is what we expect. 783 | if status := rr.Code; status != http.StatusOK { 784 | t.Errorf("handler returned wrong status code: got \n%v want %v", 785 | status, http.StatusOK) 786 | } 787 | 788 | // message needs to be requeued 789 | req, err = http.NewRequest("POST", "/", nil) 790 | if err != nil { 791 | t.Fatal(err) 792 | } 793 | 794 | form = url.Values{} 795 | form.Add("Action", "ReceiveMessage") 796 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset") 797 | form.Add("Version", "2012-11-05") 798 | req.PostForm = form 799 | 800 | rr = httptest.NewRecorder() 801 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 802 | 803 | if status := rr.Code; status != http.StatusOK { 804 | t.Errorf("handler returned wrong status code: got \n%v want %v", 805 | status, http.StatusOK) 806 | } 807 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 808 | t.Fatal("handler should return a message") 809 | } 810 | done <- struct{}{} 811 | } 812 | 813 | func TestDeadLetterQueue(t *testing.T) { 814 | done := make(chan struct{}, 0) 815 | go PeriodicTasks(1*time.Second, done) 816 | 817 | // create a queue 818 | req, err := http.NewRequest("POST", "/", nil) 819 | if err != nil { 820 | t.Fatal(err) 821 | } 822 | deadLetterQueue := &app.Queue{ 823 | Name: "failed-messages", 824 | Messages: []app.Message{}, 825 | } 826 | app.SyncQueues.Lock() 827 | app.SyncQueues.Queues["failed-messages"] = deadLetterQueue 828 | app.SyncQueues.Unlock() 829 | form := url.Values{} 830 | form.Add("Action", "CreateQueue") 831 | form.Add("QueueName", "testing-deadletter") 832 | form.Add("Attribute.1.Name", "VisibilityTimeout") 833 | form.Add("Attribute.1.Value", "1") 834 | form.Add("Attribute.2.Name", "RedrivePolicy") 835 | form.Add("Attribute.2.Value", `{"maxReceiveCount": 1, "deadLetterTargetArn":"arn:aws:sqs::000000000000:failed-messages"}`) 836 | form.Add("Version", "2012-11-05") 837 | req.PostForm = form 838 | 839 | rr := httptest.NewRecorder() 840 | http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) 841 | 842 | if status := rr.Code; status != http.StatusOK { 843 | t.Errorf("handler returned wrong status code: got \n%v want %v", 844 | status, http.StatusOK) 845 | } 846 | 847 | // send a message 848 | req, err = http.NewRequest("POST", "/", nil) 849 | if err != nil { 850 | t.Fatal(err) 851 | } 852 | 853 | form = url.Values{} 854 | form.Add("Action", "SendMessage") 855 | form.Add("QueueUrl", "http://localhost:4100/queue/testing-deadletter") 856 | form.Add("MessageBody", "1") 857 | form.Add("Version", "2012-11-05") 858 | req.PostForm = form 859 | 860 | rr = httptest.NewRecorder() 861 | http.HandlerFunc(SendMessage).ServeHTTP(rr, req) 862 | 863 | if status := rr.Code; status != http.StatusOK { 864 | t.Errorf("handler returned wrong status code: got \n%v want %v", 865 | status, http.StatusOK) 866 | } 867 | 868 | // receive message 869 | req, err = http.NewRequest("POST", "/", nil) 870 | if err != nil { 871 | t.Fatal(err) 872 | } 873 | 874 | form = url.Values{} 875 | form.Add("Action", "ReceiveMessage") 876 | form.Add("QueueUrl", "http://localhost:4100/queue/testing-deadletter") 877 | form.Add("Version", "2012-11-05") 878 | req.PostForm = form 879 | 880 | rr = httptest.NewRecorder() 881 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 882 | 883 | if status := rr.Code; status != http.StatusOK { 884 | t.Errorf("handler returned wrong status code: got \n%v want %v", 885 | status, http.StatusOK) 886 | } 887 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 888 | t.Fatal("handler should return a message") 889 | } 890 | 891 | time.Sleep(2 * time.Second) 892 | 893 | // receive the message one more time 894 | req, err = http.NewRequest("POST", "/", nil) 895 | if err != nil { 896 | t.Fatal(err) 897 | } 898 | 899 | req.PostForm = form 900 | 901 | rr = httptest.NewRecorder() 902 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 903 | 904 | if status := rr.Code; status != http.StatusOK { 905 | t.Errorf("handler returned wrong status code: got \n%v want %v", 906 | status, http.StatusOK) 907 | } 908 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 909 | t.Fatal("handler should return a message") 910 | } 911 | time.Sleep(2 * time.Second) 912 | 913 | // another receive attempt 914 | req, err = http.NewRequest("POST", "/", nil) 915 | if err != nil { 916 | t.Fatal(err) 917 | } 918 | 919 | req.PostForm = form 920 | 921 | rr = httptest.NewRecorder() 922 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 923 | 924 | if status := rr.Code; status != http.StatusOK { 925 | t.Errorf("handler returned wrong status code: got \n%v want %v", 926 | status, http.StatusOK) 927 | } 928 | if ok := strings.Contains(rr.Body.String(), ""); ok { 929 | t.Fatal("handler should not return a message") 930 | } 931 | if len(deadLetterQueue.Messages) == 0 { 932 | t.Fatal("expected a message") 933 | } 934 | 935 | } 936 | 937 | func TestReceiveMessageWaitTimeEnforced(t *testing.T) { 938 | // create a queue 939 | req, err := http.NewRequest("POST", "/", nil) 940 | if err != nil { 941 | t.Fatal(err) 942 | } 943 | form := url.Values{} 944 | form.Add("Action", "CreateQueue") 945 | form.Add("QueueName", "waiting-queue") 946 | form.Add("Attribute.1.Name", "ReceiveMessageWaitTimeSeconds") 947 | form.Add("Attribute.1.Value", "2") 948 | form.Add("Version", "2012-11-05") 949 | req.PostForm = form 950 | 951 | rr := httptest.NewRecorder() 952 | http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) 953 | 954 | if status := rr.Code; status != http.StatusOK { 955 | t.Errorf("handler returned wrong status code: got \n%v want %v", 956 | status, http.StatusOK) 957 | } 958 | 959 | // receive message ensure delay 960 | req, err = http.NewRequest("POST", "/", nil) 961 | if err != nil { 962 | t.Fatal(err) 963 | } 964 | 965 | form = url.Values{} 966 | form.Add("Action", "ReceiveMessage") 967 | form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") 968 | form.Add("Version", "2012-11-05") 969 | req.PostForm = form 970 | 971 | rr = httptest.NewRecorder() 972 | 973 | start := time.Now() 974 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 975 | elapsed := time.Since(start) 976 | 977 | if status := rr.Code; status != http.StatusOK { 978 | t.Errorf("handler returned wrong status code: got \n%v want %v", 979 | status, http.StatusOK) 980 | } 981 | if ok := strings.Contains(rr.Body.String(), ""); ok { 982 | t.Fatal("handler should not return a message") 983 | } 984 | if elapsed < 2*time.Second { 985 | t.Fatal("handler didn't wait ReceiveMessageWaitTimeSeconds") 986 | } 987 | 988 | // send a message 989 | req, err = http.NewRequest("POST", "/", nil) 990 | if err != nil { 991 | t.Fatal(err) 992 | } 993 | 994 | form = url.Values{} 995 | form.Add("Action", "SendMessage") 996 | form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") 997 | form.Add("MessageBody", "1") 998 | form.Add("Version", "2012-11-05") 999 | req.PostForm = form 1000 | 1001 | rr = httptest.NewRecorder() 1002 | http.HandlerFunc(SendMessage).ServeHTTP(rr, req) 1003 | 1004 | if status := rr.Code; status != http.StatusOK { 1005 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1006 | status, http.StatusOK) 1007 | } 1008 | 1009 | // receive message 1010 | req, err = http.NewRequest("POST", "/", nil) 1011 | if err != nil { 1012 | t.Fatal(err) 1013 | } 1014 | 1015 | form = url.Values{} 1016 | form.Add("Action", "ReceiveMessage") 1017 | form.Add("QueueUrl", "http://localhost:4100/queue/waiting-queue") 1018 | form.Add("Version", "2012-11-05") 1019 | req.PostForm = form 1020 | 1021 | rr = httptest.NewRecorder() 1022 | 1023 | start = time.Now() 1024 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 1025 | elapsed = time.Since(start) 1026 | 1027 | if status := rr.Code; status != http.StatusOK { 1028 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1029 | status, http.StatusOK) 1030 | } 1031 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 1032 | t.Fatal("handler should return a message") 1033 | } 1034 | if elapsed > 1*time.Second { 1035 | t.Fatal("handler waited when message was available, expected not to wait") 1036 | } 1037 | } 1038 | 1039 | func TestSetQueueAttributes_POST_QueueNotFound(t *testing.T) { 1040 | req, err := http.NewRequest("POST", "/", nil) 1041 | if err != nil { 1042 | t.Fatal(err) 1043 | } 1044 | 1045 | form := url.Values{} 1046 | form.Add("Action", "SetQueueAttributes") 1047 | form.Add("QueueUrl", "http://localhost:4100/queue/not-existing") 1048 | form.Add("Version", "2012-11-05") 1049 | req.PostForm = form 1050 | 1051 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 1052 | rr := httptest.NewRecorder() 1053 | handler := http.HandlerFunc(SetQueueAttributes) 1054 | 1055 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 1056 | // directly and pass in our Request and ResponseRecorder. 1057 | handler.ServeHTTP(rr, req) 1058 | 1059 | // Check the status code is what we expect. 1060 | if status := rr.Code; status != http.StatusBadRequest { 1061 | t.Errorf("handler returned wrong status code: got %v want %v", 1062 | status, http.StatusBadRequest) 1063 | } 1064 | 1065 | // Check the response body is what we expect. 1066 | expected := "NonExistentQueue" 1067 | if !strings.Contains(rr.Body.String(), expected) { 1068 | t.Errorf("handler returned unexpected body: got %v want %v", 1069 | rr.Body.String(), expected) 1070 | } 1071 | } 1072 | 1073 | func TestSendingAndReceivingFromFIFOQueueReturnsSameMessageOnError(t *testing.T) { 1074 | done := make(chan struct{}, 0) 1075 | go PeriodicTasks(1*time.Second, done) 1076 | 1077 | // create a queue 1078 | req, err := http.NewRequest("POST", "/", nil) 1079 | if err != nil { 1080 | t.Fatal(err) 1081 | } 1082 | 1083 | form := url.Values{} 1084 | form.Add("Action", "CreateQueue") 1085 | form.Add("QueueName", "requeue-reset.fifo") 1086 | form.Add("Attribute.1.Name", "VisibilityTimeout") 1087 | form.Add("Attribute.1.Value", "2") 1088 | form.Add("Version", "2012-11-05") 1089 | req.PostForm = form 1090 | 1091 | rr := httptest.NewRecorder() 1092 | http.HandlerFunc(CreateQueue).ServeHTTP(rr, req) 1093 | 1094 | if status := rr.Code; status != http.StatusOK { 1095 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1096 | status, http.StatusOK) 1097 | } 1098 | 1099 | // send a message 1100 | req, err = http.NewRequest("POST", "/", nil) 1101 | if err != nil { 1102 | t.Fatal(err) 1103 | } 1104 | 1105 | form = url.Values{} 1106 | form.Add("Action", "SendMessage") 1107 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset.fifo") 1108 | form.Add("MessageBody", "1") 1109 | form.Add("MessageGroupId", "GROUP-X") 1110 | form.Add("Version", "2012-11-05") 1111 | req.PostForm = form 1112 | 1113 | rr = httptest.NewRecorder() 1114 | http.HandlerFunc(SendMessage).ServeHTTP(rr, req) 1115 | 1116 | if status := rr.Code; status != http.StatusOK { 1117 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1118 | status, http.StatusOK) 1119 | } 1120 | 1121 | // send a message 1122 | req, err = http.NewRequest("POST", "/", nil) 1123 | if err != nil { 1124 | t.Fatal(err) 1125 | } 1126 | 1127 | form = url.Values{} 1128 | form.Add("Action", "SendMessage") 1129 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset.fifo") 1130 | form.Add("MessageBody", "2") 1131 | form.Add("MessageGroupId", "GROUP-X") 1132 | form.Add("Version", "2012-11-05") 1133 | req.PostForm = form 1134 | 1135 | rr = httptest.NewRecorder() 1136 | http.HandlerFunc(SendMessage).ServeHTTP(rr, req) 1137 | 1138 | if status := rr.Code; status != http.StatusOK { 1139 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1140 | status, http.StatusOK) 1141 | } 1142 | 1143 | // receive message 1144 | req, err = http.NewRequest("POST", "/", nil) 1145 | if err != nil { 1146 | t.Fatal(err) 1147 | } 1148 | 1149 | form = url.Values{} 1150 | form.Add("Action", "ReceiveMessage") 1151 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset.fifo") 1152 | form.Add("Version", "2012-11-05") 1153 | req.PostForm = form 1154 | 1155 | rr = httptest.NewRecorder() 1156 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 1157 | 1158 | if status := rr.Code; status != http.StatusOK { 1159 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1160 | status, http.StatusOK) 1161 | } 1162 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 1163 | t.Fatal("handler should return a message") 1164 | } 1165 | 1166 | resp := app.ReceiveMessageResponse{} 1167 | err = xml.Unmarshal(rr.Body.Bytes(), &resp) 1168 | if err != nil { 1169 | t.Fatalf("unexpected unmarshal error: %s", err) 1170 | } 1171 | receiptHandleFirst := resp.Result.Message[0].ReceiptHandle 1172 | if string(resp.Result.Message[0].Body) != "1" { 1173 | t.Fatalf("should have received body 1: %s", err) 1174 | } 1175 | 1176 | // try to receive another message and we should get none 1177 | req, err = http.NewRequest("POST", "/", nil) 1178 | if err != nil { 1179 | t.Fatal(err) 1180 | } 1181 | 1182 | form = url.Values{} 1183 | form.Add("Action", "ReceiveMessage") 1184 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset.fifo") 1185 | form.Add("Version", "2012-11-05") 1186 | req.PostForm = form 1187 | 1188 | rr = httptest.NewRecorder() 1189 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 1190 | 1191 | if status := rr.Code; status != http.StatusOK { 1192 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1193 | status, http.StatusOK) 1194 | } 1195 | if ok := strings.Contains(rr.Body.String(), ""); ok { 1196 | t.Fatal("handler should not return a message") 1197 | } 1198 | 1199 | if len(app.SyncQueues.Queues["requeue-reset.fifo"].FIFOMessages) != 1 { 1200 | t.Fatal("there should be only 1 group locked") 1201 | } 1202 | 1203 | if app.SyncQueues.Queues["requeue-reset.fifo"].FIFOMessages["GROUP-X"] != 0 { 1204 | t.Fatal("there should be GROUP-X locked") 1205 | } 1206 | 1207 | // remove message 1208 | req, err = http.NewRequest("POST", "/", nil) 1209 | if err != nil { 1210 | t.Fatal(err) 1211 | } 1212 | 1213 | form = url.Values{} 1214 | form.Add("Action", "DeleteMessage") 1215 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset.fifo") 1216 | form.Add("ReceiptHandle", receiptHandleFirst) 1217 | form.Add("Version", "2012-11-05") 1218 | req.PostForm = form 1219 | 1220 | rr = httptest.NewRecorder() 1221 | http.HandlerFunc(DeleteMessage).ServeHTTP(rr, req) 1222 | 1223 | // Check the status code is what we expect. 1224 | if status := rr.Code; status != http.StatusOK { 1225 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1226 | status, http.StatusOK) 1227 | } 1228 | if len(app.SyncQueues.Queues["requeue-reset.fifo"].Messages) != 1 { 1229 | t.Fatal("there should be only 1 message in queue") 1230 | } 1231 | 1232 | // receive message - loop until visibility timeouts 1233 | for { 1234 | req, err = http.NewRequest("POST", "/", nil) 1235 | if err != nil { 1236 | t.Fatal(err) 1237 | } 1238 | 1239 | form = url.Values{} 1240 | form.Add("Action", "ReceiveMessage") 1241 | form.Add("QueueUrl", "http://localhost:4100/queue/requeue-reset.fifo") 1242 | form.Add("Version", "2012-11-05") 1243 | req.PostForm = form 1244 | 1245 | rr = httptest.NewRecorder() 1246 | http.HandlerFunc(ReceiveMessage).ServeHTTP(rr, req) 1247 | 1248 | if status := rr.Code; status != http.StatusOK { 1249 | t.Errorf("handler returned wrong status code: got \n%v want %v", 1250 | status, http.StatusOK) 1251 | } 1252 | if ok := strings.Contains(rr.Body.String(), ""); !ok { 1253 | continue 1254 | } 1255 | 1256 | resp = app.ReceiveMessageResponse{} 1257 | err = xml.Unmarshal(rr.Body.Bytes(), &resp) 1258 | if err != nil { 1259 | t.Fatalf("unexpected unmarshal error: %s", err) 1260 | } 1261 | if string(resp.Result.Message[0].Body) != "2" { 1262 | t.Fatalf("should have received body 2: %s", err) 1263 | } 1264 | break 1265 | } 1266 | 1267 | done <- struct{}{} 1268 | } 1269 | --------------------------------------------------------------------------------