├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── AUTHORS ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── blacklist.example.yml ├── d3.v4.min.js ├── fetch.js ├── index.html ├── pad.js ├── promise.js ├── script.js ├── socket.io.js ├── style.css └── visualize.js ├── auth ├── auth.go ├── auth_test.go ├── exchanger.go ├── exchanger_test.go ├── memory_auth.go ├── redis_auth.go └── redis_auth_test.go ├── backend ├── amqp │ ├── amqp.go │ ├── amqp_test.go │ └── doc.go ├── backend.go ├── dummy │ ├── dummy.go │ ├── dummy_http.go │ ├── dummy_test.go │ └── http.go ├── mqtt │ ├── doc.go │ ├── mqtt.go │ └── mqtt_test.go ├── pktfwd │ ├── LICENSE │ ├── backend.go │ ├── backend_test.go │ ├── packettype_string.go │ ├── pktfwd.go │ ├── security.go │ ├── security_test.go │ ├── structs.go │ └── structs_test.go └── ttn │ ├── router.go │ └── router_test.go ├── cmd ├── cmd.go ├── config.go └── root.go ├── docker-compose.yml ├── exchange ├── exchange.go ├── exchange_test.go ├── metrics.go ├── state.go ├── state_test.go └── watchdog.go ├── go.mod ├── go.sum ├── main.go ├── middleware ├── blacklist │ ├── blacklist.go │ └── blacklist_test.go ├── debug │ └── debug.go ├── deduplicate │ ├── deduplicate.go │ └── deduplicate_test.go ├── gatewayinfo │ ├── gatewayinfo.go │ └── gatewayinfo_test.go ├── inject │ ├── inject.go │ └── inject_test.go ├── lorafilter │ ├── lorafilter.go │ └── lorafilter_test.go ├── middleware.go ├── middleware_test.go └── ratelimit │ ├── ratelimit.go │ └── ratelimit_test.go └── types ├── types.go ├── types.pb.go └── types.proto /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [Makefile] 19 | indent_style = tab 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | /release 27 | /vendor/*/ 28 | /.cover 29 | 30 | # Generated databases 31 | *.db 32 | 33 | # Generate coverage profile 34 | coverage.out 35 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - build 4 | - sign 5 | - package 6 | 7 | variables: 8 | GO111MODULE: 'on' 9 | CONTAINER_NAME: thethingsnetwork/gateway-connector-bridge 10 | PLATFORMS: linux-386 linux-amd64 linux-arm darwin-amd64 windows-386 windows-amd64 11 | 12 | cache: 13 | key: "$CI_PROJECT_PATH" 14 | paths: 15 | - /go/pkg/mod 16 | 17 | tests: 18 | stage: test 19 | image: golang:1.12 20 | services: 21 | - thethingsnetwork/rabbitmq 22 | - redis 23 | variables: 24 | REDIS_HOST: redis 25 | MQTT_ADDRESS: thethingsnetwork__rabbitmq:1883 26 | AMQP_ADDRESS: thethingsnetwork__rabbitmq:5672 27 | script: 28 | - make deps 29 | - make test 30 | 31 | binaries: 32 | stage: build 33 | image: golang:1.12 34 | script: 35 | - mkdir release 36 | - export CI_BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) 37 | - echo "date $CI_BUILD_DATE" >> release/info 38 | - echo "commit $CI_BUILD_REF" >> release/info 39 | - make deps 40 | - for platform in $PLATFORMS; do $(echo $platform | awk -F '-' '{print "export GOOS=" $1 " GOARCH=" $2}') && make build; done 41 | artifacts: 42 | paths: 43 | - release/ 44 | 45 | sign: 46 | only: 47 | - tags 48 | - master@thethingsnetwork/gateway-connector-bridge 49 | - develop@thethingsnetwork/gateway-connector-bridge 50 | stage: sign 51 | image: golang:1.12 52 | script: 53 | - pushd release 54 | - shasum -a 256 $(ls) > checksums 55 | - mkdir ~/.gnupg && chmod 700 ~/.gnupg 56 | - echo -e "use-agent\npinentry-mode loopback" > ~/.gnupg/gpg.conf 57 | - echo "allow-loopback-pinentry" > ~/.gnupg/gpg-agent.conf 58 | - gpg --no-tty --batch --import /gpg/signing.ci.gpg-key 59 | - echo $GPG_PASSPHRASE | gpg --batch --no-tty --yes --passphrase-fd 0 --detach-sign checksums 60 | - popd 61 | artifacts: 62 | paths: 63 | - release/checksums 64 | - release/checksums.sig 65 | 66 | gitlab-image: 67 | stage: package 68 | image: docker:git 69 | services: 70 | - "docker:dind" 71 | variables: 72 | DOCKER_DRIVER: overlay2 73 | DOCKER_TLS_CERTDIR: /certs 74 | script: 75 | - docker build -t ttn . 76 | - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" registry.gitlab.com 77 | - docker tag ttn registry.gitlab.com/$CONTAINER_NAME:$CI_BUILD_REF_NAME 78 | - docker push registry.gitlab.com/$CONTAINER_NAME:$CI_BUILD_REF_NAME 79 | 80 | dockerhub-image: 81 | only: 82 | - tags 83 | - master@thethingsnetwork/gateway-connector-bridge 84 | - develop@thethingsnetwork/gateway-connector-bridge 85 | stage: package 86 | image: docker:git 87 | services: 88 | - "docker:dind" 89 | variables: 90 | DOCKER_DRIVER: overlay2 91 | DOCKER_TLS_CERTDIR: /certs 92 | script: 93 | - docker build -t ttn . 94 | - docker login -u "$DOCKERHUB_USER" -p "$DOCKERHUB_PASSWORD" 95 | - docker tag ttn $CONTAINER_NAME:$CI_BUILD_REF_NAME 96 | - docker push $CONTAINER_NAME:$CI_BUILD_REF_NAME 97 | - if [[ "$CI_BUILD_REF_NAME" == "master" ]]; then docker tag ttn $CONTAINER_NAME:latest && docker push $CONTAINER_NAME:latest; fi 98 | 99 | azure-binaries: 100 | only: 101 | - tags 102 | - master@thethingsnetwork/gateway-connector-bridge 103 | - develop@thethingsnetwork/gateway-connector-bridge 104 | stage: package 105 | image: registry.gitlab.com/thethingsindustries/upload 106 | script: 107 | - cd release 108 | - export STORAGE_CONTAINER=gateway-connector-bridge STORAGE_KEY=$AZURE_STORAGE_KEY ZIP=true TGZ=true PREFIX=$CI_BUILD_REF_NAME/ 109 | - upload * 110 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go_import_path: github.com/TheThingsNetwork/gateway-connector-bridge 4 | 5 | sudo: required 6 | 7 | services: 8 | - docker 9 | 10 | go: 11 | - "1.12.x" 12 | 13 | env: 14 | global: 15 | - GOPROXY=https://proxy.golang.org 16 | - GO111MODULE=on 17 | 18 | cache: 19 | directories: 20 | - $HOME/gopath/pkg/mod 21 | 22 | install: 23 | - make deps 24 | - make cover-deps 25 | 26 | before_script: 27 | - docker run -d -p 127.0.0.1:6379:6379 redis 28 | - docker run -d -p 127.0.0.1:1883:1883 -p 127.0.0.1:5672:5672 thethingsnetwork/rabbitmq 29 | 30 | script: 31 | - make test 32 | - make fmt 33 | - make vet 34 | 35 | after_success: 36 | - make coveralls 37 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of The Things Network Gateway Connector Bridge authors for copyright purposes. 2 | # 3 | # The copyright owners listed in this document agree to release their work under 4 | # the MIT license that can be found in the LICENSE file. 5 | # 6 | # Names should be added to this file as 7 | # Firstname Lastname 8 | # 9 | # Please keep the list sorted. 10 | 11 | Hylke Visser 12 | Johan Stokking 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | RUN apk --update --no-cache add ca-certificates 3 | ADD ./release/gateway-connector-bridge-linux-amd64 /usr/local/bin/gateway-connector-bridge 4 | ADD ./assets ./assets 5 | RUN chmod 755 /usr/local/bin/gateway-connector-bridge 6 | ENTRYPOINT ["/usr/local/bin/gateway-connector-bridge"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 The Things Network 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = bash 2 | 3 | # Environment 4 | 5 | GIT_BRANCH = $(or $(CI_BUILD_REF_NAME) ,`git rev-parse --abbrev-ref HEAD 2>/dev/null`) 6 | GIT_COMMIT = $(or $(CI_BUILD_REF), `git rev-parse HEAD 2>/dev/null`) 7 | GIT_TAG = $(shell git describe --abbrev=0 --tags 2>/dev/null) 8 | BUILD_DATE = $(or $(CI_BUILD_DATE), `date -u +%Y-%m-%dT%H:%M:%SZ`) 9 | 10 | ifeq ($(GIT_BRANCH), $(GIT_TAG)) 11 | TTN_VERSION = $(GIT_TAG) 12 | TAGS += prod 13 | else 14 | TTN_VERSION = $(GIT_TAG)-dev 15 | TAGS += dev 16 | endif 17 | 18 | # All 19 | 20 | .PHONY: all deps dev-deps test cover-clean cover-deps cover coveralls fmt vet build dev link docs clean docker 21 | 22 | all: deps build 23 | 24 | # Deps 25 | 26 | deps: 27 | go mod download 28 | 29 | dev-deps: deps 30 | @command -v forego > /dev/null || go get github.com/ddollar/forego 31 | 32 | # Go Test 33 | 34 | GO_FILES = $(shell find . -name "*.go" | grep -vE ".git|.env|vendor") 35 | 36 | test: $(GO_FILES) 37 | go test -v ./... 38 | 39 | GO_COVER_FILE ?= coverage.out 40 | 41 | cover-clean: 42 | rm -f $(GO_COVER_FILE) 43 | 44 | cover-deps: 45 | @command -v goveralls > /dev/null || go get github.com/mattn/goveralls 46 | 47 | cover: 48 | go test -cover -coverprofile=$(GO_COVER_FILE) ./... 49 | 50 | coveralls: cover-deps $(GO_COVER_FILE) 51 | goveralls -coverprofile=$(GO_COVER_FILE) -service=travis-ci -repotoken $$COVERALLS_TOKEN 52 | 53 | fmt: 54 | go fmt ./... 55 | 56 | vet: 57 | go vet ./... 58 | 59 | # Go Build 60 | 61 | RELEASE_DIR ?= release 62 | GOOS ?= $(shell go env GOOS) 63 | GOARCH ?= $(shell go env GOARCH) 64 | GOEXE = $(shell GOOS=$(GOOS) GOARCH=$(GOARCH) go env GOEXE) 65 | CGO_ENABLED ?= 0 66 | 67 | splitfilename = $(subst ., ,$(subst -, ,$(subst $(RELEASE_DIR)/,,$1))) 68 | GOOSfromfilename = $(word 4, $(call splitfilename, $1)) 69 | GOARCHfromfilename = $(word 5, $(call splitfilename, $1)) 70 | LDFLAGS = -ldflags "-w -X main.gitBranch=${GIT_BRANCH} -X main.gitCommit=${GIT_COMMIT} -X main.buildDate=${BUILD_DATE}" 71 | GOBUILD = CGO_ENABLED=$(CGO_ENABLED) GOOS=$(call GOOSfromfilename, $@) GOARCH=$(call GOARCHfromfilename, $@) go build ${LDFLAGS} -tags "${TAGS}" -o "$@" 72 | 73 | build: $(RELEASE_DIR)/gateway-connector-bridge-$(GOOS)-$(GOARCH)$(GOEXE) 74 | 75 | $(RELEASE_DIR)/gateway-connector-bridge-%: $(GO_FILES) 76 | $(GOBUILD) . 77 | 78 | install: 79 | go install -v 80 | 81 | # Clean 82 | 83 | clean: 84 | [ -d $(RELEASE_DIR) ] && rm -rf $(RELEASE_DIR) || [ ! -d $(RELEASE_DIR) ] 85 | 86 | # Docker 87 | 88 | docker: GOOS=linux 89 | docker: GOARCH=amd64 90 | docker: $(RELEASE_DIR)/gateway-connector-bridge-linux-amd64 91 | docker build -t thethingsnetwork/gateway-connector-bridge -f Dockerfile . 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Things Network Gateway Connector Bridge 2 | 3 | [![Build Status](https://travis-ci.org/TheThingsNetwork/gateway-connector-bridge.svg?branch=master)](https://travis-ci.org/TheThingsNetwork/gateway-connector-bridge) [![Coverage Status](https://coveralls.io/repos/github/TheThingsNetwork/gateway-connector-bridge/badge.svg?branch=master)](https://coveralls.io/github/TheThingsNetwork/gateway-connector-bridge?branch=master) [![GoDoc](https://godoc.org/github.com/TheThingsNetwork/gateway-connector-bridge?status.svg)](https://godoc.org/github.com/TheThingsNetwork/gateway-connector-bridge) 4 | 5 | ![The Things Network](https://thethings.blob.core.windows.net/ttn/logo.svg) 6 | 7 | ## Installation 8 | 9 | Download precompiled binaries for [64 bit Linux][download-linux-amd64], [32 bit Linux][download-linux-386], [ARM Linux][download-linux-arm], [macOS][download-darwin-amd64], [64 bit Windows][download-windows-amd64], [32 bit Windows][download-windows-386]. 10 | 11 | [download-linux-amd64]: https://ttnreleases.blob.core.windows.net/gateway-connector-bridge/master/gateway-connector-bridge-linux-amd64.zip 12 | [download-linux-386]: https://ttnreleases.blob.core.windows.net/gateway-connector-bridge/master/gateway-connector-bridge-linux-386.zip 13 | [download-linux-arm]: https://ttnreleases.blob.core.windows.net/gateway-connector-bridge/master/gateway-connector-bridge-linux-arm.zip 14 | [download-darwin-amd64]: https://ttnreleases.blob.core.windows.net/gateway-connector-bridge/master/gateway-connector-bridge-darwin-amd64.zip 15 | [download-windows-amd64]: https://ttnreleases.blob.core.windows.net/gateway-connector-bridge/master/gateway-connector-bridge-windows-amd64.exe.zip 16 | [download-windows-386]: https://ttnreleases.blob.core.windows.net/gateway-connector-bridge/master/gateway-connector-bridge-windows-386.exe.zip 17 | 18 | Other requirements are: 19 | 20 | - [Redis](http://redis.io/download) 21 | - An MQTT Broker (see also the [Security](#security) section) 22 | 23 | ## Usage 24 | 25 | ``` 26 | Usage: 27 | gateway-connector-bridge [flags] 28 | 29 | Flags: 30 | --account-server string Use an account server for exchanging access keys and fetching gateway information (default "https://account.thethingsnetwork.org") 31 | --amqp stringSlice AMQP Broker to connect to (user:pass@host:port; disable with "disable") 32 | --debug Print debug logs 33 | --http-debug-addr string The address of the HTTP debug server to start 34 | --id string ID of this bridge 35 | --info-expire duration Gateway Information expiration time (default 1h0m0s) 36 | --inject-frequency-plan string Inject a frequency plan field into status message that don't have one 37 | --log-file string Location of the log file 38 | --mqtt stringSlice MQTT Broker to connect to (user:pass@host:port; disable with "disable") (default [guest:guest@localhost:1883]) 39 | --ratelimit Rate-limit messages 40 | --ratelimit-downlink uint Downlink rate limit (per gateway per minute) 41 | --ratelimit-status uint Status rate limit (per gateway per minute) (default 20) 42 | --ratelimit-uplink uint Uplink rate limit (per gateway per minute) (default 600) 43 | --redis Use Redis auth backend (default true) 44 | --redis-address string Redis host and port (default "localhost:6379") 45 | --redis-db int Redis database 46 | --redis-password string Redis password 47 | --root-ca-file string Location of the file containing Root CA certificates 48 | --route-unknown-gateways Route traffic for unknown gateways 49 | --status-addr string Address of the gRPC status server to start 50 | --status-key stringSlice Access key for the gRPC status server 51 | --ttn-router stringSlice TTN Router to connect to (default [discover.thethingsnetwork.org:1900/ttn-router-eu]) 52 | --udp string UDP address to listen on for Semtech Packet Forwarder gateways 53 | --udp-lock-ip Lock gateways to IP addresses for the session duration (default true) 54 | --udp-lock-port Additional to udp-lock-ip, also lock gateways to ports for the session duration 55 | --udp-session duration Duration of gateway sessions (default 1m0s) 56 | --workers int Number of parallel workers (default 1) 57 | ``` 58 | 59 | For running in Docker, please refer to [`docker-compose.yml`](docker-compose.yml). 60 | 61 | ## Protocol 62 | 63 | The Things Network's `gateway-connector` protocol sends protocol buffers over MQTT. 64 | 65 | - Connect to MQTT with your gateway's ID as username and Access Key as password. 66 | - On MQTT brokers that don't support authentication, you can connect without authentication. 67 | - After connect: send [`types.ConnectMessage`](types/types.proto) on topic `connect`. 68 | - Supply the gateway's ID and Access Key to authenticate with the backend 69 | - On disconnect: send [`types.DisconnectMessage`](types/types.proto) on topic `disconnect`. 70 | - Supply the same ID and Access Key as in the `ConnectMessage`. 71 | - Use the "will" feature of MQTT to send the `DisconnectMessage` when the gateway unexpectedly disconnects. 72 | - On uplink: send [`router.UplinkMessage`](https://github.com/TheThingsNetwork/api/blob/master/router/router.proto) on topic `/up`. 73 | - For downlink: subscribe to topic `/down` and receive [`router.DownlinkMessage`](https://github.com/TheThingsNetwork/api/blob/master/router/router.proto). 74 | - On status: send [`gateway.Status`](https://github.com/TheThingsNetwork/api/blob/master/gateway/gateway.proto) on topic `/status`. 75 | 76 | ## Security 77 | 78 | ⚠️ MQTT brokers should support authentication and access control: 79 | 80 | - The `connect`, `disconnect`, `/up`, `/status` topics **must only allow** 81 | - **publish** for authenticated gateways with ``. 82 | - **subscribe** for the bridge. 83 | - The `/down` topics **must only allow** 84 | - **publish** for the bridge. 85 | - **subscribe** for authenticated gateways with ``. 86 | 87 | ## Development 88 | 89 | - Make sure you have [Go](https://golang.org) installed (recommended version 1.11 or later). 90 | - Set up your [Go environment](https://golang.org/doc/code.html#GOPATH). 91 | - Make sure you have [Redis](http://redis.io/download) **installed** and **running**. 92 | - Make sure you have [RabbitMQ](https://www.rabbitmq.com/download.html) and its [MQTT plugin](https://www.rabbitmq.com/mqtt.html) **installed** and **running**. 93 | - Fork this repository on Github 94 | - `git clone git@github.com:YOURUSERNAME/gateway-connector-bridge.git` 95 | - `cd gateway-connector-bridge` 96 | - `make deps` 97 | - `make test` 98 | - `make build` 99 | 100 | ## License 101 | 102 | Source code for The Things Network is released under the MIT License, which can be found in the [LICENSE](LICENSE) file. A list of authors can be found in the [AUTHORS](AUTHORS) file. 103 | -------------------------------------------------------------------------------- /assets/blacklist.example.yml: -------------------------------------------------------------------------------- 1 | - gateway: malicious 2 | comment: Malicious gateway 3 | - ip: 8.8.8.8 4 | comment: This is a DNS server, not a gateway 5 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bridge Debug 5 | 6 | 7 | 8 |
Connected gateways:
9 |
Timeline:
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /assets/pad.js: -------------------------------------------------------------------------------- 1 | if (!String.prototype.padStart) { 2 | String.prototype.padStart = function (max, fillString) { 3 | return padStart(this, max, fillString); 4 | }; 5 | } 6 | 7 | function padStart (text, max, mask) { 8 | var cur = text.length; 9 | if (max <= cur) { 10 | return text; 11 | } 12 | var masked = max - cur; 13 | var filler = String(mask) || ' '; 14 | while (filler.length < masked) { 15 | filler += filler; 16 | } 17 | var fillerSlice = filler.slice(0, masked); 18 | return fillerSlice + text; 19 | } 20 | 21 | if (!String.prototype.padEnd) { 22 | String.prototype.padEnd = function (max, fillString) { 23 | return padEnd(this, max, fillString); 24 | }; 25 | } 26 | 27 | function padEnd (text, max, mask) { 28 | var cur = text.length; 29 | if (max <= cur) { 30 | return text; 31 | } 32 | var masked = max - cur; 33 | var filler = String(mask) || ' '; 34 | while (filler.length < masked) { 35 | filler += filler; 36 | } 37 | var fillerSlice = filler.slice(0, masked); 38 | return text + fillerSlice; 39 | } 40 | -------------------------------------------------------------------------------- /assets/promise.js: -------------------------------------------------------------------------------- 1 | (function (root) { 2 | 3 | // Store setTimeout reference so promise-polyfill will be unaffected by 4 | // other code modifying setTimeout (like sinon.useFakeTimers()) 5 | var setTimeoutFunc = setTimeout; 6 | 7 | function noop() {} 8 | 9 | // Polyfill for Function.prototype.bind 10 | function bind(fn, thisArg) { 11 | return function () { 12 | fn.apply(thisArg, arguments); 13 | }; 14 | } 15 | 16 | function Promise(fn) { 17 | if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new'); 18 | if (typeof fn !== 'function') throw new TypeError('not a function'); 19 | this._state = 0; 20 | this._handled = false; 21 | this._value = undefined; 22 | this._deferreds = []; 23 | 24 | doResolve(fn, this); 25 | } 26 | 27 | function handle(self, deferred) { 28 | while (self._state === 3) { 29 | self = self._value; 30 | } 31 | if (self._state === 0) { 32 | self._deferreds.push(deferred); 33 | return; 34 | } 35 | self._handled = true; 36 | Promise._immediateFn(function () { 37 | var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; 38 | if (cb === null) { 39 | (self._state === 1 ? resolve : reject)(deferred.promise, self._value); 40 | return; 41 | } 42 | var ret; 43 | try { 44 | ret = cb(self._value); 45 | } catch (e) { 46 | reject(deferred.promise, e); 47 | return; 48 | } 49 | resolve(deferred.promise, ret); 50 | }); 51 | } 52 | 53 | function resolve(self, newValue) { 54 | try { 55 | // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure 56 | if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.'); 57 | if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { 58 | var then = newValue.then; 59 | if (newValue instanceof Promise) { 60 | self._state = 3; 61 | self._value = newValue; 62 | finale(self); 63 | return; 64 | } else if (typeof then === 'function') { 65 | doResolve(bind(then, newValue), self); 66 | return; 67 | } 68 | } 69 | self._state = 1; 70 | self._value = newValue; 71 | finale(self); 72 | } catch (e) { 73 | reject(self, e); 74 | } 75 | } 76 | 77 | function reject(self, newValue) { 78 | self._state = 2; 79 | self._value = newValue; 80 | finale(self); 81 | } 82 | 83 | function finale(self) { 84 | if (self._state === 2 && self._deferreds.length === 0) { 85 | Promise._immediateFn(function() { 86 | if (!self._handled) { 87 | Promise._unhandledRejectionFn(self._value); 88 | } 89 | }); 90 | } 91 | 92 | for (var i = 0, len = self._deferreds.length; i < len; i++) { 93 | handle(self, self._deferreds[i]); 94 | } 95 | self._deferreds = null; 96 | } 97 | 98 | function Handler(onFulfilled, onRejected, promise) { 99 | this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; 100 | this.onRejected = typeof onRejected === 'function' ? onRejected : null; 101 | this.promise = promise; 102 | } 103 | 104 | /** 105 | * Take a potentially misbehaving resolver function and make sure 106 | * onFulfilled and onRejected are only called once. 107 | * 108 | * Makes no guarantees about asynchrony. 109 | */ 110 | function doResolve(fn, self) { 111 | var done = false; 112 | try { 113 | fn(function (value) { 114 | if (done) return; 115 | done = true; 116 | resolve(self, value); 117 | }, function (reason) { 118 | if (done) return; 119 | done = true; 120 | reject(self, reason); 121 | }); 122 | } catch (ex) { 123 | if (done) return; 124 | done = true; 125 | reject(self, ex); 126 | } 127 | } 128 | 129 | Promise.prototype['catch'] = function (onRejected) { 130 | return this.then(null, onRejected); 131 | }; 132 | 133 | Promise.prototype.then = function (onFulfilled, onRejected) { 134 | var prom = new (this.constructor)(noop); 135 | 136 | handle(this, new Handler(onFulfilled, onRejected, prom)); 137 | return prom; 138 | }; 139 | 140 | Promise.all = function (arr) { 141 | var args = Array.prototype.slice.call(arr); 142 | 143 | return new Promise(function (resolve, reject) { 144 | if (args.length === 0) return resolve([]); 145 | var remaining = args.length; 146 | 147 | function res(i, val) { 148 | try { 149 | if (val && (typeof val === 'object' || typeof val === 'function')) { 150 | var then = val.then; 151 | if (typeof then === 'function') { 152 | then.call(val, function (val) { 153 | res(i, val); 154 | }, reject); 155 | return; 156 | } 157 | } 158 | args[i] = val; 159 | if (--remaining === 0) { 160 | resolve(args); 161 | } 162 | } catch (ex) { 163 | reject(ex); 164 | } 165 | } 166 | 167 | for (var i = 0; i < args.length; i++) { 168 | res(i, args[i]); 169 | } 170 | }); 171 | }; 172 | 173 | Promise.resolve = function (value) { 174 | if (value && typeof value === 'object' && value.constructor === Promise) { 175 | return value; 176 | } 177 | 178 | return new Promise(function (resolve) { 179 | resolve(value); 180 | }); 181 | }; 182 | 183 | Promise.reject = function (value) { 184 | return new Promise(function (resolve, reject) { 185 | reject(value); 186 | }); 187 | }; 188 | 189 | Promise.race = function (values) { 190 | return new Promise(function (resolve, reject) { 191 | for (var i = 0, len = values.length; i < len; i++) { 192 | values[i].then(resolve, reject); 193 | } 194 | }); 195 | }; 196 | 197 | // Use polyfill for setImmediate for performance gains 198 | Promise._immediateFn = (typeof setImmediate === 'function' && function (fn) { setImmediate(fn); }) || 199 | function (fn) { 200 | setTimeoutFunc(fn, 0); 201 | }; 202 | 203 | Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) { 204 | if (typeof console !== 'undefined' && console) { 205 | console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console 206 | } 207 | }; 208 | 209 | /** 210 | * Set the immediate function to execute callbacks 211 | * @param fn {function} Function to execute 212 | * @deprecated 213 | */ 214 | Promise._setImmediateFn = function _setImmediateFn(fn) { 215 | Promise._immediateFn = fn; 216 | }; 217 | 218 | /** 219 | * Change the function to execute on unhandled rejection 220 | * @param {function} fn Function to execute on unhandled rejection 221 | * @deprecated 222 | */ 223 | Promise._setUnhandledRejectionFn = function _setUnhandledRejectionFn(fn) { 224 | Promise._unhandledRejectionFn = fn; 225 | }; 226 | 227 | if (typeof module !== 'undefined' && module.exports) { 228 | module.exports = Promise; 229 | } else if (!root.Promise) { 230 | root.Promise = Promise; 231 | } 232 | 233 | })(this); 234 | -------------------------------------------------------------------------------- /assets/script.js: -------------------------------------------------------------------------------- 1 | function updateGateways() { 2 | window.fetch('/gateways') 3 | .then(function(response) { 4 | return response.text() 5 | }) 6 | .then(function(response) { 7 | return JSON.parse(response) 8 | }) 9 | .then(function(gateways) { 10 | document.getElementById("gateways-list").innerHTML = gateways.join(", ") 11 | }); 12 | } 13 | 14 | var socket = io() 15 | var events = document.getElementById("events") 16 | socket.on('status', function (msg) { 17 | var message = JSON.parse(msg) 18 | var html = ( 19 | '
' + 20 | '
' + new Date().toLocaleString() + '
' + 21 | '
status
' + 22 | '
' + 23 | JSON.stringify(message, null, 2) + 24 | '
' + 25 | '
' 26 | ) 27 | events.insertAdjacentHTML('afterbegin', html) 28 | }) 29 | socket.on('gtw-connect', function (gatewayID) { 30 | var html = ( 31 | '
' + 32 | '
' + new Date().toLocaleString() + '
' + 33 | '
connect
' + 34 | '
' + 35 | gatewayID + 36 | '
' + 37 | '
' 38 | ) 39 | events.insertAdjacentHTML('afterbegin', html) 40 | updateGateways() 41 | }) 42 | socket.on('gtw-disconnect', function (gatewayID) { 43 | var html = ( 44 | '
' + 45 | '
' + new Date().toLocaleString() + '
' + 46 | '
disconnect
' + 47 | '
' + 48 | gatewayID + 49 | '
' + 50 | '
' 51 | ) 52 | events.insertAdjacentHTML('afterbegin', html) 53 | updateGateways() 54 | }) 55 | socket.on('uplink', function (msg) { 56 | var message = JSON.parse(msg) 57 | delete message.Message.trace 58 | var html = ( 59 | '' 66 | ) 67 | events.insertAdjacentHTML('afterbegin', html) 68 | }) 69 | socket.on('downlink', function (msg) { 70 | var message = JSON.parse(msg) 71 | var trace = message.Message.trace.parents.map(function (el) { 72 | el.time = new Date(el.time / 1000000) 73 | return el 74 | }) 75 | delete message.Message.trace 76 | var html = ( 77 | '' 84 | ) 85 | events.insertAdjacentHTML('afterbegin', html) 86 | visualizeEvents("#trace", trace) 87 | }) 88 | 89 | updateGateways() 90 | -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | background: #eee; 4 | margin: 0; 5 | } 6 | 7 | #gateways, #events, #timeline { 8 | padding: 1em; 9 | } 10 | 11 | #trace { 12 | width: 100%; 13 | } 14 | 15 | #gateways { 16 | background: #e0e0e0; 17 | } 18 | 19 | #timeline { 20 | background: #e8e8e8; 21 | } 22 | 23 | .event .title { 24 | margin-bottom: .5em; 25 | } 26 | 27 | .uplink .title { 28 | color: green; 29 | } 30 | 31 | .downlink .title { 32 | color: red; 33 | } 34 | 35 | .status .title { 36 | color: blue; 37 | } 38 | 39 | .event .time { 40 | font-family: monospace; 41 | margin-bottom: .5em; 42 | } 43 | 44 | .event .data { 45 | white-space: pre; 46 | font-family: monospace; 47 | background: #fafafa; 48 | padding: .5em; 49 | border-radius: 2px; 50 | border: 1px solid #ddd; 51 | } 52 | 53 | .event { 54 | border: 1px solid #ccc; 55 | border-bottom: 1px solid #bbb; 56 | padding: 1em; 57 | border-radius: 5px; 58 | background: white; 59 | margin-bottom: 1em; 60 | } 61 | -------------------------------------------------------------------------------- /assets/visualize.js: -------------------------------------------------------------------------------- 1 | var offsets = { 2 | "bridge": 1, 3 | "router": 2, 4 | "broker": 3, 5 | "networkserver": 4, 6 | "handler": 5, 7 | } 8 | 9 | var colors = { 10 | "bridge": "rgb(0, 51, 153)", 11 | "router": "rgb(204, 102, 0)", 12 | "broker": "rgb(51, 153, 51)", 13 | "networkserver": "rgb(255, 204, 0)", 14 | "handler": "rgb(102, 0, 153)", 15 | } 16 | 17 | var dotSize = 10 18 | var padding = 2 19 | var axisHeight = 20 20 | 21 | var height = (dotSize + padding) * 6 + axisHeight 22 | 23 | var tooltip = d3.select("body").append("div") 24 | .attr("class", "tooltip") 25 | .style("opacity", 0) 26 | 27 | function fmtTime(d) { 28 | minutes = ("" + d.getMinutes()).padStart(2, "0") 29 | seconds = ("" + d.getSeconds()).padStart(2, "0") 30 | millis = ("" + d.getMilliseconds()).padEnd(3, "0") 31 | return d.getHours() + ":" + minutes + ":" + seconds + "." + millis 32 | } 33 | 34 | function visualizeEvents(selector, evts) { 35 | var svg = d3.select(selector) 36 | .style("height", height + "px") 37 | 38 | var width = svg.node().clientWidth 39 | if (width === 0) { 40 | width = svg.node().parentNode.clientWidth 41 | } 42 | 43 | var xScale = d3.scaleTime() 44 | .domain([ 45 | d3.min(evts, function (el) { return el.time }), 46 | d3.max(evts, function (el) { return el.time }), 47 | ]) 48 | .range([dotSize, width - dotSize]) 49 | 50 | var axis = svg.select("line") 51 | .attr("x2", width + "px") 52 | 53 | var circles = svg.selectAll("circle").data(evts) 54 | 55 | circles 56 | .text(function (d) { return d.service_name }) 57 | .style("fill", function (d) { 58 | if (colors[d.service_name]) { 59 | return colors[d.service_name] 60 | } 61 | return "black" 62 | }) 63 | .transition().duration(500).ease(d3.easeBounce) 64 | .delay(function (d, i) { return i * 10 }) 65 | .attr("cy", function (d) { return (dotSize + padding) * offsets[d.service_name] + "px" }) 66 | .attr("cx", function (d) { return xScale(d.time) + "px" }) 67 | 68 | var currentEvent = d3.select("#current-event") 69 | 70 | circles.enter().append("circle") 71 | .on("mouseover", function (d) { 72 | d3.select(this).transition().duration(50).attr("r", function (d) { return dotSize + "px" }) 73 | var meta = "" 74 | if (d.metadata) { 75 | var metaList = [] 76 | Object.keys(d.metadata).forEach(function(k) { 77 | metaList.push("" + k + "=" + d.metadata[k] + "") 78 | }) 79 | meta = " (" + metaList.join(", ") + ")" 80 | } 81 | currentEvent.html(d.event + " at " + d.service_name + ' "' + d.service_id + '" ' + meta) 82 | }) 83 | .on("mouseout", function (d) { 84 | d3.select(this).transition().duration(50).attr("r", function (d) { return dotSize / 2 + "px" }) 85 | currentEvent.text("") 86 | }) 87 | .text(function (d) { return d.service_name }) 88 | .attr("r", function (d) { return dotSize / 2 + "px" }) 89 | .attr("cy", "-10px") 90 | .attr("cx", function (d) { return xScale(d.time) + "px" }) 91 | .style("fill", function (d) { 92 | if (colors[d.service_name]) { 93 | return colors[d.service_name] 94 | } 95 | return "black" 96 | }) 97 | .transition().duration(500).ease(d3.easeBounce) 98 | .delay(function (d, i) { return i * 10 }) 99 | .attr("cy", function (d) { return (dotSize + padding) * offsets[d.service_name] + "px" }) 100 | 101 | circles.exit() 102 | .transition().duration(100).ease(d3.easeCubicOut) 103 | .attr("cx", function (d) { return (width + dotSize/2 + 100) + "px" }) 104 | .transition().delay(100).remove() 105 | 106 | var axis = d3.axisBottom(xScale) 107 | .tickSizeOuter(0) 108 | .tickArguments([d3.timeMillisecond.every(100)]) 109 | .tickFormat(fmtTime) 110 | 111 | svg.select("g") 112 | .attr("transform", "translate(0," + (height - axisHeight) + ")") 113 | .call(axis) 114 | } 115 | 116 | visualizeEvents("#trace", []) 117 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package auth 5 | 6 | import ( 7 | "errors" 8 | "time" 9 | ) 10 | 11 | // Interface for gateway authentication 12 | type Interface interface { 13 | SetToken(gatewayID string, token string, expires time.Time) error 14 | SetKey(gatewayID string, key string) error 15 | 16 | ValidateKey(gatewayID string, key string) error 17 | 18 | Delete(gatewayID string) error 19 | 20 | // GetToken returns the access token for a gateway; it exchanges the key for an access token if necessary 21 | GetToken(gatewayID string) (token string, err error) 22 | SetExchanger(Exchanger) 23 | } 24 | 25 | // ErrGatewayNotFound is returned when a gateway was not found 26 | var ErrGatewayNotFound = errors.New("Gateway not found") 27 | 28 | // ErrGatewayNoValidToken is returned when a gateway does not have a valid access token 29 | var ErrGatewayNoValidToken = errors.New("Gateway does not have a valid access token") 30 | -------------------------------------------------------------------------------- /auth/auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package auth 5 | 6 | import ( 7 | "bytes" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/apex/log" 13 | "github.com/apex/log/handlers/text" 14 | . "github.com/smartystreets/goconvey/convey" 15 | ) 16 | 17 | func TestAuth(t *testing.T) { 18 | Convey("Given a new Context and Exchanger", t, func(c C) { 19 | 20 | var logs bytes.Buffer 21 | ctx := &log.Logger{ 22 | Handler: text.New(&logs), 23 | Level: log.DebugLevel, 24 | } 25 | defer func() { 26 | if logs.Len() > 0 { 27 | c.Printf("\n%s", logs.String()) 28 | } 29 | }() 30 | 31 | e := &AccountServerExchanger{ 32 | ctx: ctx, 33 | accountServer: "https://account.thethingsnetwork.org", 34 | } 35 | 36 | Convey("Given a new auth.Memory", func() { 37 | a := NewMemory() 38 | Convey("When running the standardized test", standardizedTest(a, e)) 39 | }) 40 | 41 | Convey("Given a new auth.Redis", func() { 42 | a := NewRedis(getRedisClient(), "test-auth") 43 | Convey("When running the standardized test", standardizedTest(a, e)) 44 | }) 45 | 46 | }) 47 | } 48 | 49 | func standardizedTest(a Interface, e Exchanger) func() { 50 | return func() { 51 | Convey("When getting the token for an unknown gateway", func() { 52 | _, err := a.GetToken("unknown-gateway") 53 | Convey("There should be a NotFound error", func() { 54 | So(err, ShouldNotBeNil) 55 | So(err, ShouldEqual, ErrGatewayNotFound) 56 | }) 57 | }) 58 | 59 | Convey("When setting a key", func() { 60 | err := a.SetKey("gateway-with-key", "the-key") 61 | Reset(func() { 62 | a.Delete("gateway-with-key") 63 | }) 64 | Convey("There should be no error", func() { 65 | So(err, ShouldBeNil) 66 | }) 67 | Convey("When deleting the gateway", func() { 68 | err := a.Delete("gateway-with-key") 69 | Convey("There should be no error", func() { 70 | So(err, ShouldBeNil) 71 | }) 72 | Convey("When getting the token", func() { 73 | _, err := a.GetToken("gateway-with-key") 74 | Convey("There should be a NotFound error", func() { 75 | So(err, ShouldNotBeNil) 76 | So(err, ShouldEqual, ErrGatewayNotFound) 77 | }) 78 | }) 79 | }) 80 | Convey("When getting the token", func() { 81 | _, err := a.GetToken("gateway-with-key") 82 | Convey("There should be a NoValidToken error", func() { 83 | So(err, ShouldNotBeNil) 84 | So(err, ShouldEqual, ErrGatewayNoValidToken) 85 | }) 86 | }) 87 | Convey("When updating the key", func() { 88 | err := a.SetKey("gateway-with-key", "the-new-key") 89 | Convey("There should be no error", func() { 90 | So(err, ShouldBeNil) 91 | }) 92 | Convey("When getting the token", func() { 93 | _, err := a.GetToken("gateway-with-key") 94 | Convey("There should still be a NoValidToken error", func() { 95 | So(err, ShouldNotBeNil) 96 | So(err, ShouldEqual, ErrGatewayNoValidToken) 97 | }) 98 | }) 99 | }) 100 | }) 101 | 102 | Convey("When setting a token", func() { 103 | err := a.SetToken("gateway-with-token", "the-token", time.Now().Add(time.Second)) 104 | Reset(func() { 105 | a.Delete("gateway-with-token") 106 | }) 107 | Convey("There should be no error", func() { 108 | So(err, ShouldBeNil) 109 | }) 110 | Convey("When deleting the gateway", func() { 111 | err := a.Delete("gateway-with-token") 112 | Convey("There should be no error", func() { 113 | So(err, ShouldBeNil) 114 | }) 115 | Convey("When getting the token", func() { 116 | _, err := a.GetToken("gateway-with-token") 117 | Convey("There should be a NotFound error", func() { 118 | So(err, ShouldNotBeNil) 119 | So(err, ShouldEqual, ErrGatewayNotFound) 120 | }) 121 | }) 122 | }) 123 | Convey("When getting the token", func() { 124 | token, err := a.GetToken("gateway-with-token") 125 | Convey("There should be no error", func() { 126 | So(err, ShouldBeNil) 127 | }) 128 | Convey("The token should be the one we set", func() { 129 | So(token, ShouldEqual, "the-token") 130 | }) 131 | }) 132 | Convey("When updating the token", func() { 133 | err := a.SetToken("gateway-with-token", "the-new-token", time.Now().Add(time.Second)) 134 | Convey("There should be no error", func() { 135 | So(err, ShouldBeNil) 136 | }) 137 | Convey("When getting the token", func() { 138 | token, err := a.GetToken("gateway-with-token") 139 | Convey("There should be no error", func() { 140 | So(err, ShouldBeNil) 141 | }) 142 | Convey("The token should be the one we updated", func() { 143 | So(token, ShouldEqual, "the-new-token") 144 | }) 145 | }) 146 | }) 147 | }) 148 | 149 | gatewayID := os.Getenv("GATEWAY_ID") 150 | gatewayKey := os.Getenv("GATEWAY_KEY") 151 | 152 | if gatewayID != "" && gatewayKey != "" { 153 | Convey("When setting the Exchanger", func() { 154 | a.SetExchanger(e) 155 | 156 | Convey("When setting a key", func() { 157 | err := a.SetKey(gatewayID, gatewayKey) 158 | Reset(func() { 159 | a.Delete(gatewayID) 160 | }) 161 | Convey("There should be no error", func() { 162 | So(err, ShouldBeNil) 163 | }) 164 | Convey("When validating the correct key", func() { 165 | err := a.ValidateKey(gatewayID, gatewayKey) 166 | Convey("There should be no error", func() { 167 | So(err, ShouldBeNil) 168 | }) 169 | }) 170 | Convey("When validating an incorrect key", func() { 171 | err := a.ValidateKey(gatewayID, "incorrect") 172 | Convey("There should be an error", func() { 173 | So(err, ShouldNotBeNil) 174 | }) 175 | }) 176 | Convey("When getting the token", func() { 177 | token, err := a.GetToken(gatewayID) 178 | Convey("There should be no error", func() { 179 | So(err, ShouldBeNil) 180 | }) 181 | Convey("There should be a token", func() { 182 | So(token, ShouldNotBeEmpty) 183 | }) 184 | }) 185 | Convey("When expiring the token", func() { 186 | a.SetToken(gatewayID, "expired-token", time.Now()) 187 | Convey("When getting the token again", func() { 188 | token, err := a.GetToken(gatewayID) 189 | Convey("There should be no error", func() { 190 | So(err, ShouldBeNil) 191 | }) 192 | Convey("There should be a new token", func() { 193 | So(token, ShouldNotEqual, "expired-token") 194 | }) 195 | }) 196 | }) 197 | }) 198 | 199 | }) 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /auth/exchanger.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package auth 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/TheThingsNetwork/go-account-lib/account" 10 | "github.com/apex/log" 11 | ) 12 | 13 | // Exchanger interface exchanges a gateway access key for a gateway token 14 | type Exchanger interface { 15 | Exchange(gatewayID, key string) (token string, expires time.Time, err error) 16 | } 17 | 18 | // NewAccountServer returns a new AccountServerExchanger for the given TTN Account Server 19 | func NewAccountServer(accountServer string, ctx log.Interface) Exchanger { 20 | return &AccountServerExchanger{ 21 | accountServer: accountServer, 22 | ctx: ctx, 23 | } 24 | } 25 | 26 | // AccountServerExchanger uses a TTN Account Server to exchange the key for a token 27 | type AccountServerExchanger struct { 28 | ctx log.Interface 29 | accountServer string 30 | } 31 | 32 | // Exchange implements the Exchanger interface 33 | func (a *AccountServerExchanger) Exchange(gatewayID, key string) (token string, expires time.Time, err error) { 34 | acct := account.NewWithKey(a.accountServer, key) 35 | t, err := acct.GetGatewayToken(gatewayID) 36 | if err != nil { 37 | return token, expires, err 38 | } 39 | return t.AccessToken, t.Expiry, nil 40 | } 41 | -------------------------------------------------------------------------------- /auth/exchanger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package auth 5 | 6 | import ( 7 | "bytes" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/apex/log" 13 | "github.com/apex/log/handlers/text" 14 | . "github.com/smartystreets/goconvey/convey" 15 | ) 16 | 17 | func TestAccountServerExchanger(t *testing.T) { 18 | Convey("Given a new Context", t, func(c C) { 19 | 20 | var logs bytes.Buffer 21 | ctx := &log.Logger{ 22 | Handler: text.New(&logs), 23 | Level: log.DebugLevel, 24 | } 25 | defer func() { 26 | if logs.Len() > 0 { 27 | c.Printf("\n%s", logs.String()) 28 | } 29 | }() 30 | 31 | Convey("Given a new AccountServerExchanger", func() { 32 | e := NewAccountServer("https://account.thethingsnetwork.org", ctx) 33 | 34 | Convey("When calling Exchange for an invalid gateway ID", func() { 35 | _, _, err := e.Exchange("some-invalid-gateway-id-that-does-not-exist", "some-invalid-key") 36 | Convey("There should be an error", func() { 37 | So(err, ShouldNotBeNil) 38 | }) 39 | }) 40 | 41 | if gatewayID := os.Getenv("GATEWAY_ID"); gatewayID != "" { 42 | Convey("When calling Exchange for an invalid gateway Key", func() { 43 | _, _, err := e.Exchange(gatewayID, "some-invalid-key") 44 | Convey("There should be an error", func() { 45 | So(err, ShouldNotBeNil) 46 | }) 47 | }) 48 | 49 | if gatewayKey := os.Getenv("GATEWAY_KEY"); gatewayKey != "" { 50 | Convey("When calling Exchange with a valid gateway key", func() { 51 | token, expires, err := e.Exchange(gatewayID, gatewayKey) 52 | Convey("There should be no error", func() { 53 | So(err, ShouldBeNil) 54 | }) 55 | Convey("A token should have been returned", func() { 56 | So(token, ShouldNotBeEmpty) 57 | }) 58 | Convey("The token should expire in the future", func() { 59 | So(expires, ShouldHappenAfter, time.Now()) 60 | }) 61 | }) 62 | } 63 | } 64 | 65 | }) 66 | 67 | }) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /auth/memory_auth.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package auth 5 | 6 | import ( 7 | "errors" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type memoryGateway struct { 13 | key string 14 | token string 15 | tokenExpires time.Time 16 | sync.Mutex 17 | } 18 | 19 | // Memory implements the authentication interface with an in-memory backend 20 | type Memory struct { 21 | gateways map[string]*memoryGateway 22 | mu sync.RWMutex 23 | Exchanger 24 | } 25 | 26 | // NewMemory returns a new authentication interface with an in-memory backend 27 | func NewMemory() Interface { 28 | return &Memory{ 29 | gateways: make(map[string]*memoryGateway), 30 | } 31 | } 32 | 33 | // SetToken sets the access token for a gateway 34 | func (m *Memory) SetToken(gatewayID string, token string, expires time.Time) error { 35 | m.mu.Lock() 36 | defer m.mu.Unlock() 37 | if gtw, ok := m.gateways[gatewayID]; ok { 38 | gtw.Lock() 39 | defer gtw.Unlock() 40 | gtw.token = token 41 | gtw.tokenExpires = expires 42 | } else { 43 | m.gateways[gatewayID] = &memoryGateway{ 44 | token: token, 45 | tokenExpires: expires, 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | // SetKey sets the access key for a gateway 52 | func (m *Memory) SetKey(gatewayID string, key string) error { 53 | m.mu.Lock() 54 | defer m.mu.Unlock() 55 | if gtw, ok := m.gateways[gatewayID]; ok { 56 | gtw.Lock() 57 | defer gtw.Unlock() 58 | gtw.key = key 59 | } else { 60 | m.gateways[gatewayID] = &memoryGateway{ 61 | key: key, 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | // ValidateKey validates the access key for a gateway 68 | func (m *Memory) ValidateKey(gatewayID string, key string) error { 69 | m.mu.RLock() 70 | defer m.mu.RUnlock() 71 | if gtw, ok := m.gateways[gatewayID]; ok { 72 | gtw.Lock() 73 | defer gtw.Unlock() 74 | if gtw.key == key { 75 | return nil 76 | } 77 | } else { 78 | return nil 79 | } 80 | return errors.New("Invalid Key") 81 | } 82 | 83 | // Delete gateway key and token 84 | func (m *Memory) Delete(gatewayID string) error { 85 | m.mu.Lock() 86 | defer m.mu.Unlock() 87 | delete(m.gateways, gatewayID) 88 | return nil 89 | } 90 | 91 | // GetToken returns an access token for the gateway 92 | func (m *Memory) GetToken(gatewayID string) (string, error) { 93 | m.mu.RLock() 94 | defer m.mu.RUnlock() 95 | gtw, ok := m.gateways[gatewayID] 96 | if !ok { 97 | return "", ErrGatewayNotFound 98 | } 99 | gtw.Lock() 100 | defer gtw.Unlock() 101 | if gtw.token != "" && (gtw.tokenExpires.IsZero() || gtw.tokenExpires.After(time.Now())) { 102 | return gtw.token, nil 103 | } 104 | if gtw.key != "" && m.Exchanger != nil { 105 | token, expires, err := m.Exchange(gatewayID, gtw.key) 106 | if err != nil { 107 | return "", err 108 | } 109 | gtw.token = token 110 | gtw.tokenExpires = expires 111 | return token, nil 112 | } 113 | return "", ErrGatewayNoValidToken 114 | } 115 | 116 | // SetExchanger sets the component that will exchange access keys for access tokens 117 | func (m *Memory) SetExchanger(e Exchanger) { 118 | m.Exchanger = e 119 | } 120 | -------------------------------------------------------------------------------- /auth/redis_auth.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package auth 5 | 6 | import ( 7 | "errors" 8 | "time" 9 | 10 | redis "gopkg.in/redis.v5" 11 | ) 12 | 13 | // Redis implements the authentication interface with a Redis backend 14 | type Redis struct { 15 | prefix string 16 | client *redis.Client 17 | Exchanger 18 | } 19 | 20 | // DefaultRedisPrefix is used as prefix when no prefix is given 21 | var DefaultRedisPrefix = "gateway:" 22 | 23 | var redisKey = struct { 24 | token string 25 | key string 26 | tokenExpires string 27 | }{ 28 | token: "token", 29 | key: "key", 30 | tokenExpires: "token_expires", 31 | } 32 | 33 | // NewRedis returns a new authentication interface with a redis backend 34 | func NewRedis(client *redis.Client, prefix string) Interface { 35 | if prefix == "" { 36 | prefix = DefaultRedisPrefix 37 | } 38 | return &Redis{ 39 | client: client, 40 | prefix: prefix, 41 | } 42 | } 43 | 44 | // SetToken sets the access token for a gateway 45 | func (r *Redis) SetToken(gatewayID string, token string, expires time.Time) error { 46 | data := map[string]string{ 47 | redisKey.token: token, 48 | } 49 | if expires.IsZero() { 50 | data[redisKey.tokenExpires] = "" 51 | } else { 52 | data[redisKey.tokenExpires] = expires.Format(time.RFC3339) 53 | } 54 | return r.client.HMSet(r.prefix+gatewayID, data).Err() 55 | } 56 | 57 | // SetKey sets the access key for a gateway 58 | func (r *Redis) SetKey(gatewayID string, key string) error { 59 | return r.client.HSet(r.prefix+gatewayID, redisKey.key, key).Err() 60 | } 61 | 62 | // ValidateKey validates the access key for a gateway 63 | func (r *Redis) ValidateKey(gatewayID string, key string) error { 64 | res, err := r.client.HGet(r.prefix+gatewayID, redisKey.key).Result() 65 | if err == redis.Nil || len(res) == 0 || key == res { 66 | return nil 67 | } 68 | return errors.New("Invalid Key") 69 | } 70 | 71 | // Delete gateway key and token 72 | func (r *Redis) Delete(gatewayID string) error { 73 | return r.client.Del(r.prefix + gatewayID).Err() 74 | } 75 | 76 | // GetToken returns an access token for the gateway 77 | func (r *Redis) GetToken(gatewayID string) (string, error) { 78 | res, err := r.client.HGetAll(r.prefix + gatewayID).Result() 79 | if err == redis.Nil || len(res) == 0 { 80 | return "", ErrGatewayNotFound 81 | } 82 | if err != nil { 83 | return "", err 84 | } 85 | var expires time.Time 86 | if expiresStr, ok := res[redisKey.tokenExpires]; ok && expiresStr != "" { 87 | if rExpires, err := time.Parse(time.RFC3339, expiresStr); err == nil { 88 | expires = rExpires 89 | } 90 | } 91 | if token, ok := res[redisKey.token]; ok && token != "" { 92 | if expires.IsZero() || expires.After(time.Now()) { 93 | return token, nil 94 | } 95 | } 96 | if key, ok := res[redisKey.key]; ok && key != "" && r.Exchanger != nil { 97 | token, expires, err := r.Exchange(gatewayID, key) 98 | if err != nil { 99 | return "", err 100 | } 101 | if err := r.SetToken(gatewayID, token, expires); err != nil { 102 | // TODO: Print warning 103 | } 104 | return token, nil 105 | } 106 | return "", ErrGatewayNoValidToken 107 | } 108 | 109 | // SetExchanger sets the component that will exchange access keys for access tokens 110 | func (r *Redis) SetExchanger(e Exchanger) { 111 | r.Exchanger = e 112 | } 113 | -------------------------------------------------------------------------------- /auth/redis_auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package auth 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | redis "gopkg.in/redis.v5" 11 | ) 12 | 13 | func getRedisClient() *redis.Client { 14 | host := os.Getenv("REDIS_HOST") 15 | if host == "" { 16 | host = "localhost" 17 | } 18 | return redis.NewClient(&redis.Options{ 19 | Addr: fmt.Sprintf("%s:6379", host), 20 | Password: "", // no password set 21 | DB: 1, // use default DB 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /backend/amqp/amqp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package amqp 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/TheThingsNetwork/api/gateway" 14 | "github.com/TheThingsNetwork/api/router" 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 16 | "github.com/apex/log" 17 | "github.com/apex/log/handlers/text" 18 | "github.com/gogo/protobuf/proto" 19 | . "github.com/smartystreets/goconvey/convey" 20 | ) 21 | 22 | var host string 23 | 24 | func init() { 25 | host = os.Getenv("AMQP_ADDRESS") 26 | if host == "" { 27 | host = "localhost:5672" 28 | } 29 | } 30 | 31 | func TestAMQP(t *testing.T) { 32 | Convey("Given a new Context", t, func(c C) { 33 | 34 | var logs bytes.Buffer 35 | ctx := &log.Logger{ 36 | Handler: text.New(&logs), 37 | Level: log.DebugLevel, 38 | } 39 | defer func() { 40 | if logs.Len() > 0 { 41 | c.Printf("\n%s", logs.String()) 42 | } 43 | }() 44 | 45 | Convey("When creating a new AMQP", func() { 46 | amqp, err := New(Config{ 47 | Address: host, 48 | }, ctx) 49 | Convey("There should be no error", func() { 50 | So(err, ShouldBeNil) 51 | }) 52 | Convey("amqp should be there", func() { 53 | So(amqp, ShouldNotBeNil) 54 | }) 55 | 56 | Convey("When calling Connect", func() { 57 | amqp.Connect() 58 | time.Sleep(10 * time.Millisecond) 59 | 60 | Reset(func() { amqp.Disconnect() }) 61 | 62 | Convey("When calling Disconnect", func() { 63 | amqp.Disconnect() 64 | time.Sleep(10 * time.Millisecond) 65 | }) 66 | 67 | Convey("When getting a channel", func() { 68 | ch, err := amqp.channel() 69 | Convey("There should be no error", func() { 70 | So(err, ShouldBeNil) 71 | }) 72 | ch.Close() 73 | }) 74 | 75 | Convey("When subscribing to a routing key", func() { 76 | msg, err := amqp.subscribe("some-key") 77 | time.Sleep(10 * time.Millisecond) 78 | Reset(func() { 79 | amqp.unsubscribe("some-key") 80 | }) 81 | 82 | Convey("There should be no error", func() { 83 | So(err, ShouldBeNil) 84 | }) 85 | 86 | Convey("When publishing a message", func() { 87 | err := amqp.Publish("some-key", []byte{1, 2, 3, 4}) 88 | Convey("There should be no error", func() { 89 | So(err, ShouldBeNil) 90 | }) 91 | Convey("The subscribe chan should receive a message", func() { 92 | select { 93 | case <-time.After(time.Second): 94 | So("Timeout Exceeded", ShouldBeFalse) 95 | case msg, ok := <-msg: 96 | So(ok, ShouldBeTrue) 97 | So(msg, ShouldNotBeNil) 98 | } 99 | }) 100 | }) 101 | 102 | Convey("When unsubscribing from the routing key", func() { 103 | err := amqp.unsubscribe("some-key") 104 | Convey("There should be no error", func() { 105 | So(err, ShouldBeNil) 106 | }) 107 | Convey("The channel should be closed", func() { 108 | select { 109 | case <-time.After(time.Second): 110 | So("Timeout Exceeded", ShouldBeFalse) 111 | case _, ok := <-msg: 112 | So(ok, ShouldBeFalse) 113 | } 114 | }) 115 | }) 116 | }) 117 | 118 | Convey("When subscribing to connect messages", func() { 119 | msg, err := amqp.SubscribeConnect() 120 | time.Sleep(10 * time.Millisecond) 121 | Reset(func() { 122 | amqp.UnsubscribeConnect() 123 | }) 124 | Convey("There should be no error", func() { 125 | So(err, ShouldBeNil) 126 | }) 127 | Convey("When publishing a connect message", func() { 128 | bytes, _ := (&types.ConnectMessage{GatewayID: "dev", Key: "key"}).Marshal() 129 | err := amqp.Publish(ConnectRoutingKeyFormat, bytes) 130 | Convey("There should be no error", func() { 131 | So(err, ShouldBeNil) 132 | }) 133 | Convey("The connect chan should receive a message", func() { 134 | select { 135 | case <-time.After(time.Second): 136 | So("Timeout Exceeded", ShouldBeFalse) 137 | case msg, ok := <-msg: 138 | So(ok, ShouldBeTrue) 139 | So(msg.GatewayID, ShouldEqual, "dev") 140 | So(msg.Key, ShouldEqual, "key") 141 | } 142 | }) 143 | }) 144 | }) 145 | 146 | Convey("When subscribing to disconnect messages", func() { 147 | msg, err := amqp.SubscribeDisconnect() 148 | time.Sleep(10 * time.Millisecond) 149 | Reset(func() { 150 | amqp.UnsubscribeDisconnect() 151 | }) 152 | Convey("There should be no error", func() { 153 | So(err, ShouldBeNil) 154 | }) 155 | Convey("When publishing a disconnect message", func() { 156 | bytes, _ := (&types.DisconnectMessage{GatewayID: "dev", Key: "key"}).Marshal() 157 | err := amqp.Publish(DisconnectRoutingKeyFormat, bytes) 158 | Convey("There should be no error", func() { 159 | So(err, ShouldBeNil) 160 | }) 161 | Convey("The disconnect chan should receive a message", func() { 162 | select { 163 | case <-time.After(time.Second): 164 | So("Timeout Exceeded", ShouldBeFalse) 165 | case msg, ok := <-msg: 166 | So(ok, ShouldBeTrue) 167 | So(msg, ShouldNotBeNil) 168 | So(msg.GatewayID, ShouldEqual, "dev") 169 | So(msg.Key, ShouldEqual, "key") 170 | } 171 | }) 172 | }) 173 | }) 174 | 175 | Convey("When subscribing to uplink messages", func() { 176 | msg, err := amqp.SubscribeUplink("dev") 177 | time.Sleep(10 * time.Millisecond) 178 | Reset(func() { 179 | amqp.UnsubscribeUplink("dev") 180 | }) 181 | Convey("There should be no error", func() { 182 | So(err, ShouldBeNil) 183 | }) 184 | Convey("When publishing a uplink message", func() { 185 | bytes, _ := proto.Marshal(new(router.UplinkMessage)) 186 | err := amqp.Publish(fmt.Sprintf(UplinkRoutingKeyFormat, "dev"), bytes) 187 | Convey("There should be no error", func() { 188 | So(err, ShouldBeNil) 189 | }) 190 | Convey("The uplink chan should receive a message", func() { 191 | select { 192 | case <-time.After(time.Second): 193 | So("Timeout Exceeded", ShouldBeFalse) 194 | case msg, ok := <-msg: 195 | So(ok, ShouldBeTrue) 196 | So(msg, ShouldNotBeNil) 197 | } 198 | }) 199 | }) 200 | }) 201 | 202 | Convey("When subscribing to status messages", func() { 203 | msg, err := amqp.SubscribeStatus("dev") 204 | time.Sleep(10 * time.Millisecond) 205 | Reset(func() { 206 | amqp.UnsubscribeStatus("dev") 207 | }) 208 | Convey("There should be no error", func() { 209 | So(err, ShouldBeNil) 210 | }) 211 | Convey("When publishing a status message", func() { 212 | bytes, _ := proto.Marshal(new(gateway.Status)) 213 | err := amqp.Publish(fmt.Sprintf(StatusRoutingKeyFormat, "dev"), bytes) 214 | Convey("There should be no error", func() { 215 | So(err, ShouldBeNil) 216 | }) 217 | Convey("The status chan should receive a message", func() { 218 | select { 219 | case <-time.After(time.Second): 220 | So("Timeout Exceeded", ShouldBeFalse) 221 | case msg, ok := <-msg: 222 | So(ok, ShouldBeTrue) 223 | So(msg, ShouldNotBeNil) 224 | } 225 | }) 226 | }) 227 | }) 228 | 229 | Convey("When subscribing to downlink messages key", func() { 230 | msg, err := amqp.subscribe(fmt.Sprintf(DownlinkRoutingKeyFormat, "dev")) 231 | time.Sleep(10 * time.Millisecond) 232 | Reset(func() { 233 | amqp.unsubscribe(fmt.Sprintf(DownlinkRoutingKeyFormat, "dev")) 234 | }) 235 | 236 | Convey("There should be no error", func() { 237 | So(err, ShouldBeNil) 238 | }) 239 | 240 | Convey("When publishing a downlink message", func() { 241 | downlinkMessage := new(router.DownlinkMessage) 242 | err := amqp.PublishDownlink(&types.DownlinkMessage{ 243 | GatewayID: "dev", 244 | Message: downlinkMessage, 245 | }) 246 | Convey("There should be no error", func() { 247 | So(err, ShouldBeNil) 248 | }) 249 | 250 | Convey("The downlink chan should receive a message", func() { 251 | select { 252 | case <-time.After(time.Second): 253 | So("Timeout Exceeded", ShouldBeFalse) 254 | case msg, ok := <-msg: 255 | So(ok, ShouldBeTrue) 256 | So(msg, ShouldNotBeNil) 257 | } 258 | }) 259 | }) 260 | }) 261 | 262 | }) 263 | }) 264 | }) 265 | } 266 | -------------------------------------------------------------------------------- /backend/amqp/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | // Package amqp connects to an AMQP server in order to communicate with a gateway. 5 | // 6 | // Connection/Disconnection of gateways is done by publishing messages to the 7 | // "connect" and "disconnect" routing keys. When a gateway connects, it (or a 8 | // plugin on the broker) should publish a protocol buffer of the type 9 | // types.ConnectMessage containing the gateway's ID and either a key or a token 10 | // to the "connect" routing key . 11 | // 12 | // The gateway (or the plugin) can send a protocol buffer of the type 13 | // types.DisconnectMessage containing the gateway's ID on the "disconnect" 14 | // routing key when it disconnects, in order to help the bridge clean up 15 | // connections. 16 | // 17 | // Uplink messages are sent as protocol buffers on the "[gateway-id].up" routing 18 | // key. The bridge should call `SubscribeUplink("gateway-id")` to subscribe to 19 | // these. It is also possible to subscribe to a wildcard gateway by passing "*". 20 | // 21 | // Downlink messages are sent as protocol buffers on the "[gateway-id].down" 22 | // routing key. The bridge should call `PublishDownlink(*types.DownlinkMessage)` 23 | // in order to send the downlink to the gateway. 24 | // 25 | // Gateway status messages are sent as protocol buffers on the 26 | // "[gateway-id].status" routing key. The bridge should call 27 | // `SubscribeStatus("gateway-id")` to subscribe to these. It is also possible to 28 | // subscribe to a wildcard gateway by passing "*". 29 | package amqp 30 | -------------------------------------------------------------------------------- /backend/backend.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package backend 5 | 6 | import "github.com/TheThingsNetwork/gateway-connector-bridge/types" 7 | 8 | // Northbound backends talk to servers that are up the chain 9 | type Northbound interface { 10 | Connect() error 11 | Disconnect() error 12 | CleanupGateway(gatewayID string) 13 | PublishUplink(message *types.UplinkMessage) error 14 | PublishStatus(message *types.StatusMessage) error 15 | SubscribeDownlink(gatewayID string) (<-chan *types.DownlinkMessage, error) 16 | UnsubscribeDownlink(gatewayID string) error 17 | } 18 | 19 | // Southbound backends talk to gateways or servers that are down the chain 20 | type Southbound interface { 21 | Connect() error 22 | Disconnect() error 23 | SubscribeConnect() (<-chan *types.ConnectMessage, error) 24 | UnsubscribeConnect() error 25 | SubscribeDisconnect() (<-chan *types.DisconnectMessage, error) 26 | UnsubscribeDisconnect() error 27 | SubscribeUplink(gatewayID string) (<-chan *types.UplinkMessage, error) 28 | UnsubscribeUplink(gatewayID string) error 29 | SubscribeStatus(gatewayID string) (<-chan *types.StatusMessage, error) 30 | UnsubscribeStatus(gatewayID string) error 31 | PublishDownlink(message *types.DownlinkMessage) error 32 | } 33 | -------------------------------------------------------------------------------- /backend/dummy/dummy.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package dummy 5 | 6 | import ( 7 | "sync" 8 | 9 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 10 | "github.com/apex/log" 11 | ) 12 | 13 | // BufferSize indicates the maximum number of dummy messages that should be buffered 14 | var BufferSize = 10 15 | 16 | type dummyGateway struct { 17 | sync.Mutex 18 | uplink chan *types.UplinkMessage 19 | status chan *types.StatusMessage 20 | downlink chan *types.DownlinkMessage 21 | } 22 | 23 | // Dummy backend 24 | type Dummy struct { 25 | mu sync.Mutex 26 | ctx log.Interface 27 | connect chan *types.ConnectMessage 28 | disconnect chan *types.DisconnectMessage 29 | gateways map[string]*dummyGateway 30 | } 31 | 32 | // New returns a new Dummy backend 33 | func New(ctx log.Interface) *Dummy { 34 | return &Dummy{ 35 | ctx: ctx.WithField("Connector", "Dummy"), 36 | gateways: make(map[string]*dummyGateway), 37 | } 38 | } 39 | 40 | // Connect implements backend interfaces 41 | func (d *Dummy) Connect() error { 42 | d.ctx.Debug("Connected") 43 | return nil 44 | } 45 | 46 | // Disconnect implements backend interfaces 47 | func (d *Dummy) Disconnect() error { 48 | d.ctx.Debug("Disconnected") 49 | return nil 50 | } 51 | 52 | // PublishConnect publishes connect messages to the dummy backend 53 | func (d *Dummy) PublishConnect(message *types.ConnectMessage) error { 54 | select { 55 | case d.connect <- message: 56 | d.ctx.Debug("Published connect") 57 | default: 58 | d.ctx.Debug("Did not publish connect [buffer full]") 59 | } 60 | return nil 61 | } 62 | 63 | // SubscribeConnect implements backend interfaces 64 | func (d *Dummy) SubscribeConnect() (<-chan *types.ConnectMessage, error) { 65 | d.mu.Lock() 66 | defer d.mu.Unlock() 67 | d.connect = make(chan *types.ConnectMessage, BufferSize) 68 | d.ctx.Debug("Subscribed to connect") 69 | return d.connect, nil 70 | } 71 | 72 | // UnsubscribeConnect implements backend interfaces 73 | func (d *Dummy) UnsubscribeConnect() error { 74 | d.mu.Lock() 75 | defer d.mu.Unlock() 76 | close(d.connect) 77 | d.connect = nil 78 | d.ctx.Debug("Unsubscribed from connect") 79 | return nil 80 | } 81 | 82 | // PublishDisconnect publishes disconnect messages to the dummy backend 83 | func (d *Dummy) PublishDisconnect(message *types.DisconnectMessage) error { 84 | select { 85 | case d.disconnect <- message: 86 | d.ctx.Debug("Published disconnect") 87 | default: 88 | d.ctx.Debug("Did not publish disconnect [buffer full]") 89 | } 90 | return nil 91 | } 92 | 93 | // SubscribeDisconnect implements backend interfaces 94 | func (d *Dummy) SubscribeDisconnect() (<-chan *types.DisconnectMessage, error) { 95 | d.mu.Lock() 96 | defer d.mu.Unlock() 97 | d.disconnect = make(chan *types.DisconnectMessage, BufferSize) 98 | d.ctx.Debug("Subscribed to disconnect") 99 | return d.disconnect, nil 100 | } 101 | 102 | // UnsubscribeDisconnect implements backend interfaces 103 | func (d *Dummy) UnsubscribeDisconnect() error { 104 | d.mu.Lock() 105 | defer d.mu.Unlock() 106 | close(d.disconnect) 107 | d.disconnect = nil 108 | d.ctx.Debug("Unsubscribed from disconnect") 109 | return nil 110 | } 111 | 112 | func (d *Dummy) getGateway(gatewayID string) *dummyGateway { 113 | d.mu.Lock() 114 | defer d.mu.Unlock() 115 | if gtw, ok := d.gateways[gatewayID]; ok { 116 | return gtw 117 | } 118 | d.gateways[gatewayID] = &dummyGateway{ 119 | uplink: make(chan *types.UplinkMessage, BufferSize), 120 | status: make(chan *types.StatusMessage, BufferSize), 121 | downlink: make(chan *types.DownlinkMessage, BufferSize), 122 | } 123 | return d.gateways[gatewayID] 124 | } 125 | 126 | // PublishUplink implements backend interfaces 127 | func (d *Dummy) PublishUplink(message *types.UplinkMessage) error { 128 | select { 129 | case d.getGateway(message.GatewayID).uplink <- message: 130 | d.ctx.WithField("GatewayID", message.GatewayID).Debug("Published uplink") 131 | default: 132 | d.ctx.WithField("GatewayID", message.GatewayID).Debug("Did not publish uplink [buffer full]") 133 | } 134 | return nil 135 | } 136 | 137 | // SubscribeUplink implements backend interfaces 138 | func (d *Dummy) SubscribeUplink(gatewayID string) (<-chan *types.UplinkMessage, error) { 139 | gtw := d.getGateway(gatewayID) 140 | gtw.Lock() 141 | defer gtw.Unlock() 142 | gtw.uplink = make(chan *types.UplinkMessage, BufferSize) 143 | d.ctx.WithField("GatewayID", gatewayID).Debug("Subscribed to uplink") 144 | return gtw.uplink, nil 145 | } 146 | 147 | // UnsubscribeUplink implements backend interfaces 148 | func (d *Dummy) UnsubscribeUplink(gatewayID string) error { 149 | gtw := d.getGateway(gatewayID) 150 | gtw.Lock() 151 | defer gtw.Unlock() 152 | close(gtw.uplink) 153 | gtw.uplink = nil 154 | d.ctx.WithField("GatewayID", gatewayID).Debug("Unsubscribed from uplink") 155 | return nil 156 | } 157 | 158 | // PublishDownlink implements backend interfaces 159 | func (d *Dummy) PublishDownlink(message *types.DownlinkMessage) error { 160 | select { 161 | case d.getGateway(message.GatewayID).downlink <- message: 162 | d.ctx.WithField("GatewayID", message.GatewayID).Debug("Published downlink") 163 | default: 164 | d.ctx.WithField("GatewayID", message.GatewayID).Debug("Did not publish downlink [buffer full]") 165 | } 166 | return nil 167 | } 168 | 169 | // SubscribeDownlink implements backend interfaces 170 | func (d *Dummy) SubscribeDownlink(gatewayID string) (<-chan *types.DownlinkMessage, error) { 171 | gtw := d.getGateway(gatewayID) 172 | gtw.Lock() 173 | defer gtw.Unlock() 174 | gtw.downlink = make(chan *types.DownlinkMessage, BufferSize) 175 | d.ctx.WithField("GatewayID", gatewayID).Debug("Subscribed to downlink") 176 | return gtw.downlink, nil 177 | } 178 | 179 | // UnsubscribeDownlink implements backend interfaces 180 | func (d *Dummy) UnsubscribeDownlink(gatewayID string) error { 181 | gtw := d.getGateway(gatewayID) 182 | gtw.Lock() 183 | defer gtw.Unlock() 184 | close(gtw.downlink) 185 | gtw.downlink = nil 186 | d.ctx.WithField("GatewayID", gatewayID).Debug("Unsubscribed from downlink") 187 | return nil 188 | } 189 | 190 | // PublishStatus implements backend interfaces 191 | func (d *Dummy) PublishStatus(message *types.StatusMessage) error { 192 | message.Backend = "Dummy" 193 | select { 194 | case d.getGateway(message.GatewayID).status <- message: 195 | d.ctx.WithField("GatewayID", message.GatewayID).Debug("Published status") 196 | default: 197 | d.ctx.WithField("GatewayID", message.GatewayID).Debug("Did not publish status [buffer full]") 198 | } 199 | return nil 200 | } 201 | 202 | // SubscribeStatus implements backend interfaces 203 | func (d *Dummy) SubscribeStatus(gatewayID string) (<-chan *types.StatusMessage, error) { 204 | gtw := d.getGateway(gatewayID) 205 | gtw.Lock() 206 | defer gtw.Unlock() 207 | gtw.status = make(chan *types.StatusMessage, BufferSize) 208 | d.ctx.WithField("GatewayID", gatewayID).Debug("Subscribed to status") 209 | return gtw.status, nil 210 | } 211 | 212 | // UnsubscribeStatus implements backend interfaces 213 | func (d *Dummy) UnsubscribeStatus(gatewayID string) error { 214 | gtw := d.getGateway(gatewayID) 215 | gtw.Lock() 216 | defer gtw.Unlock() 217 | close(gtw.status) 218 | gtw.status = nil 219 | d.ctx.WithField("GatewayID", gatewayID).Debug("Unsubscribed from status") 220 | return nil 221 | } 222 | 223 | // CleanupGateway implements backend interfaces 224 | func (d *Dummy) CleanupGateway(gatewayID string) { 225 | // Not closing channels here, that's not our job 226 | delete(d.gateways, gatewayID) 227 | } 228 | -------------------------------------------------------------------------------- /backend/dummy/dummy_http.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package dummy 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/TheThingsNetwork/api/trace" 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 11 | ) 12 | 13 | // WithServer is a Dummy backend that exposes some events on 14 | // a http page with websockets 15 | type WithServer struct { 16 | *Dummy 17 | server *Server 18 | } 19 | 20 | func cleanTrace(in *trace.Trace) *trace.Trace { 21 | trace := &trace.Trace{ 22 | ServiceName: "gateway-connector-bridge", 23 | Time: time.Now().UnixNano(), 24 | Event: "debug", 25 | Parents: in.Flatten(), 26 | } 27 | for _, parent := range trace.Parents { 28 | parent.Parents = nil 29 | } 30 | return trace 31 | } 32 | 33 | // WithHTTPServer returns the Dummy that also has a HTTP server exposing the events on addr 34 | func (d *Dummy) WithHTTPServer(addr string) *WithServer { 35 | ctx := d.ctx.WithField("Connector", "HTTP Debug") 36 | s, err := NewServer(ctx, addr) 37 | if err != nil { 38 | ctx.WithError(err).Fatal("Could not add server to Dummy backend") 39 | return nil 40 | } 41 | go s.Listen() 42 | return &WithServer{ 43 | Dummy: d, 44 | server: s, 45 | } 46 | } 47 | 48 | // PublishUplink implements backend interfaces 49 | func (d *WithServer) PublishUplink(message *types.UplinkMessage) error { 50 | uplink := *message.Message 51 | uplink.UnmarshalPayload() 52 | uplink.Trace = cleanTrace(uplink.Trace) 53 | d.server.Uplink(&types.UplinkMessage{GatewayID: message.GatewayID, Message: &uplink}) 54 | return nil 55 | } 56 | 57 | // PublishStatus implements backend interfaces 58 | func (d *WithServer) PublishStatus(message *types.StatusMessage) error { 59 | d.server.Status(message) 60 | return nil 61 | } 62 | 63 | // PublishDownlink implements backend interfaces 64 | func (d *WithServer) PublishDownlink(message *types.DownlinkMessage) error { 65 | downlink := *message.Message 66 | downlink.UnmarshalPayload() 67 | downlink.Trace = cleanTrace(downlink.Trace) 68 | d.server.Downlink(&types.DownlinkMessage{GatewayID: message.GatewayID, Message: &downlink}) 69 | return nil 70 | } 71 | 72 | // SubscribeUplink implements backend interfaces 73 | func (d *WithServer) SubscribeUplink(gatewayID string) (<-chan *types.UplinkMessage, error) { 74 | d.server.Connect(gatewayID) 75 | return d.Dummy.SubscribeUplink(gatewayID) 76 | } 77 | 78 | // UnsubscribeUplink implements backend interfaces 79 | func (d *WithServer) UnsubscribeUplink(gatewayID string) error { 80 | d.server.Disconnect(gatewayID) 81 | return d.Dummy.UnsubscribeUplink(gatewayID) 82 | } 83 | -------------------------------------------------------------------------------- /backend/dummy/dummy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package dummy 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | "time" 10 | 11 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 12 | "github.com/apex/log" 13 | "github.com/apex/log/handlers/text" 14 | . "github.com/smartystreets/goconvey/convey" 15 | ) 16 | 17 | func TestDummy(t *testing.T) { 18 | Convey("Given a new Context", t, func(c C) { 19 | 20 | var logs bytes.Buffer 21 | ctx := &log.Logger{ 22 | Handler: text.New(&logs), 23 | Level: log.DebugLevel, 24 | } 25 | defer func() { 26 | if logs.Len() > 0 { 27 | c.Printf("\n%s", logs.String()) 28 | } 29 | }() 30 | 31 | Convey("When creating a new Dummy", func() { 32 | dummy := New(ctx) 33 | 34 | Convey("When calling Connect on Dummy", func() { 35 | err := dummy.Connect() 36 | Convey("There should be no error", func() { 37 | So(err, ShouldBeNil) 38 | }) 39 | Convey("We can also call Disconnect", func() { 40 | dummy.Disconnect() 41 | }) 42 | 43 | Convey("When subscribing to gateway connections", func() { 44 | connect, err := dummy.SubscribeConnect() 45 | Convey("There should be no error", func() { 46 | So(err, ShouldBeNil) 47 | }) 48 | Convey("When publishing a gateway connection", func() { 49 | dummy.PublishConnect(&types.ConnectMessage{ 50 | GatewayID: "dev", 51 | Key: "key", 52 | }) 53 | Convey("There should be a corresponding ConnectMessage in the channel", func() { 54 | select { 55 | case <-time.After(time.Second): 56 | So("Timeout Exceeded", ShouldBeFalse) 57 | case msg := <-connect: 58 | So(msg.GatewayID, ShouldEqual, "dev") 59 | So(msg.Key, ShouldEqual, "key") 60 | } 61 | }) 62 | }) 63 | Convey("When unsubscribing from gateway connections", func() { 64 | err := dummy.UnsubscribeConnect() 65 | Convey("There should be no error", func() { 66 | So(err, ShouldBeNil) 67 | }) 68 | Convey("The channel should be closed", func() { 69 | for range connect { 70 | } 71 | }) 72 | }) 73 | }) 74 | 75 | Convey("When subscribing to gateway disconnections", func() { 76 | disconnect, err := dummy.SubscribeDisconnect() 77 | Convey("There should be no error", func() { 78 | So(err, ShouldBeNil) 79 | }) 80 | Convey("When publishing a gateway disconnection", func() { 81 | dummy.PublishDisconnect(&types.DisconnectMessage{ 82 | GatewayID: "dev", 83 | Key: "key", 84 | }) 85 | Convey("There should be a corresponding ConnectMessage in the channel", func() { 86 | select { 87 | case <-time.After(time.Second): 88 | So("Timeout Exceeded", ShouldBeFalse) 89 | case msg := <-disconnect: 90 | So(msg.GatewayID, ShouldEqual, "dev") 91 | So(msg.Key, ShouldEqual, "key") 92 | } 93 | }) 94 | }) 95 | Convey("When unsubscribing from gateway disconnections", func() { 96 | err := dummy.UnsubscribeDisconnect() 97 | Convey("There should be no error", func() { 98 | So(err, ShouldBeNil) 99 | }) 100 | Convey("The channel should be closed", func() { 101 | for range disconnect { 102 | } 103 | }) 104 | }) 105 | }) 106 | 107 | Convey("When subscribing to gateway uplink", func() { 108 | uplink, err := dummy.SubscribeUplink("dev") 109 | Convey("There should be no error", func() { 110 | So(err, ShouldBeNil) 111 | }) 112 | Convey("When publishing a gateway uplink", func() { 113 | dummy.PublishUplink(&types.UplinkMessage{GatewayID: "dev"}) 114 | Convey("There should be a corresponding UplinkMessage in the channel", func() { 115 | select { 116 | case <-time.After(time.Second): 117 | So("Timeout Exceeded", ShouldBeFalse) 118 | case _, ok := <-uplink: 119 | So(ok, ShouldBeTrue) 120 | } 121 | }) 122 | }) 123 | Convey("When unsubscribing from gateway uplink", func() { 124 | err := dummy.UnsubscribeUplink("dev") 125 | Convey("There should be no error", func() { 126 | So(err, ShouldBeNil) 127 | }) 128 | Convey("The channel should be closed", func() { 129 | for range uplink { 130 | } 131 | }) 132 | }) 133 | }) 134 | 135 | Convey("When subscribing to gateway status", func() { 136 | status, err := dummy.SubscribeStatus("dev") 137 | Convey("There should be no error", func() { 138 | So(err, ShouldBeNil) 139 | }) 140 | Convey("When publishing a gateway status", func() { 141 | dummy.PublishStatus(&types.StatusMessage{GatewayID: "dev"}) 142 | Convey("There should be a corresponding StatusMessage in the channel", func() { 143 | select { 144 | case <-time.After(time.Second): 145 | So("Timeout Exceeded", ShouldBeFalse) 146 | case _, ok := <-status: 147 | So(ok, ShouldBeTrue) 148 | } 149 | }) 150 | }) 151 | Convey("When unsubscribing from gateway status", func() { 152 | err := dummy.UnsubscribeStatus("dev") 153 | Convey("There should be no error", func() { 154 | So(err, ShouldBeNil) 155 | }) 156 | Convey("The channel should be closed", func() { 157 | select { 158 | case <-time.After(time.Second): 159 | So("Timeout Exceeded", ShouldBeFalse) 160 | case _, ok := <-status: 161 | So(ok, ShouldBeFalse) 162 | } 163 | }) 164 | }) 165 | }) 166 | 167 | Convey("When subscribing to gateway downlink", func() { 168 | downlink, err := dummy.SubscribeDownlink("dev") 169 | Convey("There should be no error", func() { 170 | So(err, ShouldBeNil) 171 | }) 172 | Convey("When publishing a gateway downlink", func() { 173 | dummy.PublishDownlink(&types.DownlinkMessage{GatewayID: "dev"}) 174 | Convey("There should be a corresponding DownlinkMessage in the channel", func() { 175 | select { 176 | case <-time.After(time.Second): 177 | So("Timeout Exceeded", ShouldBeFalse) 178 | case _, ok := <-downlink: 179 | So(ok, ShouldBeTrue) 180 | } 181 | }) 182 | }) 183 | Convey("When unsubscribing from gateway downlink", func() { 184 | err := dummy.UnsubscribeDownlink("dev") 185 | Convey("There should be no error", func() { 186 | So(err, ShouldBeNil) 187 | }) 188 | Convey("The channel should be closed", func() { 189 | for range downlink { 190 | } 191 | }) 192 | }) 193 | }) 194 | 195 | }) 196 | 197 | }) 198 | }) 199 | } 200 | -------------------------------------------------------------------------------- /backend/dummy/http.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package dummy 5 | 6 | import ( 7 | "encoding/json" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 12 | "github.com/apex/log" 13 | socketio "github.com/googollee/go-socket.io" 14 | ) 15 | 16 | const ( 17 | room = "evts" 18 | connectEvt = "gtw-connect" 19 | disconnectEvt = "gtw-disconnect" 20 | uplinkEvt = "uplink" 21 | downlinkEvt = "downlink" 22 | statusEvt = "status" 23 | ) 24 | 25 | // Server is a http server that exposes some events over websockets 26 | type Server struct { 27 | ctx log.Interface 28 | addr string 29 | server *socketio.Server 30 | connect chan string 31 | disconnect chan string 32 | uplink chan *types.UplinkMessage 33 | downlink chan *types.DownlinkMessage 34 | status chan *types.StatusMessage 35 | 36 | mu sync.RWMutex // Protects connectedGateways 37 | connectedGateways []string 38 | } 39 | 40 | // NewServer creates a new server 41 | func NewServer(ctx log.Interface, addr string) (*Server, error) { 42 | server, err := socketio.NewServer(nil) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &Server{ 48 | ctx: ctx.WithField("Connector", "Dummy-HTTP"), 49 | server: server, 50 | addr: addr, 51 | connect: make(chan string, BufferSize), 52 | disconnect: make(chan string, BufferSize), 53 | uplink: make(chan *types.UplinkMessage, BufferSize), 54 | downlink: make(chan *types.DownlinkMessage, BufferSize), 55 | status: make(chan *types.StatusMessage, BufferSize), 56 | connectedGateways: make([]string, 0), 57 | }, nil 58 | } 59 | 60 | // Listen opens the server and starts listening for http requests 61 | func (s *Server) Listen() { 62 | s.server.On("connection", func(so socketio.Socket) { 63 | s.handleConnect(so) 64 | }) 65 | 66 | go s.handleEvents() 67 | 68 | mux := http.NewServeMux() 69 | mux.Handle("/socket.io/", s.server) 70 | mux.Handle("/", http.FileServer(http.Dir("./assets"))) 71 | mux.HandleFunc("/gateways", func(res http.ResponseWriter, _ *http.Request) { 72 | res.Header().Add("content-type", "application/json; charset=utf-8") 73 | enc := json.NewEncoder(res) 74 | enc.Encode(s.ConnectedGateways()) 75 | }) 76 | s.ctx.Infof("HTTP server listening on %s", s.addr) 77 | err := http.ListenAndServe(s.addr, mux) 78 | if err != nil { 79 | s.ctx.WithError(err).Fatal("Could not serve HTTP") 80 | } 81 | } 82 | 83 | func (s *Server) handleConnect(so socketio.Socket) { 84 | ctx := s.ctx.WithField("ID", so.Id) 85 | ctx.Debug("Socket connected") 86 | so.Join(room) 87 | so.On("disconnection", func() { 88 | ctx.Debug("Socket disconnected") 89 | }) 90 | } 91 | 92 | func (s *Server) handleEvents() { 93 | for { 94 | select { 95 | case gtwID := <-s.connect: 96 | s.emit(connectEvt, gtwID) 97 | case gtwID := <-s.disconnect: 98 | s.emit(disconnectEvt, gtwID) 99 | case msg := <-s.uplink: 100 | s.emit(uplinkEvt, msg) 101 | case msg := <-s.downlink: 102 | s.emit(downlinkEvt, msg) 103 | case msg := <-s.status: 104 | s.emit(statusEvt, msg) 105 | } 106 | } 107 | } 108 | 109 | func (s *Server) emit(name string, v interface{}) { 110 | marshalled, err := json.Marshal(v) 111 | if err != nil { 112 | s.ctx.WithError(err).Error("Could not marshal event") 113 | return 114 | } 115 | s.server.BroadcastTo(room, name, string(marshalled)) 116 | } 117 | 118 | // Uplink emits an uplink message on the server page 119 | func (s *Server) Uplink(msg *types.UplinkMessage) { 120 | select { 121 | case s.uplink <- msg: 122 | return 123 | default: 124 | s.ctx.Warn("Dropping uplink message on websocket") 125 | } 126 | } 127 | 128 | // Downlink emits a downlink message on the server page 129 | func (s *Server) Downlink(msg *types.DownlinkMessage) { 130 | select { 131 | case s.downlink <- msg: 132 | return 133 | default: 134 | s.ctx.Warn("Dropping downlink message on websocket") 135 | } 136 | } 137 | 138 | // Status emits a status message on the server page 139 | func (s *Server) Status(msg *types.StatusMessage) { 140 | select { 141 | case s.status <- msg: 142 | return 143 | default: 144 | s.ctx.Warn("Dropping status message on websocket") 145 | } 146 | } 147 | 148 | // Connect emits a message when a gateway connects 149 | func (s *Server) Connect(gatewayID string) { 150 | select { 151 | case s.connect <- gatewayID: 152 | default: 153 | s.ctx.Warn("Dropping connect message on websocket") 154 | } 155 | s.mu.Lock() 156 | defer s.mu.Unlock() 157 | for _, existing := range s.connectedGateways { 158 | if existing == gatewayID { 159 | return 160 | } 161 | } 162 | s.connectedGateways = append(s.connectedGateways, gatewayID) 163 | } 164 | 165 | // Disconnect emits a message when a gateway disconnects 166 | func (s *Server) Disconnect(gatewayID string) { 167 | select { 168 | case s.disconnect <- gatewayID: 169 | default: 170 | s.ctx.Warn("Dropping disconnect message on websocket") 171 | } 172 | s.mu.Lock() 173 | defer s.mu.Unlock() 174 | previouslyConnectedGateways := s.connectedGateways 175 | s.connectedGateways = make([]string, 0, len(previouslyConnectedGateways)) 176 | for _, existing := range previouslyConnectedGateways { 177 | if existing != gatewayID { 178 | s.connectedGateways = append(s.connectedGateways, existing) 179 | } 180 | } 181 | } 182 | 183 | // ConnectedGateways returns the list of connected gateway IDs 184 | func (s *Server) ConnectedGateways() []string { 185 | s.mu.RLock() 186 | defer s.mu.RUnlock() 187 | return s.connectedGateways 188 | } 189 | -------------------------------------------------------------------------------- /backend/mqtt/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | // Package mqtt connects to an MQTT broker in order to communicate with a gateway. 5 | // 6 | // Connection/Disconnection of gateways is done by publishing messages to the 7 | // "connect" and "disconnect" topics. When a gateway connects, it (or a plugin 8 | // of the MQTT broker) should publish a protocol buffer of the type 9 | // types.ConnectMessage containing the gateway's ID and either a key or a 10 | // token to the "connect" topic. 11 | // 12 | // The gateway (or the plugin) can also set a will with the MQTT broker for 13 | // when it disconnects. This will should be a protocol buffer of the type 14 | // types.DisconnectMessage containing the gateway's ID on the "disconnect" 15 | // topic. 16 | // 17 | // Uplink messages are sent as protocol buffers on the "[gateway-id]/up" topic. 18 | // The bridge should call `SubscribeUplink("gateway-id")` to subscribe to this 19 | // topic. It is also possible to subscribe to a wildcard gateway by passing "+". 20 | // 21 | // Downlink messages are sent as protocol buffers on the "[gateway-id]/down" 22 | // topic. The bridge should call `PublishDownlink(*types.DownlinkMessage)` in 23 | // order to send the downlink to the gateway. 24 | // 25 | // Gateway status messages are sent as protocol buffers on the 26 | // "[gateway-id]/status" topic. The bridge should call 27 | // `SubscribeStatus("gateway-id")` to subscribe to this topic. It is also 28 | // possible to subscribe to a wildcard gateway by passing "+". 29 | package mqtt 30 | -------------------------------------------------------------------------------- /backend/mqtt/mqtt.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package mqtt 5 | 6 | import ( 7 | "crypto/tls" 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/api/gateway" 13 | "github.com/TheThingsNetwork/api/router" 14 | "github.com/TheThingsNetwork/api/trace" 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 16 | "github.com/TheThingsNetwork/ttn/utils/random" 17 | "github.com/apex/log" 18 | paho "github.com/eclipse/paho.mqtt.golang" 19 | "github.com/gogo/protobuf/proto" 20 | ) 21 | 22 | // PublishTimeout is the timeout before returning from publish without checking error 23 | var PublishTimeout = 50 * time.Millisecond 24 | 25 | // New returns a new MQTT 26 | func New(config Config, ctx log.Interface) (*MQTT, error) { 27 | mqtt := new(MQTT) 28 | 29 | mqtt.ctx = ctx.WithField("Connector", "MQTT") 30 | 31 | mqttOpts := paho.NewClientOptions() 32 | for _, broker := range config.Brokers { 33 | mqttOpts.AddBroker(broker) 34 | } 35 | if config.TLSConfig != nil { 36 | mqttOpts.SetTLSConfig(config.TLSConfig) 37 | } 38 | mqttOpts.SetClientID(fmt.Sprintf("bridge-%s", random.String(16))) 39 | mqttOpts.SetUsername(config.Username) 40 | mqttOpts.SetPassword(config.Password) 41 | mqttOpts.SetKeepAlive(30 * time.Second) 42 | mqttOpts.SetPingTimeout(10 * time.Second) 43 | mqttOpts.SetCleanSession(true) 44 | mqttOpts.SetDefaultPublishHandler(func(_ paho.Client, msg paho.Message) { 45 | mqtt.ctx.Warnf("Received unhandled message on MQTT: %v", msg) 46 | }) 47 | 48 | mqtt.subscriptions = make(map[string]subscription) 49 | var reconnecting bool 50 | mqttOpts.SetConnectionLostHandler(func(_ paho.Client, err error) { 51 | mqtt.ctx.Warnf("Disconnected (%s). Reconnecting...", err.Error()) 52 | reconnecting = true 53 | }) 54 | mqttOpts.SetOnConnectHandler(func(_ paho.Client) { 55 | mqtt.ctx.Info("Connected") 56 | if reconnecting { 57 | mqtt.resubscribe() 58 | reconnecting = false 59 | } 60 | }) 61 | 62 | mqtt.client = paho.NewClient(mqttOpts) 63 | 64 | return mqtt, nil 65 | } 66 | 67 | // QoS indicates the MQTT Quality of Service level. 68 | // 0: The broker/client will deliver the message once, with no confirmation. 69 | // 1: The broker/client will deliver the message at least once, with confirmation required. 70 | // 2: The broker/client will deliver the message exactly once by using a four step handshake. 71 | var ( 72 | PublishQoS byte = 0x00 73 | SubscribeQoS byte = 0x00 74 | ) 75 | 76 | // BufferSize indicates the maximum number of MQTT messages that should be buffered 77 | var BufferSize = 10 78 | 79 | // Topic formats for connect, disconnect, uplink, downlink and status messages 80 | var ( 81 | ConnectTopicFormat = "connect" 82 | DisconnectTopicFormat = "disconnect" 83 | UplinkTopicFormat = "%s/up" 84 | DownlinkTopicFormat = "%s/down" 85 | StatusTopicFormat = "%s/status" 86 | ) 87 | 88 | // Config contains configuration for MQTT 89 | type Config struct { 90 | Brokers []string 91 | Username string 92 | Password string 93 | TLSConfig *tls.Config 94 | } 95 | 96 | type subscription struct { 97 | handler paho.MessageHandler 98 | cancel func() 99 | } 100 | 101 | // MQTT side of the bridge 102 | type MQTT struct { 103 | ctx log.Interface 104 | client paho.Client 105 | subscriptions map[string]subscription 106 | mu sync.Mutex 107 | } 108 | 109 | var ( 110 | // ConnectRetries says how many times the client should retry a failed connection 111 | ConnectRetries = 10 112 | // ConnectRetryDelay says how long the client should wait between retries 113 | ConnectRetryDelay = time.Second 114 | ) 115 | 116 | // Connect to MQTT 117 | func (c *MQTT) Connect() error { 118 | var err error 119 | for retries := 0; retries < ConnectRetries; retries++ { 120 | token := c.client.Connect() 121 | finished := token.WaitTimeout(1 * time.Second) 122 | if !finished { 123 | c.ctx.Warn("MQTT connection took longer than expected...") 124 | token.Wait() 125 | } 126 | err = token.Error() 127 | if err == nil { 128 | break 129 | } 130 | c.ctx.Warnf("Could not connect to MQTT (%s). Retrying...", err.Error()) 131 | <-time.After(ConnectRetryDelay) 132 | } 133 | if err != nil { 134 | return fmt.Errorf("Could not connect to MQTT (%s)", err) 135 | } 136 | return err 137 | } 138 | 139 | // Disconnect from MQTT 140 | func (c *MQTT) Disconnect() error { 141 | c.client.Disconnect(100) 142 | return nil 143 | } 144 | 145 | func (c *MQTT) publish(topic string, msg []byte) paho.Token { 146 | return c.client.Publish(topic, PublishQoS, false, msg) 147 | } 148 | 149 | func (c *MQTT) subscribe(topic string, handler paho.MessageHandler, cancel func()) paho.Token { 150 | c.mu.Lock() 151 | defer c.mu.Unlock() 152 | wrappedHandler := func(client paho.Client, msg paho.Message) { 153 | if msg.Retained() { 154 | c.ctx.WithField("Topic", msg.Topic()).Debug("Ignore retained message") 155 | return 156 | } 157 | handler(client, msg) 158 | } 159 | c.subscriptions[topic] = subscription{wrappedHandler, cancel} 160 | return c.client.Subscribe(topic, SubscribeQoS, wrappedHandler) 161 | } 162 | 163 | func (c *MQTT) resubscribe() { 164 | c.mu.Lock() 165 | defer c.mu.Unlock() 166 | for topic, subscription := range c.subscriptions { 167 | c.client.Subscribe(topic, SubscribeQoS, subscription.handler) 168 | } 169 | } 170 | 171 | func (c *MQTT) unsubscribe(topic string) paho.Token { 172 | c.mu.Lock() 173 | defer c.mu.Unlock() 174 | if subscription, ok := c.subscriptions[topic]; ok && subscription.cancel != nil { 175 | subscription.cancel() 176 | } 177 | delete(c.subscriptions, topic) 178 | return c.client.Unsubscribe(topic) 179 | } 180 | 181 | // SubscribeConnect subscribes to connect messages 182 | func (c *MQTT) SubscribeConnect() (<-chan *types.ConnectMessage, error) { 183 | messages := make(chan *types.ConnectMessage, BufferSize) 184 | token := c.subscribe(ConnectTopicFormat, func(_ paho.Client, msg paho.Message) { 185 | var connect types.ConnectMessage 186 | if err := proto.Unmarshal(msg.Payload(), &connect); err != nil { 187 | c.ctx.WithError(err).Warn("Could not unmarshal connect message") 188 | return 189 | } 190 | ctx := c.ctx.WithField("GatewayID", connect.GatewayID) 191 | select { 192 | case messages <- &connect: 193 | ctx.WithField("ProtoSize", len(msg.Payload())).Debug("Received connect message") 194 | default: 195 | ctx.Warn("Could not handle connect message: buffer full") 196 | } 197 | }, func() { 198 | close(messages) 199 | }) 200 | token.Wait() 201 | return messages, token.Error() 202 | } 203 | 204 | // UnsubscribeConnect unsubscribes from connect messages 205 | func (c *MQTT) UnsubscribeConnect() error { 206 | token := c.unsubscribe(ConnectTopicFormat) 207 | token.Wait() 208 | return token.Error() 209 | } 210 | 211 | // SubscribeDisconnect subscribes to disconnect messages 212 | func (c *MQTT) SubscribeDisconnect() (<-chan *types.DisconnectMessage, error) { 213 | messages := make(chan *types.DisconnectMessage, BufferSize) 214 | token := c.subscribe(DisconnectTopicFormat, func(_ paho.Client, msg paho.Message) { 215 | var disconnect types.DisconnectMessage 216 | if err := proto.Unmarshal(msg.Payload(), &disconnect); err != nil { 217 | c.ctx.WithError(err).Warn("Could not unmarshal disconnect message") 218 | return 219 | } 220 | ctx := c.ctx.WithField("GatewayID", disconnect.GatewayID) 221 | select { 222 | case messages <- &disconnect: 223 | ctx.WithField("ProtoSize", len(msg.Payload())).Debug("Received disconnect message") 224 | default: 225 | ctx.Warn("Could not handle disconnect message: buffer full") 226 | } 227 | }, func() { 228 | close(messages) 229 | }) 230 | token.Wait() 231 | return messages, token.Error() 232 | } 233 | 234 | // UnsubscribeDisconnect unsubscribes from disconnect messages 235 | func (c *MQTT) UnsubscribeDisconnect() error { 236 | token := c.unsubscribe(DisconnectTopicFormat) 237 | token.Wait() 238 | return token.Error() 239 | } 240 | 241 | // SubscribeUplink handles uplink messages for the given gateway ID 242 | func (c *MQTT) SubscribeUplink(gatewayID string) (<-chan *types.UplinkMessage, error) { 243 | ctx := c.ctx.WithField("GatewayID", gatewayID) 244 | messages := make(chan *types.UplinkMessage, BufferSize) 245 | token := c.subscribe(fmt.Sprintf(UplinkTopicFormat, gatewayID), func(_ paho.Client, msg paho.Message) { 246 | uplink := types.UplinkMessage{ 247 | GatewayID: gatewayID, 248 | Message: new(router.UplinkMessage), 249 | } 250 | if err := proto.Unmarshal(msg.Payload(), uplink.Message); err != nil { 251 | ctx.WithError(err).Warn("Could not unmarshal uplink message") 252 | return 253 | } 254 | uplink.Message.Trace = uplink.Message.Trace.WithEvent(trace.ReceiveEvent, "backend", "mqtt") 255 | select { 256 | case messages <- &uplink: 257 | ctx.WithField("ProtoSize", len(msg.Payload())).Debug("Received uplink message") 258 | default: 259 | ctx.Warn("Could not handle uplink message: buffer full") 260 | } 261 | }, func() { 262 | close(messages) 263 | }) 264 | token.Wait() 265 | return messages, token.Error() 266 | } 267 | 268 | // UnsubscribeUplink unsubscribes from uplink messages for the given gateway ID 269 | func (c *MQTT) UnsubscribeUplink(gatewayID string) error { 270 | token := c.unsubscribe(fmt.Sprintf(UplinkTopicFormat, gatewayID)) 271 | token.Wait() 272 | return token.Error() 273 | } 274 | 275 | // SubscribeStatus handles status messages for the given gateway ID 276 | func (c *MQTT) SubscribeStatus(gatewayID string) (<-chan *types.StatusMessage, error) { 277 | ctx := c.ctx.WithField("GatewayID", gatewayID) 278 | messages := make(chan *types.StatusMessage, BufferSize) 279 | token := c.subscribe(fmt.Sprintf(StatusTopicFormat, gatewayID), func(_ paho.Client, msg paho.Message) { 280 | status := types.StatusMessage{ 281 | Backend: "MQTT", 282 | GatewayID: gatewayID, 283 | Message: new(gateway.Status), 284 | } 285 | if err := proto.Unmarshal(msg.Payload(), status.Message); err != nil { 286 | ctx.WithError(err).Warn("Could not unmarshal status message") 287 | return 288 | } 289 | select { 290 | case messages <- &status: 291 | ctx.WithField("ProtoSize", len(msg.Payload())).Debug("Received status message") 292 | default: 293 | ctx.Warn("Could not handle status message: buffer full") 294 | } 295 | }, func() { 296 | close(messages) 297 | }) 298 | token.Wait() 299 | return messages, token.Error() 300 | } 301 | 302 | // UnsubscribeStatus unsubscribes from status messages for the given gateway ID 303 | func (c *MQTT) UnsubscribeStatus(gatewayID string) error { 304 | token := c.unsubscribe(fmt.Sprintf(StatusTopicFormat, gatewayID)) 305 | token.Wait() 306 | return token.Error() 307 | } 308 | 309 | // PublishDownlink publishes a downlink message 310 | func (c *MQTT) PublishDownlink(message *types.DownlinkMessage) error { 311 | ctx := c.ctx.WithField("GatewayID", message.GatewayID) 312 | downlink := *message.Message 313 | downlink.Trace = nil 314 | msg, err := proto.Marshal(&downlink) 315 | if err != nil { 316 | return err 317 | } 318 | token := c.publish(fmt.Sprintf(DownlinkTopicFormat, message.GatewayID), msg) 319 | go func() { 320 | token.Wait() 321 | if err := token.Error(); err != nil { 322 | ctx.WithError(err).Warn("Could not publish downlink message") 323 | return 324 | } 325 | ctx.WithField("ProtoSize", len(msg)).Debug("Published downlink message") 326 | }() 327 | return nil 328 | } 329 | -------------------------------------------------------------------------------- /backend/mqtt/mqtt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package mqtt 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/TheThingsNetwork/api/gateway" 14 | "github.com/TheThingsNetwork/api/router" 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 16 | "github.com/apex/log" 17 | "github.com/apex/log/handlers/text" 18 | paho "github.com/eclipse/paho.mqtt.golang" 19 | "github.com/gogo/protobuf/proto" 20 | . "github.com/smartystreets/goconvey/convey" 21 | ) 22 | 23 | var host string 24 | 25 | func init() { 26 | host = os.Getenv("MQTT_ADDRESS") 27 | if host == "" { 28 | host = "localhost:1883" 29 | } 30 | } 31 | 32 | func TestMQTT(t *testing.T) { 33 | Convey("Given a new Context", t, func(c C) { 34 | 35 | var logs bytes.Buffer 36 | ctx := &log.Logger{ 37 | Handler: text.New(&logs), 38 | Level: log.DebugLevel, 39 | } 40 | defer func() { 41 | if logs.Len() > 0 { 42 | c.Printf("\n%s", logs.String()) 43 | } 44 | }() 45 | 46 | Convey("When calling SetupMQTT", func() { 47 | mqtt, err := New(Config{ 48 | Brokers: []string{fmt.Sprintf("tcp://%s", host)}, 49 | }, ctx) 50 | Convey("There should be no error", func() { 51 | So(err, ShouldBeNil) 52 | }) 53 | Convey("The bridge should now have MQTT", func() { 54 | So(mqtt, ShouldNotBeNil) 55 | }) 56 | 57 | Convey("When calling Connect on MQTT", func() { 58 | err := mqtt.Connect() 59 | Convey("There should be no error", func() { 60 | So(err, ShouldBeNil) 61 | }) 62 | Convey("We can also call Disconnect", func() { 63 | mqtt.Disconnect() 64 | }) 65 | 66 | Convey("When subscribing to gateway connections", func() { 67 | connect, err := mqtt.SubscribeConnect() 68 | Convey("There should be no error", func() { 69 | So(err, ShouldBeNil) 70 | }) 71 | Convey("When publishing a gateway connection", func() { 72 | bytes, _ := (&types.ConnectMessage{GatewayID: "dev", Key: "key"}).Marshal() 73 | mqtt.publish(ConnectTopicFormat, bytes) 74 | Convey("There should be a corresponding ConnectMessage in the channel", func() { 75 | select { 76 | case <-time.After(time.Second): 77 | So("Timeout Exceeded", ShouldBeFalse) 78 | case msg := <-connect: 79 | So(msg.GatewayID, ShouldEqual, "dev") 80 | So(msg.Key, ShouldEqual, "key") 81 | } 82 | }) 83 | }) 84 | Convey("When unsubscribing from gateway connections", func() { 85 | err := mqtt.UnsubscribeConnect() 86 | Convey("There should be no error", func() { 87 | So(err, ShouldBeNil) 88 | }) 89 | Convey("The channel should be closed", func() { 90 | for range connect { 91 | } 92 | }) 93 | }) 94 | }) 95 | 96 | Convey("When subscribing to gateway disconnections", func() { 97 | disconnect, err := mqtt.SubscribeDisconnect() 98 | Convey("There should be no error", func() { 99 | So(err, ShouldBeNil) 100 | }) 101 | Convey("When publishing a gateway disconnection", func() { 102 | bytes, _ := (&types.DisconnectMessage{GatewayID: "dev", Key: "key"}).Marshal() 103 | mqtt.publish(DisconnectTopicFormat, bytes) 104 | Convey("There should be a corresponding ConnectMessage in the channel", func() { 105 | select { 106 | case <-time.After(time.Second): 107 | So("Timeout Exceeded", ShouldBeFalse) 108 | case msg := <-disconnect: 109 | So(msg.GatewayID, ShouldEqual, "dev") 110 | So(msg.Key, ShouldEqual, "key") 111 | } 112 | }) 113 | }) 114 | Convey("When unsubscribing from gateway disconnections", func() { 115 | err := mqtt.UnsubscribeDisconnect() 116 | Convey("There should be no error", func() { 117 | So(err, ShouldBeNil) 118 | }) 119 | Convey("The channel should be closed", func() { 120 | for range disconnect { 121 | } 122 | }) 123 | }) 124 | }) 125 | 126 | Convey("When subscribing to gateway uplink", func() { 127 | uplink, err := mqtt.SubscribeUplink("dev") 128 | Convey("There should be no error", func() { 129 | So(err, ShouldBeNil) 130 | }) 131 | Convey("When publishing a gateway uplink", func() { 132 | uplinkMessage := new(router.UplinkMessage) 133 | uplinkMessage.Payload = []byte{1, 2, 3, 4} 134 | bin, _ := proto.Marshal(uplinkMessage) 135 | mqtt.publish(fmt.Sprintf(UplinkTopicFormat, "dev"), bin) 136 | Convey("There should be a corresponding UplinkMessage in the channel", func() { 137 | select { 138 | case <-time.After(time.Second): 139 | So("Timeout Exceeded", ShouldBeFalse) 140 | case msg := <-uplink: 141 | So(msg.Message.Payload, ShouldResemble, []byte{1, 2, 3, 4}) 142 | } 143 | }) 144 | }) 145 | Convey("When unsubscribing from gateway uplink", func() { 146 | err := mqtt.UnsubscribeUplink("dev") 147 | Convey("There should be no error", func() { 148 | So(err, ShouldBeNil) 149 | }) 150 | Convey("The channel should be closed", func() { 151 | for range uplink { 152 | } 153 | }) 154 | }) 155 | }) 156 | 157 | Convey("When subscribing to gateway status", func() { 158 | status, err := mqtt.SubscribeStatus("dev") 159 | Convey("There should be no error", func() { 160 | So(err, ShouldBeNil) 161 | }) 162 | Convey("When publishing a gateway status", func() { 163 | statusMessage := new(gateway.Status) 164 | statusMessage.Description = "Awesome Description" 165 | bin, _ := proto.Marshal(statusMessage) 166 | mqtt.publish(fmt.Sprintf(StatusTopicFormat, "dev"), bin).Wait() 167 | Convey("There should be a corresponding StatusMessage in the channel", func() { 168 | select { 169 | case <-time.After(time.Second): 170 | So("Timeout Exceeded", ShouldBeFalse) 171 | case msg := <-status: 172 | So(msg.Message.Description, ShouldEqual, "Awesome Description") 173 | } 174 | }) 175 | }) 176 | Convey("When unsubscribing from gateway status", func() { 177 | err := mqtt.UnsubscribeStatus("dev") 178 | Convey("There should be no error", func() { 179 | So(err, ShouldBeNil) 180 | }) 181 | Convey("The channel should be closed", func() { 182 | select { 183 | case <-time.After(time.Second): 184 | So("Timeout Exceeded", ShouldBeFalse) 185 | case _, ok := <-status: 186 | So(ok, ShouldBeFalse) 187 | } 188 | }) 189 | }) 190 | }) 191 | 192 | Convey("When subscribing to gateway downlink", func() { 193 | var payload []byte 194 | mqtt.subscribe(fmt.Sprintf(DownlinkTopicFormat, "dev"), func(_ paho.Client, msg paho.Message) { 195 | payload = msg.Payload() 196 | }, func() {}).Wait() 197 | 198 | Convey("When publishing a downlink message", func() { 199 | downlinkMessage := new(router.DownlinkMessage) 200 | downlinkMessage.Payload = []byte{1, 2, 3, 4} 201 | err := mqtt.PublishDownlink(&types.DownlinkMessage{ 202 | GatewayID: "dev", 203 | Message: downlinkMessage, 204 | }) 205 | Convey("There should be no error", func() { 206 | So(err, ShouldBeNil) 207 | }) 208 | Convey("The payload should be received within 100ms", func() { 209 | time.Sleep(100 * time.Millisecond) 210 | So(payload, ShouldNotBeEmpty) 211 | }) 212 | }) 213 | }) 214 | 215 | }) 216 | }) 217 | 218 | }) 219 | } 220 | -------------------------------------------------------------------------------- /backend/pktfwd/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Orne Brocaar 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 | -------------------------------------------------------------------------------- /backend/pktfwd/packettype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=PacketType"; DO NOT EDIT 2 | 3 | package pktfwd 4 | 5 | import "fmt" 6 | 7 | const _PacketType_name = "PushDataPushACKPullDataPullRespPullACKTXACK" 8 | 9 | var _PacketType_index = [...]uint8{0, 8, 15, 23, 31, 38, 43} 10 | 11 | func (i PacketType) String() string { 12 | if i >= PacketType(len(_PacketType_index)-1) { 13 | return fmt.Sprintf("PacketType(%d)", i) 14 | } 15 | return _PacketType_name[_PacketType_index[i]:_PacketType_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /backend/pktfwd/pktfwd.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package pktfwd 5 | 6 | import ( 7 | "sync" 8 | "time" 9 | 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 11 | "github.com/TheThingsNetwork/go-utils/log" 12 | "github.com/brocaar/lorawan" 13 | ) 14 | 15 | // Config contains configuration for PacketForwarder 16 | type Config struct { 17 | Bind string 18 | Session time.Duration 19 | LockIP bool 20 | LockPort bool 21 | } 22 | 23 | // New returns a new Dummy backend 24 | func New(config Config, ctx log.Interface) *PacketForwarder { 25 | f := &PacketForwarder{ 26 | config: config, 27 | ctx: ctx.WithField("Connector", "PacketForwarder"), 28 | connect: make(chan *types.ConnectMessage), 29 | disconnect: make(chan *types.DisconnectMessage), 30 | uplink: make(map[string]chan *types.UplinkMessage), 31 | status: make(map[string]chan *types.StatusMessage), 32 | } 33 | return f 34 | } 35 | 36 | // PacketForwarder backend based on github.com/brocaar/lora-gateway-bridge 37 | type PacketForwarder struct { 38 | config Config 39 | backend *Backend 40 | ctx log.Interface 41 | 42 | mu sync.RWMutex 43 | connect chan *types.ConnectMessage 44 | disconnect chan *types.DisconnectMessage 45 | uplink map[string]chan *types.UplinkMessage 46 | status map[string]chan *types.StatusMessage 47 | } 48 | 49 | // Connect implements the Southbound interface 50 | func (f *PacketForwarder) Connect() (err error) { 51 | f.backend, err = NewBackend(f.config, f.onNew, f.onDelete, false) 52 | if err != nil { 53 | return err 54 | } 55 | f.backend.log = f.ctx 56 | 57 | go func() { 58 | for uplink := range f.backend.RXPacketChan() { 59 | f.mu.RLock() 60 | if ch, ok := f.uplink[uplink.GatewayID]; ok { 61 | ch <- uplink 62 | } else if ch, ok := f.uplink[""]; ok { 63 | ch <- uplink 64 | } else { 65 | f.ctx.WithField("GatewayID", uplink.GatewayID).Debug("Dropping uplink for inactive gateway") 66 | } 67 | f.mu.RUnlock() 68 | } 69 | }() 70 | 71 | go func() { 72 | for status := range f.backend.StatsChan() { 73 | status.Backend = "PacketForwarder" 74 | f.mu.RLock() 75 | if ch, ok := f.status[status.GatewayID]; ok { 76 | ch <- status 77 | } else if ch, ok := f.status[""]; ok { 78 | ch <- status 79 | } else { 80 | f.ctx.WithField("GatewayID", status.GatewayID).Debug("Dropping status for inactive gateway") 81 | } 82 | f.mu.RUnlock() 83 | } 84 | }() 85 | 86 | return 87 | } 88 | 89 | // Disconnect implements the Southbound interface 90 | func (f *PacketForwarder) Disconnect() error { 91 | return f.backend.Close() 92 | } 93 | 94 | func (f *PacketForwarder) onNew(mac lorawan.EUI64) error { 95 | f.connect <- &types.ConnectMessage{GatewayID: getID(mac)} 96 | return nil 97 | } 98 | 99 | func (f *PacketForwarder) onDelete(mac lorawan.EUI64) error { 100 | f.disconnect <- &types.DisconnectMessage{GatewayID: getID(mac)} 101 | return nil 102 | } 103 | 104 | // SubscribeConnect implements the Southbound interface 105 | func (f *PacketForwarder) SubscribeConnect() (<-chan *types.ConnectMessage, error) { 106 | return f.connect, nil 107 | } 108 | 109 | // UnsubscribeConnect implements the Southbound interface 110 | func (f *PacketForwarder) UnsubscribeConnect() error { 111 | close(f.connect) 112 | return nil 113 | } 114 | 115 | // SubscribeDisconnect implements the Southbound interface 116 | func (f *PacketForwarder) SubscribeDisconnect() (<-chan *types.DisconnectMessage, error) { 117 | return f.disconnect, nil 118 | } 119 | 120 | // UnsubscribeDisconnect implements the Southbound interface 121 | func (f *PacketForwarder) UnsubscribeDisconnect() error { 122 | close(f.disconnect) 123 | return nil 124 | } 125 | 126 | // SubscribeUplink implements the Southbound interface 127 | func (f *PacketForwarder) SubscribeUplink(gatewayID string) (<-chan *types.UplinkMessage, error) { 128 | f.mu.Lock() 129 | defer f.mu.Unlock() 130 | f.uplink[gatewayID] = make(chan *types.UplinkMessage) 131 | return f.uplink[gatewayID], nil 132 | } 133 | 134 | // UnsubscribeUplink implements the Southbound interface 135 | func (f *PacketForwarder) UnsubscribeUplink(gatewayID string) error { 136 | f.mu.Lock() 137 | defer f.mu.Unlock() 138 | if ch, ok := f.uplink[gatewayID]; ok { 139 | close(ch) 140 | } 141 | delete(f.uplink, gatewayID) 142 | return nil 143 | } 144 | 145 | // SubscribeStatus implements the Southbound interface 146 | func (f *PacketForwarder) SubscribeStatus(gatewayID string) (<-chan *types.StatusMessage, error) { 147 | f.mu.Lock() 148 | defer f.mu.Unlock() 149 | f.status[gatewayID] = make(chan *types.StatusMessage) 150 | return f.status[gatewayID], nil 151 | } 152 | 153 | // UnsubscribeStatus implements the Southbound interface 154 | func (f *PacketForwarder) UnsubscribeStatus(gatewayID string) error { 155 | f.mu.Lock() 156 | defer f.mu.Unlock() 157 | if ch, ok := f.status[gatewayID]; ok { 158 | close(ch) 159 | } 160 | delete(f.status, gatewayID) 161 | return nil 162 | } 163 | 164 | // PublishDownlink implements the Southbound interface 165 | func (f *PacketForwarder) PublishDownlink(message *types.DownlinkMessage) error { 166 | return f.backend.Send(message) 167 | } 168 | -------------------------------------------------------------------------------- /backend/pktfwd/security.go: -------------------------------------------------------------------------------- 1 | package pktfwd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/brocaar/lorawan" 10 | ) 11 | 12 | func newSourceLocks(withPort bool, cacheTime time.Duration) *sourceLocks { 13 | c := &sourceLocks{ 14 | withPort: withPort, 15 | cacheTime: cacheTime, 16 | sources: make(map[lorawan.EUI64]source), 17 | } 18 | if cacheTime > 0 { 19 | go func() { 20 | for range time.Tick(10 * cacheTime) { 21 | c.mu.Lock() 22 | var toDelete []lorawan.EUI64 23 | for mac, source := range c.sources { 24 | if time.Since(source.lastSeen) > cacheTime { 25 | toDelete = append(toDelete, mac) 26 | } 27 | } 28 | for _, mac := range toDelete { 29 | delete(c.sources, mac) 30 | } 31 | c.mu.Unlock() 32 | } 33 | }() 34 | } 35 | return c 36 | } 37 | 38 | type sourceLocks struct { 39 | withPort bool 40 | cacheTime time.Duration 41 | 42 | mu sync.Mutex 43 | sources map[lorawan.EUI64]source 44 | } 45 | 46 | func (c *sourceLocks) Set(mac lorawan.EUI64, addr *net.UDPAddr) error { 47 | c.mu.Lock() 48 | defer c.mu.Unlock() 49 | if existing, ok := c.sources[mac]; ok { 50 | if time.Since(existing.lastSeen) < c.cacheTime { 51 | if c.withPort && existing.addr.Port != addr.Port { 52 | return fmt.Errorf("security: inconsistent port for gateway %s: %d (expected %d)", mac, addr.Port, existing.addr.Port) 53 | } 54 | if !existing.addr.IP.Equal(addr.IP) { 55 | return fmt.Errorf("security: inconsistent IP address for gateway %s: %s (expected %s)", mac, addr.IP, existing.addr) 56 | } 57 | } 58 | } 59 | c.sources[mac] = source{time.Now(), addr} 60 | return nil 61 | } 62 | 63 | type source struct { 64 | lastSeen time.Time 65 | addr *net.UDPAddr 66 | } 67 | -------------------------------------------------------------------------------- /backend/pktfwd/security_test.go: -------------------------------------------------------------------------------- 1 | package pktfwd 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/brocaar/lorawan" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func udpAddr(str string) *net.UDPAddr { 13 | addr, err := net.ResolveUDPAddr("udp", str) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return addr 18 | } 19 | 20 | func TestSecurity(t *testing.T) { 21 | Convey("Given an empty sourceLocks with port checks off", t, func() { 22 | c := newSourceLocks(false, 20*time.Millisecond) 23 | 24 | Convey("When binding a gateway to an addr", func() { 25 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.1:12345")) 26 | Convey("There should be no error", func() { 27 | So(err, ShouldBeNil) 28 | }) 29 | Convey("When re-binding a gateway to the same addr", func() { 30 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.1:12345")) 31 | Convey("There should be no error", func() { 32 | So(err, ShouldBeNil) 33 | }) 34 | }) 35 | Convey("When re-binding a gateway to a different addr", func() { 36 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.2:12345")) 37 | Convey("There should be an error", func() { 38 | So(err, ShouldNotBeNil) 39 | }) 40 | }) 41 | Convey("When re-binding a gateway to a different addr after the expiry time", func() { 42 | time.Sleep(25 * time.Millisecond) 43 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.2:12345")) 44 | Convey("There should be no error", func() { 45 | So(err, ShouldBeNil) 46 | }) 47 | }) 48 | }) 49 | }) 50 | 51 | Convey("Given an empty sourceLocks with port checks on", t, func() { 52 | c := newSourceLocks(true, 20*time.Millisecond) 53 | 54 | Convey("When binding a gateway to an addr", func() { 55 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.1:12345")) 56 | Convey("There should be no error", func() { 57 | So(err, ShouldBeNil) 58 | }) 59 | Convey("When re-binding a gateway to the same addr", func() { 60 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.1:12345")) 61 | Convey("There should be no error", func() { 62 | So(err, ShouldBeNil) 63 | }) 64 | }) 65 | Convey("When re-binding a gateway to a different port", func() { 66 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.1:12346")) 67 | Convey("There should be an error", func() { 68 | So(err, ShouldNotBeNil) 69 | }) 70 | }) 71 | Convey("When re-binding a gateway to a different port after the expiry time", func() { 72 | time.Sleep(25 * time.Millisecond) 73 | err := c.Set(lorawan.EUI64([8]byte{1, 2, 3, 4, 5, 6, 7, 8}), udpAddr("127.0.0.2:12346")) 74 | Convey("There should be no error", func() { 75 | So(err, ShouldBeNil) 76 | }) 77 | }) 78 | }) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /backend/pktfwd/structs_test.go: -------------------------------------------------------------------------------- 1 | package pktfwd 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestDatR(t *testing.T) { 12 | Convey("Given an empty DatR", t, func() { 13 | var d DatR 14 | 15 | Convey("Then MarshalJSON returns '0'", func() { 16 | b, err := d.MarshalJSON() 17 | So(err, ShouldBeNil) 18 | So(string(b), ShouldEqual, "0") 19 | }) 20 | 21 | Convey("Given LoRa=SF7BW125", func() { 22 | d.LoRa = "SF7BW125" 23 | Convey("Then MarshalJSON returns '\"SF7BW125\"'", func() { 24 | b, err := d.MarshalJSON() 25 | So(err, ShouldBeNil) 26 | So(string(b), ShouldEqual, `"SF7BW125"`) 27 | }) 28 | }) 29 | 30 | Convey("Given FSK=1234", func() { 31 | d.FSK = 1234 32 | Convey("Then MarshalJSON returns '1234'", func() { 33 | b, err := d.MarshalJSON() 34 | So(err, ShouldBeNil) 35 | So(string(b), ShouldEqual, "1234") 36 | }) 37 | }) 38 | 39 | Convey("Given the string '1234'", func() { 40 | s := "1234" 41 | Convey("Then UnmarshalJSON returns FSK=1234", func() { 42 | err := d.UnmarshalJSON([]byte(s)) 43 | So(err, ShouldBeNil) 44 | So(d.FSK, ShouldEqual, 1234) 45 | }) 46 | }) 47 | 48 | Convey("Given the string '\"SF7BW125\"'", func() { 49 | s := `"SF7BW125"` 50 | Convey("Then UnmarshalJSON returns LoRa=SF7BW125", func() { 51 | err := d.UnmarshalJSON([]byte(s)) 52 | So(err, ShouldBeNil) 53 | So(d.LoRa, ShouldEqual, "SF7BW125") 54 | }) 55 | }) 56 | }) 57 | } 58 | 59 | func TestCompactTime(t *testing.T) { 60 | Convey("Given the date 'Mon Jan 2 15:04:05 -0700 MST 2006'", t, func() { 61 | tStr := "Mon Jan 2 15:04:05 -0700 MST 2006" 62 | ts, err := time.Parse(tStr, tStr) 63 | So(err, ShouldBeNil) 64 | 65 | Convey("MarshalJSON returns '\"2006-01-02T22:04:05Z\"'", func() { 66 | 67 | b, err := CompactTime(ts).MarshalJSON() 68 | So(err, ShouldBeNil) 69 | So(string(b), ShouldEqual, `"2006-01-02T22:04:05Z"`) 70 | }) 71 | 72 | Convey("Given the JSON value of the date (\"2006-01-02T22:04:05Z\")", func() { 73 | s := `"2006-01-02T22:04:05Z"` 74 | Convey("UnmarshalJSON returns the correct date", func() { 75 | var ct CompactTime 76 | err := ct.UnmarshalJSON([]byte(s)) 77 | So(err, ShouldBeNil) 78 | So(time.Time(ct).Equal(ts), ShouldBeTrue) 79 | }) 80 | }) 81 | }) 82 | } 83 | 84 | func TestGetPacketType(t *testing.T) { 85 | Convey("Given an empty slice []byte{}", t, func() { 86 | var b []byte 87 | 88 | Convey("Then GetPacketType returns an error (length)", func() { 89 | _, err := GetPacketType(b) 90 | So(err, ShouldResemble, errors.New("gateway: at least 4 bytes of data are expected")) 91 | }) 92 | 93 | Convey("Given the slice []byte{3, 1, 3, 4}", func() { 94 | b = []byte{3, 1, 3, 4} 95 | Convey("Then GetPacketType returns an error (protocol version)", func() { 96 | _, err := GetPacketType(b) 97 | So(err, ShouldResemble, ErrInvalidProtocolVersion) 98 | }) 99 | }) 100 | 101 | Convey("Given the slice []byte{2, 1, 3, 4}", func() { 102 | b = []byte{2, 1, 3, 4} 103 | Convey("Then GetPacketType returns PullACK", func() { 104 | t, err := GetPacketType(b) 105 | So(err, ShouldBeNil) 106 | So(t, ShouldEqual, PullACK) 107 | }) 108 | }) 109 | }) 110 | } 111 | 112 | func TestPushDataPacket(t *testing.T) { 113 | Convey("Given an empty PushDataPacket", t, func() { 114 | var p PushDataPacket 115 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 125}", func() { 116 | b, err := p.MarshalBinary() 117 | So(err, ShouldBeNil) 118 | So(b, ShouldResemble, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 125}) 119 | }) 120 | 121 | Convey("Given ProtocolVersion=2, RandomToken=123, GatewayMAC=[]{2, 2, 3, 4, 5, 6, 7, 8}", func() { 122 | p = PushDataPacket{ 123 | ProtocolVersion: ProtocolVersion2, 124 | RandomToken: 123, 125 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 126 | } 127 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125}", func() { 128 | b, err := p.MarshalBinary() 129 | So(err, ShouldBeNil) 130 | So(b, ShouldResemble, []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125}) 131 | }) 132 | }) 133 | 134 | Convey("Given the slice []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125}", func() { 135 | b := []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125} 136 | Convey("Then UnmarshalBinary returns RandomToken=123, GatewayMAC=[]{1, 2, 3, 4, 5, 6, 7, 8}", func() { 137 | err := p.UnmarshalBinary(b) 138 | So(err, ShouldBeNil) 139 | So(p, ShouldResemble, PushDataPacket{ 140 | ProtocolVersion: ProtocolVersion2, 141 | RandomToken: 123, 142 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 143 | }) 144 | }) 145 | }) 146 | }) 147 | } 148 | 149 | func TestPushACKPacket(t *testing.T) { 150 | Convey("Given an empty PushACKPacket", t, func() { 151 | var p PushACKPacket 152 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 1}", func() { 153 | b, err := p.MarshalBinary() 154 | So(err, ShouldBeNil) 155 | So(b, ShouldResemble, []byte{0, 0, 0, 1}) 156 | }) 157 | 158 | Convey("Given ProtocolVersion=2, RandomToken=123", func() { 159 | p = PushACKPacket{ 160 | ProtocolVersion: ProtocolVersion2, 161 | RandomToken: 123, 162 | } 163 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 1}", func() { 164 | b, err := p.MarshalBinary() 165 | So(err, ShouldBeNil) 166 | So(b, ShouldResemble, []byte{2, 123, 0, 1}) 167 | }) 168 | }) 169 | 170 | Convey("Given the slice []byte{2, 123, 0, 1}", func() { 171 | Convey("Then UnmarshalBinary returns RandomToken=123", func() { 172 | b := []byte{2, 123, 0, 1} 173 | err := p.UnmarshalBinary(b) 174 | So(err, ShouldBeNil) 175 | So(p, ShouldResemble, PushACKPacket{ 176 | ProtocolVersion: ProtocolVersion2, 177 | RandomToken: 123, 178 | }) 179 | }) 180 | }) 181 | }) 182 | } 183 | 184 | func TestPullDataPacket(t *testing.T) { 185 | Convey("Given an empty PullDataPacket", t, func() { 186 | var p PullDataPacket 187 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}", func() { 188 | b, err := p.MarshalBinary() 189 | So(err, ShouldBeNil) 190 | So(b, ShouldResemble, []byte{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}) 191 | }) 192 | 193 | Convey("Given ProtocolVersion=2, RandomToken=123, GatewayMAC=[]byte{1, 2, 3, 4, 5, 6, 8, 8}", func() { 194 | p = PullDataPacket{ 195 | ProtocolVersion: ProtocolVersion2, 196 | RandomToken: 123, 197 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 198 | } 199 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8}", func() { 200 | b, err := p.MarshalBinary() 201 | So(err, ShouldBeNil) 202 | So(b, ShouldResemble, []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8}) 203 | }) 204 | }) 205 | 206 | Convey("Given the slice []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8}", func() { 207 | b := []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8} 208 | Convey("Then UnmarshalBinary returns RandomToken=123, GatewayMAC=[]byte{1, 2, 3, 4, 5, 6, 8, 8}", func() { 209 | err := p.UnmarshalBinary(b) 210 | So(err, ShouldBeNil) 211 | So(p, ShouldResemble, PullDataPacket{ 212 | ProtocolVersion: ProtocolVersion2, 213 | RandomToken: 123, 214 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 215 | }) 216 | }) 217 | }) 218 | }) 219 | } 220 | 221 | func TestPullACKPacket(t *testing.T) { 222 | Convey("Given an empty PullACKPacket", t, func() { 223 | var p PullACKPacket 224 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 4}", func() { 225 | b, err := p.MarshalBinary() 226 | So(err, ShouldBeNil) 227 | So(b, ShouldResemble, []byte{0, 0, 0, 4}) 228 | }) 229 | 230 | Convey("Given ProtocolVersion=2, RandomToken=123}", func() { 231 | p = PullACKPacket{ 232 | ProtocolVersion: ProtocolVersion2, 233 | RandomToken: 123, 234 | } 235 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 4}", func() { 236 | b, err := p.MarshalBinary() 237 | So(err, ShouldBeNil) 238 | So(b, ShouldResemble, []byte{2, 123, 0, 4}) 239 | }) 240 | }) 241 | 242 | Convey("Given the slice []byte{2, 123, 0, 4}", func() { 243 | b := []byte{2, 123, 0, 4} 244 | Convey("Then UnmarshalBinary returns RandomToken=123", func() { 245 | err := p.UnmarshalBinary(b) 246 | So(err, ShouldBeNil) 247 | So(p, ShouldResemble, PullACKPacket{ 248 | ProtocolVersion: ProtocolVersion2, 249 | RandomToken: 123, 250 | }) 251 | }) 252 | }) 253 | }) 254 | } 255 | 256 | func TestPullRespPacket(t *testing.T) { 257 | Convey("Given an empty PullRespPacket", t, func() { 258 | var p PullRespPacket 259 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 3} as first 4 bytes", func() { 260 | b, err := p.MarshalBinary() 261 | So(err, ShouldBeNil) 262 | So(b[0:4], ShouldResemble, []byte{0, 0, 0, 3}) 263 | }) 264 | 265 | Convey("Given ProtocolVersion=2, RandomToken=123", func() { 266 | p = PullRespPacket{ 267 | ProtocolVersion: ProtocolVersion2, 268 | RandomToken: 123, 269 | } 270 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 3} as first 4 bytes", func() { 271 | b, err := p.MarshalBinary() 272 | So(err, ShouldBeNil) 273 | So(b[0:4], ShouldResemble, []byte{2, 123, 0, 3}) 274 | }) 275 | }) 276 | 277 | Convey("Given ProtocolVersion=1, RandomToken=123", func() { 278 | p = PullRespPacket{ 279 | ProtocolVersion: ProtocolVersion1, 280 | RandomToken: 123, 281 | } 282 | Convey("Then MarshalBinary returns []byte{1, 0, 0, 3} as first 4 bytes", func() { 283 | b, err := p.MarshalBinary() 284 | So(err, ShouldBeNil) 285 | So(b[0:4], ShouldResemble, []byte{1, 0, 0, 3}) 286 | }) 287 | }) 288 | 289 | Convey("Given the slice []byte{2, 123, 0, 3, 123, 125}", func() { 290 | b := []byte{2, 123, 0, 3, 123, 125} 291 | Convey("Then UnmarshalBinary returns RandomToken=123", func() { 292 | err := p.UnmarshalBinary(b) 293 | So(err, ShouldBeNil) 294 | So(p, ShouldResemble, PullRespPacket{ 295 | ProtocolVersion: ProtocolVersion2, 296 | RandomToken: 123, 297 | }) 298 | }) 299 | }) 300 | }) 301 | } 302 | 303 | func TestTXACKPacket(t *testing.T) { 304 | Convey("Given an empty TXACKPacket", t, func() { 305 | var p TXACKPacket 306 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}", func() { 307 | b, err := p.MarshalBinary() 308 | So(err, ShouldBeNil) 309 | So(b, ShouldResemble, []byte{0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}) 310 | }) 311 | 312 | Convey("Given ProtocolVersion=2, RandomToken=123 and GatewayMAC=[]byte{8, 7, 6, 5, 4, 3, 2, 1}", func() { 313 | p.ProtocolVersion = ProtocolVersion2 314 | p.RandomToken = 123 315 | p.GatewayMAC = [8]byte{8, 7, 6, 5, 4, 3, 2, 1} 316 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1}", func() { 317 | b, err := p.MarshalBinary() 318 | So(err, ShouldBeNil) 319 | So(b, ShouldResemble, []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1}) 320 | }) 321 | }) 322 | 323 | Convey("Given the slice []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1}", func() { 324 | b := []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1} 325 | 326 | Convey("Then UnmarshalBinary return RandomToken=123 and GatewayMAC=[8]byte{8, 7, 6, 5, 4, 3, 2, 1}", func() { 327 | err := p.UnmarshalBinary(b) 328 | So(err, ShouldBeNil) 329 | So(p.RandomToken, ShouldEqual, 123) 330 | So(p.GatewayMAC[:], ShouldResemble, []byte{8, 7, 6, 5, 4, 3, 2, 1}) 331 | So(p.Payload, ShouldBeNil) 332 | So(p.ProtocolVersion, ShouldEqual, ProtocolVersion2) 333 | }) 334 | }) 335 | 336 | Convey("Given ProtocolVersion=2, RandomToken=123 and a payload with Error=COLLISION_BEACON", func() { 337 | p.ProtocolVersion = ProtocolVersion2 338 | p.RandomToken = 123 339 | p.Payload = &TXACKPayload{ 340 | TXPKACK: TXPKACK{ 341 | Error: "COLLISION_BEACON", 342 | }, 343 | } 344 | 345 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125}", func() { 346 | b, err := p.MarshalBinary() 347 | So(err, ShouldBeNil) 348 | So(b, ShouldResemble, []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125}) 349 | }) 350 | }) 351 | 352 | Convey("Given the slice []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125}", func() { 353 | b := []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125} 354 | Convey("Then UnmarshalBinary returns RandomToken=123 and a payload with Error=COLLISION_BEACON", func() { 355 | err := p.UnmarshalBinary(b) 356 | So(err, ShouldBeNil) 357 | So(p.RandomToken, ShouldEqual, 123) 358 | So(p.ProtocolVersion, ShouldEqual, ProtocolVersion2) 359 | So(p.Payload, ShouldResemble, &TXACKPayload{ 360 | TXPKACK: TXPKACK{ 361 | Error: "COLLISION_BEACON", 362 | }, 363 | }) 364 | }) 365 | }) 366 | }) 367 | } 368 | -------------------------------------------------------------------------------- /backend/ttn/router.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttn 5 | 6 | import ( 7 | "context" 8 | "sync" 9 | "time" 10 | 11 | "github.com/TheThingsNetwork/api/discovery" 12 | "github.com/TheThingsNetwork/api/discovery/discoveryclient" 13 | "github.com/TheThingsNetwork/api/router/routerclient" 14 | "github.com/TheThingsNetwork/api/trace" 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 16 | "github.com/TheThingsNetwork/go-utils/grpc/auth" 17 | "github.com/TheThingsNetwork/ttn/api" 18 | "github.com/TheThingsNetwork/ttn/api/pool" 19 | "github.com/apex/log" 20 | "google.golang.org/grpc" 21 | ) 22 | 23 | func init() { 24 | api.WaitForStreams = 0 25 | grpc.EnableTracing = false 26 | } 27 | 28 | // RouterConfig contains configuration for the TTN Router 29 | type RouterConfig struct { 30 | DiscoveryServer string 31 | RouterID string 32 | } 33 | 34 | // Router side of the bridge 35 | type Router struct { 36 | config RouterConfig 37 | Ctx log.Interface 38 | conn *grpc.ClientConn 39 | client *routerclient.Client 40 | 41 | pool *pool.Pool 42 | 43 | mu sync.Mutex 44 | gateways map[string]*gatewayConn 45 | } 46 | 47 | // New sets up a new TTN Router 48 | func New(config RouterConfig, ctx log.Interface, tokenFunc func(string) string) (*Router, error) { 49 | router := &Router{ 50 | config: config, 51 | Ctx: ctx.WithField("Connector", "TTN Router"), 52 | pool: pool.NewPool(context.Background(), append(pool.DefaultDialOptions, auth.WithTokenFunc("id", tokenFunc).DialOption())...), 53 | gateways: make(map[string]*gatewayConn), 54 | } 55 | return router, nil 56 | } 57 | 58 | // Connect to the TTN Router 59 | func (r *Router) Connect() error { 60 | r.mu.Lock() 61 | defer r.mu.Unlock() 62 | r.Ctx.WithFields(log.Fields{ 63 | "Discovery": r.config.DiscoveryServer, 64 | "RouterID": r.config.RouterID, 65 | }).Info("Discovering Router") 66 | discovery, err := discoveryclient.NewClient(r.config.DiscoveryServer, &discovery.Announcement{ 67 | ServiceName: "bridge", 68 | }, func() string { return "" }) 69 | if err != nil { 70 | return err 71 | } 72 | announcement, err := discovery.Get("router", r.config.RouterID) 73 | if err != nil { 74 | return err 75 | } 76 | r.Ctx.WithFields(log.Fields{ 77 | "RouterID": r.config.RouterID, 78 | "Address": announcement.NetAddress, 79 | }).Info("Connecting with Router") 80 | if announcement.GetCertificate() == "" { 81 | r.conn, err = announcement.Dial(nil) 82 | } else { 83 | r.conn, err = announcement.Dial(r.pool) 84 | } 85 | if err != nil { 86 | return err 87 | } 88 | r.client = routerclient.NewClient(routerclient.DefaultClientConfig) 89 | r.client.AddServer(r.config.RouterID, r.conn) 90 | return nil 91 | } 92 | 93 | // Disconnect from the TTN Router and clean up gateway connections 94 | func (r *Router) Disconnect() error { 95 | r.mu.Lock() 96 | defer r.mu.Unlock() 97 | r.gateways = make(map[string]*gatewayConn) 98 | r.pool.Close() 99 | return nil 100 | } 101 | 102 | type gatewayConn struct { 103 | stream routerclient.GenericStream 104 | lastActive time.Time 105 | } 106 | 107 | func (r *Router) getGateway(gatewayID string, downlinkActive bool) *gatewayConn { 108 | r.mu.Lock() 109 | defer r.mu.Unlock() 110 | if gtw, ok := r.gateways[gatewayID]; ok { 111 | gtw.lastActive = time.Now() 112 | return gtw 113 | } 114 | r.gateways[gatewayID] = &gatewayConn{ 115 | stream: r.client.NewGatewayStreams(gatewayID, "", downlinkActive), 116 | lastActive: time.Now(), 117 | } 118 | return r.gateways[gatewayID] 119 | } 120 | 121 | // CleanupGateway cleans up gateway clients that are no longer needed 122 | func (r *Router) CleanupGateway(gatewayID string) { 123 | r.mu.Lock() 124 | defer r.mu.Unlock() 125 | if gtw, ok := r.gateways[gatewayID]; ok { 126 | gtw.stream.Close() 127 | delete(r.gateways, gatewayID) 128 | } 129 | } 130 | 131 | // PublishUplink publishes uplink messages to the TTN Router 132 | func (r *Router) PublishUplink(message *types.UplinkMessage) error { 133 | message.Message.Trace = message.Message.Trace.WithEvent(trace.ForwardEvent, "backend", "ttn") 134 | r.getGateway(message.GatewayID, false).stream.Uplink(message.Message) 135 | return nil 136 | } 137 | 138 | // PublishStatus publishes status messages to the TTN Router 139 | func (r *Router) PublishStatus(message *types.StatusMessage) error { 140 | r.getGateway(message.GatewayID, false).stream.Status(message.Message) 141 | return nil 142 | } 143 | 144 | // SubscribeDownlink handles downlink messages for the given gateway ID 145 | func (r *Router) SubscribeDownlink(gatewayID string) (<-chan *types.DownlinkMessage, error) { 146 | downlink := make(chan *types.DownlinkMessage) 147 | 148 | gtw := r.getGateway(gatewayID, true) 149 | ctx := r.Ctx.WithField("GatewayID", gatewayID) 150 | 151 | ch, err := gtw.stream.Downlink() 152 | if err == routerclient.ErrDownlinkInactive { 153 | ctx.Debug("Downlink inactive, restarting streams with downlink") 154 | r.mu.Lock() 155 | oldStream := gtw.stream 156 | gtw.stream = r.client.NewGatewayStreams(gatewayID, "", true) 157 | r.mu.Unlock() 158 | oldStream.Close() 159 | ch, err = gtw.stream.Downlink() 160 | } 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | go func() { 166 | for in := range ch { 167 | ctx.Debug("Downlink message received") 168 | in.Trace = in.Trace.WithEvent(trace.ReceiveEvent, "backend", "ttn") 169 | downlink <- &types.DownlinkMessage{GatewayID: gatewayID, Message: in} 170 | } 171 | close(downlink) 172 | }() 173 | 174 | return downlink, nil 175 | } 176 | 177 | // UnsubscribeDownlink should unsubscribe from downlink, but in practice just disconnects the entire gateway 178 | func (r *Router) UnsubscribeDownlink(gatewayID string) error { 179 | r.CleanupGateway(gatewayID) 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /backend/ttn/router_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttn 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | "time" 10 | 11 | pb_gateway "github.com/TheThingsNetwork/api/gateway" 12 | pb_router "github.com/TheThingsNetwork/api/router" 13 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 14 | ttnlog "github.com/TheThingsNetwork/go-utils/log" 15 | "github.com/TheThingsNetwork/go-utils/log/apex" 16 | "github.com/TheThingsNetwork/ttn/api" 17 | "github.com/TheThingsNetwork/ttn/api/pool" 18 | "github.com/apex/log" 19 | "github.com/apex/log/handlers/text" 20 | . "github.com/smartystreets/goconvey/convey" 21 | ) 22 | 23 | func TestTTNRouter(t *testing.T) { 24 | Convey("Given a new Context", t, func(c C) { 25 | 26 | var logs bytes.Buffer 27 | ctx := &log.Logger{ 28 | Handler: text.New(&logs), 29 | Level: log.DebugLevel, 30 | } 31 | ttnlog.Set(apex.Wrap(ctx)) 32 | defer func() { 33 | if logs.Len() > 0 { 34 | c.Printf("\n%s", logs.String()) 35 | } 36 | }() 37 | 38 | api.WaitForStreams = time.Second 39 | 40 | Convey("When creating a new TTN Router", func() { 41 | router, err := New(RouterConfig{ 42 | DiscoveryServer: "discovery.thethingsnetwork.org:1900", 43 | RouterID: "ttn-router-eu", 44 | }, ctx, func(string) string { 45 | return "token" 46 | }) 47 | Convey("There should be no error", func() { 48 | So(err, ShouldBeNil) 49 | }) 50 | Convey("The bridge should now have TTNRouter", func() { 51 | So(router, ShouldNotBeNil) 52 | }) 53 | Reset(func() { 54 | pool.Global.Close() 55 | router.pool.Close() 56 | }) 57 | 58 | Convey("When calling Connect on TTNRouter", func() { 59 | err := router.Connect() 60 | Convey("There should be no error", func() { 61 | So(err, ShouldBeNil) 62 | }) 63 | Convey("There should be a router client", func() { 64 | So(router.client, ShouldNotBeNil) 65 | }) 66 | 67 | Convey("When publishing an uplink message", func() { 68 | router.PublishUplink(&types.UplinkMessage{ 69 | GatewayID: "dev", 70 | Message: &pb_router.UplinkMessage{}, 71 | }) 72 | oldStream := router.gateways["dev"].stream 73 | _, err := router.SubscribeDownlink("dev") 74 | Convey("There should be no error", func() { 75 | So(err, ShouldBeNil) 76 | }) 77 | Convey("The streams should have been replaced", func() { 78 | So(router.gateways["dev"].stream, ShouldNotEqual, oldStream) 79 | }) 80 | }) 81 | 82 | Convey("When publishing a status message", func() { 83 | router.PublishStatus(&types.StatusMessage{ 84 | GatewayID: "dev", 85 | Message: &pb_gateway.Status{}, 86 | }) 87 | }) 88 | 89 | Convey("When subscribing to downlink messages", func() { 90 | _, err := router.SubscribeDownlink("dev") 91 | Convey("There should be no error", func() { 92 | So(err, ShouldBeNil) 93 | }) 94 | }) 95 | }) 96 | }) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/user" 10 | "strings" 11 | 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | // EnvPrefix is the environment prefix that is used for configuration 16 | const EnvPrefix = "bridge" 17 | 18 | var cfgFile string 19 | 20 | func initConfig() { 21 | viper.SetEnvPrefix(EnvPrefix) 22 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 23 | viper.AutomaticEnv() 24 | if cfgFile != "" { 25 | viper.SetConfigFile(cfgFile) 26 | err := viper.ReadInConfig() 27 | if err != nil { 28 | fmt.Println("Error when reading config file:", err) 29 | } else if err == nil { 30 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 31 | } 32 | } 33 | viper.BindEnv("debug") 34 | 35 | var defaultID string 36 | if user, err := user.Current(); err == nil { 37 | defaultID = user.Username + "@" 38 | } 39 | if hostname, err := os.Hostname(); err == nil { 40 | defaultID += hostname 41 | } 42 | viper.SetDefault("id", defaultID) 43 | } 44 | 45 | var config = viper.GetViper() 46 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "runtime" 10 | 11 | "github.com/apex/log" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ctx *log.Logger 16 | 17 | var logFile *os.File 18 | 19 | // Execute is called by main.go 20 | func Execute() { 21 | defer func() { 22 | buf := make([]byte, 1<<16) 23 | runtime.Stack(buf, false) 24 | if thePanic := recover(); thePanic != nil && ctx != nil { 25 | ctx.WithField("panic", thePanic).WithField("stack", string(buf)).Fatal("Stopping because of panic") 26 | } 27 | }() 28 | 29 | if err := BridgeCmd.Execute(); err != nil { 30 | fmt.Println(err) 31 | os.Exit(-1) 32 | } 33 | } 34 | 35 | func init() { 36 | cobra.OnInitialize(initConfig) 37 | } 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | redis: 4 | image: redis 5 | command: redis-server --appendonly yes 6 | ports: 7 | - "6379:6379" 8 | volumes: 9 | - /data 10 | rabbitmq: 11 | image: thethingsnetwork/rabbitmq 12 | hostname: rabbitserver 13 | ports: 14 | - "1883:1883" 15 | - "5672:5672" 16 | - "15672:15672" 17 | volumes: 18 | - /var/lib/rabbitmq 19 | bridge: 20 | image: thethingsnetwork/gateway-connector-bridge 21 | hostname: bridge 22 | restart: always 23 | depends_on: 24 | - redis 25 | - rabbitmq 26 | ports: 27 | - "1700:1700/udp" 28 | environment: 29 | BRIDGE_UDP: :1700 30 | BRIDGE_REDIS_ADDRESS: redis:6379 31 | BRIDGE_AMQP: rabbitmq:5672 32 | BRIDGE_MQTT: disable 33 | BRIDGE_TTN_ROUTER: disable 34 | BRIDGE_BLACKLIST: https://ttn.fyi/blacklist.yml 35 | # BRIDGE_TTN_ROUTER: discovery.thethingsnetwork.org:1900/the-router-id 36 | -------------------------------------------------------------------------------- /exchange/exchange_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package exchange 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | "time" 10 | 11 | pb_gateway "github.com/TheThingsNetwork/api/gateway" 12 | pb_router "github.com/TheThingsNetwork/api/router" 13 | "github.com/TheThingsNetwork/gateway-connector-bridge/auth" 14 | "github.com/TheThingsNetwork/gateway-connector-bridge/backend/dummy" 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 16 | "github.com/apex/log" 17 | "github.com/apex/log/handlers/text" 18 | . "github.com/smartystreets/goconvey/convey" 19 | ) 20 | 21 | func TestExchange(t *testing.T) { 22 | Convey("Given a new Context and Backends", t, func(c C) { 23 | 24 | var logs bytes.Buffer 25 | ctx := &log.Logger{ 26 | Handler: text.New(&logs), 27 | Level: log.DebugLevel, 28 | } 29 | defer func() { 30 | if logs.Len() > 0 { 31 | c.Printf("\n%s", logs.String()) 32 | } 33 | }() 34 | 35 | ttn := dummy.New(ctx.WithField("Direction", "TTN")) 36 | gateway := dummy.New(ctx.WithField("Direction", "Gateway")) 37 | 38 | auth := auth.NewMemory() 39 | 40 | Convey("When creating a new Exchange", func() { 41 | b := New(ctx, 0) 42 | b.SetAuth(auth) 43 | 44 | Convey("When adding a Northbound and Southbound backend", func() { 45 | b.AddNorthbound(ttn) 46 | b.AddSouthbound(gateway) 47 | 48 | Convey("When starting the Exchange", func() { 49 | b.Start(1, 10*time.Millisecond) 50 | 51 | Convey("When stopping the Exchange", func() { 52 | b.Stop() 53 | }) 54 | 55 | Convey("When sending a connect message with a Key", func() { 56 | err := gateway.PublishConnect(&types.ConnectMessage{ 57 | GatewayID: "dev", 58 | Key: "key", 59 | }) 60 | time.Sleep(10 * time.Millisecond) 61 | Convey("There should be no error", func() { 62 | So(err, ShouldBeNil) 63 | }) 64 | Convey("The gateway should be connected", func() { 65 | So(b.gateways.Contains("dev"), ShouldBeTrue) 66 | }) 67 | 68 | Convey("When sending a disconnect message with the same Key", func() { 69 | err := gateway.PublishDisconnect(&types.DisconnectMessage{ 70 | GatewayID: "dev", 71 | Key: "key", 72 | }) 73 | time.Sleep(10 * time.Millisecond) 74 | Convey("There should be no error", func() { 75 | So(err, ShouldBeNil) 76 | }) 77 | Convey("The gateway should be disconnected", func() { 78 | So(b.gateways.Contains("dev"), ShouldBeFalse) 79 | }) 80 | }) 81 | 82 | Convey("When sending a disconnect message with a different Key", func() { 83 | err := gateway.PublishDisconnect(&types.DisconnectMessage{ 84 | GatewayID: "dev", 85 | Key: "other-key", 86 | }) 87 | time.Sleep(10 * time.Millisecond) 88 | Convey("There should be no error", func() { 89 | So(err, ShouldBeNil) 90 | }) 91 | Convey("The gateway should not be disconnected", func() { 92 | So(b.gateways.Contains("dev"), ShouldBeTrue) 93 | }) 94 | }) 95 | }) 96 | 97 | Convey("When sending a connect message", func() { 98 | err := gateway.PublishConnect(&types.ConnectMessage{ 99 | GatewayID: "dev", 100 | }) 101 | time.Sleep(10 * time.Millisecond) 102 | Convey("There should be no error", func() { 103 | So(err, ShouldBeNil) 104 | }) 105 | Convey("The gateway should be connected", func() { 106 | So(b.gateways.Contains("dev"), ShouldBeTrue) 107 | }) 108 | 109 | Convey("When sending another connect message", func() { 110 | err := gateway.PublishConnect(&types.ConnectMessage{ 111 | GatewayID: "dev", 112 | }) 113 | time.Sleep(10 * time.Millisecond) 114 | Convey("There should be no error", func() { 115 | So(err, ShouldBeNil) 116 | }) 117 | Convey("The gateway should still be connected", func() { 118 | So(b.gateways.Contains("dev"), ShouldBeTrue) 119 | }) 120 | }) 121 | 122 | Convey("When subscribing to uplink messages on the TTN side", func() { 123 | msg, _ := ttn.SubscribeUplink("dev") 124 | time.Sleep(10 * time.Millisecond) 125 | 126 | Convey("When sending an uplink message on the Gateway side", func() { 127 | err := gateway.PublishUplink(&types.UplinkMessage{ 128 | GatewayID: "dev", 129 | Message: &pb_router.UplinkMessage{}, 130 | }) 131 | Convey("There should be no error", func() { 132 | So(err, ShouldBeNil) 133 | }) 134 | 135 | Convey("Then it should arrive on the TTN side", func() { 136 | select { 137 | case <-time.After(time.Second): 138 | So("Timeout Exceeded", ShouldBeFalse) 139 | case _, ok := <-msg: 140 | So(ok, ShouldBeTrue) 141 | } 142 | }) 143 | }) 144 | }) 145 | 146 | Convey("When subscribing to downlink messages on the Gateway side", func() { 147 | msg, _ := gateway.SubscribeDownlink("dev") 148 | time.Sleep(10 * time.Millisecond) 149 | 150 | Convey("When sending a downlink message on the TTN side", func() { 151 | err := ttn.PublishDownlink(&types.DownlinkMessage{ 152 | GatewayID: "dev", 153 | Message: &pb_router.DownlinkMessage{}, 154 | }) 155 | Convey("There should be no error", func() { 156 | So(err, ShouldBeNil) 157 | }) 158 | 159 | Convey("Then it should arrive on the Gateway side", func() { 160 | select { 161 | case <-time.After(time.Second): 162 | So("Timeout Exceeded", ShouldBeFalse) 163 | case _, ok := <-msg: 164 | So(ok, ShouldBeTrue) 165 | } 166 | }) 167 | }) 168 | }) 169 | 170 | Convey("When subscribing to status messages on the TTN side", func() { 171 | msg, _ := ttn.SubscribeStatus("dev") 172 | time.Sleep(10 * time.Millisecond) 173 | 174 | Convey("When sending an status message on the Gateway side", func() { 175 | err := gateway.PublishStatus(&types.StatusMessage{ 176 | GatewayID: "dev", 177 | Message: &pb_gateway.Status{}, 178 | }) 179 | Convey("There should be no error", func() { 180 | So(err, ShouldBeNil) 181 | }) 182 | 183 | Convey("Then it should arrive on the TTN side", func() { 184 | select { 185 | case <-time.After(time.Second): 186 | So("Timeout Exceeded", ShouldBeFalse) 187 | case _, ok := <-msg: 188 | So(ok, ShouldBeTrue) 189 | } 190 | }) 191 | }) 192 | }) 193 | 194 | Convey("When sending a disconnect message", func() { 195 | err := gateway.PublishDisconnect(&types.DisconnectMessage{ 196 | GatewayID: "dev", 197 | }) 198 | time.Sleep(10 * time.Millisecond) 199 | Convey("There should be no error", func() { 200 | So(err, ShouldBeNil) 201 | }) 202 | Convey("The gateway should be disconnected", func() { 203 | So(b.gateways.Contains("dev"), ShouldBeFalse) 204 | }) 205 | 206 | Convey("When sending another disconnect message", func() { 207 | err := gateway.PublishDisconnect(&types.DisconnectMessage{ 208 | GatewayID: "dev", 209 | }) 210 | time.Sleep(10 * time.Millisecond) 211 | Convey("There should be no error", func() { 212 | So(err, ShouldBeNil) 213 | }) 214 | Convey("The gateway should still be disconnected", func() { 215 | So(b.gateways.Contains("dev"), ShouldBeFalse) 216 | }) 217 | }) 218 | }) 219 | 220 | Convey("When stopping the Exchange", func() { 221 | b.Stop() 222 | }) 223 | 224 | }) 225 | }) 226 | }) 227 | 228 | Convey("When starting the Exchange", func() { 229 | b.Start(1, 10*time.Millisecond) 230 | 231 | Convey("When stopping the Exchange", func() { 232 | b.Stop() 233 | }) 234 | }) 235 | 236 | }) 237 | 238 | }) 239 | } 240 | -------------------------------------------------------------------------------- /exchange/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package exchange 5 | 6 | import ( 7 | "github.com/TheThingsNetwork/api/protocol" 8 | "github.com/TheThingsNetwork/api/protocol/lorawan" 9 | "github.com/prometheus/client_golang/prometheus" 10 | ) 11 | 12 | var info = prometheus.NewGaugeVec( 13 | prometheus.GaugeOpts{ 14 | Namespace: "ttn", 15 | Subsystem: "bridge", 16 | Name: "info", 17 | Help: "Information about the TTN environment.", 18 | }, []string{ 19 | "build_date", "git_commit", "id", "version", 20 | }, 21 | ) 22 | 23 | var connectedGateways = prometheus.NewGauge( 24 | prometheus.GaugeOpts{ 25 | Namespace: "ttn", 26 | Subsystem: "bridge", 27 | Name: "connected_gateways", 28 | Help: "Number of connected gateways.", 29 | }, 30 | ) 31 | 32 | var handledCounter = prometheus.NewCounterVec( 33 | prometheus.CounterOpts{ 34 | Namespace: "ttn", 35 | Subsystem: "bridge", 36 | Name: "messages_handled_total", 37 | Help: "Total number of messages handled.", 38 | }, []string{"message_type"}, 39 | ) 40 | 41 | func mTypeToString(mType lorawan.MType) string { 42 | switch mType { 43 | case lorawan.MType_JOIN_REQUEST: 44 | return "JoinRequest" 45 | case lorawan.MType_JOIN_ACCEPT: 46 | return "JoinAccept" 47 | case lorawan.MType_UNCONFIRMED_UP: 48 | return "UnconfirmedUp" 49 | case lorawan.MType_UNCONFIRMED_DOWN: 50 | return "UnconfirmedDown" 51 | case lorawan.MType_CONFIRMED_UP: 52 | return "ConfirmedUp" 53 | case lorawan.MType_CONFIRMED_DOWN: 54 | return "ConfirmedDown" 55 | case 6: 56 | return "RejoinRequest" 57 | case 7: 58 | return "Proprietary" 59 | default: 60 | return "Unknown" 61 | } 62 | } 63 | 64 | func messageType(msg *protocol.Message) string { 65 | if msg := msg.GetLoRaWAN(); msg != nil { 66 | return mTypeToString(msg.GetMType()) 67 | } 68 | return "Unknown" 69 | } 70 | 71 | type message interface { 72 | UnmarshalPayload() error 73 | GetPayload() []byte 74 | GetMessage() *protocol.Message 75 | } 76 | 77 | func registerHandled(msg message) { 78 | msg.UnmarshalPayload() 79 | handledCounter.WithLabelValues(messageType(msg.GetMessage())).Inc() 80 | } 81 | 82 | func registerStatus() { 83 | handledCounter.WithLabelValues("GatewayStatus").Inc() 84 | } 85 | 86 | func init() { 87 | prometheus.MustRegister(info) 88 | prometheus.MustRegister(connectedGateways) 89 | prometheus.MustRegister(handledCounter) 90 | for mType := lorawan.MType(0); mType < 8; mType++ { 91 | handledCounter.WithLabelValues(mTypeToString(mType)).Add(0) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /exchange/state.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package exchange 5 | 6 | import ( 7 | redis "gopkg.in/redis.v5" 8 | ) 9 | 10 | type gatewayState interface { 11 | // Adds an element to the set. Returns whether 12 | // the item was added. 13 | Add(i interface{}) bool 14 | 15 | // Returns whether the given items 16 | // are all in the set. 17 | Contains(i ...interface{}) bool 18 | 19 | // Remove a single element from the set. 20 | Remove(i interface{}) 21 | 22 | // Returns the members of the set as a slice. 23 | ToSlice() []interface{} 24 | } 25 | 26 | // defaultRedisStateKey is used as key when no key is given 27 | var defaultRedisStateKey = "gatewaystate" 28 | 29 | // InitRedisState initializes Redis-backed connection state for the exchange and returns the state stored in the database 30 | func (b *Exchange) InitRedisState(client *redis.Client, key string) (gatewayIDs []string) { 31 | if key == "" { 32 | key = defaultRedisStateKey 33 | } 34 | b.gateways = &gatewayStateWithRedisPersistence{ 35 | gatewayState: b.gateways, 36 | client: client, 37 | key: key, 38 | } 39 | gatewayIDs, _ = client.SMembers(key).Result() 40 | return 41 | } 42 | 43 | type gatewayStateWithRedisPersistence struct { 44 | key string 45 | client *redis.Client 46 | gatewayState 47 | } 48 | 49 | func (s *gatewayStateWithRedisPersistence) Add(i interface{}) bool { 50 | added := s.gatewayState.Add(i) 51 | if added && i != "" { 52 | go s.client.SAdd(s.key, i).Result() 53 | } 54 | return added 55 | } 56 | 57 | func (s *gatewayStateWithRedisPersistence) Remove(i interface{}) { 58 | s.gatewayState.Remove(i) 59 | if i != "" { 60 | go s.client.SRem(s.key, i).Result() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /exchange/state_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package exchange 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "os" 10 | "testing" 11 | 12 | "github.com/apex/log" 13 | "github.com/apex/log/handlers/text" 14 | . "github.com/smartystreets/goconvey/convey" 15 | redis "gopkg.in/redis.v5" 16 | ) 17 | 18 | func getRedisClient() *redis.Client { 19 | host := os.Getenv("REDIS_HOST") 20 | if host == "" { 21 | host = "localhost" 22 | } 23 | return redis.NewClient(&redis.Options{ 24 | Addr: fmt.Sprintf("%s:6379", host), 25 | Password: "", // no password set 26 | DB: 1, // use default DB 27 | }) 28 | } 29 | 30 | func TestExchangeState(t *testing.T) { 31 | Convey("Given a new Context and Backends", t, func(c C) { 32 | 33 | var logs bytes.Buffer 34 | ctx := &log.Logger{ 35 | Handler: text.New(&logs), 36 | Level: log.DebugLevel, 37 | } 38 | defer func() { 39 | if logs.Len() > 0 { 40 | c.Printf("\n%s", logs.String()) 41 | } 42 | }() 43 | 44 | Convey("When creating a new Exchange", func() { 45 | b := New(ctx, 0) 46 | 47 | Convey("When calling InitRedisState", func() { 48 | gatewayIDs := b.InitRedisState(getRedisClient(), "") 49 | Convey("It should not return any gateways", func() { 50 | So(gatewayIDs, ShouldBeEmpty) 51 | }) 52 | 53 | Convey("When adding a gateway", func() { 54 | b.gateways.Add("dev") 55 | Reset(func() { 56 | b.gateways.Remove("dev") 57 | }) 58 | Convey("When calling InitRedisState on another Exchange", func() { 59 | gatewayIDs := New(ctx, 0).InitRedisState(getRedisClient(), "") 60 | Convey("It should return the gateway", func() { 61 | So(gatewayIDs, ShouldContain, "dev") 62 | }) 63 | }) 64 | Convey("When removing that gateway", func() { 65 | b.gateways.Remove("dev") 66 | Reset(func() { 67 | b.gateways.Add("dev") 68 | }) 69 | Convey("When calling InitRedisState on another Exchange", func() { 70 | gatewayIDs := New(ctx, 0).InitRedisState(getRedisClient(), "") 71 | Convey("It should not return the gateway", func() { 72 | So(gatewayIDs, ShouldNotContain, "dev") 73 | }) 74 | }) 75 | }) 76 | }) 77 | }) 78 | 79 | }) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /exchange/watchdog.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package exchange 5 | 6 | import ( 7 | "time" 8 | ) 9 | 10 | type watchdog struct { 11 | *time.Timer 12 | } 13 | 14 | const watchdogExpire = time.Second 15 | 16 | func newWatchdog(callback func()) *watchdog { 17 | return &watchdog{ 18 | Timer: time.AfterFunc(watchdogExpire, callback), 19 | } 20 | } 21 | 22 | // Kick the watchdog. No effect if already expired 23 | func (w *watchdog) Kick() { 24 | if w.Stop() { 25 | w.Reset(watchdogExpire) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TheThingsNetwork/gateway-connector-bridge 2 | 3 | go 1.11 4 | 5 | replace github.com/brocaar/lorawan => github.com/ThethingsIndustries/legacy-lorawan-lib v0.0.0-20190212122748-b905ab327304 6 | 7 | require ( 8 | github.com/TheThingsNetwork/api v0.0.0-20190517100736-d4d18a220bfe 9 | github.com/TheThingsNetwork/go-account-lib v0.0.0-20190516094738-77d15a3f8875 10 | github.com/TheThingsNetwork/go-utils v0.0.0-20190516083235-bdd4967fab4e 11 | github.com/TheThingsNetwork/ttn/api v0.0.0-20190516113615-648de2b33240 12 | github.com/TheThingsNetwork/ttn/core/types v0.0.0-20190516113615-648de2b33240 // indirect 13 | github.com/TheThingsNetwork/ttn/utils/errors v0.0.0-20190516113615-648de2b33240 // indirect 14 | github.com/TheThingsNetwork/ttn/utils/random v0.0.0-20190516113615-648de2b33240 15 | github.com/TheThingsNetwork/ttn/utils/security v0.0.0-20190516113615-648de2b33240 // indirect 16 | github.com/apex/log v1.1.0 17 | github.com/brocaar/lorawan v0.0.0-20190402092148-5bca41b178e9 18 | github.com/deckarep/golang-set v1.7.1 19 | github.com/eclipse/paho.mqtt.golang v1.2.0 20 | github.com/fsnotify/fsnotify v1.4.7 21 | github.com/gogo/protobuf v1.2.1 22 | github.com/golang/protobuf v1.3.1 23 | github.com/googollee/go-engine.io v0.0.0-20170224222511-80ae0e43aca1 // indirect 24 | github.com/googollee/go-socket.io v0.0.0-20170525141029-5447e71f36d3 25 | github.com/gorilla/websocket v1.4.0 // indirect 26 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 27 | github.com/prometheus/client_golang v0.9.3 28 | github.com/prometheus/procfs v0.0.0-20190516194456-169873baca24 // indirect 29 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a 30 | github.com/spf13/cobra v0.0.3 31 | github.com/spf13/viper v1.3.2 32 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 33 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a // indirect 34 | google.golang.org/appengine v1.6.0 // indirect 35 | google.golang.org/genproto v0.0.0-20190516172635-bb713bdc0e52 // indirect 36 | google.golang.org/grpc v1.20.1 37 | gopkg.in/redis.v5 v5.2.9 38 | gopkg.in/yaml.v2 v2.2.2 39 | ) 40 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/TheThingsNetwork/gateway-connector-bridge/cmd" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var ( 12 | version = "2.x.x" 13 | gitBranch = "unknown" 14 | gitCommit = "unknown" 15 | buildDate = "unknown" 16 | ) 17 | 18 | func main() { 19 | viper.Set("version", version) 20 | viper.Set("gitBranch", gitBranch) 21 | viper.Set("gitCommit", gitCommit) 22 | viper.Set("buildDate", buildDate) 23 | cmd.Execute() 24 | } 25 | -------------------------------------------------------------------------------- /middleware/blacklist/blacklist.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package blacklist 5 | 6 | import ( 7 | "errors" 8 | "io/ioutil" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "path/filepath" 13 | "sync" 14 | 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 16 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 17 | "github.com/fsnotify/fsnotify" 18 | "gopkg.in/yaml.v2" 19 | ) 20 | 21 | type blacklistedItem struct { 22 | Gateway string `yaml:"gateway"` 23 | IP string `yaml:"ip"` 24 | } 25 | 26 | // NewBlacklist returns a middleware that filters traffic from blacklisted gateways 27 | func NewBlacklist(lists ...string) (b *Blacklist, err error) { 28 | b = &Blacklist{ 29 | lists: make(map[string][]blacklistedItem), 30 | ipLookup: make(map[string]bool), 31 | idLookup: make(map[string]bool), 32 | } 33 | b.watcher, err = fsnotify.NewWatcher() 34 | if err != nil { 35 | return nil, err 36 | } 37 | for _, location := range lists { 38 | b.addList(location) // ignore errors for mvp 39 | } 40 | b.FetchRemotes() 41 | go func() { 42 | for e := range b.watcher.Events { 43 | if e.Op&fsnotify.Write == fsnotify.Write { 44 | b.read(e.Name) // ignore errors for mvp 45 | } 46 | } 47 | }() 48 | return b, nil 49 | } 50 | 51 | // Blacklist middleware 52 | type Blacklist struct { 53 | watcher *fsnotify.Watcher 54 | urls []string 55 | 56 | mu sync.RWMutex 57 | lists map[string][]blacklistedItem 58 | ipLookup map[string]bool 59 | idLookup map[string]bool 60 | } 61 | 62 | func (b *Blacklist) addList(location string) error { 63 | url, err := url.Parse(location) 64 | if err != nil { 65 | return err 66 | } 67 | switch url.Scheme { 68 | case "", "file": 69 | return b.addFile(location) 70 | case "http", "https": 71 | return b.addURL(url) 72 | } 73 | return errors.New("blacklist: unknown list type") 74 | } 75 | 76 | func (b *Blacklist) addFile(filename string) (err error) { 77 | filename, err = filepath.Abs(filename) 78 | if err != nil { 79 | return err 80 | } 81 | if err = b.watcher.Add(filename); err != nil { 82 | return err 83 | } 84 | return b.read(filename) 85 | } 86 | 87 | func (b *Blacklist) addURL(url *url.URL) error { 88 | b.urls = append(b.urls, url.String()) 89 | return nil 90 | } 91 | 92 | // FetchRemotes fetches remote blacklists 93 | func (b *Blacklist) FetchRemotes() error { 94 | for _, url := range b.urls { 95 | b.fetch(url) // ignore errors for mvp 96 | } 97 | return nil 98 | } 99 | 100 | // Close the blacklist watcher 101 | func (b *Blacklist) Close() { 102 | b.watcher.Close() 103 | } 104 | 105 | func (b *Blacklist) read(filename string) error { 106 | contents, err := ioutil.ReadFile(filename) 107 | if err != nil { 108 | return err 109 | } 110 | var blacklist []blacklistedItem 111 | err = yaml.Unmarshal(contents, &blacklist) 112 | if err != nil { 113 | return err 114 | } 115 | b.mu.Lock() 116 | b.lists[filename] = blacklist 117 | b.updateLookup() 118 | b.mu.Unlock() 119 | return nil 120 | } 121 | 122 | func (b *Blacklist) fetch(location string) error { 123 | resp, err := http.Get(location) 124 | if err != nil { 125 | return err 126 | } 127 | defer resp.Body.Close() 128 | body, err := ioutil.ReadAll(resp.Body) 129 | if err != nil { 130 | return err 131 | } 132 | var blacklist []blacklistedItem 133 | err = yaml.Unmarshal(body, &blacklist) 134 | if err != nil { 135 | return err 136 | } 137 | b.mu.Lock() 138 | b.lists[location] = blacklist 139 | b.updateLookup() 140 | b.mu.Unlock() 141 | return nil 142 | } 143 | 144 | func (b *Blacklist) updateLookup() { 145 | var n int 146 | for _, blacklist := range b.lists { 147 | n += len(blacklist) 148 | } 149 | b.ipLookup = make(map[string]bool, n) 150 | b.idLookup = make(map[string]bool, n) 151 | for _, blacklist := range b.lists { 152 | for _, item := range blacklist { 153 | if item.IP != "" { 154 | b.ipLookup[item.IP] = true 155 | } 156 | if item.Gateway != "" { 157 | b.idLookup[item.Gateway] = true 158 | } 159 | } 160 | } 161 | } 162 | 163 | // Blacklist errors 164 | var ( 165 | ErrBlacklistedID = errors.New("blacklist: Gateway ID is blacklisted") 166 | ErrBlacklistedIP = errors.New("blacklist: Gateway IP is blacklisted") 167 | ) 168 | 169 | func (b *Blacklist) check(id string, ip net.Addr) error { 170 | b.mu.RLock() 171 | defer b.mu.RUnlock() 172 | if b.idLookup[id] { 173 | return ErrBlacklistedID 174 | } 175 | if ip != nil { 176 | ip, _, err := net.SplitHostPort(ip.String()) 177 | if err != nil { 178 | return err 179 | } 180 | if b.ipLookup[ip] { 181 | return ErrBlacklistedIP 182 | } 183 | } 184 | return nil 185 | } 186 | 187 | // HandleUplink blocks uplink messages from blacklisted gateways 188 | func (b *Blacklist) HandleUplink(_ middleware.Context, msg *types.UplinkMessage) error { 189 | return b.check(msg.GatewayID, msg.GatewayAddr) 190 | } 191 | 192 | // HandleStatus blocks status messages from blacklisted gateways 193 | func (b *Blacklist) HandleStatus(ctx middleware.Context, msg *types.StatusMessage) error { 194 | return b.check(msg.GatewayID, msg.GatewayAddr) 195 | } 196 | -------------------------------------------------------------------------------- /middleware/blacklist/blacklist_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package blacklist 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "testing" 13 | 14 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 16 | . "github.com/smartystreets/goconvey/convey" 17 | ) 18 | 19 | func TestBlacklist(t *testing.T) { 20 | exampleBlacklist, err := filepath.Abs("../../assets/blacklist.example.yml") 21 | if err != nil { 22 | panic(fmt.Errorf("blacklist example file path could not be determined; %s", err)) 23 | } 24 | if _, err := os.Stat(exampleBlacklist); err != nil { 25 | panic(fmt.Errorf("blacklist example file not found: %s", err)) 26 | } 27 | 28 | mux := http.NewServeMux() 29 | 30 | mux.HandleFunc("/blacklist.yml", func(w http.ResponseWriter, r *http.Request) { 31 | http.ServeFile(w, r, exampleBlacklist) 32 | }) 33 | 34 | testExample := func(list string) { 35 | b, err := NewBlacklist(list) 36 | Convey("Then there should be no error", func() { So(err, ShouldBeNil) }) 37 | Reset(func() { b.Close() }) 38 | Convey("Then the blacklist should contain 2 items", func() { 39 | So(b.lists[list], ShouldHaveLength, 2) 40 | }) 41 | Convey("When a gateway with a blacklisted ID sends a message", func() { 42 | err := b.HandleStatus(middleware.NewContext(), &types.StatusMessage{GatewayID: "malicious"}) 43 | Convey("Then the BlacklistedID error should be returned", func() { So(err, ShouldEqual, ErrBlacklistedID) }) 44 | }) 45 | Convey("When a gateway with a blacklisted IP sends a message", func() { 46 | err := b.HandleStatus(middleware.NewContext(), &types.StatusMessage{GatewayAddr: &net.TCPAddr{IP: net.IP{8, 8, 8, 8}}}) 47 | Convey("Then the BlacklistedIP error should be returned", func() { So(err, ShouldEqual, ErrBlacklistedIP) }) 48 | }) 49 | } 50 | 51 | Convey("When creating a new Blacklist using the example file", t, func(c C) { 52 | testExample(exampleBlacklist) 53 | }) 54 | 55 | Convey("When creating a new Blacklist using the example file on an HTTP server", t, func(c C) { 56 | var lis net.Listener 57 | var err error 58 | for { 59 | lis, err = net.Listen("tcp", "127.0.0.1:0") 60 | if err == nil { 61 | break 62 | } 63 | } 64 | Reset(func() { lis.Close() }) 65 | go http.Serve(lis, mux) 66 | testExample(fmt.Sprintf("http://%s/blacklist.yml", lis.Addr().String())) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /middleware/debug/debug.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package debug 5 | 6 | import ( 7 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 8 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 9 | ) 10 | 11 | // New returns a middleware that debugs traffic 12 | func New() *Debug { 13 | return &Debug{} 14 | } 15 | 16 | // Debug middleware 17 | type Debug struct{} 18 | 19 | // HandleUplink debugs uplink traffic 20 | func (*Debug) HandleUplink(_ middleware.Context, msg *types.UplinkMessage) error { 21 | if lorawan := msg.Message.ProtocolMetadata.GetLoRaWAN(); lorawan != nil { 22 | if lorawan.FCnt != 0 { 23 | lorawan.FCnt = 0 24 | } 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /middleware/deduplicate/deduplicate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package deduplicate 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "sync" 10 | 11 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 12 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 13 | "github.com/TheThingsNetwork/go-utils/log" 14 | ) 15 | 16 | // NewDeduplicate returns a middleware that deduplicates duplicate uplink messages received from broken gateways 17 | func NewDeduplicate() *Deduplicate { 18 | return &Deduplicate{ 19 | log: log.Get(), 20 | lastMessage: make(map[string]*types.UplinkMessage), 21 | } 22 | } 23 | 24 | // Deduplicate middleware 25 | type Deduplicate struct { 26 | log log.Interface 27 | mu sync.RWMutex 28 | lastMessage map[string]*types.UplinkMessage 29 | } 30 | 31 | // HandleDisconnect cleans up 32 | func (d *Deduplicate) HandleDisconnect(ctx middleware.Context, msg *types.DisconnectMessage) error { 33 | d.mu.Lock() 34 | defer d.mu.Unlock() 35 | delete(d.lastMessage, msg.GatewayID) 36 | return nil 37 | } 38 | 39 | // ErrDuplicateMessage is returned when an uplink message is received multiple times 40 | var ErrDuplicateMessage = errors.New("deduplicate: already handled this message") 41 | 42 | // HandleUplink blocks duplicate messages 43 | func (d *Deduplicate) HandleUplink(_ middleware.Context, msg *types.UplinkMessage) error { 44 | d.mu.Lock() 45 | defer d.mu.Unlock() 46 | if lastMessage, ok := d.lastMessage[msg.GatewayID]; ok { 47 | if bytes.Equal(msg.Message.Payload, lastMessage.Message.Payload) && // length check on slice is fast 48 | msg.Message.GatewayMetadata.GetTimestamp() == lastMessage.Message.GatewayMetadata.GetTimestamp() { 49 | return ErrDuplicateMessage 50 | } 51 | } 52 | d.lastMessage[msg.GatewayID] = msg 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /middleware/deduplicate/deduplicate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package deduplicate 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/TheThingsNetwork/api/router" 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 11 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 12 | . "github.com/smartystreets/goconvey/convey" 13 | ) 14 | 15 | func TestDeduplicate(t *testing.T) { 16 | Convey("Given a new Deduplicate", t, func(c C) { 17 | i := NewDeduplicate() 18 | 19 | up := &types.UplinkMessage{GatewayID: "test", Message: &router.UplinkMessage{ 20 | Payload: []byte{1, 2, 3, 4}, 21 | }} 22 | upDup := &types.UplinkMessage{GatewayID: "test", Message: &router.UplinkMessage{ 23 | Payload: []byte{1, 2, 3, 4}, 24 | }} 25 | nextUp := &types.UplinkMessage{GatewayID: "test", Message: &router.UplinkMessage{ 26 | Payload: []byte{1, 2, 3, 4, 5}, 27 | }} 28 | 29 | Convey("When sending an UplinkMessage", func() { 30 | Reset(func() { 31 | i.HandleDisconnect(middleware.NewContext(), &types.DisconnectMessage{GatewayID: "test"}) 32 | }) 33 | err := i.HandleUplink(middleware.NewContext(), up) 34 | Convey("There should be no error", func() { 35 | So(err, ShouldBeNil) 36 | }) 37 | Convey("When sending a duplicate of that UplinkMessage", func() { 38 | err := i.HandleUplink(middleware.NewContext(), upDup) 39 | Convey("There should be an error", func() { 40 | So(err, ShouldEqual, ErrDuplicateMessage) 41 | }) 42 | }) 43 | Convey("When sending another UplinkMessage", func() { 44 | err := i.HandleUplink(middleware.NewContext(), nextUp) 45 | Convey("There should be no error", func() { 46 | So(err, ShouldBeNil) 47 | }) 48 | }) 49 | 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /middleware/gatewayinfo/gatewayinfo.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package gatewayinfo 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/TheThingsNetwork/api/gateway" 14 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 15 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 16 | "github.com/TheThingsNetwork/go-account-lib/account" 17 | "github.com/TheThingsNetwork/go-utils/log" 18 | redis "gopkg.in/redis.v5" 19 | ) 20 | 21 | // RequestInterval sets how often the account server may be queried 22 | var RequestInterval = 50 * time.Millisecond 23 | 24 | // RequestBurst sets the burst of requests to the account server 25 | var RequestBurst = 50 26 | 27 | // NewPublic returns a middleware that injects public gateway information 28 | func NewPublic(accountServer string) *Public { 29 | p := &Public{ 30 | log: log.Get(), 31 | account: account.New(accountServer), 32 | info: make(map[string]*info), 33 | available: make(chan struct{}, RequestBurst), 34 | } 35 | for i := 0; i < RequestBurst; i++ { 36 | p.available <- struct{}{} 37 | } 38 | go func() { 39 | for range time.Tick(RequestInterval) { 40 | select { 41 | case p.available <- struct{}{}: 42 | default: 43 | } 44 | } 45 | }() 46 | return p 47 | } 48 | 49 | // WithRedis initializes the Redis store for persistence between restarts 50 | func (p *Public) WithRedis(client *redis.Client, prefix string) (*Public, error) { 51 | p.redisPrefix = prefix 52 | 53 | // Initialize the data from the store 54 | var allKeys []string 55 | var cursor uint64 56 | for { 57 | keys, next, err := client.Scan(cursor, p.redisKey("*"), 0).Result() 58 | if err != nil { 59 | return nil, err 60 | } 61 | allKeys = append(allKeys, keys...) 62 | cursor = next 63 | if cursor == 0 { 64 | break 65 | } 66 | } 67 | 68 | for _, key := range allKeys { 69 | res, err := client.Get(key).Result() 70 | if err != nil { 71 | continue 72 | } 73 | var gateway account.Gateway 74 | err = json.Unmarshal([]byte(res), &gateway) 75 | if err != nil { 76 | continue 77 | } 78 | gatewayID := strings.TrimPrefix(key, p.redisKey("")) 79 | p.set(gatewayID, gateway) 80 | } 81 | 82 | // Now set the client 83 | p.redisClient = client 84 | 85 | return p, nil 86 | } 87 | 88 | // WithExpire adds an expiration to gateway information. Information is re-fetched if expired 89 | func (p *Public) WithExpire(duration time.Duration) *Public { 90 | p.expire = duration 91 | return p 92 | } 93 | 94 | // Public gateway information will be injected 95 | type Public struct { 96 | log log.Interface 97 | account *account.Account 98 | expire time.Duration 99 | 100 | redisClient *redis.Client 101 | redisPrefix string 102 | 103 | mu sync.Mutex 104 | info map[string]*info 105 | 106 | available chan struct{} 107 | } 108 | 109 | func (p *Public) redisKey(gatewayID string) string { 110 | return fmt.Sprintf("%s:%s", p.redisPrefix, gatewayID) 111 | } 112 | 113 | type info struct { 114 | lastUpdated time.Time 115 | err error 116 | gateway account.Gateway 117 | } 118 | 119 | func (p *Public) fetch(gatewayID string) error { 120 | <-p.available 121 | gateway, err := p.account.FindGateway(gatewayID) 122 | if err != nil { 123 | p.setErr(gatewayID, err) 124 | return err 125 | } 126 | p.set(gatewayID, gateway) 127 | return nil 128 | } 129 | 130 | func (p *Public) setErr(gatewayID string, err error) { 131 | p.mu.Lock() 132 | defer p.mu.Unlock() 133 | if gtw, ok := p.info[gatewayID]; ok { 134 | gtw.lastUpdated = time.Now() 135 | gtw.err = err 136 | } else { 137 | p.info[gatewayID] = &info{ 138 | lastUpdated: time.Now(), 139 | err: err, 140 | } 141 | } 142 | } 143 | 144 | func (p *Public) set(gatewayID string, gateway account.Gateway) { 145 | log := p.log.WithField("GatewayID", gatewayID) 146 | p.mu.Lock() 147 | log.Debug("Setting public gateway info") 148 | p.info[gatewayID] = &info{ 149 | lastUpdated: time.Now(), 150 | gateway: gateway, 151 | } 152 | p.mu.Unlock() 153 | if p.redisClient != nil { 154 | data, _ := json.Marshal(gateway) 155 | if err := p.redisClient.Set(p.redisKey(gatewayID), string(data), p.expire).Err(); err != nil { 156 | log.WithError(err).Warn("Could not set public Gateway information in Redis") 157 | } 158 | } 159 | } 160 | 161 | func (p *Public) get(gatewayID string) (gateway account.Gateway, err error) { 162 | if gatewayID == "" { 163 | return 164 | } 165 | log := p.log.WithField("GatewayID", gatewayID) 166 | p.mu.Lock() 167 | defer p.mu.Unlock() 168 | info, ok := p.info[gatewayID] 169 | if ok { 170 | if p.expire == 0 || time.Since(info.lastUpdated) < p.expire { 171 | return info.gateway, info.err 172 | } 173 | info.lastUpdated = time.Now() 174 | } 175 | go func() { 176 | err := p.fetch(gatewayID) 177 | if err != nil { 178 | log.WithError(err).Warn("Could not get public Gateway information") 179 | } else { 180 | log.Debug("Got public Gateway information") 181 | } 182 | }() 183 | return gateway, nil 184 | } 185 | 186 | func (p *Public) unset(gatewayID string) { 187 | p.mu.Lock() 188 | defer p.mu.Unlock() 189 | delete(p.info, gatewayID) 190 | } 191 | 192 | // HandleConnect fetches public gateway information in the background when a ConnectMessage is received 193 | func (p *Public) HandleConnect(ctx middleware.Context, msg *types.ConnectMessage) error { 194 | p.get(msg.GatewayID) 195 | return nil 196 | } 197 | 198 | // HandleDisconnect cleans up 199 | func (p *Public) HandleDisconnect(ctx middleware.Context, msg *types.DisconnectMessage) error { 200 | go p.unset(msg.GatewayID) 201 | return nil 202 | } 203 | 204 | // HandleStatus inserts metadata if set in info, but not present in message 205 | func (p *Public) HandleStatus(ctx middleware.Context, msg *types.StatusMessage) error { 206 | info, _ := p.get(msg.GatewayID) 207 | 208 | if msg.Message.Location == nil || msg.Message.Location.Validate() != nil { 209 | msg.Message.Location = nil 210 | } 211 | 212 | if info.AntennaLocation != nil { 213 | if msg.Message.Location == nil { 214 | msg.Message.Location = new(gateway.LocationMetadata) 215 | } 216 | if msg.Message.Location.IsZero() { 217 | msg.Message.Location.Latitude = float32(info.AntennaLocation.Latitude) 218 | msg.Message.Location.Longitude = float32(info.AntennaLocation.Longitude) 219 | msg.Message.Location.Source = gateway.LocationMetadata_REGISTRY 220 | } 221 | if msg.Message.Location.Altitude == 0 { 222 | msg.Message.Location.Altitude = int32(info.AntennaLocation.Altitude) 223 | } 224 | } 225 | 226 | if msg.Message.FrequencyPlan == "" && info.FrequencyPlan != "" { 227 | msg.Message.FrequencyPlan = info.FrequencyPlan 228 | } 229 | 230 | if msg.Message.Platform == "" { 231 | platform := []string{} 232 | if info.Attributes.Brand != nil { 233 | platform = append(platform, *info.Attributes.Brand) 234 | } 235 | if info.Attributes.Model != nil { 236 | platform = append(platform, *info.Attributes.Model) 237 | } 238 | msg.Message.Platform = strings.Join(platform, " ") 239 | } 240 | 241 | if msg.Message.Description == "" && info.Attributes.Description != nil { 242 | msg.Message.Description = *info.Attributes.Description 243 | } 244 | 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /middleware/gatewayinfo/gatewayinfo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package gatewayinfo 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/api/gateway" 13 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 14 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 15 | "github.com/TheThingsNetwork/go-account-lib/account" 16 | . "github.com/smartystreets/goconvey/convey" 17 | redis "gopkg.in/redis.v5" 18 | ) 19 | 20 | func getRedisClient() *redis.Client { 21 | host := os.Getenv("REDIS_HOST") 22 | if host == "" { 23 | host = "localhost" 24 | } 25 | return redis.NewClient(&redis.Options{ 26 | Addr: fmt.Sprintf("%s:6379", host), 27 | Password: "", // no password set 28 | DB: 1, // use default DB 29 | }) 30 | } 31 | 32 | func TestPublic(t *testing.T) { 33 | Convey("Given a new Public GatewayInfo", t, func(c C) { 34 | p := NewPublic("https://account.thethingsnetwork.org") 35 | gatewayID := "eui-0000024b08060112" 36 | 37 | Convey("When fetching the info of a non-existent Gateway", func() { 38 | err := p.fetch("dev") 39 | Convey("There should be an error", func() { 40 | So(err, ShouldNotBeNil) 41 | }) 42 | gateway, err := p.get("dev") 43 | Convey("The info should not be stored", func() { 44 | So(gateway.ID, ShouldBeEmpty) 45 | }) 46 | Convey("An error should be stored", func() { 47 | So(err, ShouldNotBeNil) 48 | }) 49 | }) 50 | 51 | Convey("When fetching the info of a Gateway", func() { 52 | err := p.fetch(gatewayID) 53 | Convey("There should be no error", func() { 54 | So(err, ShouldBeNil) 55 | }) 56 | Convey("The info should be stored", func() { 57 | gateway, _ := p.get(gatewayID) 58 | So(gateway.ID, ShouldEqual, gatewayID) 59 | }) 60 | }) 61 | 62 | Convey("When handling ConnectMessages", func() { 63 | Convey("For a non-existent Gateway", func() { 64 | err := p.HandleConnect(middleware.NewContext(), &types.ConnectMessage{ 65 | GatewayID: "dev", 66 | }) 67 | Convey("There should be no error (we don't want to break on this)", func() { 68 | So(err, ShouldBeNil) 69 | }) 70 | }) 71 | 72 | err := p.HandleConnect(middleware.NewContext(), &types.ConnectMessage{ 73 | GatewayID: gatewayID, 74 | }) 75 | Convey("There should be no error", func() { 76 | So(err, ShouldBeNil) 77 | }) 78 | 79 | time.Sleep(500 * time.Millisecond) 80 | Convey("The info should be stored", func() { 81 | gateway, _ := p.get(gatewayID) 82 | So(gateway.ID, ShouldEqual, gatewayID) 83 | }) 84 | }) 85 | 86 | Convey("Given some stored gateway information", func() { 87 | strptr := func(s string) *string { return &s } 88 | 89 | gatewayID := "gateway-id" 90 | p.set(gatewayID, account.Gateway{ 91 | ID: gatewayID, 92 | FrequencyPlan: "EU_868", 93 | AntennaLocation: &account.Location{ 94 | Latitude: 12.34, 95 | Longitude: 56.78, 96 | }, 97 | Attributes: account.GatewayAttributes{ 98 | Brand: strptr("Test"), 99 | Model: strptr("Gateway"), 100 | Description: strptr("My Test Gateway"), 101 | }, 102 | }) 103 | 104 | Convey("When sending a DisconnectMessage", func() { 105 | err := p.HandleDisconnect(middleware.NewContext(), &types.DisconnectMessage{ 106 | GatewayID: gatewayID, 107 | }) 108 | Convey("There should be no error", func() { 109 | So(err, ShouldBeNil) 110 | }) 111 | time.Sleep(10 * time.Millisecond) 112 | Convey("The info should no longer be stored", func() { 113 | gateway, _ := p.get(gatewayID) 114 | So(gateway.ID, ShouldBeEmpty) 115 | }) 116 | }) 117 | 118 | Convey("When sending a StatusMessage", func() { 119 | status := &types.StatusMessage{ 120 | GatewayID: gatewayID, 121 | Message: &gateway.Status{}, 122 | } 123 | err := p.HandleStatus(middleware.NewContext(), status) 124 | Convey("There should be no error", func() { 125 | So(err, ShouldBeNil) 126 | }) 127 | Convey("The StatusMessage should have Metadata", func() { 128 | So(status.Message.GetLocation(), ShouldNotBeNil) 129 | So(status.Message.GetLocation().Latitude, ShouldEqual, 12.34) 130 | So(status.Message.Description, ShouldEqual, "My Test Gateway") 131 | So(status.Message.Platform, ShouldEqual, "Test Gateway") 132 | So(status.Message.FrequencyPlan, ShouldEqual, "EU_868") 133 | }) 134 | }) 135 | }) 136 | 137 | }) 138 | 139 | Convey("Given a new Public GatewayInfo that Expires", t, func(c C) { 140 | p := NewPublic("https://account.thethingsnetwork.org").WithExpire(10 * time.Millisecond) 141 | gatewayID := "eui-0000024b08060112" 142 | 143 | Convey("When setting the info of a Gateway", func() { 144 | p.set(gatewayID, account.Gateway{}) 145 | lastUpdated := p.info[gatewayID].lastUpdated 146 | Convey("When getting the info of a Gateway some time later", func() { 147 | time.Sleep(20 * time.Millisecond) 148 | p.get(gatewayID) 149 | time.Sleep(500 * time.Millisecond) 150 | Convey("It should have updated", func() { 151 | So(p.info[gatewayID].lastUpdated, ShouldNotEqual, lastUpdated) 152 | }) 153 | }) 154 | }) 155 | }) 156 | 157 | Convey("Given a new Public GatewayInfo with Redis", t, func(c C) { 158 | p, _ := NewPublic("https://account.thethingsnetwork.org").WithRedis(getRedisClient(), "test-public") 159 | gatewayID := "eui-0000024b08060112" 160 | Reset(func() { 161 | getRedisClient().Del(p.redisKey(gatewayID)).Err() 162 | }) 163 | Convey("When setting the info of a Gateway", func() { 164 | p.set(gatewayID, account.Gateway{}) 165 | Convey("It should be stored in Redis", func() { 166 | So(getRedisClient().Exists(p.redisKey(gatewayID)).Val(), ShouldBeTrue) 167 | }) 168 | }) 169 | Convey("After re-initializing", func() { 170 | getRedisClient().Set(p.redisKey(gatewayID), `{"activated":true}`, 0).Err() 171 | p.info = make(map[string]*info) 172 | _, err := p.WithRedis(getRedisClient(), "test-public") 173 | Convey("There should be no error", func() { 174 | So(err, ShouldBeNil) 175 | }) 176 | Convey("When getting the info of the Gateway", func() { 177 | gateway, err := p.get(gatewayID) 178 | Convey("There should be no error", func() { 179 | So(err, ShouldBeNil) 180 | }) 181 | Convey("It should be restored from Redis", func() { 182 | So(gateway.Activated, ShouldBeTrue) 183 | }) 184 | }) 185 | }) 186 | }) 187 | } 188 | -------------------------------------------------------------------------------- /middleware/inject/inject.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package inject 5 | 6 | import ( 7 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 8 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 9 | "github.com/TheThingsNetwork/go-utils/log" 10 | ) 11 | 12 | // Fields to inject 13 | type Fields struct { 14 | FrequencyPlan string 15 | Bridge string 16 | } 17 | 18 | // NewInject returns a middleware that injects fields into all status messages 19 | func NewInject(fields Fields) *Inject { 20 | return &Inject{ 21 | fields: fields, 22 | log: log.Get(), 23 | } 24 | } 25 | 26 | // Inject fields into all status messages 27 | type Inject struct { 28 | log log.Interface 29 | fields Fields 30 | } 31 | 32 | // HandleStatus inserts fields into status messages if not present 33 | func (i *Inject) HandleStatus(ctx middleware.Context, msg *types.StatusMessage) error { 34 | if msg.Message.FrequencyPlan == "" { 35 | msg.Message.FrequencyPlan = i.fields.FrequencyPlan 36 | } 37 | if msg.Message.Bridge == "" { 38 | msg.Message.Bridge = i.fields.Bridge 39 | if msg.Backend != "" { 40 | msg.Message.Bridge += " " + msg.Backend + " Backend" 41 | } 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /middleware/inject/inject_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package inject 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/TheThingsNetwork/api/gateway" 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 11 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 12 | . "github.com/smartystreets/goconvey/convey" 13 | ) 14 | 15 | func TestInject(t *testing.T) { 16 | Convey("Given a new Inject", t, func(c C) { 17 | i := NewInject(Fields{ 18 | FrequencyPlan: "EU_868", 19 | Bridge: "bridge", 20 | }) 21 | 22 | Convey("When sending a StatusMessage", func() { 23 | status := &types.StatusMessage{ 24 | Message: &gateway.Status{}, 25 | } 26 | err := i.HandleStatus(middleware.NewContext(), status) 27 | Convey("There should be no error", func() { 28 | So(err, ShouldBeNil) 29 | }) 30 | Convey("The StatusMessage should contain the injected fields", func() { 31 | So(status.Message.FrequencyPlan, ShouldEqual, "EU_868") 32 | So(status.Message.Bridge, ShouldEqual, "bridge") 33 | }) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /middleware/lorafilter/lorafilter.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package lorafilter 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 11 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 12 | "github.com/brocaar/lorawan" 13 | ) 14 | 15 | // NewFilter returns a middleware that filters traffic so that non-LoRaWAN messages are ignored 16 | func NewFilter() *Filter { 17 | return &Filter{} 18 | } 19 | 20 | // Filter middleware 21 | type Filter struct{} 22 | 23 | // HandleUplink blocks duplicate messages 24 | func (*Filter) HandleUplink(_ middleware.Context, msg *types.UplinkMessage) error { 25 | payload := msg.Message.GetPayload() 26 | if len(payload) < 5 { 27 | return fmt.Errorf("lorafilter: %d payload bytes is not enough for a LoRaWAN packet", len(payload)) 28 | } 29 | var mhdr lorawan.MHDR 30 | if err := mhdr.UnmarshalBinary(payload[:1]); err != nil { 31 | return err 32 | } 33 | if mhdr.Major != lorawan.LoRaWANR1 { 34 | return fmt.Errorf("lorafilter: unsupported LoRaWAN version 0x%x", byte(mhdr.Major)) 35 | } 36 | switch mhdr.MType { 37 | case lorawan.JoinAccept: 38 | return errors.New("lorafilter: found JoinAccept payload in UplinkMessage") 39 | case lorawan.UnconfirmedDataDown, lorawan.ConfirmedDataDown: 40 | return errors.New("lorafilter: found Downlink payload in UplinkMessage") 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /middleware/lorafilter/lorafilter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package lorafilter 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/TheThingsNetwork/api/router" 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 11 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 12 | . "github.com/smartystreets/goconvey/convey" 13 | ) 14 | 15 | func TestLoraFilter(t *testing.T) { 16 | Convey("Given a new Filter", t, func(c C) { 17 | f := NewFilter() 18 | 19 | Convey("When sending an UplinkMessage without payload", func() { 20 | err := f.HandleUplink(middleware.NewContext(), &types.UplinkMessage{Message: &router.UplinkMessage{ 21 | Payload: []byte{}, 22 | }}) 23 | Convey("There should be an error", func() { 24 | So(err, ShouldNotBeNil) 25 | }) 26 | }) 27 | 28 | Convey("When sending an UplinkMessage with an invalid LoRaWAN Major version", func() { 29 | err := f.HandleUplink(middleware.NewContext(), &types.UplinkMessage{Message: &router.UplinkMessage{ 30 | Payload: []byte{1, 2, 3, 4, 5}, 31 | }}) 32 | Convey("There should be an error", func() { 33 | So(err, ShouldNotBeNil) 34 | }) 35 | }) 36 | 37 | Convey("When sending an UplinkMessage with a JoinAccept payload", func() { 38 | err := f.HandleUplink(middleware.NewContext(), &types.UplinkMessage{Message: &router.UplinkMessage{ 39 | Payload: []byte{0x20, 2, 3, 4, 5}, 40 | }}) 41 | Convey("There should be an error", func() { 42 | So(err, ShouldNotBeNil) 43 | }) 44 | }) 45 | 46 | Convey("When sending an UplinkMessage with a Downlink payload", func() { 47 | err := f.HandleUplink(middleware.NewContext(), &types.UplinkMessage{Message: &router.UplinkMessage{ 48 | Payload: []byte{0x60, 2, 3, 4, 5}, 49 | }}) 50 | Convey("There should be an error", func() { 51 | So(err, ShouldNotBeNil) 52 | }) 53 | }) 54 | 55 | Convey("When sending an UplinkMessage with a valid message type", func() { 56 | err := f.HandleUplink(middleware.NewContext(), &types.UplinkMessage{Message: &router.UplinkMessage{ 57 | Payload: []byte{0x40, 2, 3, 4, 5}, 58 | }}) 59 | Convey("There should be no error", func() { 60 | So(err, ShouldBeNil) 61 | }) 62 | }) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package middleware 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 11 | ) 12 | 13 | // Timeout for middleware 14 | var Timeout = 10 * time.Millisecond 15 | 16 | // Context for middleware 17 | type Context interface { 18 | Set(k, v interface{}) 19 | Get(k interface{}) interface{} 20 | } 21 | 22 | // NewContext returns a new middleware context 23 | func NewContext() Context { 24 | return &context{ 25 | data: make(map[interface{}]interface{}), 26 | } 27 | } 28 | 29 | type context struct { 30 | data map[interface{}]interface{} 31 | } 32 | 33 | func (c *context) Set(k, v interface{}) { 34 | c.data[k] = v 35 | } 36 | 37 | func (c *context) Get(k interface{}) interface{} { 38 | if v, ok := c.data[k]; ok { 39 | return v 40 | } 41 | return nil 42 | } 43 | 44 | // Chain of middleware 45 | type Chain []interface{} 46 | 47 | // Execute the chain 48 | func (c Chain) Execute(ctx Context, msg interface{}) error { 49 | errCh := make(chan error) 50 | go func() { 51 | switch msg := msg.(type) { 52 | case *types.ConnectMessage: 53 | errCh <- c.filterConnect().Execute(ctx, msg) 54 | case *types.DisconnectMessage: 55 | errCh <- c.filterDisconnect().Execute(ctx, msg) 56 | case *types.UplinkMessage: 57 | errCh <- c.filterUplink().Execute(ctx, msg) 58 | case *types.StatusMessage: 59 | errCh <- c.filterStatus().Execute(ctx, msg) 60 | case *types.DownlinkMessage: 61 | errCh <- c.filterDownlink().Execute(ctx, msg) 62 | default: 63 | errCh <- nil 64 | } 65 | }() 66 | select { 67 | case err := <-errCh: 68 | return err 69 | case <-time.After(Timeout): 70 | return fmt.Errorf("middleware: timeout for %T", msg) 71 | } 72 | } 73 | 74 | // Connect middleware 75 | type Connect interface { 76 | HandleConnect(Context, *types.ConnectMessage) error 77 | } 78 | 79 | type connectChain []Connect 80 | 81 | func (c connectChain) Execute(ctx Context, msg *types.ConnectMessage) error { 82 | for _, middleware := range c { 83 | err := middleware.HandleConnect(ctx, msg) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | func (c Chain) filterConnect() (filtered connectChain) { 92 | for _, middleware := range c { 93 | if c, ok := middleware.(Connect); ok { 94 | filtered = append(filtered, c) 95 | } 96 | } 97 | return 98 | } 99 | 100 | // Disconnect middleware 101 | type Disconnect interface { 102 | HandleDisconnect(Context, *types.DisconnectMessage) error 103 | } 104 | 105 | type disconnectChain []Disconnect 106 | 107 | func (c disconnectChain) Execute(ctx Context, msg *types.DisconnectMessage) error { 108 | for _, middleware := range c { 109 | err := middleware.HandleDisconnect(ctx, msg) 110 | if err != nil { 111 | return err 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func (c Chain) filterDisconnect() (filtered disconnectChain) { 118 | for _, middleware := range c { 119 | if c, ok := middleware.(Disconnect); ok { 120 | filtered = append(filtered, c) 121 | } 122 | } 123 | return 124 | } 125 | 126 | // Uplink middleware 127 | type Uplink interface { 128 | HandleUplink(Context, *types.UplinkMessage) error 129 | } 130 | 131 | type uplinkChain []Uplink 132 | 133 | func (c uplinkChain) Execute(ctx Context, msg *types.UplinkMessage) error { 134 | for _, middleware := range c { 135 | err := middleware.HandleUplink(ctx, msg) 136 | if err != nil { 137 | return err 138 | } 139 | } 140 | return nil 141 | } 142 | 143 | func (c Chain) filterUplink() (filtered uplinkChain) { 144 | for _, middleware := range c { 145 | if c, ok := middleware.(Uplink); ok { 146 | filtered = append(filtered, c) 147 | } 148 | } 149 | return 150 | } 151 | 152 | // Status middleware 153 | type Status interface { 154 | HandleStatus(Context, *types.StatusMessage) error 155 | } 156 | 157 | type statusChain []Status 158 | 159 | func (c statusChain) Execute(ctx Context, msg *types.StatusMessage) error { 160 | for _, middleware := range c { 161 | err := middleware.HandleStatus(ctx, msg) 162 | if err != nil { 163 | return err 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | func (c Chain) filterStatus() (filtered statusChain) { 170 | for _, middleware := range c { 171 | if c, ok := middleware.(Status); ok { 172 | filtered = append(filtered, c) 173 | } 174 | } 175 | return 176 | } 177 | 178 | // Downlink middleware 179 | type Downlink interface { 180 | HandleDownlink(Context, *types.DownlinkMessage) error 181 | } 182 | 183 | type downlinkChain []Downlink 184 | 185 | func (c downlinkChain) Execute(ctx Context, msg *types.DownlinkMessage) error { 186 | for _, middleware := range c { 187 | err := middleware.HandleDownlink(ctx, msg) 188 | if err != nil { 189 | return err 190 | } 191 | } 192 | return nil 193 | } 194 | 195 | func (c Chain) filterDownlink() (filtered downlinkChain) { 196 | for _, middleware := range c { 197 | if c, ok := middleware.(Downlink); ok { 198 | filtered = append(filtered, c) 199 | } 200 | } 201 | return 202 | } 203 | -------------------------------------------------------------------------------- /middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package middleware 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | type something struct{} 15 | 16 | func TestContext(t *testing.T) { 17 | some := new(something) 18 | 19 | Convey("Given a new Context", t, func(c C) { 20 | ctx := NewContext() 21 | Convey("When setting some items in the Context", func() { 22 | ctx.Set(1, 2) 23 | ctx.Set("str", "hi") 24 | ctx.Set(some, some) 25 | Convey("Then getting those items from the Context should return the items", func() { 26 | So(ctx.Get(1), ShouldEqual, 2) 27 | So(ctx.Get("str"), ShouldEqual, "hi") 28 | So(ctx.Get(some), ShouldEqual, some) 29 | }) 30 | }) 31 | Convey("Getting an item that is not in the context, returns nil", func() { 32 | So(ctx.Get("other"), ShouldBeNil) 33 | }) 34 | }) 35 | } 36 | 37 | type testMiddleware struct { 38 | err error 39 | 40 | connect int 41 | disconnect int 42 | uplink int 43 | status int 44 | downlink int 45 | } 46 | 47 | func (c *testMiddleware) HandleConnect(ctx Context, msg *types.ConnectMessage) error { 48 | c.connect++ 49 | return c.err 50 | } 51 | func (c *testMiddleware) HandleDisconnect(ctx Context, msg *types.DisconnectMessage) error { 52 | c.disconnect++ 53 | return c.err 54 | } 55 | func (c *testMiddleware) HandleUplink(ctx Context, msg *types.UplinkMessage) error { 56 | c.uplink++ 57 | return c.err 58 | } 59 | func (c *testMiddleware) HandleStatus(ctx Context, msg *types.StatusMessage) error { 60 | c.status++ 61 | return c.err 62 | } 63 | func (c *testMiddleware) HandleDownlink(ctx Context, msg *types.DownlinkMessage) error { 64 | c.downlink++ 65 | return c.err 66 | } 67 | 68 | func TestMiddleware(t *testing.T) { 69 | Convey("Given a new Middleware Chain", t, func(c C) { 70 | m := new(testMiddleware) 71 | chain := Chain{m} 72 | 73 | Convey("When executing it on a ConnectMessage", func() { 74 | err := chain.Execute(NewContext(), &types.ConnectMessage{}) 75 | Convey("There should be no error", func() { 76 | So(err, ShouldBeNil) 77 | }) 78 | Convey("The middleware should have been called", func() { 79 | So(m.connect, ShouldEqual, 1) 80 | }) 81 | }) 82 | 83 | Convey("When executing it on a DisconnectMessage", func() { 84 | err := chain.Execute(NewContext(), &types.DisconnectMessage{}) 85 | Convey("There should be no error", func() { 86 | So(err, ShouldBeNil) 87 | }) 88 | Convey("The middleware should have been called", func() { 89 | So(m.disconnect, ShouldEqual, 1) 90 | }) 91 | }) 92 | 93 | Convey("When executing it on a UplinkMessage", func() { 94 | err := chain.Execute(NewContext(), &types.UplinkMessage{}) 95 | Convey("There should be no error", func() { 96 | So(err, ShouldBeNil) 97 | }) 98 | Convey("The middleware should have been called", func() { 99 | So(m.uplink, ShouldEqual, 1) 100 | }) 101 | }) 102 | 103 | Convey("When executing it on a StatusMessage", func() { 104 | err := chain.Execute(NewContext(), &types.StatusMessage{}) 105 | Convey("There should be no error", func() { 106 | So(err, ShouldBeNil) 107 | }) 108 | Convey("The middleware should have been called", func() { 109 | So(m.status, ShouldEqual, 1) 110 | }) 111 | }) 112 | 113 | Convey("When executing it on a DownlinkMessage", func() { 114 | err := chain.Execute(NewContext(), &types.DownlinkMessage{}) 115 | Convey("There should be no error", func() { 116 | So(err, ShouldBeNil) 117 | }) 118 | Convey("The middleware should have been called", func() { 119 | So(m.downlink, ShouldEqual, 1) 120 | }) 121 | }) 122 | 123 | Convey("When the middleware would return an error", func() { 124 | m.err = errors.New("some error") 125 | 126 | Convey("When executing it on a ConnectMessage", func() { 127 | err := chain.Execute(NewContext(), &types.ConnectMessage{}) 128 | Convey("There should be an error in the chain", func() { 129 | So(err, ShouldNotBeNil) 130 | }) 131 | }) 132 | 133 | Convey("When executing it on a DisconnectMessage", func() { 134 | err := chain.Execute(NewContext(), &types.DisconnectMessage{}) 135 | Convey("There should be an error in the chain", func() { 136 | So(err, ShouldNotBeNil) 137 | }) 138 | }) 139 | 140 | Convey("When executing it on a UplinkMessage", func() { 141 | err := chain.Execute(NewContext(), &types.UplinkMessage{}) 142 | Convey("There should be an error in the chain", func() { 143 | So(err, ShouldNotBeNil) 144 | }) 145 | }) 146 | 147 | Convey("When executing it on a StatusMessage", func() { 148 | err := chain.Execute(NewContext(), &types.StatusMessage{}) 149 | Convey("There should be an error in the chain", func() { 150 | So(err, ShouldNotBeNil) 151 | }) 152 | }) 153 | 154 | Convey("When executing it on a DownlinkMessage", func() { 155 | err := chain.Execute(NewContext(), &types.DownlinkMessage{}) 156 | Convey("There should be an error in the chain", func() { 157 | So(err, ShouldNotBeNil) 158 | }) 159 | }) 160 | 161 | }) 162 | 163 | Convey("When executing it on any other type", func() { 164 | err := chain.Execute(NewContext(), "hello") 165 | Convey("There should be no error", func() { 166 | So(err, ShouldBeNil) 167 | }) 168 | Convey("The middleware should not have been called", func() { 169 | So(m.connect, ShouldEqual, 0) 170 | So(m.disconnect, ShouldEqual, 0) 171 | So(m.uplink, ShouldEqual, 0) 172 | So(m.status, ShouldEqual, 0) 173 | So(m.downlink, ShouldEqual, 0) 174 | }) 175 | }) 176 | 177 | }) 178 | } 179 | -------------------------------------------------------------------------------- /middleware/ratelimit/ratelimit.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ratelimit 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 13 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 14 | "github.com/TheThingsNetwork/go-utils/log" 15 | "github.com/TheThingsNetwork/go-utils/rate" 16 | redis "gopkg.in/redis.v5" 17 | ) 18 | 19 | // Limits per minute 20 | type Limits struct { 21 | Uplink int 22 | Downlink int 23 | Status int 24 | } 25 | 26 | // NewRateLimit returns a middleware that rate-limits uplink, downlink and status messages per gateway 27 | func NewRateLimit(conf Limits) *RateLimit { 28 | return &RateLimit{ 29 | log: log.Get(), 30 | limits: conf, 31 | gateways: make(map[string]*limits), 32 | } 33 | } 34 | 35 | // NewRedisRateLimit returns a middleware that rate-limits uplink, downlink and status messages per gateway 36 | func NewRedisRateLimit(client *redis.Client, conf Limits) *RateLimit { 37 | l := NewRateLimit(conf) 38 | l.client = client 39 | return l 40 | } 41 | 42 | // RateLimit uplink, downlink and status messages per gateway 43 | type RateLimit struct { 44 | log log.Interface 45 | limits Limits 46 | client *redis.Client 47 | 48 | mu sync.RWMutex 49 | gateways map[string]*limits 50 | } 51 | 52 | func (l *RateLimit) newLimits(gatewayID string) *limits { 53 | limits := new(limits) 54 | 55 | if l.limits.Uplink != 0 { 56 | var uplink rate.Counter 57 | if l.client != nil { 58 | uplink = rate.NewRedisCounter(l.client, fmt.Sprintf("ratelimit:%s:uplink", gatewayID), time.Second, time.Minute) 59 | } else { 60 | uplink = rate.NewCounter(time.Second, time.Minute) 61 | } 62 | limits.uplink = rate.NewLimiter(uplink, time.Minute, uint64(l.limits.Uplink)) 63 | } 64 | 65 | if l.limits.Downlink != 0 { 66 | var downlink rate.Counter 67 | if l.client != nil { 68 | downlink = rate.NewRedisCounter(l.client, fmt.Sprintf("ratelimit:%s:downlink", gatewayID), time.Second, time.Minute) 69 | } else { 70 | downlink = rate.NewCounter(time.Second, time.Minute) 71 | } 72 | limits.downlink = rate.NewLimiter(downlink, time.Minute, uint64(l.limits.Downlink)) 73 | } 74 | 75 | if l.limits.Status != 0 { 76 | var status rate.Counter 77 | if l.client != nil { 78 | status = rate.NewRedisCounter(l.client, fmt.Sprintf("ratelimit:%s:status", gatewayID), time.Second, time.Minute) 79 | } else { 80 | status = rate.NewCounter(time.Second, time.Minute) 81 | } 82 | limits.status = rate.NewLimiter(status, time.Minute, uint64(l.limits.Status)) 83 | } 84 | 85 | return limits 86 | 87 | } 88 | 89 | type limits struct { 90 | uplink rate.Limiter 91 | downlink rate.Limiter 92 | status rate.Limiter 93 | } 94 | 95 | // HandleConnect initializes the rate limiter 96 | func (l *RateLimit) HandleConnect(ctx middleware.Context, msg *types.ConnectMessage) error { 97 | l.mu.Lock() 98 | defer l.mu.Unlock() 99 | l.gateways[msg.GatewayID] = l.newLimits(msg.GatewayID) 100 | return nil 101 | } 102 | 103 | // HandleDisconnect cleans up 104 | func (l *RateLimit) HandleDisconnect(ctx middleware.Context, msg *types.DisconnectMessage) error { 105 | l.mu.Lock() 106 | defer l.mu.Unlock() 107 | delete(l.gateways, msg.GatewayID) 108 | return nil 109 | } 110 | 111 | func (l *RateLimit) get(gatewayID string) *limits { 112 | // Try getting with only RLock 113 | l.mu.RLock() 114 | limits, ok := l.gateways[gatewayID] 115 | if ok { 116 | l.mu.RUnlock() 117 | return limits 118 | } 119 | l.mu.RUnlock() 120 | 121 | // Then try getting with full Lock 122 | l.mu.Lock() 123 | defer l.mu.Unlock() 124 | limits, ok = l.gateways[gatewayID] 125 | if ok { 126 | return limits 127 | } 128 | 129 | // Set new 130 | limits = l.newLimits(gatewayID) 131 | l.gateways[gatewayID] = limits 132 | return limits 133 | } 134 | 135 | // ErrRateLimited is returned if the rate limit has been reached 136 | var ErrRateLimited = errors.New("rate limit reached") 137 | 138 | // HandleUplink rate-limits status messages 139 | func (l *RateLimit) HandleUplink(ctx middleware.Context, msg *types.UplinkMessage) error { 140 | if limits := l.get(msg.GatewayID); limits != nil && limits.uplink != nil { 141 | limit, err := limits.uplink.Limit() 142 | if err != nil { 143 | return err 144 | } 145 | if limit { 146 | return ErrRateLimited 147 | } 148 | } 149 | return nil 150 | } 151 | 152 | // HandleDownlink rate-limits downlink messages 153 | func (l *RateLimit) HandleDownlink(ctx middleware.Context, msg *types.DownlinkMessage) error { 154 | if limits := l.get(msg.GatewayID); limits != nil && limits.downlink != nil { 155 | limit, err := limits.downlink.Limit() 156 | if err != nil { 157 | return err 158 | } 159 | if limit { 160 | return ErrRateLimited 161 | } 162 | } 163 | return nil 164 | } 165 | 166 | // HandleStatus rate-limits status messages 167 | func (l *RateLimit) HandleStatus(ctx middleware.Context, msg *types.StatusMessage) error { 168 | if limits := l.get(msg.GatewayID); limits != nil && limits.status != nil { 169 | limit, err := limits.status.Limit() 170 | if err != nil { 171 | return err 172 | } 173 | if limit { 174 | return ErrRateLimited 175 | } 176 | } 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /middleware/ratelimit/ratelimit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ratelimit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/TheThingsNetwork/gateway-connector-bridge/middleware" 10 | "github.com/TheThingsNetwork/gateway-connector-bridge/types" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestRateLimit(t *testing.T) { 15 | Convey("Given a new RateLimit", t, func(c C) { 16 | i := NewRateLimit(Limits{ 17 | Uplink: 1, 18 | Downlink: 1, 19 | Status: 1, 20 | }) 21 | 22 | Convey("When sending a ConnectMessage", func() { 23 | 24 | err := i.HandleConnect(middleware.NewContext(), &types.ConnectMessage{GatewayID: "test"}) 25 | Convey("There should be no error", func() { 26 | So(err, ShouldBeNil) 27 | }) 28 | Convey("The gateway limits should have been initialized", func() { 29 | So(i.gateways, ShouldContainKey, "test") 30 | }) 31 | 32 | Convey("When sending an UplinkMessage", func() { 33 | err := i.HandleUplink(middleware.NewContext(), &types.UplinkMessage{GatewayID: "test"}) 34 | Convey("There should be no error", func() { 35 | So(err, ShouldBeNil) 36 | }) 37 | Convey("When sending another UplinkMessage", func() { 38 | err := i.HandleUplink(middleware.NewContext(), &types.UplinkMessage{GatewayID: "test"}) 39 | Convey("There should be an error", func() { 40 | So(err, ShouldEqual, ErrRateLimited) 41 | }) 42 | }) 43 | }) 44 | 45 | Convey("When sending a DownlinkMessage", func() { 46 | err := i.HandleDownlink(middleware.NewContext(), &types.DownlinkMessage{GatewayID: "test"}) 47 | Convey("There should be no error", func() { 48 | So(err, ShouldBeNil) 49 | }) 50 | Convey("When sending another DownlinkMessage", func() { 51 | err := i.HandleDownlink(middleware.NewContext(), &types.DownlinkMessage{GatewayID: "test"}) 52 | Convey("There should be an error", func() { 53 | So(err, ShouldEqual, ErrRateLimited) 54 | }) 55 | }) 56 | }) 57 | 58 | Convey("When sending a StatusMessage", func() { 59 | err := i.HandleStatus(middleware.NewContext(), &types.StatusMessage{GatewayID: "test"}) 60 | Convey("There should be no error", func() { 61 | So(err, ShouldBeNil) 62 | }) 63 | Convey("When sending a StatusMessage", func() { 64 | err := i.HandleStatus(middleware.NewContext(), &types.StatusMessage{GatewayID: "test"}) 65 | Convey("There should be an error", func() { 66 | So(err, ShouldEqual, ErrRateLimited) 67 | }) 68 | }) 69 | }) 70 | 71 | Convey("When sending a DisconnectMessage", func() { 72 | err := i.HandleDisconnect(middleware.NewContext(), &types.DisconnectMessage{GatewayID: "test"}) 73 | Convey("There should be no error", func() { 74 | So(err, ShouldBeNil) 75 | }) 76 | Convey("The gateway limits should have been unset", func() { 77 | So(i.gateways, ShouldNotContainKey, "test") 78 | }) 79 | }) 80 | 81 | }) 82 | 83 | Convey("When sending an UplinkMessage", func() { 84 | err := i.HandleUplink(middleware.NewContext(), &types.UplinkMessage{GatewayID: "test"}) 85 | Convey("There should be no error", func() { 86 | So(err, ShouldBeNil) 87 | }) 88 | Convey("When sending another UplinkMessage", func() { 89 | err := i.HandleUplink(middleware.NewContext(), &types.UplinkMessage{GatewayID: "test"}) 90 | Convey("There should be an error", func() { 91 | So(err, ShouldEqual, ErrRateLimited) 92 | }) 93 | }) 94 | }) 95 | 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package types 5 | 6 | import ( 7 | "net" 8 | 9 | "github.com/TheThingsNetwork/api/gateway" 10 | "github.com/TheThingsNetwork/api/router" 11 | ) 12 | 13 | // UplinkMessage is used internally 14 | type UplinkMessage struct { 15 | GatewayID string 16 | GatewayAddr net.Addr 17 | Message *router.UplinkMessage 18 | } 19 | 20 | // DownlinkMessage is used internally 21 | type DownlinkMessage struct { 22 | GatewayID string 23 | Message *router.DownlinkMessage 24 | } 25 | 26 | // StatusMessage is used internally 27 | type StatusMessage struct { 28 | Backend string 29 | GatewayID string 30 | GatewayAddr net.Addr 31 | Message *gateway.Status 32 | } 33 | -------------------------------------------------------------------------------- /types/types.proto: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | syntax = "proto3"; 5 | 6 | import "github.com/gogo/protobuf/gogoproto/gogo.proto"; 7 | 8 | package types; 9 | 10 | option go_package = "github.com/TheThingsNetwork/gateway-connector-bridge/types"; 11 | 12 | message ConnectMessage { 13 | string id = 1 [(gogoproto.customname) = "GatewayID"]; 14 | string key = 3; 15 | } 16 | 17 | message DisconnectMessage { 18 | string id = 1 [(gogoproto.customname) = "GatewayID"]; 19 | string key = 3; 20 | } 21 | --------------------------------------------------------------------------------