1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-24 13:48:49 +08:00

Use PostgreSQL as primary persistence solution (#175)

* Use normalizer as stream source

Renamed 'writer' service to 'normalizer' and dropped Cassandra
facilities from it. Extracted the common dependencies to 'mainflux'
package for easier sharing. Fixed the API docs and unified environment
variables.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Use docker build arguments to specify build

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Remove cassandra libraries

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Update go-kit version to 0.6.0

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Fix manager configuration

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Refactor docker-compose

Merged individual compose files and dropped external links. Remove CoAP
container since it is not referenced from NginX config at the moment.
Update port mapping in compose and nginx.conf. Dropped bin scripts.
Updated service documentation.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Drop content-type check

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Implement users data access layer in PostgreSQL

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Bump version to 0.1.0

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Use go-kit logger everywhere (except CoAP)

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Improve factory methods naming

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Implement clients data access layer on PostgreSQL

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Make tests stateless

All tests are refactored to use map-based table-driven tests. No
cross-tests dependencies is present anymore.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Remove gitignore

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Fix nginx proxying

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Mark client-user FK explicit

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Update API documentation

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Update channel model

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Add channel PostgreSQL repository tests

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Implement PostgreSQL channels DAO

Replaced update queries with raw SQL. Explicitly defined M2M table due
to difficulties of ensuring the referential integrity through GORM.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Expose connection endpoints

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Fix swagger docs and remove DB logging

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Fix nested query remarks

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Add unique indices

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
This commit is contained in:
Dejan Mijić 2018-03-11 18:06:01 +01:00 committed by Drasko DRASKOVIC
parent b23ecb64e5
commit ccd8965d6f
989 changed files with 233402 additions and 25667 deletions

View File

@ -1,8 +0,0 @@
.git/
.github/
bin/
glide.lock
glide.yaml
LICENSE
MAINTAINERS
README.md

1
.gitignore vendored
View File

@ -1 +0,0 @@
build

View File

@ -1,9 +0,0 @@
FROM golang:1.8-alpine AS builder
WORKDIR /go/src/github.com/mainflux/mainflux
COPY . .
RUN cd cmd/coap && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
FROM scratch
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/coap/exe /
EXPOSE 5683
ENTRYPOINT ["/exe"]

View File

@ -1,9 +0,0 @@
FROM golang:1.8-alpine AS builder
WORKDIR /go/src/github.com/mainflux/mainflux
COPY . .
RUN cd cmd/http && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
FROM scratch
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/http/exe /
EXPOSE 7070
ENTRYPOINT ["/exe"]

View File

@ -1,9 +0,0 @@
FROM golang:1.8-alpine AS builder
WORKDIR /go/src/github.com/mainflux/mainflux
COPY . .
RUN cd cmd/manager && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
FROM scratch
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/manager/exe /
EXPOSE 8180
ENTRYPOINT ["/exe"]

View File

@ -1,8 +0,0 @@
FROM golang:1.8-alpine AS builder
WORKDIR /go/src/github.com/mainflux/mainflux
COPY . .
RUN cd cmd/writer && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
FROM scratch
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/writer/exe /
ENTRYPOINT ["/exe"]

236
Gopkg.lock generated
View File

@ -1,6 +1,27 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/Azure/go-ansiterm"
packages = [
".",
"winterm"
]
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
[[projects]]
name = "github.com/Microsoft/go-winio"
packages = ["."]
revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f"
version = "v0.4.7"
[[projects]]
branch = "master"
name = "github.com/Nvveen/Gotty"
packages = ["."]
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
[[projects]]
name = "github.com/asaskevich/govalidator"
packages = ["."]
@ -14,14 +35,28 @@
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "61153c768f31ee5f130071d08fc82b85208528de"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/cisco/senml"
packages = ["."]
revision = "eb356817a51441202197049d8e7ae3c3755d8965"
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
[[projects]]
branch = "master"
name = "github.com/containerd/continuity"
packages = ["pathdriver"]
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "6d212800a42e8ab5c146b8ace3490ee17e5225f9"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
@ -32,8 +67,52 @@
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "d2709f9f1f31ebcda9651b03077758c1f3a0018c"
version = "v3.0.0"
revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29"
version = "v3.1.0"
[[projects]]
branch = "master"
name = "github.com/docker/docker"
packages = [
"api/types",
"api/types/blkiodev",
"api/types/container",
"api/types/filters",
"api/types/mount",
"api/types/network",
"api/types/registry",
"api/types/strslice",
"api/types/swarm",
"api/types/swarm/runtime",
"api/types/versions",
"opts",
"pkg/archive",
"pkg/fileutils",
"pkg/homedir",
"pkg/idtools",
"pkg/ioutils",
"pkg/jsonmessage",
"pkg/longpath",
"pkg/mount",
"pkg/pools",
"pkg/stdcopy",
"pkg/system",
"pkg/term",
"pkg/term/windows"
]
revision = "4d9beb4607404e4d756052aca7041517788f7e75"
[[projects]]
name = "github.com/docker/go-connections"
packages = ["nat"]
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
version = "v0.3.0"
[[projects]]
name = "github.com/docker/go-units"
packages = ["."]
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
version = "v0.3.2"
[[projects]]
branch = "master"
@ -41,6 +120,12 @@
packages = ["."]
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
[[projects]]
name = "github.com/fsouza/go-dockerclient"
packages = ["."]
revision = "ca33ff277b527ce11b793e62f9ba244129b01caf"
version = "1.2.0"
[[projects]]
name = "github.com/go-kit/kit"
packages = [
@ -51,8 +136,8 @@
"metrics/prometheus",
"transport/http"
]
revision = "fadad6fffe0466b19df9efd9acde5c9a52df5fa4"
version = "v0.4.0"
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
version = "v0.6.0"
[[projects]]
name = "github.com/go-logfmt/logfmt"
@ -73,31 +158,31 @@
version = "1.2"
[[projects]]
name = "github.com/gocql/gocql"
packages = [
".",
"internal/lru",
"internal/murmur",
"internal/streams"
]
revision = "9ce8b08dfa5559eb86592129986654503cf40cf7"
name = "github.com/gogo/protobuf"
packages = ["proto"]
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
version = "v1.0.0"
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/golang/snappy"
packages = ["."]
revision = "553a641470496b2327abcac10b36396bd98e45c9"
name = "github.com/jinzhu/gorm"
packages = [
".",
"dialects/postgres"
]
revision = "58e34726dfc069b558038efbaa25555f182d1f7a"
[[projects]]
branch = "master"
name = "github.com/hailocab/go-hostpool"
name = "github.com/jinzhu/inflection"
packages = ["."]
revision = "e80d13ce29ede4452c43dea11e79b9bc8a15b478"
revision = "1c35d901db3da928c72a72d8458480cc9ade058f"
[[projects]]
branch = "master"
@ -107,9 +192,19 @@
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid"
]
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
name = "github.com/nats-io/go-nats"
@ -118,19 +213,50 @@
"encoders/builtin",
"util"
]
revision = "29f9728a183bf3fa7e809e14edac00b33be72088"
version = "v1.3.0"
revision = "d66cb54e6b7bdd93f0b28afc8450d84c780dfb68"
version = "v1.4.0"
[[projects]]
branch = "master"
name = "github.com/nats-io/nuid"
packages = ["."]
revision = "33c603157d6fd1b0ac2599bcc4a286b36479a06d"
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
version = "v1.0.0"
[[projects]]
name = "github.com/opencontainers/go-digest"
packages = ["."]
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
version = "v1.0.0-rc1"
[[projects]]
name = "github.com/opencontainers/image-spec"
packages = [
"specs-go",
"specs-go/v1"
]
revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1"
[[projects]]
name = "github.com/opencontainers/runc"
packages = [
"libcontainer/system",
"libcontainer/user"
]
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
version = "v0.1.1"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "d8ed2627bdf02c080bf22230dbb337003b7aba2d"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/prometheus/client_golang"
@ -148,21 +274,37 @@
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
]
revision = "2e54d0b93cba2fd133edc32211dcc32c06ef72ca"
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
[[projects]]
branch = "master"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
]
revision = "a6e9df898b1336106c743392c48ee0b71f5c4efa"
revision = "282c8707aa210456a825798969cc27edda34992a"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
version = "v1.0.4"
[[projects]]
name = "github.com/sony/gobreaker"
@ -173,14 +315,14 @@
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
name = "github.com/ugorji/go"
packages = ["codec"]
revision = "f57d8945648dbfe4c332cff9c50fb57548958e3f"
version = "v.1.1-beta"
revision = "9831f2c3ac1068a78f50999a30db84270f647af6"
version = "v1.1"
[[projects]]
name = "go.uber.org/atomic"
@ -204,34 +346,46 @@
"internal/exit",
"zapcore"
]
revision = "7a9ca91fa627ed52c7ff4fcc95cd044dc2c82a51"
version = "v1.7.0"
revision = "35aad584952c3e7020db7b839f6b102de6271f89"
version = "v1.7.1"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish"
"blowfish",
"ssh/terminal"
]
revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122"
revision = "650f4a345ab4e5b245a3034b110ebc7299e68186"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp"
]
revision = "d866cfc389cec985d6fda2859936a575a55a3ab6"
revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb"
[[projects]]
name = "gopkg.in/inf.v0"
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "88d2dcc510266da9f7f8c7f34e1940716cab5f5c"
[[projects]]
name = "gopkg.in/ory-am/dockertest.v3"
packages = ["."]
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
version = "v0.9.0"
revision = "15c8e8835bba04e0d7c2b57958ffe294d5e643dc"
version = "v3.1.6"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "31542f4b815cef69fc1980da7cf626cf5e95b832660811039da81df223e43adc"
inputs-digest = "9df3ffee2bc61a6ce786baa7570eb61d8275197ea36560cf0cfd7cc0ea53af96"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -4,44 +4,60 @@
[[constraint]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
name = "github.com/cisco/senml"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.0.0"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-coap"
[[constraint]]
name = "github.com/go-kit/kit"
version = "0.4.0"
version = "0.6.0"
[[constraint]]
name = "github.com/go-zoo/bone"
version = "1.2.0"
[[constraint]]
branch = "master"
name = "github.com/jinzhu/gorm"
[[constraint]]
name = "github.com/nats-io/go-nats"
version = "1.3.0"
[[constraint]]
name = "gopkg.in/ory-am/dockertest.v3"
version = "3.1.6"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
[[constraint]]
name = "github.com/sony/gobreaker"
version = "0.3.0"
name = "github.com/satori/go.uuid"
version = "1.2.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.1.4"
name = "github.com/sony/gobreaker"
version = "0.3.0"
[[constraint]]
name = "go.uber.org/zap"
version = "1.7.0"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-coap"
[[constraint]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true

View File

@ -1,23 +0,0 @@
BUILD_DIR=build
all: manager http writer coap
.PHONY: all manager http writer coap
manager:
go build -o ${BUILD_DIR}/mainflux-manager cmd/manager/main.go
http:
go build -o ${BUILD_DIR}/mainflux-http cmd/http/main.go
writer:
go build -o ${BUILD_DIR}/mainflux-writer cmd/writer/main.go
coap:
go build -o ${BUILD_DIR}/mainflux-coap cmd/coap/main.go
clean:
rm -rf ${BUILD_DIR}
install:
cp ${BUILD_DIR}/* $(GOBIN)

View File

@ -19,40 +19,23 @@ For more details, check out the [official documentation][docs].
- Protocol bridging (i.e. HTTP, MQTT, WebSocket, CoAP)
- Device management and provisioning
- Linearly scalable [data storage][cassandra]
- Fine-grained access control
- Platform logging and instrumentation support
- Container-based deployment using [Docker][docker]
## Quickstart
#### Docker
Before proceeding, install the following prerequisites:
To start the docker composition, execute the [startup script](bin/mainflux-docker.sh) from `bin`
directory:
- [Docker](https://docs.docker.com/install/)
- [Docker compose](https://docs.docker.com/compose/install/)
Once everything is installed, execute the following commands from project root:
```bash
cd docker/
docker-compose up -d
```
./mainflux-docker.sh start
```
If the command successfully completes, you can verify that the all images are up & running by
executing the following command in the terminal window:
```
docker ps
```
The composition can be terminated in the following way:
```
./mainflux-docker.sh stop
```
#### From sources
To download all the sources, and place them in appropriate locations (i.e. $GOPATH), use the
[installation script](bin/mainflux-install.sh). Once it completes, the script will provide the
instructions on how to finish the manual installation (i.e. install the required infrastructure).
## Contributing
@ -80,4 +63,3 @@ Thank you for your interest in Mainflux and wish to contribute!
[grc-url]: https://goreportcard.com/report/github.com/mainflux/mainflux
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
[twitter]: https://twitter.com/mainflux
[cassandra]: http://cassandra.apache.org

View File

@ -1,33 +0,0 @@
###
# Copyright (c) 2015-2017 Mainflux
#
# Mainflux server is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
version: "3"
services:
###
# NATS
###
nats:
image: nats:1.0.2
container_name: mainflux-nats
network_mode: bridge
ports:
- "4222:4222"
- "8222:8222"
###
# Cassandra
###
cassandra:
image: cassandra:3.0.14
container_name: mainflux-cassandra
network_mode: bridge
ports:
- "9042:9042"
- "9160:9160"

View File

@ -1,92 +0,0 @@
###
# Copyright (c) 2015-2017 Mainflux
#
# Mainflux server is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
version: "3"
services:
###
# Manager
###
manager:
image: mainflux/manager:latest
container_name: mainflux-manager
network_mode: bridge
ports:
- "8180:8180"
external_links:
- mainflux-cassandra:cassandra
environment:
- MANAGER_DB_CLUSTER=cassandra
- MANAGER_DB_KEYSPACE=manager
###
# Message Writer
###
message-writer:
image: mainflux/writer:latest
container_name: mainflux-message-writer
network_mode: bridge
external_links:
- mainflux-nats:nats
- mainflux-cassandra:cassandra
environment:
- MESSAGE_WRITER_DB_CLUSTER=cassandra
- MESSAGE_WRITER_DB_KEYSPACE=message_writer
- MESSAGE_WRITER_NATS_URL=nats://nats:4222
###
# MQTT Broker
###
mqtt-adapter:
image: mainflux/mqtt-adapter:latest
container_name: mainflux-mqtt
ports:
- "1883:1883"
network_mode: bridge
depends_on:
- manager
external_links:
- mainflux-nats:nats
- mainflux-manager:manager
environment:
- MQTT_ADAPTER_NATS_URL=nats://nats:4222
- AUTH_URL=http://manager
- AUTH_PORT=8180
###
# HTTP Server
###
http-adapter:
image: mainflux/http:latest
container_name: mainflux-http
network_mode: bridge
depends_on:
- manager
ports:
- "7070:7070"
external_links:
- mainflux-nats:nats
- mainflux-manager:manager
environment:
- HTTP_ADAPTER_NATS_URL=nats://nats:4222
- HTTP_ADAPTER_MANAGER_URL=http://manager:8180
###
# CoAP Server
###
coap-adapter:
image: mainflux/coap:latest
container_name: mainflux-coap
network_mode: bridge
ports:
- "5683:5683"
external_links:
- mainflux-nats:nats
environment:
- COAP_ADAPTER_NATS_URL=nats://nats:4222

View File

@ -1,32 +0,0 @@
###
# Copyright (c) 2015-2017 Mainflux
#
# Mainflux server is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
version: "3"
services:
###
# NGINX
###
nginx:
image: nginx:1.13-alpine
container_name: mainflux-nginx
volumes:
- $PWD/docker/nginx.conf:/etc/nginx/nginx.conf
- $PWD/docker/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- $PWD/docker/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- $PWD/docker/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
network_mode: bridge
ports:
- "80:80"
- "443:443"
- "8883:8883"
external_links:
- mainflux-manager
- mainflux-http
- mainflux-mqtt

View File

@ -1,219 +0,0 @@
#!/usr/bin/env bash
# Derived from https://github.com/alphabetum/bash-boilerplate
# Strict Mode
set -o nounset
# Exit immediately if a pipeline returns non-zero.
#set -o errexit
# Print a helpful message if a pipeline with non-zero exit code causes the
# script to exit as described above.
trap 'echo "Aborting due to errexit on line $LINENO. Exit code: $?" >&2' ERR
# Allow the above trap be inherited by all functions in the script.
# Short form: set -E
set -o errtrace
# Return value of a pipeline is the value of the last (rightmost) command to
# exit with a non-zero status, or zero if all commands in the pipeline exit
# successfully.
set -o pipefail
# Set IFS to just newline and tab at the start
DEFAULT_IFS="${IFS}"
SAFER_IFS=$'\n\t'
IFS="${SAFER_IFS}"
###############################################################################
# Environment
###############################################################################
# $_ME
#
# Set to the program's basename.
_ME=$(basename "${0}")
###############################################################################
# Help
###############################################################################
# _print_help()
#
# Usage:
# _print_help
#
# Print the program help information.
_print_help() {
cat <<HEREDOC
MAINFLUX-DOCKER
Starts or stops Mainflux Docker composition.
Commands:
start Start Docker composition
stop Stop Docker composition
Options:
-h, --help Show this screen.
HEREDOC
}
###############################################################################
# Program Functions
###############################################################################
_start() {
# Start NATS, Cassandra and Nginx
printf "Starting NATS and Cassandra...\n\n"
NB_DOCKERS=$(docker ps -a -f name=mainflux-nats -f name=mainflux-cassandra | wc -l)
if [[ $NB_DOCKERS -lt 3 ]]
then
docker-compose -f docker/docker-compose.infrastructure.yml pull
docker-compose -f docker/docker-compose.infrastructure.yml create
fi
docker-compose -f docker/docker-compose.infrastructure.yml start
# Check if C* is alive
printf "\nWaiting for Cassandra to start. This takes time, please be patient...\n"
# Wait until Cassandra is ready to accept cqlsh commands
# or timeout after 15 sec
c_on=0
retries=0
while [[ $retries -lt 15 ]]
do
# Check if C* port 9042 is open
if $(docker exec -it mainflux-cassandra cqlsh -e "exit" > /dev/null 2>&1)
then
# Sucess
c_on=1
break
fi
sleep 1
printf "."
retries=$((retries + 1))
done
# If Cassandra did not start then shut down everything and exit
if [[ $c_on -eq 0 ]]
then
printf "\nCassandra did not start - shuting down everything.\n"
docker-compose -f docker/docker-compose.infrastructure.yml stop
exit 0
else
printf "OK\n"
fi
# Create C* keyspaces, if missing
docker exec -it mainflux-cassandra cqlsh -e "CREATE KEYSPACE IF NOT EXISTS manager WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"
docker exec -it mainflux-cassandra cqlsh -e "CREATE KEYSPACE IF NOT EXISTS message_writer WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"
# Start Mainflux
printf "\nStarting Mainflux composition...\n\n"
NB_DOCKERS=$(docker ps -a -f name=mainflux-manager -f name=mainflux-http -f name=mainflux-mqtt -f name=mainflux-coap -f name=mainflux-message-writer | wc -l)
if [[ $NB_DOCKERS -lt 6 ]]
then
docker-compose -f docker/docker-compose.mainflux.yml pull
docker-compose -f docker/docker-compose.mainflux.yml create
fi
docker-compose -f docker/docker-compose.mainflux.yml start
# Start Nginx
printf "\nStarting Nginx...\n\n"
NB_DOCKERS=$(docker ps -a -f name=mainflux-nginx | wc -l)
if [[ $NB_DOCKERS -lt 2 ]]
then
docker-compose -f docker/docker-compose.nginx.yml pull
docker-compose -f docker/docker-compose.nginx.yml create
fi
docker-compose -f docker/docker-compose.nginx.yml start
if [[ $? -ne 0 ]]
then
_stop
exit 1
fi
printf "\n*** MAINFLUX IS ON ***\n\n"
docker ps
}
_stop() {
printf "\nStopping Nginx...\n\n"
docker-compose -f docker/docker-compose.nginx.yml stop
printf "\nStopping Mainflux composition...\n\n"
docker-compose -f docker/docker-compose.mainflux.yml stop
printf "\nStopping NATS and Cassandra...\n\n"
docker-compose -f docker/docker-compose.infrastructure.yml stop
printf "\n*** MAINFLUX IS OFF ***\n\n"
}
_clean() {
printf "\nCleaning NATS and Cassandra containers...\n\n"
docker-compose -f docker/docker-compose.infrastructure.yml rm -f
printf "\nCleaning Mainflux containers...\n\n"
docker-compose -f docker/docker-compose.mainflux.yml rm -f
printf "\nCleaning Nginx container...\n\n"
docker-compose -f docker/docker-compose.nginx.yml rm -f
printf "\n*** Docker containers cleaned ***\n\n"
}
_mainflux_docker() {
if [[ $1 == "start" ]]
then
_start
elif [[ $1 == "stop" ]]
then
_stop
elif [[ $1 == "clean" ]]
then
_clean
else
printf "Unknown command.\n"
fi
}
###############################################################################
# Main
###############################################################################
# _main()
#
# Usage:
# _main [<options>] [<arguments>]
#
# Description:
# Entry point for the program, handling basic option parsing and dispatching.
_main() {
# No arguments provided
if [[ $# -eq 0 ]] ; then
_print_help
exit 1
fi
# Avoid complex option parsing when only one program option is expected.
if [[ "${1:-}" =~ ^-h|--help$ ]]
then
_print_help
else
_mainflux_docker "$@"
fi
}
# Call `_main` after everything has been defined.
_main "$@"

View File

@ -1,85 +0,0 @@
#!/bin/bash
DIR=$PWD
mkdir -p ./mainflux
cd ./mainflux
if [ -z "$GOPATH" ]; then
mkdir -p $PWD/go
export GOPATH=$PWD/go
fi
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN
mkdir -p $GOBIN
# Mainflux Go microservices
go get -d -v github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
make
make install
cd -
# MQTT
git clone https://github.com/mainflux/mqtt-adapter
cd mqtt-adapter
npm install
cd ..
# NGINX Conf
git clone https://github.com/mainflux/proxy
# NATS
go get -v github.com/nats-io/gnatsd
# Make symlink to go mainflux sources
ln -s $GOPATH/src/github.com/mainflux mainflux-go
# Go back to where we started
cd $DIR
# Print info
cat << EOF
***
# Mainflux is now installed #
- Go sources are located at $GOPATH/src
- Go binaries are located are $GOBIN
- MQTT NodeJS sources are located at $PWD/mainflux/mqtt-adapter
- NGINX config files are located in $PWD/mainflux/nginx-conf
External dependencies needed for Mainflux are:
- Cassandra
- NATS
- NGINX
NATS has been installed.
For Cassandra follow the instructions at http://cassandra.apache.org/download/
After installing Cassandra you should create the two keyspaces that Mainflux uses. This can be done with something similar to:
cqlsh> CREATE KEYSPACE IF NOT EXISTS manager WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
cqlsh> CREATE KEYSPACE IF NOT EXISTS message_writer WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
Please note that in the example SQL statment above, the keyspaces will be created in a single datacenter (single cluster) and there will
only be one replica (copy) of the data. You should create the keyspaces with parameters appropriate for your Cassandra installation. Take a look
at the Cassandra documentation for creating keyspaces for more details. For production usage you should always configure multiple replicas in order
to have data redundancy and be safe in case one or more Cassandra cluster nodes fail.
For NGINX follow the instructions here: http://nginx.org/en/docs/install.html
NGINX config has been cloned in nginx-conf,
and these config files have to be copied to /etc/nginx once NGINX server
is installed on the system.
After copying these files you have to re-start the nginx service:
sudo systemctl restart nginx.service
***
EOF

View File

@ -1,30 +0,0 @@
#!/bin/bash
###
# Launches all Mainflux Go binaries when they are installed globally.
# Also launches NATS broker instance, expecting that
# `gnatsd` is installed globally.
#
# Expects that Cassandra is already installed and running.
#
# Does not launch NodeJS MQTT service - this one must be launched by hand for now.
###
# Kill all mainflux-* stuff
function cleanup {
pkill mainflux
}
gnatsd &
# Wait a bit for NATS to be on
sleep 0.1
mainflux-http &
mainflux-manager &
mainflux-writer &
mainflux-coap &
trap cleanup EXIT
while : ; do sleep 1 ; done

View File

@ -33,11 +33,11 @@ func main() {
nc := connectToNats(cfg, logger)
defer nc.Close()
repo := nats.NewMessageRepository(nc)
ca := adapter.NewCoAPAdapter(logger, repo)
pub := nats.NewMessagePublisher(nc)
ca := adapter.NewCoAPAdapter(logger, pub)
nc.Subscribe("msg.http", ca.BridgeHandler)
nc.Subscribe("msg.mqtt", ca.BridgeHandler)
nc.Subscribe("src.http", ca.BridgeHandler)
nc.Subscribe("src.mqtt", ca.BridgeHandler)
errs := make(chan error, 2)

View File

@ -9,6 +9,7 @@ import (
"github.com/go-kit/kit/log"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
adapter "github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/http/api"
"github.com/mainflux/mainflux/http/nats"
@ -18,24 +19,25 @@ import (
)
const (
port int = 7070
defPort string = "8180"
defNatsURL string = broker.DefaultURL
envNatsURL string = "HTTP_ADAPTER_NATS_URL"
defManagerURL string = "http://localhost:8180"
envManagerURL string = "HTTP_ADAPTER_MANAGER_URL"
envPort string = "MF_HTTP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envManagerURL string = "MF_MANAGER_URL"
)
type config struct {
Port int
NatsURL string
ManagerURL string
NatsURL string
Port string
}
func main() {
cfg := config{
Port: port,
NatsURL: getenv(envNatsURL, defNatsURL),
ManagerURL: getenv(envManagerURL, defManagerURL),
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
@ -43,37 +45,35 @@ func main() {
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
logger.Log("aborted", err)
logger.Log("error", err)
os.Exit(1)
}
defer nc.Close()
repo := nats.NewMessageRepository(nc)
svc := adapter.NewService(repo)
pub := nats.NewMessagePublisher(nc)
svc = api.NewLoggingService(logger, svc)
fields := []string{"method"}
svc = api.NewMetricService(
svc := adapter.New(pub)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "http_adapter",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, fields),
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "http_adapter",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, fields),
svc,
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%d", cfg.Port)
p := fmt.Sprintf(":%s", cfg.Port)
mc := manager.NewClient(cfg.ManagerURL)
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
}()
@ -86,12 +86,3 @@ func main() {
logger.Log("terminated", <-errs)
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}

View File

@ -5,101 +5,95 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"github.com/go-kit/kit/log"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/api"
"github.com/mainflux/mainflux/manager/bcrypt"
"github.com/mainflux/mainflux/manager/cassandra"
"github.com/mainflux/mainflux/manager/jwt"
"github.com/mainflux/mainflux/manager/postgres"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
port int = 8180
sep string = ","
defCluster string = "127.0.0.1"
defKeyspace string = "manager"
defSecret string = "manager"
envCluster string = "MANAGER_DB_CLUSTER"
envKeyspace string = "MANAGER_DB_KEYSPACE"
envSecret string = "MANAGER_SECRET"
defDBHost string = "localhost"
defDBPort string = "5432"
defDBUser string = "mainflux"
defDBPass string = "mainflux"
defDBName string = "manager"
defPort string = "8180"
defSecret string = "manager"
envDBHost string = "MF_DB_HOST"
envDBPort string = "MF_DB_PORT"
envDBUser string = "MF_DB_USER"
envDBPass string = "MF_DB_PASS"
envDBName string = "MF_MANAGER_DB"
envPort string = "MF_MANAGER_PORT"
envSecret string = "MF_MANAGER_SECRET"
)
type config struct {
Port int
Cluster string
Keyspace string
Secret string
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
DBHost string
DBPort string
DBUser string
DBPass string
DBName string
Port string
Secret string
}
func main() {
cfg := config{
Port: port,
Cluster: getenv(envCluster, defCluster),
Keyspace: getenv(envKeyspace, defKeyspace),
Secret: getenv(envSecret, defSecret),
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
DBUser: mainflux.Env(envDBUser, defDBUser),
DBPass: mainflux.Env(envDBPass, defDBPass),
DBName: mainflux.Env(envDBName, defDBName),
Port: mainflux.Env(envPort, defPort),
Secret: mainflux.Env(envSecret, defSecret),
}
var logger log.Logger
logger = log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
session, err := cassandra.Connect(strings.Split(cfg.Cluster, sep), cfg.Keyspace)
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
if err != nil {
logger.Log("error", err)
os.Exit(1)
}
defer session.Close()
defer db.Close()
if err := cassandra.Initialize(session); err != nil {
logger.Log("error", err)
os.Exit(1)
}
users := postgres.NewUserRepository(db)
clients := postgres.NewClientRepository(db)
channels := postgres.NewChannelRepository(db)
hasher := bcrypt.New()
idp := jwt.New(cfg.Secret)
users := cassandra.NewUserRepository(session)
clients := cassandra.NewClientRepository(session)
channels := cassandra.NewChannelRepository(session)
hasher := bcrypt.NewHasher()
idp := jwt.NewIdentityProvider(cfg.Secret)
var svc manager.Service
svc = manager.NewService(users, clients, channels, hasher, idp)
svc = api.NewLoggingService(logger, svc)
fields := []string{"method"}
svc = api.NewMetricService(
svc := manager.New(users, clients, channels, hasher, idp)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "manager",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, fields),
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "manager",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, fields),
svc,
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%d", cfg.Port)
p := fmt.Sprintf(":%s", cfg.Port)
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
}()

59
cmd/normalizer/main.go Normal file
View File

@ -0,0 +1,59 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/normalizer"
nats "github.com/nats-io/go-nats"
)
const (
defNatsURL string = nats.DefaultURL
defPort string = "8180"
envNatsURL string = "MF_NATS_URL"
envPort string = "MF_NORMALIZER_PORT"
)
type config struct {
NatsURL string
Port string
}
func main() {
cfg := config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Log("error", fmt.Sprintf("Failed to connect: %s", err))
os.Exit(1)
}
defer nc.Close()
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
normalizer.Subscribe(nc, logger)
logger.Log("terminated", <-errs)
}

View File

@ -1,124 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/gocql/gocql"
"github.com/mainflux/mainflux/writer"
"github.com/mainflux/mainflux/writer/cassandra"
nats "github.com/nats-io/go-nats"
"go.uber.org/zap"
)
const (
sep string = ","
subject string = "msg.*"
queue string = "message_writers"
defCluster string = "127.0.0.1"
defKeyspace string = "message_writer"
defNatsURL string = nats.DefaultURL
envCluster string = "MESSAGE_WRITER_DB_CLUSTER"
envKeyspace string = "MESSAGE_WRITER_DB_KEYSPACE"
envNatsURL string = "MESSAGE_WRITER_NATS_URL"
)
var logger *zap.Logger
type config struct {
Cluster string
Keyspace string
NatsURL string
}
func main() {
cfg := loadConfig()
logger, _ = zap.NewProduction()
defer logger.Sync()
session := connectToCassandra(cfg)
defer session.Close()
nc := connectToNats(cfg)
defer nc.Close()
repo := makeRepository(session)
nc.QueueSubscribe(subject, queue, func(m *nats.Msg) {
msg := writer.RawMessage{}
if err := json.Unmarshal(m.Data, &msg); err != nil {
logger.Error("Failed to unmarshal raw message.", zap.Error(err))
return
}
if err := repo.Save(msg); err != nil {
logger.Error("Failed to save message.", zap.Error(err))
return
}
})
forever()
}
func loadConfig() *config {
return &config{
Cluster: env(envCluster, defCluster),
Keyspace: env(envKeyspace, defKeyspace),
NatsURL: env(envNatsURL, defNatsURL),
}
}
func env(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func connectToCassandra(cfg *config) *gocql.Session {
hosts := strings.Split(cfg.Cluster, sep)
s, err := cassandra.Connect(hosts, cfg.Keyspace)
if err != nil {
logger.Error("Failed to connect to DB", zap.Error(err))
}
return s
}
func makeRepository(session *gocql.Session) writer.MessageRepository {
if err := cassandra.Initialize(session); err != nil {
logger.Error("Failed to initialize message repository.", zap.Error(err))
}
return cassandra.NewMessageRepository(session)
}
func connectToNats(cfg *config) *nats.Conn {
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Error("Failed to connect to NATS.", zap.Error(err))
}
return nc
}
func forever() {
errs := make(chan error, 1)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
<-errs
}

View File

@ -6,7 +6,7 @@ import (
"net"
"github.com/dustin/go-coap"
"github.com/mainflux/mainflux/writer"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
"go.uber.org/zap"
)
@ -22,18 +22,17 @@ type Observer struct {
type CoAPAdapter struct {
obsMap map[string][]Observer
logger *zap.Logger
repo writer.MessageRepository
pub mainflux.MessagePublisher
}
// NewCoAPAdapter creates new CoAP adapter struct
func NewCoAPAdapter(logger *zap.Logger, repo writer.MessageRepository) *CoAPAdapter {
func NewCoAPAdapter(logger *zap.Logger, pub mainflux.MessagePublisher) *CoAPAdapter {
ca := &CoAPAdapter{
logger: logger,
repo: repo,
pub: pub,
obsMap: make(map[string][]Observer),
}
ca.obsMap = make(map[string][]Observer)
return ca
}
@ -48,7 +47,7 @@ func (ca *CoAPAdapter) BridgeHandler(nm *broker.Msg) {
log.Printf("Received a message: %s\n", string(nm.Data))
// And write it into the database
m := writer.RawMessage{}
m := mainflux.RawMessage{}
if len(nm.Data) > 0 {
if err := json.Unmarshal(nm.Data, &m); err != nil {
log.Println("Can not decode adapter msg")

View File

@ -7,7 +7,7 @@ import (
mux "github.com/dereulenspiegel/coap-mux"
coap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux/writer"
"github.com/mainflux/mainflux"
)
func (ca *CoAPAdapter) sendMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
@ -34,14 +34,14 @@ func (ca *CoAPAdapter) sendMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Messa
// Channel ID
cid := mux.Var(m, "channel_id")
n := writer.RawMessage{
n := mainflux.RawMessage{
Channel: cid,
Publisher: "",
Protocol: protocol,
Payload: m.Payload,
}
if err := ca.repo.Save(n); err != nil {
if err := ca.pub.Publish(n); err != nil {
if m.IsConfirmable() {
res.Code = coap.InternalServerError
}
@ -123,7 +123,7 @@ func (ca *CoAPAdapter) observeMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Me
return res
}
func (ca *CoAPAdapter) obsTransmit(n writer.RawMessage) {
func (ca *CoAPAdapter) obsTransmit(n mainflux.RawMessage) {
for _, v := range ca.obsMap[n.Channel] {
msg := *(v.message)
msg.Payload = n.Payload

View File

@ -4,31 +4,28 @@ package nats
import (
"encoding/json"
"github.com/mainflux/mainflux/writer"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
)
const topic string = "msg.coap"
const topic string = "src.coap"
var _ writer.MessageRepository = (*natsRepository)(nil)
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
type natsRepository struct {
type natsPublisher struct {
nc *broker.Conn
}
// NewMessageRepository instantiates NATS message repository. Note that the
// repository will not truly persist messages, but instead they will be
// published to the topic and made available for persisting by all interested
// parties, i.e. the message-writer service.
func NewMessageRepository(nc *broker.Conn) writer.MessageRepository {
return &natsRepository{nc}
// NewMessagePublisher instantiates NATS message publisher.
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
return &natsPublisher{nc}
}
func (repo *natsRepository) Save(msg writer.RawMessage) error {
b, err := json.Marshal(msg)
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
data, err := json.Marshal(msg)
if err != nil {
return err
}
return repo.nc.Publish(topic, b)
return pub.nc.Publish(topic, data)
}

3
doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package mainflux acts as an umbrella package containing multiple different
// microservices and defines all shared domain concepts.
package mainflux

3
docker/.dockerignore Normal file
View File

@ -0,0 +1,3 @@
docker-compose.yml
nginx.conf
ssl/

11
docker/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM golang:1.9-alpine AS builder
ARG SVC_NAME
RUN apk update \
&& apk add git \
&& go get github.com/mainflux/mainflux \
&& cd /go/src/github.com/mainflux/mainflux/cmd/$SVC_NAME \
&& CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o /exe
FROM scratch
COPY --from=builder /exe /
ENTRYPOINT ["/exe"]

78
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,78 @@
###
# Copyright (c) 2015-2017 Mainflux
#
# Mainflux is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
version: "3"
services:
nginx:
image: nginx:1.13-alpine
container_name: mainflux-nginx
network_mode: bridge
volumes:
- $PWD/nginx.conf:/etc/nginx/nginx.conf
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
ports:
- "80:80"
- "443:443"
- "8883:8883"
nats:
image: nats:1.0.2
container_name: mainflux-nats
network_mode: bridge
postgres:
image: postgres:10.2-alpine
container_name: mainflux-postgres
network_mode: bridge
environment:
POSTGRES_USER: mainflux
POSTGRES_PASSWORD: mainflux
POSTGRES_DB: mainflux
manager:
image: mainflux/manager:latest
container_name: mainflux-manager
network_mode: bridge
environment:
MF_DB_HOST: postgres
MF_MANAGER_DB: mainflux
MF_MANAGER_PORT: 8180
MF_MANAGER_SECRET: test-secret
normalizer:
image: mainflux/normalizer:latest
container_name: mainflux-normalizer
network_mode: bridge
environment:
MF_NATS_URL: "nats://nats:4222"
MF_NORMALIZER_PORT: 8181
http-adapter:
image: mainflux/http:latest
container_name: mainflux-http
network_mode: bridge
depends_on:
- manager
environment:
MF_MANAGER_URL: "http://manager:8180"
MF_NATS_URL: "nats://nats:4222"
MF_HTTP_ADAPTER_PORT: 8182
mqtt-adapter:
image: mainflux/mqtt-adapter:latest
container_name: mainflux-mqtt
network_mode: bridge
depends_on:
- manager
environment:
MQTT_ADAPTER_NATS_URL: "nats://nats:4222"
AUTH_URL: "http://manager"
AUTH_PORT: 8180

View File

@ -17,8 +17,8 @@ pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
worker_connections 768;
# multi_accept on;
}
###
@ -26,44 +26,44 @@ events {
###
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
upstream docker-manager {
server mainflux-manager:8180;
}
upstream docker-http {
server mainflux-http:7070;
server mainflux-http:8182;
}
##
# Virtual Host Configs
##
##
# Virtual Host Configs
##
# HTTP
server {
@ -124,8 +124,9 @@ http {
server_name localhost;
# Proxy pass to manager service
location /api/ {
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -140,8 +141,9 @@ http {
return 200;
}
}
# Proxy pass to mainflux-http-adapter
location /pub/ {
location ~ ^/channels/[a-zA-Z0-9-]+/messages$ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

View File

@ -1,7 +1,6 @@
# Mainflux HTTP adapter
# HTTP adapter
Mainflux HTTP adapter provides an HTTP API for sending messages through the
platform.
HTTP adapter provides an HTTP API for sending messages through the platform.
## Configuration
@ -9,9 +8,11 @@ The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|-----------------------|-------------------|-----------------------|
| HTTP_ADAPTER_NATS_URL | NATS instance URL | nats://localhost:4222 |
| Variable | Description | Default |
|----------------------|---------------------|-----------------------|
| MF_MANAGER_URL | Manager service URL | http://localhost:8180 |
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_HTTP_ADAPTER_PORT | Service HTTP port | 8180 |
## Deployment
@ -22,12 +23,14 @@ a compose file template that can be used to deploy the service container locally
version: "2"
services:
adapter:
image: mainflux/http-adapter:[version]
image: mainflux/http:[version]
container_name: [instance name]
ports:
- [host machine port]:8180
environment:
HTTP_ADAPTER_NATS_URL: [NATS instance URL]
MF_MANAGER_URL: [Manager service URL]
MF_NATS_URL: [NATS instance URL]
MF_HTTP_ADAPTER_PORT: [Service HTTP port]
```
To start the service outside of the container, execute the following shell script:
@ -42,7 +45,7 @@ cd $GOPATH/src/github.com/mainflux/mainflux/cmd/http
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
# set the environment variables and run the service
HTTP_ADAPTER_NATS_URL=[NATS instance URL] app
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] app
```
## Usage

View File

@ -1,18 +1,18 @@
package http
import "github.com/mainflux/mainflux/writer"
import "github.com/mainflux/mainflux"
var _ Service = (*adapterService)(nil)
var _ mainflux.MessagePublisher = (*adapterService)(nil)
type adapterService struct {
mr writer.MessageRepository
pub mainflux.MessagePublisher
}
// NewService instantiates the domain service implementation.
func NewService(mr writer.MessageRepository) Service {
return &adapterService{mr}
// New instantiates the domain service implementation.
func New(pub mainflux.MessagePublisher) mainflux.MessagePublisher {
return &adapterService{pub}
}
func (as *adapterService) Publish(msg writer.RawMessage) error {
return as.mr.Save(msg)
func (as *adapterService) Publish(msg mainflux.RawMessage) error {
return as.pub.Publish(msg)
}

View File

@ -4,13 +4,12 @@ import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/writer"
"github.com/mainflux/mainflux"
)
func sendMessageEndpoint(svc http.Service) endpoint.Endpoint {
func sendMessageEndpoint(svc mainflux.MessagePublisher) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
msg := request.(writer.RawMessage)
msg := request.(mainflux.RawMessage)
err := svc.Publish(msg)
return nil, err
}

View File

@ -4,29 +4,28 @@ import (
"time"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/writer"
"github.com/mainflux/mainflux"
)
var _ http.Service = (*loggingService)(nil)
var _ mainflux.MessagePublisher = (*loggingMiddleware)(nil)
type loggingService struct {
type loggingMiddleware struct {
logger log.Logger
http.Service
svc mainflux.MessagePublisher
}
// NewLoggingService adds logging facilities to the adapter.
func NewLoggingService(logger log.Logger, s http.Service) http.Service {
return &loggingService{logger, s}
// LoggingMiddleware adds logging facilities to the adapter.
func LoggingMiddleware(svc mainflux.MessagePublisher, logger log.Logger) mainflux.MessagePublisher {
return &loggingMiddleware{logger, svc}
}
func (ls *loggingService) Publish(msg writer.RawMessage) error {
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) error {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "publish",
"took", time.Since(begin),
)
}(time.Now())
return ls.Service.Publish(msg)
return lm.svc.Publish(msg)
}

View File

@ -4,32 +4,31 @@ import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/writer"
"github.com/mainflux/mainflux"
)
var _ http.Service = (*metricService)(nil)
var _ mainflux.MessagePublisher = (*metricsMiddleware)(nil)
type metricService struct {
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
http.Service
svc mainflux.MessagePublisher
}
// NewMetricService instruments adapter by tracking request count and latency.
func NewMetricService(counter metrics.Counter, latency metrics.Histogram, s http.Service) http.Service {
return &metricService{
// MetricsMiddleware instruments adapter by tracking request count and latency.
func MetricsMiddleware(svc mainflux.MessagePublisher, counter metrics.Counter, latency metrics.Histogram) mainflux.MessagePublisher {
return &metricsMiddleware{
counter: counter,
latency: latency,
Service: s,
svc: svc,
}
}
func (ms *metricService) Publish(msg writer.RawMessage) error {
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
defer func(begin time.Time) {
ms.counter.With("method", "publish").Add(1)
ms.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
mm.counter.With("method", "publish").Add(1)
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.Publish(msg)
return mm.svc.Publish(msg)
}

View File

@ -11,16 +11,11 @@ import (
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
adapter "github.com/mainflux/mainflux/http"
manager "github.com/mainflux/mainflux/manager/client"
"github.com/mainflux/mainflux/writer"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
protocol string = "http"
ctJson string = "application/senml+json"
)
const protocol string = "http"
var (
errMalformedData error = errors.New("malformed SenML data")
@ -30,7 +25,7 @@ var (
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc adapter.Service, mc manager.ManagerClient) http.Handler {
func MakeHandler(svc mainflux.MessagePublisher, mc manager.ManagerClient) http.Handler {
auth = mc
opts := []kithttp.ServerOption{
@ -53,11 +48,6 @@ func MakeHandler(svc adapter.Service, mc manager.ManagerClient) http.Handler {
}
func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
ct, err := checkContentType(r)
if err != nil {
return nil, err
}
publisher, err := authorize(r)
if err != nil {
return nil, err
@ -68,13 +58,11 @@ func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
return nil, err
}
channel := bone.GetValue(r, "id")
msg := writer.RawMessage{
msg := mainflux.RawMessage{
Publisher: publisher,
Protocol: protocol,
ContentType: ct,
Channel: channel,
ContentType: r.Header.Get("Content-Type"),
Channel: bone.GetValue(r, "id"),
Payload: payload,
}
@ -88,7 +76,7 @@ func authorize(r *http.Request) (string, error) {
return "", errUnauthorizedAccess
}
// Path is `/channels/:id/messages`, we need chanID.
// extract ID from /channels/:id/messages
c := strings.Split(r.URL.Path, "/")[2]
id, err := auth.CanAccess(c, apiKey)
@ -99,16 +87,6 @@ func authorize(r *http.Request) (string, error) {
return id, nil
}
func checkContentType(r *http.Request) (string, error) {
ct := r.Header.Get("Content-Type")
if ct != ctJson {
return "", errUnknownType
}
return ct, nil
}
func decodePayload(body io.ReadCloser) ([]byte, error) {
payload, err := ioutil.ReadAll(body)
if err != nil {

View File

@ -1,34 +0,0 @@
// Package nats contains NATS-specific message repository implementation.
package nats
import (
"encoding/json"
"github.com/mainflux/mainflux/writer"
broker "github.com/nats-io/go-nats"
)
const topic string = "msg.http"
var _ writer.MessageRepository = (*natsRepository)(nil)
type natsRepository struct {
nc *broker.Conn
}
// NewMessageRepository instantiates NATS message repository. Note that the
// repository will not truly persist messages, but instead they will be
// published to the topic and made available for persisting by all interested
// parties, i.e. the message-writer service.
func NewMessageRepository(nc *broker.Conn) writer.MessageRepository {
return &natsRepository{nc}
}
func (repo *natsRepository) Save(msg writer.RawMessage) error {
b, err := json.Marshal(msg)
if err != nil {
return err
}
return repo.nc.Publish(topic, b)
}

31
http/nats/publisher.go Normal file
View File

@ -0,0 +1,31 @@
// Package nats contains NATS message publisher implementation.
package nats
import (
"encoding/json"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
)
const topic string = "src.http"
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
type natsPublisher struct {
nc *broker.Conn
}
// NewMessagePublisher instantiates NATS message publisher.
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
return &natsPublisher{nc}
}
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
data, err := json.Marshal(msg)
if err != nil {
return err
}
return pub.nc.Publish(topic, data)
}

View File

@ -1,11 +0,0 @@
package http
import "github.com/mainflux/mainflux/writer"
// Service specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// Publish accepts the raw SenML message and publishes it to the event bus
// for post processing.
Publish(writer.RawMessage) error
}

View File

@ -14,6 +14,7 @@ paths:
- messages
consumes:
- "application/senml+json"
- "text/plain"
produces: []
parameters:
- name: Authorization
@ -28,11 +29,15 @@ paths:
format: uuid
required: true
- name: message
description: Message to be sent.
description: |
Message to be distributed. Since the platform expects messages to be
properly formatted SenML in order to be post-processed, clients are
obliged to specify Content-Type header for each published message.
Note that all messages that aren't SenML will be accepted and published,
but no post-processing will be applied.
in: body
required: true
schema:
$ref: '#/definitions/Message'
type: string
responses:
202:
description: Message is accepted for processing.
@ -41,66 +46,6 @@ paths:
403:
description: Message discarded due to missing or invalid credentials.
415:
description: Message discarded due to invalid content type.
description: Message discarded due to invalid or missing content type.
500:
description: Unexpected server-side error occured.
definitions:
Message:
type: array
minItems: 1
uniqueItems: true
items:
type: object
properties:
bn:
type: string
description: Base name
bt:
type: number
format: double
description: Base time
bu:
type: string
description: Base unit
bv:
type: number
format: double
description: Base value
bver:
type: integer
description: Base version
n:
type: string
description: Name
u:
type: string
description: Unit
v:
type: number
format: double
description: Value
vs:
type: string
description: String value
vb:
type: boolean
description: Boolean value
vd:
type: string
description: Data value
s:
type: number
format: double
description: Value sum
t:
type: number
format: double
description: Time
ut:
type: number
format: double
description: Update time
l:
type: string
description: Link

View File

@ -1,8 +1,8 @@
# Mainflux manager
# Manager
Mainflux manager provides an HTTP API for managing platform resources: users,
devices, applications and channels. Through this API clients are able to do
the following actions:
Manager provides an HTTP API for managing platform resources: users, devices,
applications and channels. Through this API clients are able to do the following
actions:
- register new accounts and obtain access tokens
- provision new clients (i.e. devices & applications)
@ -18,21 +18,18 @@ The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|---------------------|------------------------------------------|-----------|
| MANAGER_DB_CLUSTER | comma-separated Cassandra contact points | 127.0.0.1 |
| MANAGER_DB_KEYSPACE | name of the Cassandra keyspace | manager |
| MANAGER_SECRET | string used for signing tokens | manager |
| Variable | Description | Default |
|-------------------|------------------------------------------|-----------|
| MF_DB_HOST | Database host address | localhost |
| MF_DB_PORT | Database host port | 5432 |
| MF_DB_USER | Database user | mainflux |
| MF_DB_PASSWORD | Database password | mainflux |
| MF_MANAGER_DB | Name of the database used by the service | manager |
| MF_MANAGER_PORT | Manager service HTTP port | 8180 |
| MF_MANAGER_SECRET | string used for signing tokens | manager |
## Deployment
Before proceeding to deployment, make sure to check out the [Apache Cassandra 3.0.x
documentation][www:cassandra]. Developers are advised to get acquainted with
basic architectural concepts, data modeling techniques and deployment strategies.
> Prior to deploying the service, make sure to set up the database and create
the keyspace that will be used by the service.
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
@ -44,11 +41,15 @@ services:
image: mainflux/manager:[version]
container_name: [instance name]
ports:
- [host machine port]:8180
- [host machine port]:[configured HTTP port]
environment:
MANAGER_DB_CLUSTER: [comma-separated Cassandra endpoints]
MANAGER_DB_KEYSPACE: [name of Cassandra keyspace]
MANAGER_SECRET: [string used for signing tokens]
MF_DB_HOST: [Database host address]
MF_DB_PORT: [Database host port]
MF_DB_USER: [Database user]
MF_DB_PASS: [Database password]
MF_MANAGER_DB: [Name of the database used by the service]
MF_MANAGER_PORT: [Service HTTP port]
MF_MANAGER_SECRET: [String used for signing tokens]
```
To start the service outside of the container, execute the following shell script:
@ -63,7 +64,7 @@ cd $GOPATH/src/github.com/mainflux/mainflux/cmd/manager
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
# set the environment variables and run the service
MANAGER_DB_CLUSTER=[comma-separated Cassandra endpoints] MANAGER_DB_KEYSPACE=[name of Cassandra keyspace] MANAGER_SECRET=[string used for signing tokens] app
MF_DB_HOST=[Database host address] MF_DB_PORT=[Database host port] MF_DB_USER=[Database user] MF_DB_PASS=[Database password] MF_MANAGER_DB=[Name of the database used by the service] MF_MANAGER_PORT=[Service HTTP port] MF_MANAGER_SECRET=[String used for signing tokens] app
```
## Usage
@ -72,4 +73,3 @@ For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
[www:cassandra]: http://docs.datastax.com

View File

@ -200,22 +200,51 @@ func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
err := req.validate()
if err == manager.ErrNotFound {
return removeRes{}, nil
}
if err != nil {
if err := req.validate(); err != nil {
if err == manager.ErrNotFound {
return removeRes{}, nil
}
return nil, err
}
if err = svc.RemoveChannel(req.key, req.id); err != nil {
if err := svc.RemoveChannel(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func connectEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
if err := svc.Connect(cr.key, cr.chanId, cr.clientId); err != nil {
return nil, err
}
return connectionRes{}, nil
}
}
func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
if err := svc.Disconnect(cr.key, cr.chanId, cr.clientId); err != nil {
return nil, err
}
return disconnectionRes{}, nil
}
}
func identityEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {

View File

@ -7,21 +7,21 @@ import (
"github.com/mainflux/mainflux/manager"
)
var _ manager.Service = (*loggingService)(nil)
var _ manager.Service = (*loggingMiddleware)(nil)
type loggingService struct {
type loggingMiddleware struct {
logger log.Logger
manager.Service
svc manager.Service
}
// NewLoggingService adds logging facilities to the core service.
func NewLoggingService(logger log.Logger, s manager.Service) manager.Service {
return &loggingService{logger, s}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
return &loggingMiddleware{logger, svc}
}
func (ls *loggingService) Register(user manager.User) (err error) {
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "register",
"email", user.Email,
"error", err,
@ -29,12 +29,12 @@ func (ls *loggingService) Register(user manager.User) (err error) {
)
}(time.Now())
return ls.Service.Register(user)
return lm.svc.Register(user)
}
func (ls *loggingService) Login(user manager.User) (token string, err error) {
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "login",
"email", user.Email,
"error", err,
@ -42,12 +42,12 @@ func (ls *loggingService) Login(user manager.User) (token string, err error) {
)
}(time.Now())
return ls.Service.Login(user)
return lm.svc.Login(user)
}
func (ls *loggingService) AddClient(key string, client manager.Client) (id string, err error) {
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "add_client",
"key", key,
"id", id,
@ -56,12 +56,12 @@ func (ls *loggingService) AddClient(key string, client manager.Client) (id strin
)
}(time.Now())
return ls.Service.AddClient(key, client)
return lm.svc.AddClient(key, client)
}
func (ls *loggingService) UpdateClient(key string, client manager.Client) (err error) {
func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "update_client",
"key", key,
"id", client.ID,
@ -70,12 +70,12 @@ func (ls *loggingService) UpdateClient(key string, client manager.Client) (err e
)
}(time.Now())
return ls.Service.UpdateClient(key, client)
return lm.svc.UpdateClient(key, client)
}
func (ls *loggingService) ViewClient(key string, id string) (client manager.Client, err error) {
func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.Client, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "view_client",
"key", key,
"id", id,
@ -84,12 +84,12 @@ func (ls *loggingService) ViewClient(key string, id string) (client manager.Clie
)
}(time.Now())
return ls.Service.ViewClient(key, id)
return lm.svc.ViewClient(key, id)
}
func (ls *loggingService) ListClients(key string) (clients []manager.Client, err error) {
func (lm *loggingMiddleware) ListClients(key string) (clients []manager.Client, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "list_clients",
"key", key,
"error", err,
@ -97,12 +97,12 @@ func (ls *loggingService) ListClients(key string) (clients []manager.Client, err
)
}(time.Now())
return ls.Service.ListClients(key)
return lm.svc.ListClients(key)
}
func (ls *loggingService) RemoveClient(key string, id string) (err error) {
func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "remove_client",
"key", key,
"id", id,
@ -111,12 +111,12 @@ func (ls *loggingService) RemoveClient(key string, id string) (err error) {
)
}(time.Now())
return ls.Service.RemoveClient(key, id)
return lm.svc.RemoveClient(key, id)
}
func (ls *loggingService) CreateChannel(key string, channel manager.Channel) (id string, err error) {
func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel) (id string, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "create_channel",
"key", key,
"id", id,
@ -125,12 +125,12 @@ func (ls *loggingService) CreateChannel(key string, channel manager.Channel) (id
)
}(time.Now())
return ls.Service.CreateChannel(key, channel)
return lm.svc.CreateChannel(key, channel)
}
func (ls *loggingService) UpdateChannel(key string, channel manager.Channel) (err error) {
func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel) (err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "update_channel",
"key", key,
"id", channel.ID,
@ -139,12 +139,12 @@ func (ls *loggingService) UpdateChannel(key string, channel manager.Channel) (er
)
}(time.Now())
return ls.Service.UpdateChannel(key, channel)
return lm.svc.UpdateChannel(key, channel)
}
func (ls *loggingService) ViewChannel(key string, id string) (channel manager.Channel, err error) {
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager.Channel, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "view_channel",
"key", key,
"id", id,
@ -153,12 +153,12 @@ func (ls *loggingService) ViewChannel(key string, id string) (channel manager.Ch
)
}(time.Now())
return ls.Service.ViewChannel(key, id)
return lm.svc.ViewChannel(key, id)
}
func (ls *loggingService) ListChannels(key string) (channels []manager.Channel, err error) {
func (lm *loggingMiddleware) ListChannels(key string) (channels []manager.Channel, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "list_channels",
"key", key,
"error", err,
@ -166,12 +166,12 @@ func (ls *loggingService) ListChannels(key string) (channels []manager.Channel,
)
}(time.Now())
return ls.Service.ListChannels(key)
return lm.svc.ListChannels(key)
}
func (ls *loggingService) RemoveChannel(key string, id string) (err error) {
func (lm *loggingMiddleware) RemoveChannel(key string, id string) (err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "remove_channel",
"key", key,
"id", id,
@ -180,12 +180,42 @@ func (ls *loggingService) RemoveChannel(key string, id string) (err error) {
)
}(time.Now())
return ls.Service.RemoveChannel(key, id)
return lm.svc.RemoveChannel(key, id)
}
func (ls *loggingService) Identity(key string) (id string, err error) {
func (lm *loggingMiddleware) Connect(key, chanId, clientId string) (err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "connect",
"key", key,
"channel", chanId,
"client", clientId,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Connect(key, chanId, clientId)
}
func (lm *loggingMiddleware) Disconnect(key, chanId, clientId string) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "disconnect",
"key", key,
"channel", chanId,
"client", clientId,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Disconnect(key, chanId, clientId)
}
func (lm *loggingMiddleware) Identity(key string) (id string, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "identity",
"id", id,
"error", err,
@ -193,12 +223,12 @@ func (ls *loggingService) Identity(key string) (id string, err error) {
)
}(time.Now())
return ls.Service.Identity(key)
return lm.svc.Identity(key)
}
func (ls *loggingService) CanAccess(key string, id string) (pub string, err error) {
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
defer func(begin time.Time) {
ls.logger.Log(
lm.logger.Log(
"method", "can_access",
"key", key,
"id", id,
@ -208,5 +238,5 @@ func (ls *loggingService) CanAccess(key string, id string) (pub string, err erro
)
}(time.Now())
return ls.Service.CanAccess(key, id)
return lm.svc.CanAccess(key, id)
}

View File

@ -7,146 +7,164 @@ import (
"github.com/mainflux/mainflux/manager"
)
var _ manager.Service = (*metricService)(nil)
var _ manager.Service = (*metricsMiddleware)(nil)
type metricService struct {
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
manager.Service
svc manager.Service
}
// NewMetricService instruments core service by tracking request count and
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func NewMetricService(counter metrics.Counter, latency metrics.Histogram, s manager.Service) manager.Service {
return &metricService{
func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency metrics.Histogram) manager.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
Service: s,
svc: svc,
}
}
func (ms *metricService) Register(user manager.User) error {
func (ms *metricsMiddleware) Register(user manager.User) error {
defer func(begin time.Time) {
ms.counter.With("method", "register").Add(1)
ms.latency.With("method", "register").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.Register(user)
return ms.svc.Register(user)
}
func (ms *metricService) Login(user manager.User) (string, error) {
func (ms *metricsMiddleware) Login(user manager.User) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "login").Add(1)
ms.latency.With("method", "login").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.Login(user)
return ms.svc.Login(user)
}
func (ms *metricService) AddClient(key string, client manager.Client) (string, error) {
func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "add_client").Add(1)
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.AddClient(key, client)
return ms.svc.AddClient(key, client)
}
func (ms *metricService) UpdateClient(key string, client manager.Client) error {
func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_client").Add(1)
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.UpdateClient(key, client)
return ms.svc.UpdateClient(key, client)
}
func (ms *metricService) ViewClient(key string, id string) (manager.Client, error) {
func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_client").Add(1)
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.ViewClient(key, id)
return ms.svc.ViewClient(key, id)
}
func (ms *metricService) ListClients(key string) ([]manager.Client, error) {
func (ms *metricsMiddleware) ListClients(key string) ([]manager.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_clients").Add(1)
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.ListClients(key)
return ms.svc.ListClients(key)
}
func (ms *metricService) RemoveClient(key string, id string) error {
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_client").Add(1)
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.RemoveClient(key, id)
return ms.svc.RemoveClient(key, id)
}
func (ms *metricService) CreateChannel(key string, channel manager.Channel) (string, error) {
func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "create_channel").Add(1)
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.CreateChannel(key, channel)
return ms.svc.CreateChannel(key, channel)
}
func (ms *metricService) UpdateChannel(key string, channel manager.Channel) error {
func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_channel").Add(1)
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.UpdateChannel(key, channel)
return ms.svc.UpdateChannel(key, channel)
}
func (ms *metricService) ViewChannel(key string, id string) (manager.Channel, error) {
func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_channel").Add(1)
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.ViewChannel(key, id)
return ms.svc.ViewChannel(key, id)
}
func (ms *metricService) ListChannels(key string) ([]manager.Channel, error) {
func (ms *metricsMiddleware) ListChannels(key string) ([]manager.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_channels").Add(1)
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.ListChannels(key)
return ms.svc.ListChannels(key)
}
func (ms *metricService) RemoveChannel(key string, id string) error {
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_channel").Add(1)
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.RemoveChannel(key, id)
return ms.svc.RemoveChannel(key, id)
}
func (ms *metricService) Identity(key string) (string, error) {
func (ms *metricsMiddleware) Connect(key, chanId, clientId string) error {
defer func(begin time.Time) {
ms.counter.With("method", "connect").Add(1)
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Connect(key, chanId, clientId)
}
func (ms *metricsMiddleware) Disconnect(key, chanId, clientId string) error {
defer func(begin time.Time) {
ms.counter.With("method", "disconnect").Add(1)
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Disconnect(key, chanId, clientId)
}
func (ms *metricsMiddleware) Identity(key string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "identity").Add(1)
ms.latency.With("method", "identity").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.Identity(key)
return ms.svc.Identity(key)
}
func (ms *metricService) CanAccess(key string, id string) (string, error) {
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "can_access").Add(1)
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.Service.CanAccess(key, id)
return ms.svc.CanAccess(key, id)
}

View File

@ -125,3 +125,21 @@ func (req listResourcesReq) validate() error {
return manager.ErrMalformedEntity
}
type connectionReq struct {
key string
chanId string
clientId string
}
func (req connectionReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.chanId) && !govalidator.IsUUID(req.clientId) {
return manager.ErrNotFound
}
return nil
}

View File

@ -4,88 +4,93 @@ import (
"fmt"
"testing"
"github.com/gocql/gocql"
"github.com/mainflux/mainflux/manager"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
)
const wrong string = "?"
var (
client manager.Client = manager.Client{Type: "app"}
channel manager.Channel = manager.Channel{}
)
func TestUserReqValidation(t *testing.T) {
cases := []struct {
cases := map[string]struct {
user manager.User
err error
}{
{manager.User{"foo@example.com", "pass"}, nil},
{manager.User{"invalid", "pass"}, manager.ErrMalformedEntity},
{manager.User{"", "pass"}, manager.ErrMalformedEntity},
{manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
"valid user request": {manager.User{"foo@example.com", "pass"}, nil},
"malformed e-mail": {manager.User{wrong, "pass"}, manager.ErrMalformedEntity},
"empty e-mail": {manager.User{"", "pass"}, manager.ErrMalformedEntity},
"empty password": {manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
}
for i, tc := range cases {
for desc, tc := range cases {
req := userReq{tc.user}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestIdentityReqValidation(t *testing.T) {
cases := []struct {
cases := map[string]struct {
key string
err error
}{
{"valid", nil},
{"", manager.ErrUnauthorizedAccess},
"non-empty token": {uuid.NewV4().String(), nil},
"empty token": {"", manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
for desc, tc := range cases {
req := identityReq{tc.key}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClientReqValidation(t *testing.T) {
key := "key"
vc := manager.Client{Type: "app"}
key := uuid.NewV4().String()
cases := []struct {
key string
cases := map[string]struct {
client manager.Client
key string
err error
}{
{key, vc, nil},
{"", vc, manager.ErrUnauthorizedAccess},
{key, manager.Client{Type: "invalid"}, manager.ErrMalformedEntity},
"valid client addition request": {client, key, nil},
"missing token": {client, "", manager.ErrUnauthorizedAccess},
"wrong client type": {manager.Client{Type: wrong}, key, manager.ErrMalformedEntity},
}
for i, tc := range cases {
for desc, tc := range cases {
req := addClientReq{
key: tc.key,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClientReqValidation(t *testing.T) {
key := "key"
uuid := gocql.TimeUUID().String()
vc := manager.Client{Type: "app"}
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := []struct {
key string
id string
cases := map[string]struct {
client manager.Client
id string
key string
err error
}{
{key, uuid, vc, nil},
{key, "non-uuid", vc, manager.ErrNotFound},
{"", uuid, vc, manager.ErrUnauthorizedAccess},
{key, uuid, manager.Client{Type: "invalid"}, manager.ErrMalformedEntity},
"valid client update request": {client, id, key, nil},
"non-uuid client ID": {client, wrong, key, manager.ErrNotFound},
"missing token": {client, id, "", manager.ErrUnauthorizedAccess},
"wrong client type": {manager.Client{Type: "invalid"}, id, key, manager.ErrMalformedEntity},
}
for i, tc := range cases {
for desc, tc := range cases {
req := updateClientReq{
key: tc.key,
id: tc.id,
@ -93,51 +98,49 @@ func TestUpdateClientReqValidation(t *testing.T) {
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannelReqValidation(t *testing.T) {
key := "key"
vc := manager.Channel{}
key := uuid.NewV4().String()
cases := []struct {
key string
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
{key, vc, nil},
{"", vc, manager.ErrUnauthorizedAccess},
"valid channel creation request": {channel, key, nil},
"missing token": {channel, "", manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
for desc, tc := range cases {
req := createChannelReq{
key: tc.key,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannelReqValidation(t *testing.T) {
key := "key"
uuid := gocql.TimeUUID().String()
vc := manager.Channel{}
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := []struct {
key string
id string
cases := map[string]struct {
channel manager.Channel
id string
key string
err error
}{
{key, uuid, vc, nil},
{key, "non-uuid", vc, manager.ErrNotFound},
{"", uuid, vc, manager.ErrUnauthorizedAccess},
"valid channel update request": {channel, id, key, nil},
"non-uuid channel ID": {channel, wrong, key, manager.ErrNotFound},
"missing token": {channel, id, "", manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
for desc, tc := range cases {
req := updateChannelReq{
key: tc.key,
id: tc.id,
@ -145,46 +148,49 @@ func TestUpdateChannelReqValidation(t *testing.T) {
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewResourceReqValidation(t *testing.T) {
key := "key"
uuid := gocql.TimeUUID().String()
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := []struct {
key string
cases := map[string]struct {
id string
key string
err error
}{
{key, uuid, nil},
{"", uuid, manager.ErrUnauthorizedAccess},
{key, "non-uuid", manager.ErrNotFound},
"valid resource viewing request": {id, key, nil},
"missing token": {id, "", manager.ErrUnauthorizedAccess},
"non-uuid resource ID": {wrong, key, manager.ErrNotFound},
}
for i, tc := range cases {
for desc, tc := range cases {
req := viewResourceReq{tc.key, tc.id}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListResourcesReqValidation(t *testing.T) {
cases := []struct {
key := uuid.NewV4().String()
value := 10
cases := map[string]struct {
key string
size int
offset int
err error
}{
{"key", 10, 10, nil},
{"", 10, 10, manager.ErrUnauthorizedAccess},
{"key", 10, -10, manager.ErrMalformedEntity},
{"key", 0, 10, manager.ErrMalformedEntity},
{"key", -10, 10, manager.ErrMalformedEntity},
"valid listing request": {key, value, value, nil},
"missing token": {"", value, value, manager.ErrUnauthorizedAccess},
"negative offset": {key, value, -value, manager.ErrMalformedEntity},
"zero size": {key, 0, value, manager.ErrMalformedEntity},
"negative size": {key, -value, value, manager.ErrMalformedEntity},
}
for i, tc := range cases {
for desc, tc := range cases {
req := listResourcesReq{
key: tc.key,
size: tc.size,
@ -192,6 +198,6 @@ func TestListResourcesReqValidation(t *testing.T) {
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}

View File

@ -186,3 +186,31 @@ func (res listChannelsRes) headers() map[string]string {
func (res listChannelsRes) empty() bool {
return false
}
type connectionRes struct{}
func (res connectionRes) code() int {
return http.StatusOK
}
func (res connectionRes) headers() map[string]string {
return map[string]string{}
}
func (res connectionRes) empty() bool {
return true
}
type disconnectionRes struct{}
func (res disconnectionRes) code() int {
return http.StatusNoContent
}
func (res disconnectionRes) headers() map[string]string {
return map[string]string{}
}
func (res disconnectionRes) empty() bool {
return true
}

View File

@ -104,6 +104,20 @@ func MakeHandler(svc manager.Service) http.Handler {
opts...,
))
r.Put("/channels/:chanId/clients/:clientId", kithttp.NewServer(
connectEndpoint(svc),
decodeConnection,
encodeResponse,
opts...,
))
r.Delete("/channels/:chanId/clients/:clientId", kithttp.NewServer(
disconnectEndpoint(svc),
decodeConnection,
encodeResponse,
opts...,
))
r.Get("/access-grant", kithttp.NewServer(
identityEndpoint(svc),
decodeIdentity,
@ -218,6 +232,16 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
return req, nil
}
func decodeConnection(_ context.Context, r *http.Request) (interface{}, error) {
req := connectionReq{
key: r.Header.Get("Authorization"),
chanId: bone.GetValue(r, "chanId"),
clientId: bone.GetValue(r, "clientId"),
}
return req, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", contentType)

View File

@ -11,8 +11,8 @@ var _ manager.Hasher = (*bcryptHasher)(nil)
type bcryptHasher struct{}
// NewHasher instantiates a bcrypt-based hasher implementation.
func NewHasher() manager.Hasher {
// New instantiates a bcrypt-based hasher implementation.
func New() manager.Hasher {
return &bcryptHasher{}
}

View File

@ -1,115 +0,0 @@
package cassandra
import (
"github.com/gocql/gocql"
"github.com/mainflux/mainflux/manager"
)
var _ manager.ChannelRepository = (*channelRepository)(nil)
type channelRepository struct {
session *gocql.Session
}
// NewChannelRepository instantiates Cassandra channel repository.
func NewChannelRepository(session *gocql.Session) manager.ChannelRepository {
return &channelRepository{session}
}
func (repo *channelRepository) Save(channel manager.Channel) (string, error) {
cql := `INSERT INTO channels_by_user (user, id, name, connected)
VALUES (?, ?, ?, ?)`
id := gocql.TimeUUID()
if err := repo.session.Query(cql, channel.Owner, id,
channel.Name, channel.Connected).Exec(); err != nil {
return "", err
}
return id.String(), nil
}
func (repo *channelRepository) Update(channel manager.Channel) error {
cql := `UPDATE channels_by_user SET name = ?, connected = ?
WHERE user = ? AND id = ? IF EXISTS`
if applied, _ := repo.session.Query(cql, channel.Name, channel.Connected,
channel.Owner, channel.ID).ScanCAS(); !applied {
return manager.ErrNotFound
}
return nil
}
func (repo *channelRepository) One(owner, id string) (manager.Channel, error) {
cql := `SELECT name, connected FROM channels_by_user
WHERE user = ? AND id = ? LIMIT 1`
ch := manager.Channel{
Owner: owner,
ID: id,
}
if err := repo.session.Query(cql, owner, id).Scan(&ch.Name, &ch.Connected); err != nil {
return ch, manager.ErrNotFound
}
return ch, nil
}
func (repo *channelRepository) All(owner string) []manager.Channel {
cql := `SELECT id, name, connected FROM channels_by_user WHERE user = ?`
var id string
var name string
var connected []string
// NOTE: the closing might failed
iter := repo.session.Query(cql, owner).Iter()
defer iter.Close()
channels := make([]manager.Channel, 0)
for iter.Scan(&id, &name, &connected) {
c := manager.Channel{
Owner: owner,
ID: id,
Name: name,
Connected: replaceNilWithEmpty(connected),
}
channels = append(channels, c)
}
return channels
}
func replaceNilWithEmpty(items []string) []string {
if items != nil {
return items
}
return make([]string, 0)
}
func (repo *channelRepository) Remove(owner, id string) error {
cql := `DELETE FROM channels_by_user WHERE user = ? AND id = ?`
return repo.session.Query(cql, owner, id).Exec()
}
func (repo *channelRepository) HasClient(channel, client string) bool {
cql := `SELECT connected FROM clients_by_channel WHERE id = ? LIMIT 1`
var connected []string
if err := repo.session.Query(cql, channel).Scan(&connected); err != nil {
return false
}
for _, v := range connected {
if v == client {
return true
}
}
return false
}

View File

@ -1,99 +0,0 @@
package cassandra
import (
"github.com/gocql/gocql"
"github.com/mainflux/mainflux/manager"
)
var _ manager.ClientRepository = (*clientRepository)(nil)
type clientRepository struct {
session *gocql.Session
}
// NewClientRepository instantiates Cassandra client repository.
func NewClientRepository(session *gocql.Session) manager.ClientRepository {
return &clientRepository{session}
}
func (repo *clientRepository) Id() string {
return gocql.TimeUUID().String()
}
func (repo *clientRepository) Save(client manager.Client) error {
cql := `INSERT INTO clients_by_user (user, id, type, name, access_key, meta)
VALUES (?, ?, ?, ?, ?, ?)`
if err := repo.session.Query(cql, client.Owner, client.ID,
client.Type, client.Name, client.Key, client.Meta).Exec(); err != nil {
return err
}
return nil
}
func (repo *clientRepository) Update(client manager.Client) error {
cql := `UPDATE clients_by_user SET type = ?, name = ?, meta = ?
WHERE user = ? AND id = ? IF EXISTS`
applied, err := repo.session.Query(cql, client.Type, client.Name, client.Meta,
client.Owner, client.ID).ScanCAS()
if !applied {
return manager.ErrNotFound
}
return err
}
func (repo *clientRepository) One(owner string, id string) (manager.Client, error) {
cql := `SELECT type, name, access_key, meta FROM clients_by_user
WHERE user = ? AND id = ? LIMIT 1`
cli := manager.Client{
Owner: owner,
ID: id,
}
if err := repo.session.Query(cql, owner, id).
Scan(&cli.Type, &cli.Name, &cli.Key, &cli.Meta); err != nil {
return cli, manager.ErrNotFound
}
return cli, nil
}
func (repo *clientRepository) All(owner string) []manager.Client {
cql := `SELECT id, type, name, access_key, meta FROM clients_by_user WHERE user = ?`
var id string
var cType string
var name string
var key string
var meta map[string]string
// NOTE: the closing might failed
iter := repo.session.Query(cql, owner).Iter()
defer iter.Close()
clients := make([]manager.Client, 0)
for iter.Scan(&id, &cType, &name, &key, &meta) {
c := manager.Client{
Owner: owner,
ID: id,
Type: cType,
Name: name,
Key: key,
Meta: meta,
}
clients = append(clients, c)
}
return clients
}
func (repo *clientRepository) Remove(owner string, id string) error {
cql := `DELETE FROM clients_by_user WHERE user = ? AND id = ?`
return repo.session.Query(cql, owner, id).Exec()
}

View File

@ -1,2 +0,0 @@
// Package cassandra contains Cassandra-specific repository implementations.
package cassandra

View File

@ -1,52 +0,0 @@
package cassandra
import "github.com/gocql/gocql"
var tables []string = []string{
`CREATE TABLE IF NOT EXISTS users (
email text,
password text,
PRIMARY KEY (email)
)`,
`CREATE TABLE IF NOT EXISTS clients_by_user (
user text,
id timeuuid,
type text,
name text,
access_key text,
meta map<text, text>,
PRIMARY KEY ((user), id)
)`,
`CREATE TABLE IF NOT EXISTS channels_by_user (
user text,
id timeuuid,
name text,
connected set<text>,
PRIMARY KEY ((user), id)
)`,
`CREATE MATERIALIZED VIEW IF NOT EXISTS clients_by_channel
AS SELECT user, id, connected FROM channels_by_user
WHERE id IS NOT NULL
PRIMARY KEY (id, user)
`,
}
// Connect establishes connection to the Cassandra cluster.
func Connect(hosts []string, keyspace string) (*gocql.Session, error) {
cluster := gocql.NewCluster(hosts...)
cluster.Keyspace = keyspace
cluster.Consistency = gocql.Quorum
return cluster.CreateSession()
}
// Initialize creates tables used by the service.
func Initialize(session *gocql.Session) error {
for _, table := range tables {
if err := session.Query(table).Exec(); err != nil {
return err
}
}
return nil
}

View File

@ -1,41 +0,0 @@
package cassandra
import (
"github.com/gocql/gocql"
"github.com/mainflux/mainflux/manager"
)
var _ manager.UserRepository = (*userRepository)(nil)
type userRepository struct {
session *gocql.Session
}
// NewUserRepository instantiates Cassandra user repository.
func NewUserRepository(session *gocql.Session) manager.UserRepository {
return &userRepository{session}
}
func (repo *userRepository) Save(user manager.User) error {
cql := `INSERT INTO users (email, password) VALUES (?, ?) IF NOT EXISTS`
applied, err := repo.session.Query(cql, user.Email, user.Password).ScanCAS()
if !applied {
return manager.ErrConflict
}
return err
}
func (repo *userRepository) One(email string) (manager.User, error) {
cql := `SELECT email, password FROM users WHERE email = ? LIMIT 1`
user := manager.User{}
if err := repo.session.Query(cql, email).
Scan(&user.Email, &user.Password); err != nil {
return user, manager.ErrUnauthorizedAccess
}
return user, nil
}

View File

@ -3,10 +3,10 @@ package manager
// Channel represents a Mainflux "communication group". This group contains the
// clients that can exchange messages between eachother.
type Channel struct {
Owner string `json:"-"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
Connected []string `json:"connected"`
ID string `gorm:"type:char(36);primary_key" json:"id"`
Owner string `gorm:"type:varchar(254);not null" json:"-"`
Name string `json:"name,omitempty"`
Clients []Client `gorm:"many2many:channel_clients" json:"connected,omitempty"`
}
// ChannelRepository specifies a channel persistence API.
@ -31,6 +31,13 @@ type ChannelRepository interface {
// by the specified user.
Remove(string, string) error
// Connect adds client to the channel's list of connected clients.
Connect(string, string, string) error
// Disconnect removes client from the channel's list of connected
// clients.
Disconnect(string, string, string) error
// HasClient determines whether the client with the provided identifier, is
// "connected" to the specified channel.
HasClient(string, string) bool

View File

@ -5,12 +5,12 @@ import "strings"
// Client represents a Mainflux client. Each client is owned by one user, and
// it is assigned with the unique identifier and (temporary) access key.
type Client struct {
Owner string `json:"-"`
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name,omitempty"`
Key string `json:"key"`
Meta map[string]string `json:"meta,omitempty"`
ID string `gorm:"type:char(36);primary_key" json:"id"`
Owner string `gorm:"type:varchar(254);not null" json:"-"`
Type string `gorm:"type:varchar(10);not null" json:"type"`
Name string `json:"name,omitempty"`
Key string `json:"key"`
Payload string `json:"payload,omitempty"`
}
var clientTypes map[string]bool = map[string]bool{

View File

@ -18,8 +18,8 @@ type jwtIdentityProvider struct {
secret string
}
// NewIdentityProvider instantiates a JWT identity provider.
func NewIdentityProvider(secret string) manager.IdentityProvider {
// New instantiates a JWT identity provider.
func New(secret string) manager.IdentityProvider {
return &jwtIdentityProvider{}
}

View File

@ -10,9 +10,8 @@ type managerService struct {
idp IdentityProvider
}
// NewService instantiates the domain service implementation.
func NewService(users UserRepository, clients ClientRepository, channels ChannelRepository,
hasher Hasher, idp IdentityProvider) Service {
// New instantiates the domain service implementation.
func New(users UserRepository, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
return &managerService{
users: users,
clients: clients,
@ -183,6 +182,32 @@ func (ms *managerService) RemoveChannel(key, id string) error {
return ms.channels.Remove(sub, id)
}
func (ms *managerService) Connect(key, chanId, clientId string) error {
owner, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(owner); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Connect(owner, chanId, clientId)
}
func (ms *managerService) Disconnect(key, chanId, clientId string) error {
owner, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(owner); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Disconnect(owner, chanId, clientId)
}
func (ms *managerService) Identity(key string) (string, error) {
client, err := ms.idp.Identity(key)
if err != nil {

View File

@ -9,254 +9,367 @@ import (
"github.com/stretchr/testify/assert"
)
const wrong string = "wrong-value"
var (
users manager.UserRepository = mocks.NewUserRepository()
clients manager.ClientRepository = mocks.NewClientRepository()
channels manager.ChannelRepository = mocks.NewChannelRepository()
hasher manager.Hasher = mocks.NewHasher()
idp manager.IdentityProvider = mocks.NewIdentityProvider()
svc manager.Service = manager.NewService(users, clients, channels, hasher, idp)
user manager.User = manager.User{"user@example.com", "password"}
client manager.Client = manager.Client{ID: "1", Type: "app", Name: "test", Key: "1"}
channel manager.Channel = manager.Channel{ID: "1", Name: "test", Clients: []manager.Client{client}}
)
func newService() manager.Service {
users := mocks.NewUserRepository()
clients := mocks.NewClientRepository()
channels := mocks.NewChannelRepository()
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return manager.New(users, clients, channels, hasher, idp)
}
func TestRegister(t *testing.T) {
cases := []struct {
svc := newService()
cases := map[string]struct {
user manager.User
err error
}{
{manager.User{"foo@bar.com", "pass"}, nil},
{manager.User{"foo@bar.com", "pass"}, manager.ErrConflict},
"register new user": {user, nil},
"register existing user": {user, manager.ErrConflict},
}
for i, tc := range cases {
e := svc.Register(tc.user)
assert.Equal(t, tc.err, e, fmt.Sprintf("failed %d\n", i))
for desc, tc := range cases {
err := svc.Register(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestLogin(t *testing.T) {
cases := []struct {
svc := newService()
svc.Register(user)
cases := map[string]struct {
user manager.User
key string
err error
}{
{manager.User{"foo@bar.com", "pass"}, "foo@bar.com", nil},
{manager.User{"new@bar.com", "pass"}, "", manager.ErrUnauthorizedAccess},
{manager.User{"foo@bar.com", ""}, "", manager.ErrUnauthorizedAccess},
"login with good credentials": {user, nil},
"login with wrong e-mail": {manager.User{wrong, user.Password}, manager.ErrUnauthorizedAccess},
"login with wrong password": {manager.User{user.Email, wrong}, manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
k, e := svc.Login(tc.user)
assert.Equal(t, tc.key, k, fmt.Sprintf("bad key at %d\n", i))
assert.Equal(t, tc.err, e, fmt.Sprintf("failed %d\n", i))
for desc, tc := range cases {
_, err := svc.Login(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClient(t *testing.T) {
cases := []struct {
key string
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
client manager.Client
id string
key string
err error
}{
{"foo@bar.com", manager.Client{Type: "app", Name: "a"}, "1", nil},
{"foo@bar.com", manager.Client{Type: "device", Name: "b"}, "2", nil},
{"", manager.Client{Type: "app", Name: "d"}, "", manager.ErrUnauthorizedAccess},
"add new app": {manager.Client{Type: "app", Name: "a"}, key, nil},
"add new device": {manager.Client{Type: "device", Name: "b"}, key, nil},
"add client with wrong credentials": {manager.Client{Type: "app", Name: "d"}, wrong, manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
id, err := svc.AddClient(tc.key, tc.client)
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
for desc, tc := range cases {
_, err := svc.AddClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClient(t *testing.T) {
cases := []struct {
key string
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
cases := map[string]struct {
client manager.Client
key string
err error
}{
{"foo@bar.com", manager.Client{ID: "1", Type: "app", Name: "aa"}, nil},
{"foo@bar.com", manager.Client{ID: "2", Type: "device", Name: "bb"}, nil},
{"", manager.Client{ID: "2", Type: "app", Name: "cc"}, manager.ErrUnauthorizedAccess},
{"foo@bar.com", manager.Client{ID: "3", Type: "app", Name: "d"}, manager.ErrNotFound},
"update existing client": {client, key, nil},
"update client with wrong credentials": {client, wrong, manager.ErrUnauthorizedAccess},
"update non-existing client": {manager.Client{ID: "2", Type: "app", Name: "d"}, key, manager.ErrNotFound},
}
for i, tc := range cases {
for desc, tc := range cases {
err := svc.UpdateClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewClient(t *testing.T) {
cases := []struct {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
cases := map[string]struct {
id string
key string
err error
}{
{"1", "foo@bar.com", nil},
{"1", "", manager.ErrUnauthorizedAccess},
{"5", "foo@bar.com", manager.ErrNotFound},
"view existing client": {client.ID, key, nil},
"view client with wrong credentials": {client.ID, wrong, manager.ErrUnauthorizedAccess},
"view non-existing client": {wrong, key, manager.ErrNotFound},
}
for i, tc := range cases {
for desc, tc := range cases {
_, err := svc.ViewClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListClients(t *testing.T) {
cases := []struct {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
key string
err error
}{
{"foo@bar.com", nil},
{"", manager.ErrUnauthorizedAccess},
"list clients": {key, nil},
"list clients with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
for desc, tc := range cases {
_, err := svc.ListClients(tc.key)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestRemoveClient(t *testing.T) {
cases := []struct {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
cases := map[string]struct {
id string
key string
err error
}{
{"1", "", manager.ErrUnauthorizedAccess},
{"1", "foo@bar.com", nil},
{"1", "foo@bar.com", nil},
{"2", "foo@bar.com", nil},
"remove client with wrong credentials": {client.ID, "?", manager.ErrUnauthorizedAccess},
"remove existing client": {client.ID, key, nil},
"remove removed client": {client.ID, key, nil},
"remove non-existing client": {"?", key, nil},
}
for i, tc := range cases {
for desc, tc := range cases {
err := svc.RemoveClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannel(t *testing.T) {
cases := []struct {
key string
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
channel manager.Channel
id string
key string
err error
}{
{"foo@bar.com", manager.Channel{Connected: []string{"1", "2"}}, "1", nil},
{"foo@bar.com", manager.Channel{Connected: []string{"2"}}, "2", nil},
{"", manager.Channel{Connected: []string{"1"}}, "", manager.ErrUnauthorizedAccess},
"create channel": {manager.Channel{}, key, nil},
"create channel with wrong credentials": {manager.Channel{}, wrong, manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
id, err := svc.CreateChannel(tc.key, tc.channel)
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
for desc, tc := range cases {
_, err := svc.CreateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannel(t *testing.T) {
cases := []struct {
key string
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.CreateChannel(key, channel)
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
{"foo@bar.com", manager.Channel{ID: "1", Connected: []string{"1"}}, nil},
{"foo@bar.com", manager.Channel{ID: "2", Connected: []string{}}, nil},
{"", manager.Channel{ID: "2", Connected: []string{"1"}}, manager.ErrUnauthorizedAccess},
{"foo@bar.com", manager.Channel{ID: "3", Connected: []string{"1"}}, manager.ErrNotFound},
"update existing channel": {channel, key, nil},
"update channel with wrong credentials": {channel, wrong, manager.ErrUnauthorizedAccess},
"update non-existing channel": {manager.Channel{ID: "2", Name: "test"}, key, manager.ErrNotFound},
}
for i, tc := range cases {
for desc, tc := range cases {
err := svc.UpdateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewChannel(t *testing.T) {
cases := []struct {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.CreateChannel(key, channel)
cases := map[string]struct {
id string
key string
err error
}{
{"1", "foo@bar.com", nil},
{"1", "", manager.ErrUnauthorizedAccess},
{"5", "foo@bar.com", manager.ErrNotFound},
"view existing channel": {channel.ID, key, nil},
"view channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
"view non-existing channel": {wrong, key, manager.ErrNotFound},
}
for i, tc := range cases {
for desc, tc := range cases {
_, err := svc.ViewChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListChannels(t *testing.T) {
cases := []struct {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
key string
err error
}{
{"foo@bar.com", nil},
{"", manager.ErrUnauthorizedAccess},
"list channels": {key, nil},
"list channels with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
for desc, tc := range cases {
_, err := svc.ListChannels(tc.key)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
}
}
func TestIdentity(t *testing.T) {
cases := []struct {
key string
id string
err error
}{
{"foo@bar.com", "foo@bar.com", nil},
{"", "", manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
id, err := svc.Identity(tc.key)
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
}
}
func TestCanAccess(t *testing.T) {
cases := []struct {
key string
channel string
id string
err error
}{
{"1", "1", "1", nil},
{"1", "2", "", manager.ErrUnauthorizedAccess},
{"", "1", "", manager.ErrUnauthorizedAccess},
}
for i, tc := range cases {
id, err := svc.CanAccess(tc.key, tc.channel)
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestRemoveChannel(t *testing.T) {
cases := []struct {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.CreateChannel(key, channel)
cases := map[string]struct {
id string
key string
err error
}{
{"1", "", manager.ErrUnauthorizedAccess},
{"1", "foo@bar.com", nil},
{"1", "foo@bar.com", nil},
{"2", "foo@bar.com", nil},
"remove channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
"remove existing channel": {channel.ID, key, nil},
"remove removed channel": {channel.ID, key, nil},
"remove non-existing channel": {channel.ID, key, nil},
}
for i, tc := range cases {
for desc, tc := range cases {
err := svc.RemoveChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestConnect(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
chanId, _ := svc.CreateChannel(key, channel)
cases := map[string]struct {
key string
chanId string
clientId string
err error
}{
"connect client": {key, chanId, clientId, nil},
"connect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
"connect client to non-existing channel": {key, wrong, clientId, manager.ErrNotFound},
}
for desc, tc := range cases {
err := svc.Connect(tc.key, tc.chanId, tc.clientId)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestDisconnect(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
chanId, _ := svc.CreateChannel(key, channel)
svc.Connect(key, chanId, clientId)
cases := map[string]struct {
key string
chanId string
clientId string
err error
}{
"disconnect connected client": {key, chanId, clientId, nil},
"disconnect disconnected client": {key, chanId, clientId, manager.ErrNotFound},
"disconnect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
"disconnect client from non-existing channel": {key, wrong, clientId, manager.ErrNotFound},
"disconnect non-existing client": {key, chanId, wrong, manager.ErrNotFound},
}
for desc, tc := range cases {
err := svc.Disconnect(tc.key, tc.chanId, tc.clientId)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestIdentity(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
key string
err error
}{
"valid token's identity": {key, nil},
"invalid token's identity": {"", manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.Identity(tc.key)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCanAccess(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
svc.CreateChannel(key, channel)
cases := map[string]struct {
key string
channel string
err error
}{
"allowed access": {client.Key, channel.ID, nil},
"not-connected cannot access": {wrong, channel.ID, manager.ErrUnauthorizedAccess},
"access non-existing channel": {client.Key, wrong, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.CanAccess(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}

View File

@ -24,46 +24,48 @@ func NewChannelRepository() manager.ChannelRepository {
}
}
func (repo *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
repo.mu.Lock()
defer repo.mu.Unlock()
func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
repo.counter += 1
channel.ID = strconv.Itoa(repo.counter)
crm.counter += 1
channel.ID = strconv.Itoa(crm.counter)
repo.channels[key(channel.Owner, channel.ID)] = channel
crm.channels[key(channel.Owner, channel.ID)] = channel
return channel.ID, nil
}
func (repo *channelRepositoryMock) Update(channel manager.Channel) error {
repo.mu.Lock()
defer repo.mu.Unlock()
func (crm *channelRepositoryMock) Update(channel manager.Channel) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(channel.Owner, channel.ID)
if _, ok := repo.channels[dbKey]; !ok {
if _, ok := crm.channels[dbKey]; !ok {
return manager.ErrNotFound
}
repo.channels[dbKey] = channel
crm.channels[dbKey] = channel
return nil
}
func (repo *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
if c, ok := repo.channels[key(owner, id)]; ok {
func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
if c, ok := crm.channels[key(owner, id)]; ok {
return c, nil
}
return manager.Channel{}, manager.ErrNotFound
}
func (repo *channelRepositoryMock) All(owner string) []manager.Channel {
func (crm *channelRepositoryMock) All(owner string) []manager.Channel {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
channels := make([]manager.Channel, 0)
for k, v := range repo.channels {
for k, v := range crm.channels {
if strings.HasPrefix(k, prefix) {
channels = append(channels, v)
}
@ -72,21 +74,58 @@ func (repo *channelRepositoryMock) All(owner string) []manager.Channel {
return channels
}
func (repo *channelRepositoryMock) Remove(owner, id string) error {
delete(repo.channels, key(owner, id))
func (crm *channelRepositoryMock) Remove(owner, id string) error {
delete(crm.channels, key(owner, id))
return nil
}
func (repo *channelRepositoryMock) HasClient(channel, client string) bool {
func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error {
channel, err := crm.One(owner, chanId)
if err != nil {
return err
}
// Since the current implementation has no way to retrieve a real client
// instance, the implementation will assume client always exist and create
// a dummy one, containing only the provided ID.
channel.Clients = append(channel.Clients, manager.Client{ID: clientId})
return crm.Update(channel)
}
func (crm *channelRepositoryMock) Disconnect(owner, chanId, clientId string) error {
channel, err := crm.One(owner, chanId)
if err != nil {
return err
}
if !crm.HasClient(chanId, clientId) {
return manager.ErrNotFound
}
connected := make([]manager.Client, len(channel.Clients)-1)
for _, client := range channel.Clients {
if client.ID != clientId {
connected = append(connected, client)
}
}
channel.Clients = connected
return crm.Update(channel)
}
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
suffix := fmt.Sprintf("-%s", channel)
for k, v := range repo.channels {
for k, v := range crm.channels {
if strings.HasSuffix(k, suffix) {
for _, c := range v.Connected {
if c == client {
for _, c := range v.Clients {
if c.ID == client {
return true
}
}
break
}
}

View File

@ -24,52 +24,54 @@ func NewClientRepository() manager.ClientRepository {
}
}
func (repo *clientRepositoryMock) Id() string {
repo.mu.Lock()
defer repo.mu.Unlock()
func (crm *clientRepositoryMock) Id() string {
crm.mu.Lock()
defer crm.mu.Unlock()
repo.counter += 1
return strconv.Itoa(repo.counter)
crm.counter += 1
return strconv.Itoa(crm.counter)
}
func (repo *clientRepositoryMock) Save(client manager.Client) error {
repo.mu.Lock()
defer repo.mu.Unlock()
func (crm *clientRepositoryMock) Save(client manager.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
repo.clients[key(client.Owner, client.ID)] = client
crm.clients[key(client.Owner, client.ID)] = client
return nil
}
func (repo *clientRepositoryMock) Update(client manager.Client) error {
repo.mu.Lock()
defer repo.mu.Unlock()
func (crm *clientRepositoryMock) Update(client manager.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(client.Owner, client.ID)
if _, ok := repo.clients[dbKey]; !ok {
if _, ok := crm.clients[dbKey]; !ok {
return manager.ErrNotFound
}
repo.clients[dbKey] = client
crm.clients[dbKey] = client
return nil
}
func (repo *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
if c, ok := repo.clients[key(owner, id)]; ok {
func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
if c, ok := crm.clients[key(owner, id)]; ok {
return c, nil
}
return manager.Client{}, manager.ErrNotFound
}
func (repo *clientRepositoryMock) All(owner string) []manager.Client {
func (crm *clientRepositoryMock) All(owner string) []manager.Client {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
clients := make([]manager.Client, 0)
for k, v := range repo.clients {
for k, v := range crm.clients {
if strings.HasPrefix(k, prefix) {
clients = append(clients, v)
}
@ -78,7 +80,7 @@ func (repo *clientRepositoryMock) All(owner string) []manager.Client {
return clients
}
func (repo *clientRepositoryMock) Remove(owner, id string) error {
delete(repo.clients, key(owner, id))
func (crm *clientRepositoryMock) Remove(owner, id string) error {
delete(crm.clients, key(owner, id))
return nil
}

View File

@ -2,6 +2,10 @@ package mocks
import "fmt"
// Since mocks will store data in map, and they need to resemble the real
// identifiers as much as possible, a key will be created as combination of
// owner and their own identifiers. This will allow searching either by
// prefix or suffix.
func key(owner, id string) string {
return fmt.Sprintf("%s-%s", owner, id)
}

View File

@ -20,23 +20,23 @@ func NewUserRepository() manager.UserRepository {
}
}
func (ur *userRepositoryMock) Save(user manager.User) error {
ur.mu.Lock()
defer ur.mu.Unlock()
func (urm *userRepositoryMock) Save(user manager.User) error {
urm.mu.Lock()
defer urm.mu.Unlock()
if _, ok := ur.users[user.Email]; ok {
if _, ok := urm.users[user.Email]; ok {
return manager.ErrConflict
}
ur.users[user.Email] = user
urm.users[user.Email] = user
return nil
}
func (ur *userRepositoryMock) One(email string) (manager.User, error) {
ur.mu.Lock()
defer ur.mu.Unlock()
func (urm *userRepositoryMock) One(email string) (manager.User, error) {
urm.mu.Lock()
defer urm.mu.Unlock()
if val, ok := ur.users[email]; ok {
if val, ok := urm.users[email]; ok {
return val, nil
}

View File

@ -0,0 +1,119 @@
package postgres
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/mainflux/mainflux/manager"
uuid "github.com/satori/go.uuid"
)
var _ manager.ChannelRepository = (*channelRepository)(nil)
type channelRepository struct {
db *gorm.DB
}
func NewChannelRepository(db *gorm.DB) manager.ChannelRepository {
return &channelRepository{db}
}
func (cr channelRepository) Save(channel manager.Channel) (string, error) {
channel.ID = uuid.NewV4().String()
if err := cr.db.Create(&channel).Error; err != nil {
return "", err
}
return channel.ID, nil
}
func (cr channelRepository) Update(channel manager.Channel) error {
sql := "UPDATE channels SET name = ? WHERE owner = ? AND id = ?;"
res := cr.db.Exec(sql, channel.Name, channel.Owner, channel.ID)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr channelRepository) One(owner, id string) (manager.Channel, error) {
channel := manager.Channel{}
res := cr.db.Preload("Clients").First(&channel, "owner = ? AND id = ?", owner, id)
if err := res.Error; err != nil {
if gorm.IsRecordNotFoundError(err) {
return channel, manager.ErrNotFound
}
return channel, err
}
return channel, nil
}
func (cr channelRepository) All(owner string) []manager.Channel {
var channels []manager.Channel
cr.db.Find(&channels, "owner = ?", owner)
return channels
}
func (cr channelRepository) Remove(owner, id string) error {
cr.db.Delete(&manager.Channel{}, "owner = ? AND id = ?", owner, id)
return nil
}
func (cr channelRepository) Connect(owner, chanId, clientId string) error {
// This approach can be replaced by declaring composite keys on both tables
// (clients and channels), and then propagate them into the m2m table. For
// some reason GORM does not infer these kind of connections well and
// raises a "no unique constraint for referenced table". Until we find a
// way to properly represent this relationship, let's stick with the nested
// query approach and observe its behaviour.
sql := `INSERT INTO channel_clients (channel_id, client_id)
SELECT ?, ? WHERE
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr channelRepository) Disconnect(owner, chanId, clientId string) error {
// The same remark given in Connect applies here.
sql := `DELETE FROM channel_clients WHERE
channel_id = ? AND client_id = ? AND
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr channelRepository) HasClient(chanId, clientId string) bool {
sql := "SELECT EXISTS (SELECT 1 FROM channel_clients WHERE channel_id = $1 AND client_id = $2);"
row := cr.db.DB().QueryRow(sql, chanId, clientId)
var exists bool
if err := row.Scan(&exists); err != nil {
// TODO: this error should be logged
return false
}
return exists
}

View File

@ -0,0 +1,246 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/postgres"
"github.com/stretchr/testify/assert"
)
func TestChannelSave(t *testing.T) {
email := "channel-save@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
c1 := manager.Channel{Owner: email}
c2 := manager.Channel{Owner: wrong}
cases := map[string]struct {
channel manager.Channel
hasErr bool
}{
"new channel, existing user": {c1, false},
"new channel, non-existing user": {c2, true},
}
channelRepo := postgres.NewChannelRepository(db)
for desc, tc := range cases {
_, err := channelRepo.Save(tc.channel)
hasErr := err != nil
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t", desc, tc.hasErr, hasErr))
}
}
func TestChannelUpdate(t *testing.T) {
email := "channel-update@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
c := manager.Channel{Owner: email}
id, _ := chanRepo.Save(c)
c.ID = id
cases := map[string]struct {
channel manager.Channel
err error
}{
"existing channel": {c, nil},
"non-existing channel with existing user": {manager.Channel{ID: wrong, Owner: email}, manager.ErrNotFound},
"non-existing channel with non-existing user": {manager.Channel{ID: wrong, Owner: wrong}, manager.ErrNotFound},
}
for desc, tc := range cases {
err := chanRepo.Update(tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleChannelRetrieval(t *testing.T) {
email := "channel-single-retrieval@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
c := manager.Channel{Owner: email}
id, _ := chanRepo.Save(c)
cases := map[string]struct {
owner string
ID string
err error
}{
"existing user": {c.Owner, id, nil},
"existing user, non-existing channel": {c.Owner, wrong, manager.ErrNotFound},
"non-existing owner": {wrong, id, manager.ErrNotFound},
}
for desc, tc := range cases {
_, err := chanRepo.One(tc.owner, tc.ID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestMultiChannelRetrieval(t *testing.T) {
email := "channel-multi-retrieval@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
n := 10
for i := 0; i < n; i++ {
c := manager.Channel{Owner: email}
chanRepo.Save(c)
}
cases := map[string]struct {
owner string
len int
}{
"existing owner": {email, n},
"non-existing owner": {wrong, 0},
}
for desc, tc := range cases {
n := len(chanRepo.All(tc.owner))
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
}
}
func TestChannelRemoval(t *testing.T) {
email := "channel-removal@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
// show that the removal works the same for both existing and non-existing
// (removed) channel
for i := 0; i < 2; i++ {
if err := chanRepo.Remove(email, chanId); err != nil {
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
}
if _, err := chanRepo.One(email, chanId); err != manager.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
}
}
}
func TestChannelConnect(t *testing.T) {
email := "channel-connect@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
cases := map[string]struct {
owner string
chanId string
clientId string
err error
}{
"existing user, channel and client": {email, chanId, client.ID, nil},
"with non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
}
for desc, tc := range cases {
err := chanRepo.Connect(tc.owner, tc.chanId, tc.clientId)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestChannelDisconnect(t *testing.T) {
email := "channel-disconnect@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
chanRepo.Connect(email, chanId, client.ID)
cases := map[string]struct {
owner string
chanId string
clientId string
err error
}{
"connected client": {email, chanId, client.ID, nil},
"non-connected client": {email, chanId, client.ID, manager.ErrNotFound},
"non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
}
for desc, tc := range cases {
err := chanRepo.Disconnect(tc.owner, tc.chanId, tc.clientId)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestChannelAccessCheck(t *testing.T) {
email := "channel-access-check@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
chanRepo.Connect(email, chanId, client.ID)
cases := map[string]struct {
chanId string
clientId string
hasAccess bool
}{
"client that has access": {chanId, client.ID, true},
"client without access": {chanId, wrong, false},
"check access to non-existing channel": {wrong, client.ID, false},
}
for desc, tc := range cases {
hasAccess := chanRepo.HasClient(tc.chanId, tc.clientId)
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
}
}

View File

@ -0,0 +1,72 @@
package postgres
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/mainflux/mainflux/manager"
uuid "github.com/satori/go.uuid"
)
var _ manager.ClientRepository = (*clientRepository)(nil)
type clientRepository struct {
db *gorm.DB
}
// NewClientRepository instantiates a PostgreSQL implementation of client
// repository.
func NewClientRepository(db *gorm.DB) manager.ClientRepository {
return &clientRepository{db}
}
func (cr *clientRepository) Id() string {
return uuid.NewV4().String()
}
func (cr *clientRepository) Save(client manager.Client) error {
if err := cr.db.Create(&client).Error; err != nil {
return err
}
return nil
}
func (cr *clientRepository) Update(client manager.Client) error {
sql := "UPDATE clients SET name = ?, payload = ? WHERE owner = ? AND id = ?;"
res := cr.db.Exec(sql, client.Name, client.Payload, client.Owner, client.ID)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr *clientRepository) One(owner, id string) (manager.Client, error) {
client := manager.Client{}
res := cr.db.First(&client, "owner = ? AND id = ?", owner, id)
if err := res.Error; err != nil {
if gorm.IsRecordNotFoundError(err) {
return client, manager.ErrNotFound
}
return client, err
}
return client, nil
}
func (cr *clientRepository) All(owner string) []manager.Client {
var clients []manager.Client
cr.db.Find(&clients, "owner = ?", owner)
return clients
}
func (cr *clientRepository) Remove(owner, id string) error {
cr.db.Delete(&manager.Client{}, "owner = ? AND id = ?", owner, id)
return nil
}

View File

@ -0,0 +1,162 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/postgres"
"github.com/stretchr/testify/assert"
)
func TestClientSave(t *testing.T) {
email := "client-save@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
c1 := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
c2 := manager.Client{
ID: clientRepo.Id(),
Owner: "unknown@example.com",
}
cases := map[string]struct {
client manager.Client
hasErr bool
}{
"new client, existing user": {c1, false},
"new client, non-existing user": {c2, true},
}
for desc, tc := range cases {
hasErr := clientRepo.Save(tc.client) != nil
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasErr, hasErr))
}
}
func TestClientUpdate(t *testing.T) {
email := "client-update@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
c := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(c)
cases := map[string]struct {
client manager.Client
err error
}{
"existing client": {c, nil},
"non-existing client with existing user": {manager.Client{ID: wrong, Owner: email}, manager.ErrNotFound},
"non-existing client with non-existing user": {manager.Client{ID: wrong, Owner: wrong}, manager.ErrNotFound},
}
for desc, tc := range cases {
err := clientRepo.Update(tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleClientRetrieval(t *testing.T) {
email := "client-single-retrieval@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
c := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(c)
cases := map[string]struct {
owner string
ID string
err error
}{
"existing user": {c.Owner, c.ID, nil},
"existing user, non-existing client": {c.Owner, wrong, manager.ErrNotFound},
"non-existing owner": {wrong, c.ID, manager.ErrNotFound},
}
for desc, tc := range cases {
_, err := clientRepo.One(tc.owner, tc.ID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestMultiClientRetrieval(t *testing.T) {
email := "client-multi-retrieval@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
n := 10
for i := 0; i < n; i++ {
c := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(c)
}
cases := map[string]struct {
owner string
len int
}{
"existing owner": {email, n},
"non-existing owner": {wrong, 0},
}
for desc, tc := range cases {
n := len(clientRepo.All(tc.owner))
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
}
}
func TestClientRemoval(t *testing.T) {
email := "client-removal@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(client)
// show that the removal works the same for both existing and non-existing
// (removed) client
for i := 0; i < 2; i++ {
if err := clientRepo.Remove(email, client.ID); err != nil {
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
}
if _, err := clientRepo.One(email, client.ID); err != manager.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
}
}
}

3
manager/postgres/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package postgres contains repository implementations using PostgreSQL as
// the underlying database.
package postgres

48
manager/postgres/init.go Normal file
View File

@ -0,0 +1,48 @@
package postgres
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/mainflux/mainflux/manager"
)
const errDuplicate string = "unique_violation"
type connection struct {
ClientID string `gorm:"primary_key"`
ChannelID string `gorm:"primary_key"`
}
func (c connection) TableName() string {
return "channel_clients"
}
// Connect creates a connection to the PostgreSQL instance. A non-nil error
// is returned to indicate failure.
func Connect(host, port, name, user, pass string) (*gorm.DB, error) {
t := "host=%s port=%s user=%s dbname=%s password=%s sslmode=disable"
url := fmt.Sprintf(t, host, port, user, name, pass)
db, err := gorm.Open("postgres", url)
if err != nil {
return nil, err
}
db = db.AutoMigrate(&manager.User{}, &manager.Client{}, &manager.Channel{}, &connection{})
db = db.Model(&manager.Client{}).
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
AddUniqueIndex("idx_pk_client", "id", "owner")
db = db.Model(&manager.Channel{}).
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
AddUniqueIndex("idx_pk_channel", "id", "owner")
db = db.Model(&connection{}).
AddForeignKey("client_id", "clients(id)", "CASCADE", "CASCADE").
AddForeignKey("channel_id", "channels(id)", "CASCADE", "CASCADE")
return db.LogMode(false), nil
}

View File

@ -0,0 +1,62 @@
// Package postgres_test contains tests for PostgreSQL repository
// implementations.
package postgres_test
import (
"database/sql"
"fmt"
"log"
"os"
"testing"
"github.com/jinzhu/gorm"
"github.com/mainflux/mainflux/manager/postgres"
"gopkg.in/ory-am/dockertest.v3"
)
const wrong string = "wrong-value"
var db *gorm.DB
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
cfg := []string{
"POSTGRES_USER=test",
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.2-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
port := container.GetPort("5432/tcp")
if err := pool.Retry(func() error {
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
db, err := sql.Open("postgres", url)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
if db, err = postgres.Connect("localhost", port, "test", "test", "test"); err != nil {
log.Fatalf("Could not setup test DB connection: %s", err)
}
defer db.Close()
code := m.Run()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(code)
}

48
manager/postgres/users.go Normal file
View File

@ -0,0 +1,48 @@
package postgres
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/lib/pq"
"github.com/mainflux/mainflux/manager"
)
var _ manager.UserRepository = (*userRepository)(nil)
type userRepository struct {
db *gorm.DB
}
// NewUserRepository instantiates a PostgreSQL implementation of user
// repository.
func NewUserRepository(db *gorm.DB) manager.UserRepository {
return &userRepository{db}
}
func (ur *userRepository) Save(user manager.User) error {
if err := ur.db.Create(&user).Error; err != nil {
if pqErr, ok := err.(*pq.Error); ok && errDuplicate == pqErr.Code.Name() {
return manager.ErrConflict
}
return err
}
return nil
}
func (ur *userRepository) One(email string) (manager.User, error) {
user := manager.User{}
q := ur.db.First(&user, "email = ?", email)
if err := q.Error; err != nil {
if err == gorm.ErrRecordNotFound {
return user, manager.ErrNotFound
}
return user, err
}
return user, nil
}

View File

@ -0,0 +1,49 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/postgres"
"github.com/stretchr/testify/assert"
)
func TestUserSave(t *testing.T) {
email := "user-save@example.com"
cases := map[string]struct {
user manager.User
err error
}{
"new user": {manager.User{email, "pass"}, nil},
"duplicate user": {manager.User{email, "pass"}, manager.ErrConflict},
}
repo := postgres.NewUserRepository(db)
for desc, tc := range cases {
err := repo.Save(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleUserRetrieval(t *testing.T) {
email := "user-retrieval@example.com"
repo := postgres.NewUserRepository(db)
repo.Save(manager.User{email, "pass"})
cases := map[string]struct {
email string
err error
}{
"existing user": {email, nil},
"non-existing user": {"unknown@example.com", manager.ErrNotFound},
}
for desc, tc := range cases {
_, err := repo.One(tc.email)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}

View File

@ -69,6 +69,13 @@ type Service interface {
// belongs to the user identified by the provided key.
RemoveChannel(string, string) error
// Connect adds client to the channel's list of connected clients.
Connect(string, string, string) error
// Disconnect removes client from the channel's list of connected
// clients.
Disconnect(string, string, string) error
// Identity retrieves Client ID for provided client token.
Identity(string) (string, error)

View File

@ -115,14 +115,14 @@ paths:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/clients/{id}:
/clients/{clientId}:
get:
summary: Retrieves client info
tags:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Id"
- $ref: "#/parameters/ClientId"
responses:
200:
description: Data retrieved.
@ -138,13 +138,13 @@ paths:
summary: Updates client info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Resource's unique identifier will not be
affected. Note that the client's type and ID cannot be changed.
provided in a request payload. Note that the client's type and ID
cannot be changed.
tags:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Id"
- $ref: "#/parameters/ClientId"
- name: client
description: JSON-formatted document describing the updated client.
in: body
@ -171,7 +171,7 @@ paths:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Id"
- $ref: "#/parameters/ClientId"
responses:
204:
description: Client removed.
@ -238,14 +238,14 @@ paths:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/channels/{id}:
/channels/{chanId}:
get:
summary: Retrieves channel info
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Id"
- $ref: "#/parameters/ChanId"
responses:
200:
description: Data retrieved.
@ -261,13 +261,13 @@ paths:
summary: Updates channel info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Resource's unique identifier will not be
provided in a request payload. Note that the channel's ID will not be
affected.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Id"
- $ref: "#/parameters/ChanId"
- name: channel
description: JSON-formatted document describing the updated channel.
in: body
@ -294,7 +294,7 @@ paths:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Id"
- $ref: "#/parameters/ChanId"
responses:
204:
description: Channel removed.
@ -302,6 +302,47 @@ paths:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/channels/{chanId}/clients/{clientId}:
put:
summary: Connects the client to the channel
description: |
Creates connection between a client and a channel. Once connected to
the channel, clients are allowed to exchange messages through it.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ChanId"
- $ref: "#/parameters/ClientId"
responses:
200:
description: Client connected.
403:
description: Missing or invalid access token provided.
404:
description: Channel or client does not exist.
500:
$ref: "#/responses/ServiceError"
delete:
summary: Disconnects the client from the channel
description: |
Removes connection between a client and a channel. Once connection is
removed, client can no longer exchange messages through the channel.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ChanId"
- $ref: "#/parameters/ClientId"
responses:
204:
description: Client disconnected.
403:
description: Missing or invalid access token provided.
404:
description: Channel or client does not exist.
500:
$ref: "#/responses/ServiceError"
/access-grant:
get:
summary: Checks the token validity
@ -322,7 +363,7 @@ paths:
description: ID of the entity bound to the provided access key.
403:
description: Missing or invalid access token provided.
/channels/{id}/access-grant:
/channels/{chanId}/access-grant:
get:
summary: Checks channel accessibility
description: |
@ -332,7 +373,7 @@ paths:
- access control
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Id"
- $ref: "#/parameters/ChanId"
responses:
200:
description: Client can access the channel.
@ -352,9 +393,16 @@ parameters:
in: header
type: string
required: true
Id:
name: id
description: Unique resource identifier.
ChanId:
name: chanId
description: Unique channel identifier.
in: path
type: string
format: uuid
required: true
ClientId:
name: clientId
description: Unique client identifier.
in: path
type: string
format: uuid
@ -387,7 +435,16 @@ definitions:
minItems: 0
uniqueItems: true
items:
$ref: "#/definitions/ChannelRes"
type: object
properties:
id:
type: string
description: Unique channel identifier generated by the service.
name:
type: string
description: Free-form channel name.
required:
- id
required:
- channels
ChannelRes:
@ -404,23 +461,15 @@ definitions:
minItems: 0
uniqueItems: true
items:
type: string
$ref: '#/definitions/ClientRes'
required:
- id
- name
- connected
ChannelReq:
type: object
properties:
name:
type: string
description: Free-form channel name.
connected:
type: array
minItems: 0
uniqueItems: true
items:
type: string
ClientList:
type: object
properties:
@ -450,11 +499,9 @@ definitions:
key:
type: string
description: Auto-generated access key.
meta:
type: object
description: Client's meta-data.
additionalProperties:
type: string
payload:
type: string
description: Arbitrary, string-encoded client's data.
required:
- id
- type
@ -471,11 +518,9 @@ definitions:
name:
type: string
description: Free-form client name.
meta:
type: object
description: Client's meta-data.
additionalProperties:
type: string
payload:
type: string
description: Arbitrary, string-encoded client's data.
required:
- type
Token:

View File

@ -5,8 +5,8 @@ import "github.com/asaskevich/govalidator"
// User represents a Mainflux user account. Each user is identified given its
// email and password.
type User struct {
Email string
Password string
Email string `gorm:"type:varchar(254);primary_key"`
Password string `gorm:"type:char(60)"`
}
// Validate returns an error if user representation is invalid.

View File

@ -1,5 +1,4 @@
// Package writer provides message writer concept definitions.
package writer
package mainflux
// Message represents a resolved (normalized) raw message.
type Message struct {
@ -27,9 +26,9 @@ type RawMessage struct {
Payload []byte `json:"payload"`
}
// MessageRepository specifies a message persistence API.
type MessageRepository interface {
// Save persists the message. A non-nil error is returned to indicate
// MessagePublisher specifies a message publishing API.
type MessagePublisher interface {
// Publishes message to the stream. A non-nil error is returned to indicate
// operation failure.
Save(RawMessage) error
Publish(RawMessage) error
}

47
normalizer/README.md Normal file
View File

@ -0,0 +1,47 @@
# Message normalizer
Normalizer service consumes events published by adapters, normalizes SenML-formatted
ones, and publishes them to the post-processing stream.
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|--------------------|------------------------------|-----------------------|
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_NORMALIZER_PORT | Normalizer service HTTP port | 8180 |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
services:
manager:
image: mainflux/normalizer:[version]
container_name: [instance name]
environment:
MF_NATS_URL: [NATS instance URL]
MF_NORMALIZER_PORT: [Service HTTP port]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux/cmd/normalizer
# compile the app; make sure to set the proper GOOS value
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
# set the environment variables and run the service
MF_NATS_URL=[NATS instance URL] MF_NORMALIZER_PORT=[Service HTTP port] app
```

18
normalizer/api.go Normal file
View File

@ -0,0 +1,18 @@
package normalizer
import (
"net/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler() http.Handler {
r := bone.New()
r.GetFunc("/version", mainflux.Version())
r.Handle("/metrics", promhttp.Handler())
return r
}

3
normalizer/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package normalizer contains the domain concept definitions needed to
// support Mainflux normalizer service functionality.
package normalizer

112
normalizer/normalizer.go Normal file
View File

@ -0,0 +1,112 @@
package normalizer
import (
"encoding/json"
"fmt"
"github.com/cisco/senml"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux"
nats "github.com/nats-io/go-nats"
)
const (
queue string = "normalizers"
subject string = "src.*"
output string = "normalized"
)
type eventFlow struct {
nc *nats.Conn
logger log.Logger
}
// Subscribe instantiates and starts a new NATS message flow.
func Subscribe(nc *nats.Conn, logger log.Logger) {
flow := eventFlow{nc, logger}
flow.start()
}
func (ef eventFlow) start() {
ef.nc.QueueSubscribe(subject, queue, func(m *nats.Msg) {
msg := mainflux.RawMessage{}
if err := json.Unmarshal(m.Data, &msg); err != nil {
ef.logger.Log("error", fmt.Sprintf("Unmarshalling failed: %s", err))
return
}
if err := ef.publish(msg); err != nil {
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
return
}
})
}
func (ef eventFlow) publish(msg mainflux.RawMessage) error {
normalized, err := ef.normalize(msg)
if err != nil {
ef.logger.Log("error", fmt.Sprintf("Normalization failed: %s", err))
return err
}
for _, v := range normalized {
data, err := json.Marshal(v)
if err != nil {
ef.logger.Log("error", fmt.Sprintf("Marshalling failed: %s", err))
return err
}
if err = ef.nc.Publish(subject, data); err != nil {
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
return err
}
}
return nil
}
func (ef eventFlow) normalize(msg mainflux.RawMessage) ([]mainflux.Message, error) {
var (
raw, normalized senml.SenML
err error
)
if raw, err = senml.Decode(msg.Payload, senml.JSON); err != nil {
return nil, err
}
normalized = senml.Normalize(raw)
msgs := make([]mainflux.Message, len(normalized.Records))
for k, v := range normalized.Records {
m := mainflux.Message{
Channel: msg.Channel,
Publisher: msg.Publisher,
Protocol: msg.Protocol,
Name: v.Name,
Unit: v.Unit,
StringValue: v.StringValue,
DataValue: v.DataValue,
Time: v.Time,
UpdateTime: v.UpdateTime,
Link: v.Link,
}
if v.Value != nil {
m.Value = *v.Value
}
if v.BoolValue != nil {
m.BoolValue = *v.BoolValue
}
if v.Sum != nil {
m.ValueSum = *v.Sum
}
msgs[k] = m
}
return msgs, nil
}

13
utils.go Normal file
View File

@ -0,0 +1,13 @@
package mainflux
import "os"
// Env reads specified environment variable. If no value has been found,
// fallback is returned.
func Env(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

21
vendor/github.com/Azure/go-ansiterm/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

12
vendor/github.com/Azure/go-ansiterm/README.md generated vendored Normal file
View File

@ -0,0 +1,12 @@
# go-ansiterm
This is a cross platform Ansi Terminal Emulation library. It reads a stream of Ansi characters and produces the appropriate function calls. The results of the function calls are platform dependent.
For example the parser might receive "ESC, [, A" as a stream of three characters. This is the code for Cursor Up (http://www.vt100.net/docs/vt510-rm/CUU). The parser then calls the cursor up function (CUU()) on an event handler. The event handler determines what platform specific work must be done to cause the cursor to move up one position.
The parser (parser.go) is a partial implementation of this state machine (http://vt100.net/emu/vt500_parser.png). There are also two event handler implementations, one for tests (test_event_handler.go) to validate that the expected events are being produced and called, the other is a Windows implementation (winterm/win_event_handler.go).
See parser_test.go for examples exercising the state machine and generating appropriate function calls.
-----
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

188
vendor/github.com/Azure/go-ansiterm/constants.go generated vendored Normal file
View File

@ -0,0 +1,188 @@
package ansiterm
const LogEnv = "DEBUG_TERMINAL"
// ANSI constants
// References:
// -- http://www.ecma-international.org/publications/standards/Ecma-048.htm
// -- http://man7.org/linux/man-pages/man4/console_codes.4.html
// -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
// -- http://en.wikipedia.org/wiki/ANSI_escape_code
// -- http://vt100.net/emu/dec_ansi_parser
// -- http://vt100.net/emu/vt500_parser.svg
// -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
// -- http://www.inwap.com/pdp10/ansicode.txt
const (
// ECMA-48 Set Graphics Rendition
// Note:
// -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved
// -- Fonts could possibly be supported via SetCurrentConsoleFontEx
// -- Windows does not expose the per-window cursor (i.e., caret) blink times
ANSI_SGR_RESET = 0
ANSI_SGR_BOLD = 1
ANSI_SGR_DIM = 2
_ANSI_SGR_ITALIC = 3
ANSI_SGR_UNDERLINE = 4
_ANSI_SGR_BLINKSLOW = 5
_ANSI_SGR_BLINKFAST = 6
ANSI_SGR_REVERSE = 7
_ANSI_SGR_INVISIBLE = 8
_ANSI_SGR_LINETHROUGH = 9
_ANSI_SGR_FONT_00 = 10
_ANSI_SGR_FONT_01 = 11
_ANSI_SGR_FONT_02 = 12
_ANSI_SGR_FONT_03 = 13
_ANSI_SGR_FONT_04 = 14
_ANSI_SGR_FONT_05 = 15
_ANSI_SGR_FONT_06 = 16
_ANSI_SGR_FONT_07 = 17
_ANSI_SGR_FONT_08 = 18
_ANSI_SGR_FONT_09 = 19
_ANSI_SGR_FONT_10 = 20
_ANSI_SGR_DOUBLEUNDERLINE = 21
ANSI_SGR_BOLD_DIM_OFF = 22
_ANSI_SGR_ITALIC_OFF = 23
ANSI_SGR_UNDERLINE_OFF = 24
_ANSI_SGR_BLINK_OFF = 25
_ANSI_SGR_RESERVED_00 = 26
ANSI_SGR_REVERSE_OFF = 27
_ANSI_SGR_INVISIBLE_OFF = 28
_ANSI_SGR_LINETHROUGH_OFF = 29
ANSI_SGR_FOREGROUND_BLACK = 30
ANSI_SGR_FOREGROUND_RED = 31
ANSI_SGR_FOREGROUND_GREEN = 32
ANSI_SGR_FOREGROUND_YELLOW = 33
ANSI_SGR_FOREGROUND_BLUE = 34
ANSI_SGR_FOREGROUND_MAGENTA = 35
ANSI_SGR_FOREGROUND_CYAN = 36
ANSI_SGR_FOREGROUND_WHITE = 37
_ANSI_SGR_RESERVED_01 = 38
ANSI_SGR_FOREGROUND_DEFAULT = 39
ANSI_SGR_BACKGROUND_BLACK = 40
ANSI_SGR_BACKGROUND_RED = 41
ANSI_SGR_BACKGROUND_GREEN = 42
ANSI_SGR_BACKGROUND_YELLOW = 43
ANSI_SGR_BACKGROUND_BLUE = 44
ANSI_SGR_BACKGROUND_MAGENTA = 45
ANSI_SGR_BACKGROUND_CYAN = 46
ANSI_SGR_BACKGROUND_WHITE = 47
_ANSI_SGR_RESERVED_02 = 48
ANSI_SGR_BACKGROUND_DEFAULT = 49
// 50 - 65: Unsupported
ANSI_MAX_CMD_LENGTH = 4096
MAX_INPUT_EVENTS = 128
DEFAULT_WIDTH = 80
DEFAULT_HEIGHT = 24
ANSI_BEL = 0x07
ANSI_BACKSPACE = 0x08
ANSI_TAB = 0x09
ANSI_LINE_FEED = 0x0A
ANSI_VERTICAL_TAB = 0x0B
ANSI_FORM_FEED = 0x0C
ANSI_CARRIAGE_RETURN = 0x0D
ANSI_ESCAPE_PRIMARY = 0x1B
ANSI_ESCAPE_SECONDARY = 0x5B
ANSI_OSC_STRING_ENTRY = 0x5D
ANSI_COMMAND_FIRST = 0x40
ANSI_COMMAND_LAST = 0x7E
DCS_ENTRY = 0x90
CSI_ENTRY = 0x9B
OSC_STRING = 0x9D
ANSI_PARAMETER_SEP = ";"
ANSI_CMD_G0 = '('
ANSI_CMD_G1 = ')'
ANSI_CMD_G2 = '*'
ANSI_CMD_G3 = '+'
ANSI_CMD_DECPNM = '>'
ANSI_CMD_DECPAM = '='
ANSI_CMD_OSC = ']'
ANSI_CMD_STR_TERM = '\\'
KEY_CONTROL_PARAM_2 = ";2"
KEY_CONTROL_PARAM_3 = ";3"
KEY_CONTROL_PARAM_4 = ";4"
KEY_CONTROL_PARAM_5 = ";5"
KEY_CONTROL_PARAM_6 = ";6"
KEY_CONTROL_PARAM_7 = ";7"
KEY_CONTROL_PARAM_8 = ";8"
KEY_ESC_CSI = "\x1B["
KEY_ESC_N = "\x1BN"
KEY_ESC_O = "\x1BO"
FILL_CHARACTER = ' '
)
func getByteRange(start byte, end byte) []byte {
bytes := make([]byte, 0, 32)
for i := start; i <= end; i++ {
bytes = append(bytes, byte(i))
}
return bytes
}
var toGroundBytes = getToGroundBytes()
var executors = getExecuteBytes()
// SPACE 20+A0 hex Always and everywhere a blank space
// Intermediate 20-2F hex !"#$%&'()*+,-./
var intermeds = getByteRange(0x20, 0x2F)
// Parameters 30-3F hex 0123456789:;<=>?
// CSI Parameters 30-39, 3B hex 0123456789;
var csiParams = getByteRange(0x30, 0x3F)
var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...)
// Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
var upperCase = getByteRange(0x40, 0x5F)
// Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
var lowerCase = getByteRange(0x60, 0x7E)
// Alphabetics 40-7E hex (all of upper and lower case)
var alphabetics = append(upperCase, lowerCase...)
var printables = getByteRange(0x20, 0x7F)
var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E)
var escapeToGroundBytes = getEscapeToGroundBytes()
// See http://www.vt100.net/emu/vt500_parser.png for description of the complex
// byte ranges below
func getEscapeToGroundBytes() []byte {
escapeToGroundBytes := getByteRange(0x30, 0x4F)
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...)
escapeToGroundBytes = append(escapeToGroundBytes, 0x59)
escapeToGroundBytes = append(escapeToGroundBytes, 0x5A)
escapeToGroundBytes = append(escapeToGroundBytes, 0x5C)
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...)
return escapeToGroundBytes
}
func getExecuteBytes() []byte {
executeBytes := getByteRange(0x00, 0x17)
executeBytes = append(executeBytes, 0x19)
executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...)
return executeBytes
}
func getToGroundBytes() []byte {
groundBytes := []byte{0x18}
groundBytes = append(groundBytes, 0x1A)
groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...)
groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...)
groundBytes = append(groundBytes, 0x99)
groundBytes = append(groundBytes, 0x9A)
groundBytes = append(groundBytes, 0x9C)
return groundBytes
}
// Delete 7F hex Always and everywhere ignored
// C1 Control 80-9F hex 32 additional control characters
// G1 Displayable A1-FE hex 94 additional displayable characters
// Special A0+FF hex Same as SPACE and DELETE

7
vendor/github.com/Azure/go-ansiterm/context.go generated vendored Normal file
View File

@ -0,0 +1,7 @@
package ansiterm
type ansiContext struct {
currentChar byte
paramBuffer []byte
interBuffer []byte
}

49
vendor/github.com/Azure/go-ansiterm/csi_entry_state.go generated vendored Normal file
View File

@ -0,0 +1,49 @@
package ansiterm
type csiEntryState struct {
baseState
}
func (csiState csiEntryState) Handle(b byte) (s state, e error) {
csiState.parser.logf("CsiEntry::Handle %#x", b)
nextState, err := csiState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(alphabetics, b):
return csiState.parser.ground, nil
case sliceContains(csiCollectables, b):
return csiState.parser.csiParam, nil
case sliceContains(executors, b):
return csiState, csiState.parser.execute()
}
return csiState, nil
}
func (csiState csiEntryState) Transition(s state) error {
csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
csiState.baseState.Transition(s)
switch s {
case csiState.parser.ground:
return csiState.parser.csiDispatch()
case csiState.parser.csiParam:
switch {
case sliceContains(csiParams, csiState.parser.context.currentChar):
csiState.parser.collectParam()
case sliceContains(intermeds, csiState.parser.context.currentChar):
csiState.parser.collectInter()
}
}
return nil
}
func (csiState csiEntryState) Enter() error {
csiState.parser.clear()
return nil
}

38
vendor/github.com/Azure/go-ansiterm/csi_param_state.go generated vendored Normal file
View File

@ -0,0 +1,38 @@
package ansiterm
type csiParamState struct {
baseState
}
func (csiState csiParamState) Handle(b byte) (s state, e error) {
csiState.parser.logf("CsiParam::Handle %#x", b)
nextState, err := csiState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(alphabetics, b):
return csiState.parser.ground, nil
case sliceContains(csiCollectables, b):
csiState.parser.collectParam()
return csiState, nil
case sliceContains(executors, b):
return csiState, csiState.parser.execute()
}
return csiState, nil
}
func (csiState csiParamState) Transition(s state) error {
csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
csiState.baseState.Transition(s)
switch s {
case csiState.parser.ground:
return csiState.parser.csiDispatch()
}
return nil
}

View File

@ -0,0 +1,36 @@
package ansiterm
type escapeIntermediateState struct {
baseState
}
func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
escState.parser.logf("escapeIntermediateState::Handle %#x", b)
nextState, err := escState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(intermeds, b):
return escState, escState.parser.collectInter()
case sliceContains(executors, b):
return escState, escState.parser.execute()
case sliceContains(escapeIntermediateToGroundBytes, b):
return escState.parser.ground, nil
}
return escState, nil
}
func (escState escapeIntermediateState) Transition(s state) error {
escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
escState.baseState.Transition(s)
switch s {
case escState.parser.ground:
return escState.parser.escDispatch()
}
return nil
}

47
vendor/github.com/Azure/go-ansiterm/escape_state.go generated vendored Normal file
View File

@ -0,0 +1,47 @@
package ansiterm
type escapeState struct {
baseState
}
func (escState escapeState) Handle(b byte) (s state, e error) {
escState.parser.logf("escapeState::Handle %#x", b)
nextState, err := escState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case b == ANSI_ESCAPE_SECONDARY:
return escState.parser.csiEntry, nil
case b == ANSI_OSC_STRING_ENTRY:
return escState.parser.oscString, nil
case sliceContains(executors, b):
return escState, escState.parser.execute()
case sliceContains(escapeToGroundBytes, b):
return escState.parser.ground, nil
case sliceContains(intermeds, b):
return escState.parser.escapeIntermediate, nil
}
return escState, nil
}
func (escState escapeState) Transition(s state) error {
escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name())
escState.baseState.Transition(s)
switch s {
case escState.parser.ground:
return escState.parser.escDispatch()
case escState.parser.escapeIntermediate:
return escState.parser.collectInter()
}
return nil
}
func (escState escapeState) Enter() error {
escState.parser.clear()
return nil
}

90
vendor/github.com/Azure/go-ansiterm/event_handler.go generated vendored Normal file
View File

@ -0,0 +1,90 @@
package ansiterm
type AnsiEventHandler interface {
// Print
Print(b byte) error
// Execute C0 commands
Execute(b byte) error
// CUrsor Up
CUU(int) error
// CUrsor Down
CUD(int) error
// CUrsor Forward
CUF(int) error
// CUrsor Backward
CUB(int) error
// Cursor to Next Line
CNL(int) error
// Cursor to Previous Line
CPL(int) error
// Cursor Horizontal position Absolute
CHA(int) error
// Vertical line Position Absolute
VPA(int) error
// CUrsor Position
CUP(int, int) error
// Horizontal and Vertical Position (depends on PUM)
HVP(int, int) error
// Text Cursor Enable Mode
DECTCEM(bool) error
// Origin Mode
DECOM(bool) error
// 132 Column Mode
DECCOLM(bool) error
// Erase in Display
ED(int) error
// Erase in Line
EL(int) error
// Insert Line
IL(int) error
// Delete Line
DL(int) error
// Insert Character
ICH(int) error
// Delete Character
DCH(int) error
// Set Graphics Rendition
SGR([]int) error
// Pan Down
SU(int) error
// Pan Up
SD(int) error
// Device Attributes
DA([]string) error
// Set Top and Bottom Margins
DECSTBM(int, int) error
// Index
IND() error
// Reverse Index
RI() error
// Flush updates from previous commands
Flush() error
}

24
vendor/github.com/Azure/go-ansiterm/ground_state.go generated vendored Normal file
View File

@ -0,0 +1,24 @@
package ansiterm
type groundState struct {
baseState
}
func (gs groundState) Handle(b byte) (s state, e error) {
gs.parser.context.currentChar = b
nextState, err := gs.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(printables, b):
return gs, gs.parser.print()
case sliceContains(executors, b):
return gs, gs.parser.execute()
}
return gs, nil
}

View File

@ -0,0 +1,31 @@
package ansiterm
type oscStringState struct {
baseState
}
func (oscState oscStringState) Handle(b byte) (s state, e error) {
oscState.parser.logf("OscString::Handle %#x", b)
nextState, err := oscState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case isOscStringTerminator(b):
return oscState.parser.ground, nil
}
return oscState, nil
}
// See below for OSC string terminators for linux
// http://man7.org/linux/man-pages/man4/console_codes.4.html
func isOscStringTerminator(b byte) bool {
if b == ANSI_BEL || b == 0x5C {
return true
}
return false
}

151
vendor/github.com/Azure/go-ansiterm/parser.go generated vendored Normal file
View File

@ -0,0 +1,151 @@
package ansiterm
import (
"errors"
"log"
"os"
)
type AnsiParser struct {
currState state
eventHandler AnsiEventHandler
context *ansiContext
csiEntry state
csiParam state
dcsEntry state
escape state
escapeIntermediate state
error state
ground state
oscString state
stateMap []state
logf func(string, ...interface{})
}
type Option func(*AnsiParser)
func WithLogf(f func(string, ...interface{})) Option {
return func(ap *AnsiParser) {
ap.logf = f
}
}
func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Option) *AnsiParser {
ap := &AnsiParser{
eventHandler: evtHandler,
context: &ansiContext{},
}
for _, o := range opts {
o(ap)
}
if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" {
logFile, _ := os.Create("ansiParser.log")
logger := log.New(logFile, "", log.LstdFlags)
if ap.logf != nil {
l := ap.logf
ap.logf = func(s string, v ...interface{}) {
l(s, v...)
logger.Printf(s, v...)
}
} else {
ap.logf = logger.Printf
}
}
if ap.logf == nil {
ap.logf = func(string, ...interface{}) {}
}
ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}}
ap.csiParam = csiParamState{baseState{name: "CsiParam", parser: ap}}
ap.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: ap}}
ap.escape = escapeState{baseState{name: "Escape", parser: ap}}
ap.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: ap}}
ap.error = errorState{baseState{name: "Error", parser: ap}}
ap.ground = groundState{baseState{name: "Ground", parser: ap}}
ap.oscString = oscStringState{baseState{name: "OscString", parser: ap}}
ap.stateMap = []state{
ap.csiEntry,
ap.csiParam,
ap.dcsEntry,
ap.escape,
ap.escapeIntermediate,
ap.error,
ap.ground,
ap.oscString,
}
ap.currState = getState(initialState, ap.stateMap)
ap.logf("CreateParser: parser %p", ap)
return ap
}
func getState(name string, states []state) state {
for _, el := range states {
if el.Name() == name {
return el
}
}
return nil
}
func (ap *AnsiParser) Parse(bytes []byte) (int, error) {
for i, b := range bytes {
if err := ap.handle(b); err != nil {
return i, err
}
}
return len(bytes), ap.eventHandler.Flush()
}
func (ap *AnsiParser) handle(b byte) error {
ap.context.currentChar = b
newState, err := ap.currState.Handle(b)
if err != nil {
return err
}
if newState == nil {
ap.logf("WARNING: newState is nil")
return errors.New("New state of 'nil' is invalid.")
}
if newState != ap.currState {
if err := ap.changeState(newState); err != nil {
return err
}
}
return nil
}
func (ap *AnsiParser) changeState(newState state) error {
ap.logf("ChangeState %s --> %s", ap.currState.Name(), newState.Name())
// Exit old state
if err := ap.currState.Exit(); err != nil {
ap.logf("Exit state '%s' failed with : '%v'", ap.currState.Name(), err)
return err
}
// Perform transition action
if err := ap.currState.Transition(newState); err != nil {
ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
return err
}
// Enter new state
if err := newState.Enter(); err != nil {
ap.logf("Enter state '%s' failed with: '%v'", newState.Name(), err)
return err
}
ap.currState = newState
return nil
}

View File

@ -0,0 +1,99 @@
package ansiterm
import (
"strconv"
)
func parseParams(bytes []byte) ([]string, error) {
paramBuff := make([]byte, 0, 0)
params := []string{}
for _, v := range bytes {
if v == ';' {
if len(paramBuff) > 0 {
// Completed parameter, append it to the list
s := string(paramBuff)
params = append(params, s)
paramBuff = make([]byte, 0, 0)
}
} else {
paramBuff = append(paramBuff, v)
}
}
// Last parameter may not be terminated with ';'
if len(paramBuff) > 0 {
s := string(paramBuff)
params = append(params, s)
}
return params, nil
}
func parseCmd(context ansiContext) (string, error) {
return string(context.currentChar), nil
}
func getInt(params []string, dflt int) int {
i := getInts(params, 1, dflt)[0]
return i
}
func getInts(params []string, minCount int, dflt int) []int {
ints := []int{}
for _, v := range params {
i, _ := strconv.Atoi(v)
// Zero is mapped to the default value in VT100.
if i == 0 {
i = dflt
}
ints = append(ints, i)
}
if len(ints) < minCount {
remaining := minCount - len(ints)
for i := 0; i < remaining; i++ {
ints = append(ints, dflt)
}
}
return ints
}
func (ap *AnsiParser) modeDispatch(param string, set bool) error {
switch param {
case "?3":
return ap.eventHandler.DECCOLM(set)
case "?6":
return ap.eventHandler.DECOM(set)
case "?25":
return ap.eventHandler.DECTCEM(set)
}
return nil
}
func (ap *AnsiParser) hDispatch(params []string) error {
if len(params) == 1 {
return ap.modeDispatch(params[0], true)
}
return nil
}
func (ap *AnsiParser) lDispatch(params []string) error {
if len(params) == 1 {
return ap.modeDispatch(params[0], false)
}
return nil
}
func getEraseParam(params []string) int {
param := getInt(params, 0)
if param < 0 || 3 < param {
param = 0
}
return param
}

119
vendor/github.com/Azure/go-ansiterm/parser_actions.go generated vendored Normal file
View File

@ -0,0 +1,119 @@
package ansiterm
func (ap *AnsiParser) collectParam() error {
currChar := ap.context.currentChar
ap.logf("collectParam %#x", currChar)
ap.context.paramBuffer = append(ap.context.paramBuffer, currChar)
return nil
}
func (ap *AnsiParser) collectInter() error {
currChar := ap.context.currentChar
ap.logf("collectInter %#x", currChar)
ap.context.paramBuffer = append(ap.context.interBuffer, currChar)
return nil
}
func (ap *AnsiParser) escDispatch() error {
cmd, _ := parseCmd(*ap.context)
intermeds := ap.context.interBuffer
ap.logf("escDispatch currentChar: %#x", ap.context.currentChar)
ap.logf("escDispatch: %v(%v)", cmd, intermeds)
switch cmd {
case "D": // IND
return ap.eventHandler.IND()
case "E": // NEL, equivalent to CRLF
err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN)
if err == nil {
err = ap.eventHandler.Execute(ANSI_LINE_FEED)
}
return err
case "M": // RI
return ap.eventHandler.RI()
}
return nil
}
func (ap *AnsiParser) csiDispatch() error {
cmd, _ := parseCmd(*ap.context)
params, _ := parseParams(ap.context.paramBuffer)
ap.logf("Parsed params: %v with length: %d", params, len(params))
ap.logf("csiDispatch: %v(%v)", cmd, params)
switch cmd {
case "@":
return ap.eventHandler.ICH(getInt(params, 1))
case "A":
return ap.eventHandler.CUU(getInt(params, 1))
case "B":
return ap.eventHandler.CUD(getInt(params, 1))
case "C":
return ap.eventHandler.CUF(getInt(params, 1))
case "D":
return ap.eventHandler.CUB(getInt(params, 1))
case "E":
return ap.eventHandler.CNL(getInt(params, 1))
case "F":
return ap.eventHandler.CPL(getInt(params, 1))
case "G":
return ap.eventHandler.CHA(getInt(params, 1))
case "H":
ints := getInts(params, 2, 1)
x, y := ints[0], ints[1]
return ap.eventHandler.CUP(x, y)
case "J":
param := getEraseParam(params)
return ap.eventHandler.ED(param)
case "K":
param := getEraseParam(params)
return ap.eventHandler.EL(param)
case "L":
return ap.eventHandler.IL(getInt(params, 1))
case "M":
return ap.eventHandler.DL(getInt(params, 1))
case "P":
return ap.eventHandler.DCH(getInt(params, 1))
case "S":
return ap.eventHandler.SU(getInt(params, 1))
case "T":
return ap.eventHandler.SD(getInt(params, 1))
case "c":
return ap.eventHandler.DA(params)
case "d":
return ap.eventHandler.VPA(getInt(params, 1))
case "f":
ints := getInts(params, 2, 1)
x, y := ints[0], ints[1]
return ap.eventHandler.HVP(x, y)
case "h":
return ap.hDispatch(params)
case "l":
return ap.lDispatch(params)
case "m":
return ap.eventHandler.SGR(getInts(params, 1, 0))
case "r":
ints := getInts(params, 2, 1)
top, bottom := ints[0], ints[1]
return ap.eventHandler.DECSTBM(top, bottom)
default:
ap.logf("ERROR: Unsupported CSI command: '%s', with full context: %v", cmd, ap.context)
return nil
}
}
func (ap *AnsiParser) print() error {
return ap.eventHandler.Print(ap.context.currentChar)
}
func (ap *AnsiParser) clear() error {
ap.context = &ansiContext{}
return nil
}
func (ap *AnsiParser) execute() error {
return ap.eventHandler.Execute(ap.context.currentChar)
}

71
vendor/github.com/Azure/go-ansiterm/states.go generated vendored Normal file
View File

@ -0,0 +1,71 @@
package ansiterm
type stateID int
type state interface {
Enter() error
Exit() error
Handle(byte) (state, error)
Name() string
Transition(state) error
}
type baseState struct {
name string
parser *AnsiParser
}
func (base baseState) Enter() error {
return nil
}
func (base baseState) Exit() error {
return nil
}
func (base baseState) Handle(b byte) (s state, e error) {
switch {
case b == CSI_ENTRY:
return base.parser.csiEntry, nil
case b == DCS_ENTRY:
return base.parser.dcsEntry, nil
case b == ANSI_ESCAPE_PRIMARY:
return base.parser.escape, nil
case b == OSC_STRING:
return base.parser.oscString, nil
case sliceContains(toGroundBytes, b):
return base.parser.ground, nil
}
return nil, nil
}
func (base baseState) Name() string {
return base.name
}
func (base baseState) Transition(s state) error {
if s == base.parser.ground {
execBytes := []byte{0x18}
execBytes = append(execBytes, 0x1A)
execBytes = append(execBytes, getByteRange(0x80, 0x8F)...)
execBytes = append(execBytes, getByteRange(0x91, 0x97)...)
execBytes = append(execBytes, 0x99)
execBytes = append(execBytes, 0x9A)
if sliceContains(execBytes, base.parser.context.currentChar) {
return base.parser.execute()
}
}
return nil
}
type dcsEntryState struct {
baseState
}
type errorState struct {
baseState
}

21
vendor/github.com/Azure/go-ansiterm/utilities.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package ansiterm
import (
"strconv"
)
func sliceContains(bytes []byte, b byte) bool {
for _, v := range bytes {
if v == b {
return true
}
}
return false
}
func convertBytesToInteger(bytes []byte) int {
s := string(bytes)
i, _ := strconv.Atoi(s)
return i
}

182
vendor/github.com/Azure/go-ansiterm/winterm/ansi.go generated vendored Normal file
View File

@ -0,0 +1,182 @@
// +build windows
package winterm
import (
"fmt"
"os"
"strconv"
"strings"
"syscall"
"github.com/Azure/go-ansiterm"
)
// Windows keyboard constants
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx.
const (
VK_PRIOR = 0x21 // PAGE UP key
VK_NEXT = 0x22 // PAGE DOWN key
VK_END = 0x23 // END key
VK_HOME = 0x24 // HOME key
VK_LEFT = 0x25 // LEFT ARROW key
VK_UP = 0x26 // UP ARROW key
VK_RIGHT = 0x27 // RIGHT ARROW key
VK_DOWN = 0x28 // DOWN ARROW key
VK_SELECT = 0x29 // SELECT key
VK_PRINT = 0x2A // PRINT key
VK_EXECUTE = 0x2B // EXECUTE key
VK_SNAPSHOT = 0x2C // PRINT SCREEN key
VK_INSERT = 0x2D // INS key
VK_DELETE = 0x2E // DEL key
VK_HELP = 0x2F // HELP key
VK_F1 = 0x70 // F1 key
VK_F2 = 0x71 // F2 key
VK_F3 = 0x72 // F3 key
VK_F4 = 0x73 // F4 key
VK_F5 = 0x74 // F5 key
VK_F6 = 0x75 // F6 key
VK_F7 = 0x76 // F7 key
VK_F8 = 0x77 // F8 key
VK_F9 = 0x78 // F9 key
VK_F10 = 0x79 // F10 key
VK_F11 = 0x7A // F11 key
VK_F12 = 0x7B // F12 key
RIGHT_ALT_PRESSED = 0x0001
LEFT_ALT_PRESSED = 0x0002
RIGHT_CTRL_PRESSED = 0x0004
LEFT_CTRL_PRESSED = 0x0008
SHIFT_PRESSED = 0x0010
NUMLOCK_ON = 0x0020
SCROLLLOCK_ON = 0x0040
CAPSLOCK_ON = 0x0080
ENHANCED_KEY = 0x0100
)
type ansiCommand struct {
CommandBytes []byte
Command string
Parameters []string
IsSpecial bool
}
func newAnsiCommand(command []byte) *ansiCommand {
if isCharacterSelectionCmdChar(command[1]) {
// Is Character Set Selection commands
return &ansiCommand{
CommandBytes: command,
Command: string(command),
IsSpecial: true,
}
}
// last char is command character
lastCharIndex := len(command) - 1
ac := &ansiCommand{
CommandBytes: command,
Command: string(command[lastCharIndex]),
IsSpecial: false,
}
// more than a single escape
if lastCharIndex != 0 {
start := 1
// skip if double char escape sequence
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
start++
}
// convert this to GetNextParam method
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
}
return ac
}
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
if index < 0 || index >= len(ac.Parameters) {
return defaultValue
}
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
if err != nil {
return defaultValue
}
return int16(param)
}
func (ac *ansiCommand) String() string {
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
bytesToHex(ac.CommandBytes),
ac.Command,
strings.Join(ac.Parameters, "\",\""))
}
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
func isAnsiCommandChar(b byte) bool {
switch {
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
return true
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
// non-CSI escape sequence terminator
return true
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
// String escape sequence terminator
return true
}
return false
}
func isXtermOscSequence(command []byte, current byte) bool {
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
}
func isCharacterSelectionCmdChar(b byte) bool {
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
}
// bytesToHex converts a slice of bytes to a human-readable string.
func bytesToHex(b []byte) string {
hex := make([]string, len(b))
for i, ch := range b {
hex[i] = fmt.Sprintf("%X", ch)
}
return strings.Join(hex, "")
}
// ensureInRange adjusts the passed value, if necessary, to ensure it is within
// the passed min / max range.
func ensureInRange(n int16, min int16, max int16) int16 {
if n < min {
return min
} else if n > max {
return max
} else {
return n
}
}
func GetStdFile(nFile int) (*os.File, uintptr) {
var file *os.File
switch nFile {
case syscall.STD_INPUT_HANDLE:
file = os.Stdin
case syscall.STD_OUTPUT_HANDLE:
file = os.Stdout
case syscall.STD_ERROR_HANDLE:
file = os.Stderr
default:
panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile))
}
fd, err := syscall.GetStdHandle(nFile)
if err != nil {
panic(fmt.Errorf("Invalid standard handle identifier: %v -- %v", nFile, err))
}
return file, uintptr(fd)
}

Some files were not shown because too many files have changed in this diff Show More