mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e858e86ed9 | |||
| 84679ed42a | |||
| b0610f034c | |||
| cb9c0ee32a | |||
| 3a5f4395e4 | |||
| 36a00f3464 | |||
| 703d0543af | |||
| fb3619645d | |||
| 46a9cc086d | |||
| 367d3edbef | |||
| d5b913d432 | |||
| 2dace5564f | |||
| 88b30626dd | |||
| 301d855015 | |||
| 7a193829f4 | |||
| 523716b090 | |||
| 36ca9015ff | |||
| a6dfc26959 | |||
| 924f6f120a | |||
| 2c1c94af9b | |||
| f5fbd6f22a | |||
| 55bdc029c3 | |||
| e42d422410 | |||
| bf5d1544b6 | |||
| 8876acbfec |
+38
-48
@@ -1,7 +1,7 @@
|
||||
# Contributing to Mainflux
|
||||
|
||||
The following is a set of guidelines for contributing to Mainflux and its libraries, which are
|
||||
hosted in the [Mainflux Organization](https://github.com/mainflux) on GitHub.
|
||||
hosted in the [Mainflux Organization](https://github.com/mainflux) on GitHub.
|
||||
|
||||
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to
|
||||
@@ -10,7 +10,7 @@ By participating, you are expected to uphold this code. Please report unacceptab
|
||||
## Reporting issues
|
||||
|
||||
Reporting issues is a great way to contribute to the project. We always appreciate a well-written,
|
||||
thorough bug reports.
|
||||
thorough bug reports.
|
||||
|
||||
Prior to raising a new issue, check out [our issue
|
||||
list](https://github.com/mainflux/mainflux/issues) to determine whether it already include the
|
||||
@@ -36,63 +36,53 @@ refactoring code etc.), otherwise you risk spending a lot of time working on som
|
||||
maintainers might not want to merge into the project.
|
||||
|
||||
Please adhere to the coding conventions used throughout the project. If in doubt, consult the
|
||||
following style guides:
|
||||
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
|
||||
|
||||
- [Effective Go](https://golang.org/doc/effective_go.html)
|
||||
- [Google's JavaScript styleguide](https://google.github.io/styleguide/jsguide.html)
|
||||
To start contributing to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
|
||||
clone your fork repository, and configure the remotes:
|
||||
|
||||
Adhering to the following process is the best way to get your work included in the project:
|
||||
```
|
||||
git clone https://github.com/<your-username>/mainflux.git
|
||||
cd mainflux
|
||||
git remote add upstream https://github.com/mainflux/mainflux.git
|
||||
```
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure
|
||||
the remotes:
|
||||
If you cloned a while ago, get the latest changes from upstream:
|
||||
|
||||
```bash
|
||||
# Clone your fork of the repo into the current directory
|
||||
git clone https://github.com/<your-username>/mainflux.git
|
||||
```
|
||||
git checkout master
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
# Navigate to the newly cloned directory
|
||||
cd mainflux
|
||||
Create a new topic branch from `master` using the naming convention `MF-[issue-number]`
|
||||
to help us keep track of your contribution scope:
|
||||
|
||||
# Assign the original repo to a remote called "upstream"
|
||||
git remote add upstream https://github.com/mainflux/mainflux.git
|
||||
```
|
||||
```
|
||||
git checkout -b MF-[issue-number]
|
||||
```
|
||||
|
||||
2. If you cloned a while ago, get the latest changes from upstream:
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure
|
||||
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
|
||||
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of working before making them public.
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
```
|
||||
Note that every commit you make must be signed. By signing off your work you indicate that you
|
||||
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||
|
||||
3. Create a new topic branch from `master` using the naming convention `mainflux-[issue-num]` to
|
||||
help us keep track of your contribution scope:
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
|
||||
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
|
||||
```bash
|
||||
git checkout -b mainflux-[issue-num]
|
||||
```
|
||||
Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
4. Commit your changes in logical chunks. When you are ready to commit, make sure to write a Good
|
||||
Commit Message™. Consult the [https://github.com/erlang/otp/wiki/Writing-good-commit-messages](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
|
||||
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of working before making them public.
|
||||
```
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
Note that every commit you make must be signed. By signing off your work you indicate that you
|
||||
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||
Push your topic branch up to your fork:
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
|
||||
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
```
|
||||
git push origin MF-[issue-number]
|
||||
```
|
||||
|
||||
5. Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
```bash
|
||||
git pull [--rebase] upstream master
|
||||
```
|
||||
|
||||
6. Push your topic branch up to your fork:
|
||||
|
||||
```bash
|
||||
git push origin mainflux-[issue-num]
|
||||
```
|
||||
|
||||
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
and detailed description.
|
||||
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
and detailed description.
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
build
|
||||
*.pb.go
|
||||
|
||||
site/
|
||||
|
||||
Generated
+23
-17
@@ -25,8 +25,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/asaskevich/govalidator"
|
||||
packages = ["."]
|
||||
revision = "73945b6115bfbbcc57d89b7316e28109364124e1"
|
||||
version = "v7"
|
||||
revision = "ccb8e960c48f04d6935e72476ae4a51028f9e22f"
|
||||
version = "v9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -67,8 +67,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29"
|
||||
version = "v3.1.0"
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -100,7 +100,7 @@
|
||||
"pkg/term",
|
||||
"pkg/term/windows"
|
||||
]
|
||||
revision = "4d9beb4607404e4d756052aca7041517788f7e75"
|
||||
revision = "72ba7f593fa4dbb628cf5ee83cd7daf955934cf5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-connections"
|
||||
@@ -124,7 +124,7 @@
|
||||
name = "github.com/fsouza/go-dockerclient"
|
||||
packages = ["."]
|
||||
revision = "ca33ff277b527ce11b793e62f9ba244129b01caf"
|
||||
version = "1.2.0"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-kit/kit"
|
||||
@@ -170,19 +170,25 @@
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jinzhu/gorm"
|
||||
packages = [
|
||||
".",
|
||||
"dialects/postgres"
|
||||
]
|
||||
revision = "58e34726dfc069b558038efbaa25555f182d1f7a"
|
||||
revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166"
|
||||
version = "v1.9.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/inflection"
|
||||
packages = ["."]
|
||||
revision = "1c35d901db3da928c72a72d8458480cc9ade058f"
|
||||
revision = "04140366298a54a039076d798123ffa108fff46c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -281,7 +287,7 @@
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
|
||||
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -292,7 +298,7 @@
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "282c8707aa210456a825798969cc27edda34992a"
|
||||
revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
@@ -303,8 +309,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
|
||||
version = "v1.0.4"
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sony/gobreaker"
|
||||
@@ -357,7 +363,7 @@
|
||||
"blowfish",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "650f4a345ab4e5b245a3034b110ebc7299e68186"
|
||||
revision = "374053ea96cb300f8671b8d3b07edeeb06e203b4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -366,7 +372,7 @@
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb"
|
||||
revision = "24dd3780ca4f75fed9f321890729414a4b5d3f13"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -375,7 +381,7 @@
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "88d2dcc510266da9f7f8c7f34e1940716cab5f5c"
|
||||
revision = "01acb38716e021ed1fc03a602bdb5838e1358c5e"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
@@ -386,6 +392,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "9df3ffee2bc61a6ce786baa7570eb61d8275197ea36560cf0cfd7cc0ea53af96"
|
||||
inputs-digest = "c1b28e90e21e838cec2d16c95d8eb414b5de9bbe9667acb01b5375db047cc6db"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+36
-20
@@ -1,14 +1,22 @@
|
||||
[[constraint]]
|
||||
name = "github.com/asaskevich/govalidator"
|
||||
version = "7.0.0"
|
||||
version = "9.0.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
version = "3.0.0"
|
||||
version = "3.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-kit/kit"
|
||||
@@ -19,16 +27,20 @@
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jinzhu/gorm"
|
||||
version = "1.9.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
version = "1.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
version = "3.1.6"
|
||||
version = "1.4.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
@@ -42,22 +54,26 @@
|
||||
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"
|
||||
|
||||
[[constraint]]
|
||||
name = "go.uber.org/zap"
|
||||
version = "1.7.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
version = "3.1.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Mainflux
|
||||
Copyright 2015-2018 Mainflux
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
BUILD_DIR=build
|
||||
SERVICES=manager http normalizer coap
|
||||
DOCKERS=$(addprefix docker_,$(SERVICES))
|
||||
|
||||
all: $(SERVICES)
|
||||
.PHONY: all $(SERVICES) docker
|
||||
BUILD_DIR = build
|
||||
SERVICES = manager http normalizer ws
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOOS ?= linux
|
||||
|
||||
define compile_service
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
endef
|
||||
|
||||
define make_docker
|
||||
docker build --build-arg SVC_NAME=$(subst docker_,,$(1)) --tag=mainflux/$(subst docker_,,$(1)) -f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
manager:
|
||||
$(call compile_service,$(@))
|
||||
all: $(SERVICES)
|
||||
|
||||
http:
|
||||
$(call compile_service,$(@))
|
||||
|
||||
normalizer:
|
||||
$(call compile_service,$(@))
|
||||
|
||||
coap:
|
||||
$(call compile_service,$(@))
|
||||
.PHONY: all $(SERVICES) dockers latest release
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
@@ -31,18 +22,27 @@ clean:
|
||||
install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
# Docker
|
||||
docker_manager:
|
||||
proto:
|
||||
protoc --go_out=. *.proto
|
||||
|
||||
$(SERVICES): proto
|
||||
$(call compile_service,$(@))
|
||||
|
||||
$(DOCKERS):
|
||||
$(call make_docker,$(@))
|
||||
|
||||
docker_http:
|
||||
$(call make_docker,$(@))
|
||||
dockers: $(DOCKERS)
|
||||
|
||||
docker_normalizer:
|
||||
$(call make_docker,$(@))
|
||||
|
||||
docker_coap:
|
||||
$(call make_docker,$(@))
|
||||
|
||||
docker: $(DOCKERS)
|
||||
latest: dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc; \
|
||||
done
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
docker push mainflux/$$svc:$(version); \
|
||||
done
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[![build][ci-badge]][ci-url]
|
||||
[![go report card][grc-badge]][grc-url]
|
||||
[![coverage][cov-badge]][cov-url]
|
||||
[![license][license]](LICENSE)
|
||||
[![chat][gitter-badge]][gitter]
|
||||
|
||||
@@ -33,8 +34,7 @@ Before proceeding, install the following prerequisites:
|
||||
Once everything is installed, execute the following commands from project root:
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
docker-compose up -d
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Contributing
|
||||
@@ -51,7 +51,7 @@ Thank you for your interest in Mainflux and wish to contribute!
|
||||
- [Gitter][gitter]
|
||||
- [Twitter][twitter]
|
||||
|
||||
[banner]: https://github.com/mainflux/doc/blob/master/docs/img/gopherBanner.jpg
|
||||
[banner]: https://github.com/mainflux/mainflux/blob/master/docs/img/gopherBanner.jpg
|
||||
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
|
||||
[ci-url]: https://semaphoreci.com/mainflux/mainflux
|
||||
[docs]: http://mainflux.readthedocs.io
|
||||
@@ -61,5 +61,7 @@ Thank you for your interest in Mainflux and wish to contribute!
|
||||
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
|
||||
[grc-badge]: https://goreportcard.com/badge/github.com/mainflux/mainflux
|
||||
[grc-url]: https://goreportcard.com/report/github.com/mainflux/mainflux
|
||||
[cov-badge]: https://codecov.io/gh/mainflux/mainflux/branch/master/graph/badge.svg
|
||||
[cov-url]: https://codecov.io/gh/mainflux/mainflux
|
||||
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
|
||||
+6
-5
@@ -7,12 +7,12 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"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"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
@@ -40,12 +40,11 @@ func main() {
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Log("error", err)
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
@@ -75,6 +74,7 @@ func main() {
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
|
||||
}()
|
||||
|
||||
@@ -84,5 +84,6 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
logger.Log("terminated", <-errs)
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("HTTP adapter terminated: %s", err))
|
||||
}
|
||||
|
||||
+6
-5
@@ -7,9 +7,9 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/api"
|
||||
"github.com/mainflux/mainflux/manager/bcrypt"
|
||||
@@ -56,12 +56,11 @@ func main() {
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Log("error", err)
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
@@ -94,6 +93,7 @@ func main() {
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("Manager service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}()
|
||||
|
||||
@@ -103,5 +103,6 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
logger.Log("terminated", <-errs)
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Manager service terminated: %s", err))
|
||||
}
|
||||
|
||||
+25
-6
@@ -7,10 +7,13 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/normalizer"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,12 +34,11 @@ func main() {
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := nats.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Log("error", fmt.Sprintf("Failed to connect: %s", err))
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
@@ -45,6 +47,7 @@ func main() {
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("Normalizer service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
|
||||
}()
|
||||
|
||||
@@ -54,6 +57,22 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
normalizer.Subscribe(nc, logger)
|
||||
logger.Log("terminated", <-errs)
|
||||
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "normalizer",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"})
|
||||
|
||||
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "normalizer",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"})
|
||||
|
||||
normalizer.Subscribe(nc, logger, counter, latency)
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Normalizer service terminated: %s", err))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
adapter "github.com/mainflux/mainflux/ws"
|
||||
"github.com/mainflux/mainflux/ws/api"
|
||||
"github.com/mainflux/mainflux/ws/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort = "8180"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defManagerURL = "http://localhost:8180"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envManagerURL = "MF_MANAGER_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ManagerURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
pubsub := nats.New(nc)
|
||||
svc := adapter.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "ws_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "ws_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc, logger))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
|
||||
}
|
||||
+1
-1
@@ -42,7 +42,7 @@ func (ca *CoAPAdapter) Serve(addr string) error {
|
||||
return coap.ListenAndServe("udp", addr, ca.COAPServer())
|
||||
}
|
||||
|
||||
// BridgeHandler functions is a handler for messages recieved via NATS
|
||||
// BridgeHandler functions is a handler for messages received via NATS
|
||||
func (ca *CoAPAdapter) BridgeHandler(nm *broker.Msg) {
|
||||
log.Printf("Received a message: %s\n", string(nm.Data))
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
@@ -22,7 +21,7 @@ func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
|
||||
}
|
||||
|
||||
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+7
-4
@@ -1,10 +1,13 @@
|
||||
FROM golang:1.9-alpine AS builder
|
||||
ARG SVC_NAME
|
||||
|
||||
WORKDIR /go/src/github.com/mainflux/mainflux
|
||||
COPY . .
|
||||
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
|
||||
&& apk add make protobuf git \
|
||||
&& go get -u github.com/golang/protobuf/protoc-gen-go \
|
||||
&& make $SVC_NAME \
|
||||
&& mv build/mainflux-$SVC_NAME /exe
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /exe /
|
||||
|
||||
@@ -12,7 +12,8 @@ services:
|
||||
nginx:
|
||||
image: nginx:1.13-alpine
|
||||
container_name: mainflux-nginx
|
||||
network_mode: bridge
|
||||
restart:
|
||||
on-failure
|
||||
volumes:
|
||||
- $PWD/nginx.conf:/etc/nginx/nginx.conf
|
||||
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
|
||||
@@ -26,21 +27,28 @@ services:
|
||||
nats:
|
||||
image: nats:1.0.2
|
||||
container_name: mainflux-nats
|
||||
network_mode: bridge
|
||||
restart:
|
||||
on-failure
|
||||
|
||||
postgres:
|
||||
image: postgres:10.2-alpine
|
||||
container_name: mainflux-postgres
|
||||
network_mode: bridge
|
||||
restart:
|
||||
on-failure
|
||||
environment:
|
||||
POSTGRES_USER: mainflux
|
||||
POSTGRES_PASSWORD: mainflux
|
||||
POSTGRES_DB: mainflux
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
manager:
|
||||
image: mainflux/manager:latest
|
||||
container_name: mainflux-manager
|
||||
network_mode: bridge
|
||||
expose:
|
||||
- 8180
|
||||
restart:
|
||||
on-failure
|
||||
environment:
|
||||
MF_DB_HOST: postgres
|
||||
MF_MANAGER_DB: mainflux
|
||||
@@ -50,7 +58,10 @@ services:
|
||||
normalizer:
|
||||
image: mainflux/normalizer:latest
|
||||
container_name: mainflux-normalizer
|
||||
network_mode: bridge
|
||||
restart:
|
||||
on-failure
|
||||
expose:
|
||||
- 8181
|
||||
environment:
|
||||
MF_NATS_URL: "nats://nats:4222"
|
||||
MF_NORMALIZER_PORT: 8181
|
||||
@@ -58,9 +69,12 @@ services:
|
||||
http-adapter:
|
||||
image: mainflux/http:latest
|
||||
container_name: mainflux-http
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
restart:
|
||||
on-failure
|
||||
expose:
|
||||
- 8182
|
||||
environment:
|
||||
MF_MANAGER_URL: "http://manager:8180"
|
||||
MF_NATS_URL: "nats://nats:4222"
|
||||
@@ -69,10 +83,13 @@ services:
|
||||
mqtt-adapter:
|
||||
image: mainflux/mqtt-adapter:latest
|
||||
container_name: mainflux-mqtt
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
restart:
|
||||
on-failure
|
||||
environment:
|
||||
MQTT_ADAPTER_NATS_URL: "nats://nats:4222"
|
||||
AUTH_URL: "http://manager"
|
||||
AUTH_PORT: 8180
|
||||
ports:
|
||||
- "1883:1883"
|
||||
|
||||
+5
-4
@@ -69,9 +69,10 @@ http {
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
server_name localhost;
|
||||
return 302 https://$server_name$request_uri;
|
||||
access_log off;
|
||||
error_log off;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS
|
||||
@@ -132,7 +133,7 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://docker-manager/;
|
||||
proxy_pass http://docker-manager;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
@@ -149,7 +150,7 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://docker-http/;
|
||||
proxy_pass http://docker-http;
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDhDCCAmygAwIBAgIJAPOWQ/k/52/dMA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsMA0lvVDEg
|
||||
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTcwMTA3MDA0MzE4
|
||||
WhcNMzIwMTA0MDA0MzE4WjBXMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAPBgNVBAoM
|
||||
CE1haW5mbHV4MQwwCgYDVQQLDANJb1QxIDAeBgkqhkiG9w0BCQEWEWluZm9AbWFp
|
||||
bmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzOueTshY
|
||||
eIDpcedEB+QcgmON72NXVkZ9s9rbNMCCLRoEl947m0SBfz4RXXXRh/plHNgEInsC
|
||||
Z90LpKhyo5X8U1FIzTNefg3iMdov+wOGnGQJHydLSEbzWCDHjP54+W7XlQaA4/GT
|
||||
76Os4m6e5LyLdb6jIQQx7EzZnJFQ9g85eldqEGz/tBn6eDLpHSt0ZidClUEGIJcV
|
||||
5eeGRxKTrExnD1mSkDa63/s6M4fxkacxrwxnwVUdnSvHDxTOt5Mctrqlun63Sn+W
|
||||
+9Afr5fZUdgROLkLhxWRVUbXMvh3OI5zMHpcONCvIr+gMEeM4q+AyVjCzYzMrDZ7
|
||||
pvuhM4PGeAMAIwIDAQABo1MwUTAdBgNVHQ4EFgQUiBGMTsVJvNuSKvwoi/7aEvGf
|
||||
RX0wHwYDVR0jBBgwFoAUiBGMTsVJvNuSKvwoi/7aEvGfRX0wDwYDVR0TAQH/BAUw
|
||||
AwEB/zANBgkqhkiG9w0BAQ0FAAOCAQEARgi7Cr4JJmoOuP795OYxpVxZBxqnHOMi
|
||||
2qFeiAwlg3M310YvZaJoewVK44hGNKtRbxc1CelEhRMCFabU5/xQOJmmx3bogNUc
|
||||
944IfreJyJuUy7q3/6Ix3jf/rH1dpUVZ0S6baldIPKPzNNvmO6VFfmb+dymrMPCM
|
||||
VQ4w+XoipLS5lPvG4z2/xz2auEAM5qtVjWQFB51Ju63bqKDQTwi+TaQSAu+9Dwr+
|
||||
10w7qRZogM0anb8Q4Aq14Y7kplnu22VTVHaTyWb5FW4loWKP8FabzNCTOWneidVV
|
||||
ApEfujCy2Bxw+bNxDtIp1ovPv2vljtJfdnFAKfBElw9BSPg/exUMHw==
|
||||
-----END CERTIFICATE-----
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.github/CONTRIBUTING.md
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -0,0 +1,47 @@
|
||||
## Components
|
||||
|
||||
Mainflux IoT platform is comprised of the following services:
|
||||
|
||||
| Service | Description |
|
||||
|:--------------------------------------------------------------------------|:------------------------------------------------------------------------|
|
||||
| [manager](https://github.com/mainflux/mainflux/tree/master/manager) | Manages platform entities, and auth concerns |
|
||||
| [http-adapter](https://github.com/mainflux/mainflux/tree/master/http) | Provides an HTTP interface for accessing communication channels |
|
||||
| [normalizer](https://github.com/mainflux/mainflux/tree/master/normalizer) | Normalizes SenML messages and generates the "processed" messages stream |
|
||||
|
||||
> The following diagram is an (obsolete) overview of platform architecture
|
||||
|
||||

|
||||
|
||||
## Domain model
|
||||
|
||||
The platform is built around 3 main entities: **users**, **clients** and **channels**.
|
||||
|
||||
`User` represents the real (human) user of the system. It is represented via its
|
||||
e-mail and password, which he uses as platform access credentials in order to obtain
|
||||
an access token. Once logged into the system, user can manage his resources in
|
||||
CRUD fashion (i.e. channels and clients), and define access control policies
|
||||
between them.
|
||||
|
||||
`Device` is used to represent any device that connects to Mainflux. It is a
|
||||
generic model that describes any client device of the system.
|
||||
|
||||
`Application` is very similar to the `Device` and is represented by the same
|
||||
`Client` structure (just with different `type` info). Application represents
|
||||
an end-user application that communicates with devices through Mainflux, and
|
||||
can be running somewhere in the cloud, locally on the PC or on the mobile phone.
|
||||
Usually it acquires data from sensor measurement and displays it on various
|
||||
dashboards.
|
||||
|
||||
`Channel` represents a communication channel. It serves as message topic that
|
||||
can be consumed by all of the clients connected to it.
|
||||
|
||||
## Messaging
|
||||
|
||||
Mainflux uses [NATS](https://nats.io) as its messaging backbone, due to its
|
||||
lightweight and performant nature. You can treat its *subjects* as physical
|
||||
representation of Mainflux channels, where subject name is constructed using
|
||||
channel unique identifier.
|
||||
|
||||
In general, there is no constrained put on content that is being exchanged
|
||||
through channels. However, in order to be post-processed and normalized,
|
||||
messages should be formatted using [SenML](https://tools.ietf.org/html/draft-ietf-core-senml-08).
|
||||
@@ -0,0 +1,284 @@
|
||||
## Prerequisites
|
||||
|
||||
Before proceeding, install the following prerequisites:
|
||||
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/)
|
||||
- [jsonpp](https://jmhodges.github.io/jsonpp/) (optional)
|
||||
|
||||
Once everything is installed, execute the following commands from project root:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## User management
|
||||
|
||||
### Account creation
|
||||
|
||||
Use the Mainflux API to create user account:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" https://localhost/users -d '{"email":"john.doe@email.com", "password":"123"}'
|
||||
```
|
||||
|
||||
Note that when using official `docker-compose`, all services are behind `nginx`
|
||||
proxy and all traffic is `TLS` encrypted.
|
||||
|
||||
### Obtaining an authorization key
|
||||
|
||||
In order for this user to be able to authenticate to the system, you will have
|
||||
to create an authorization token for him:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" https://localhost/tokens -d '{"email":"john.doe@email.com", "password":"123"}'
|
||||
```
|
||||
|
||||
Response should look like this:
|
||||
```
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjMzODg0NzcsImlhdCI6MTUyMzM1MjQ3NywiaXNzIjoibWFpbmZsdXgiLCJzdWIiOiJqb2huLmRvZUBlbWFpbC5jb20ifQ.cygz9zoqD7Rd8f88hpQNilTCAS1DrLLgLg4PRcH-iAI"
|
||||
}
|
||||
```
|
||||
|
||||
## System provisioning
|
||||
|
||||
Before proceeding, make sure that you have created a new account, and obtained
|
||||
an authorization key.
|
||||
|
||||
### Provisioning devices
|
||||
|
||||
Devices are provisioned by executing request `POST /clients`, with a
|
||||
`"type":"device"` specified in JSON payload. Note that you will also need
|
||||
`user_auth_token` in order to provision clients (both devices and application)
|
||||
that belong to this particular user.
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"device", "name":"weio"}'
|
||||
```
|
||||
|
||||
Response will contain `Location` header whose value represents path to newly
|
||||
created client:
|
||||
|
||||
```
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Location: /clients/81380742-7116-4f6f-9800-14fe464f6773
|
||||
Date: Tue, 10 Apr 2018 10:02:59 GMT
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
### Provisioning applications
|
||||
|
||||
Applications are provisioned by executing HTTP request `POST /clients`, with
|
||||
`"type":"app"` specified in JSON payload.
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"app", "name":"myapp"}'
|
||||
```
|
||||
|
||||
Response will contain `Location` header whose value represents path to newly
|
||||
created client (same as for devices):
|
||||
|
||||
```
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Location: /clients/cb63f852-2d48-44f0-a0cf-e450496c6c92
|
||||
Date: Tue, 10 Apr 2018 10:33:17 GMT
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
### Retrieving provisioned clients
|
||||
|
||||
In order to retrieve data of provisioned clients that is written in database, you
|
||||
can send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients
|
||||
```
|
||||
|
||||
Notice that you will receive only those clients that were provisioned by
|
||||
`user_auth_token` owner.
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Tue, 10 Apr 2018 10:50:12 GMT
|
||||
Content-Length: 1105
|
||||
|
||||
{
|
||||
"clients": [
|
||||
{
|
||||
"id": "81380742-7116-4f6f-9800-14fe464f6773",
|
||||
"type": "device",
|
||||
"name": "weio",
|
||||
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTQ1NzksImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiODEzODA3NDItNzExNi00ZjZmLTk4MDAtMTRmZTQ2NGY2NzczIn0.5s8s1hlK-l30kQAyHxEZO_M2NIQw53MQuy7b3Wf3OOE"
|
||||
},
|
||||
{
|
||||
"id": "cb63f852-2d48-44f0-a0cf-e450496c6c92",
|
||||
"type": "app",
|
||||
"name": "myapp",
|
||||
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTYzOTcsImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiY2I2M2Y4NTItMmQ0OC00NGYwLWEwY2YtZTQ1MDQ5NmM2YzkyIn0.FE6DWB3yJmBb8uojpQJaKUEbD0Elrjx0HhJA28bVzkU"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can specify `offset` and `limit` parameters in order to fetch specific
|
||||
group of clients. In that case, your request should look like:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients?offset=0&limit=5
|
||||
```
|
||||
|
||||
If you don't provide them, default values will be used instead: 0 for `offset`,
|
||||
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
|
||||
invalid values will be considered malformed request.
|
||||
|
||||
### Removing clients
|
||||
|
||||
In order to remove you own client you can send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/clients/<client_id>
|
||||
```
|
||||
|
||||
### Provisioning channels
|
||||
|
||||
Channels are provisioned by executing request `POST /channels`:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" -H "Authorization: <user_auth_token>" https://localhost/channels -d '{"name":"mychan"}'
|
||||
```
|
||||
|
||||
After sending request you should receive response with `Location` header that
|
||||
contains path to newly created channel:
|
||||
|
||||
```
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Location: /channels/19daa7a8-a489-4571-8714-ef1a214ed914
|
||||
Date: Tue, 10 Apr 2018 11:30:07 GMT
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
### Retrieving provisioned channels
|
||||
|
||||
To retreve provisioned channels you should send request to `/channels` with
|
||||
authorization token in `Authorization` header:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels
|
||||
```
|
||||
|
||||
Note that you will receive only those channels that were created by authorization
|
||||
token's owner.
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Tue, 10 Apr 2018 11:38:06 GMT
|
||||
Content-Length: 139
|
||||
|
||||
{
|
||||
"channels": [
|
||||
{
|
||||
"id": "19daa7a8-a489-4571-8714-ef1a214ed914",
|
||||
"name": "mychan"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can specify `offset` and `limit` parameters in order to fetch specific
|
||||
group of channels. In that case, your request should look like:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels?offset=0&limit=5
|
||||
```
|
||||
|
||||
If you don't provide them, default values will be used instead: 0 for `offset`,
|
||||
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
|
||||
invalid values will be considered malformed request.
|
||||
|
||||
### Removing channels
|
||||
|
||||
In order to remove specific channel you should send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
|
||||
```
|
||||
|
||||
## Access control
|
||||
|
||||
Channel can be observed as a communication group of clients. Only clients that
|
||||
are connected to the channel can send and receive messages from other clients
|
||||
in this channel. Clients that are not connected to this channel are not allowed
|
||||
to communicate over it.
|
||||
|
||||
Only user, who is the owner of a channel and of the clients, can connect the
|
||||
clients to the channel (which is equivalent of giving permissions to these clients
|
||||
to communicate over given communication group).
|
||||
|
||||
To connect client to the channel you should send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X PUT -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
|
||||
```
|
||||
|
||||
You can observe which clients are connected to specific channel:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
|
||||
```
|
||||
|
||||
You should receive response with the lists of connected clients in `connected` field
|
||||
similar to this one:
|
||||
|
||||
```
|
||||
{
|
||||
"id": "19daa7a8-a489-4571-8714-ef1a214ed914",
|
||||
"name": "mychan",
|
||||
"connected": [
|
||||
{
|
||||
"id": "81380742-7116-4f6f-9800-14fe464f6773",
|
||||
"type": "device",
|
||||
"name": "weio",
|
||||
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTQ1NzksImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiODEzODA3NDItNzExNi00ZjZmLTk4MDAtMTRmZTQ2NGY2NzczIn0.5s8s1hlK-l30kQAyHxEZO_M2NIQw53MQuy7b3Wf3OOE"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you want to disconnect your device from the channel, send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
|
||||
```
|
||||
|
||||
## Sending messages
|
||||
|
||||
Once a channel is provisioned and client is connected to it, it can start to
|
||||
publish messages on the channel. The following sections will provide an example
|
||||
of message publishing for each of the supported protocols.
|
||||
|
||||
### HTTP
|
||||
|
||||
To publish message over channel, client should send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/senml+json" -H "Authorization: <client_token>" https://localhost/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
|
||||
```
|
||||
|
||||
Note that you should always send array of messages in senML format.
|
||||
|
||||
### WebSocket
|
||||
|
||||
To publish and receive messages over channel using web socket, you should first
|
||||
send handshake request to `/channels/<channel_id>/messages` path. Don't forget
|
||||
to send `Authorization` header with client authorization token.
|
||||
|
||||
If you are not able to send custom headers in your handshake request, send it as
|
||||
query parameter `authorization`. Then your path should look like this
|
||||
`/channels/<channel_id>/messages?authorization=<client_auth_key>`.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,17 @@
|
||||
## What is Mainflux?
|
||||
|
||||
Mainflux is modern, scalable, secure open source and patent-free IoT cloud platform written in Go.
|
||||
|
||||
It accepts user, device, and application connections over various network protocols (i.e. HTTP,
|
||||
MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the IoT middleware
|
||||
for building complex IoT solutions.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Protocol bridging (i.e. HTTP, MQTT, WebSocket, CoAP)
|
||||
- Device management and provisioning
|
||||
- Fine-grained access control
|
||||
- Platform logging and instrumentation support
|
||||
- Container-based deployment using Docker
|
||||
+9
-6
@@ -1,10 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ mainflux.MessagePublisher = (*loggingMiddleware)(nil)
|
||||
@@ -19,12 +20,14 @@ func LoggingMiddleware(svc mainflux.MessagePublisher, logger log.Logger) mainflu
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "publish",
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method publish took %s to complete", time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Publish(msg)
|
||||
|
||||
+10
-7
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
@@ -18,10 +19,9 @@ import (
|
||||
const protocol string = "http"
|
||||
|
||||
var (
|
||||
errMalformedData error = errors.New("malformed SenML data")
|
||||
errUnknownType error = errors.New("unknown content type")
|
||||
errUnauthorizedAccess error = errors.New("missing or invalid credentials provided")
|
||||
auth manager.ManagerClient
|
||||
errMalformedData error = errors.New("malformed SenML data")
|
||||
errNotFound error = errors.New("non-existent entity")
|
||||
auth manager.ManagerClient
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
@@ -73,11 +73,14 @@ func authorize(r *http.Request) (string, error) {
|
||||
apiKey := r.Header.Get("Authorization")
|
||||
|
||||
if apiKey == "" {
|
||||
return "", errUnauthorizedAccess
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
// extract ID from /channels/:id/messages
|
||||
c := strings.Split(r.URL.Path, "/")[2]
|
||||
if !govalidator.IsUUID(c) {
|
||||
return "", errNotFound
|
||||
}
|
||||
|
||||
id, err := auth.CanAccess(c, apiKey)
|
||||
if err != nil {
|
||||
@@ -106,8 +109,8 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch err {
|
||||
case errMalformedData:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errUnknownType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case manager.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/mocks"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
token = "auth_token"
|
||||
invalidToken = "invalid_token"
|
||||
msg = `[{"n":"current","t":-1,"v":1.6}]`
|
||||
)
|
||||
|
||||
func newService() mainflux.MessagePublisher {
|
||||
pub := mocks.NewPublisher()
|
||||
return adapter.New(pub)
|
||||
}
|
||||
|
||||
func newHTTPServer(pub mainflux.MessagePublisher, mc manager.ManagerClient) *httptest.Server {
|
||||
mux := api.MakeHandler(pub, mc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func newManagerServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") == "invalid_token" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
||||
|
||||
func newManagerClient(url string) manager.ManagerClient {
|
||||
return manager.NewClient(url)
|
||||
}
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
mcServer := newManagerServer()
|
||||
defer mcServer.Close()
|
||||
mc := newManagerClient(mcServer.URL)
|
||||
|
||||
pub := newService()
|
||||
ts := newHTTPServer(pub, mc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := map[string]struct {
|
||||
chanID string
|
||||
msg string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
"publish message": {id, msg, "application/senml+json", token, http.StatusAccepted},
|
||||
"publish message with no authorization token": {id, msg, "application/senml+json", "", http.StatusForbidden},
|
||||
"publish message with invalid authorization token": {id, msg, "application/senml+json", invalidToken, http.StatusForbidden},
|
||||
"publish message with no content type": {id, msg, "", token, http.StatusAccepted},
|
||||
"publish message with invalid channel id": {"1", msg, "application/senml+json", token, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/channels/%s/messages", ts.URL, tc.chanID),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.msg),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package mocks
|
||||
|
||||
import "github.com/mainflux/mainflux"
|
||||
|
||||
var _ (mainflux.MessagePublisher) = (*mockPublisher)(nil)
|
||||
|
||||
type mockPublisher struct{}
|
||||
|
||||
// NewPublisher returns mock message publisher.
|
||||
func NewPublisher() mainflux.MessagePublisher {
|
||||
return mockPublisher{}
|
||||
}
|
||||
|
||||
func (pub mockPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
return nil
|
||||
}
|
||||
@@ -2,14 +2,13 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const topic string = "src.http"
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
type natsPublisher struct {
|
||||
@@ -22,10 +21,11 @@ func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
|
||||
}
|
||||
|
||||
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pub.nc.Publish(topic, data)
|
||||
subject := fmt.Sprintf("channel.%s", msg.Channel)
|
||||
return pub.nc.Publish(subject, data)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ paths:
|
||||
description: Message discarded due to its malformed content.
|
||||
403:
|
||||
description: Message discarded due to missing or invalid credentials.
|
||||
404:
|
||||
description: Message discarded due to invalid channel id.
|
||||
415:
|
||||
description: Message discarded due to invalid or missing content type.
|
||||
500:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package logger contains logger API definition, wrapper that
|
||||
// can be used around any other logger.
|
||||
package logger
|
||||
@@ -0,0 +1,23 @@
|
||||
package logger
|
||||
|
||||
const (
|
||||
// Error level is used when logging errors.
|
||||
Error Level = iota + 1
|
||||
// Warn level is used when logging warnings.
|
||||
Warn
|
||||
// Info level is used when logging info data.
|
||||
Info
|
||||
)
|
||||
|
||||
// Level represents severity level while logging.
|
||||
type Level int
|
||||
|
||||
var levels = map[Level]string{
|
||||
Error: "error",
|
||||
Warn: "warn",
|
||||
Info: "info",
|
||||
}
|
||||
|
||||
func (lvl Level) String() string {
|
||||
return levels[lvl]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
)
|
||||
|
||||
// Logger specifies logging API.
|
||||
type Logger interface {
|
||||
// Info logs any object in JSON format on info level.
|
||||
Info(string)
|
||||
// Warn logs any object in JSON format on warning level.
|
||||
Warn(string)
|
||||
// Error logs any object in JSON format on error level.
|
||||
Error(string)
|
||||
}
|
||||
|
||||
var _ Logger = (*logger)(nil)
|
||||
|
||||
type logger struct {
|
||||
kitLogger log.Logger
|
||||
}
|
||||
|
||||
// New returns wrapped go kit logger.
|
||||
func New(out io.Writer) Logger {
|
||||
l := log.NewJSONLogger(log.NewSyncWriter(out))
|
||||
l = log.With(l, "ts", log.DefaultTimestampUTC)
|
||||
return &logger{l}
|
||||
}
|
||||
|
||||
func (l logger) Info(msg string) {
|
||||
l.kitLogger.Log("level", Info.String(), "message", msg)
|
||||
}
|
||||
|
||||
func (l logger) Warn(msg string) {
|
||||
l.kitLogger.Log("level", Warn.String(), "message", msg)
|
||||
}
|
||||
|
||||
func (l logger) Error(msg string) {
|
||||
l.kitLogger.Log("level", Error.String(), "message", msg)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var _ io.Writer = (*mockWriter)(nil)
|
||||
|
||||
type mockWriter struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (writer *mockWriter) Write(p []byte) (int, error) {
|
||||
writer.value = p
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (writer *mockWriter) Read() (logMsg, error) {
|
||||
var output logMsg
|
||||
err := json.Unmarshal(writer.value, &output)
|
||||
return output, err
|
||||
}
|
||||
|
||||
type logMsg struct {
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
output logMsg
|
||||
}{
|
||||
"info log ordinary string": {"input_string", logMsg{log.Info.String(), "input_string"}},
|
||||
"info log empty string": {"", logMsg{log.Info.String(), ""}},
|
||||
}
|
||||
|
||||
writer := mockWriter{}
|
||||
logger := log.New(&writer)
|
||||
|
||||
for desc, tc := range cases {
|
||||
logger.Info(tc.input)
|
||||
output, err := writer.Read()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
|
||||
assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", desc, tc.output, output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarn(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
output logMsg
|
||||
}{
|
||||
"warn log ordinary string": {"input_string", logMsg{log.Warn.String(), "input_string"}},
|
||||
"warn log empty string": {"", logMsg{log.Warn.String(), ""}},
|
||||
}
|
||||
|
||||
writer := mockWriter{}
|
||||
logger := log.New(&writer)
|
||||
|
||||
for desc, tc := range cases {
|
||||
logger.Warn(tc.input)
|
||||
output, err := writer.Read()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
|
||||
assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", desc, tc.output, output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
output logMsg
|
||||
}{
|
||||
"error log ordinary string": {"input_string", logMsg{log.Error.String(), "input_string"}},
|
||||
"error log empty string": {"", logMsg{log.Error.String(), ""}},
|
||||
}
|
||||
|
||||
writer := mockWriter{}
|
||||
logger := log.New(&writer)
|
||||
|
||||
for desc, tc := range cases {
|
||||
logger.Error(tc.input)
|
||||
output, err := writer.Read()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
|
||||
assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", desc, tc.output, output))
|
||||
}
|
||||
}
|
||||
@@ -97,12 +97,12 @@ func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clients, err := svc.ListClients(req.key)
|
||||
clients, err := svc.ListClients(req.key, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listClientsRes{clients, len(clients)}, nil
|
||||
return listClientsRes{clients}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,12 +187,12 @@ func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels, err := svc.ListChannels(req.key)
|
||||
channels, err := svc.ListChannels(req.key, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listChannelsRes{channels, len(channels)}, nil
|
||||
return listChannelsRes{channels}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,783 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/api"
|
||||
"github.com/mainflux/mainflux/manager/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json; charset=utf-8"
|
||||
invalidEmail = "userexample.com"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
)
|
||||
|
||||
var (
|
||||
user = manager.User{"user@example.com", "password"}
|
||||
client = manager.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = manager.Channel{Name: "test"}
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() manager.Service {
|
||||
users := mocks.NewUserRepository()
|
||||
clients := mocks.NewClientRepository()
|
||||
channels := mocks.NewChannelRepository(clients)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return manager.New(users, clients, channels, hasher, idp)
|
||||
}
|
||||
|
||||
func newServer(svc manager.Service) *httptest.Server {
|
||||
mux := api.MakeHandler(svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
data := toJSON(user)
|
||||
invalidData := toJSON(manager.User{Email: invalidEmail, Password: "password"})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
status int
|
||||
}{
|
||||
{"register new user", data, contentType, http.StatusCreated},
|
||||
{"register existing user", data, contentType, http.StatusConflict},
|
||||
{"register user with invalid email address", invalidData, contentType, http.StatusBadRequest},
|
||||
{"register user with invalid request format", "{", contentType, http.StatusBadRequest},
|
||||
{"register user with empty JSON request", "{}", contentType, http.StatusBadRequest},
|
||||
{"register user with empty request", "", contentType, http.StatusBadRequest},
|
||||
{"register user with missing content type", data, "", http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/users", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
tokenData := toJSON(map[string]string{"token": user.Email})
|
||||
data := toJSON(user)
|
||||
invalidEmailData := toJSON(manager.User{Email: invalidEmail, Password: "password"})
|
||||
invalidData := toJSON(manager.User{"user@example.com", "invalid_password"})
|
||||
nonexistentData := toJSON(manager.User{"non-existentuser@example.com", "pass"})
|
||||
svc.Register(user)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"login with valid credentials", data, contentType, http.StatusCreated, tokenData},
|
||||
{"login with invalid credentials", invalidData, contentType, http.StatusForbidden, ""},
|
||||
{"login with invalid email address", invalidEmailData, contentType, http.StatusBadRequest, ""},
|
||||
{"login non-existent user", nonexistentData, contentType, http.StatusForbidden, ""},
|
||||
{"login with invalid request format", "{", contentType, http.StatusBadRequest, ""},
|
||||
{"login with empty JSON request", "{}", contentType, http.StatusBadRequest, ""},
|
||||
{"login with empty request", "", contentType, http.StatusBadRequest, ""},
|
||||
{"login with missing content type", data, "", http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/tokens", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
token := strings.Trim(string(body), "\n")
|
||||
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, token, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, token))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(manager.Client{
|
||||
Type: "foo",
|
||||
Name: "invalid_client",
|
||||
Payload: "some_payload",
|
||||
})
|
||||
svc.Register(user)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"add valid client", data, contentType, user.Email, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
|
||||
{"add client with invalid data", invalidData, contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with invalid auth token", data, contentType, "invalid_token", http.StatusForbidden, ""},
|
||||
{"add client with invalid request format", "}", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with empty JSON request", "{}", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with empty request", "", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with missing content type", data, "", user.Email, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/clients", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(manager.Client{
|
||||
Type: "foo",
|
||||
Name: client.Name,
|
||||
Payload: client.Payload,
|
||||
})
|
||||
svc.Register(user)
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing client", data, id, contentType, user.Email, http.StatusOK},
|
||||
{"update non-existent client", data, wrongID, contentType, user.Email, http.StatusNotFound},
|
||||
{"update client with invalid id", data, "1", contentType, user.Email, http.StatusNotFound},
|
||||
{"update client with invalid data", invalidData, id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with invalid user token", data, id, contentType, invalidEmail, http.StatusForbidden},
|
||||
{"update client with invalid data format", "{", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with empty JSON request", "{}", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with empty request", "", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with missing content type", data, id, "", user.Email, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data := toJSON(client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing client", id, user.Email, http.StatusOK, data},
|
||||
{"view non-existent client", wrongID, user.Email, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid id", "1", user.Email, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid token", id, invalidEmail, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data := strings.Trim(string(body), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
noClientsUser := manager.User{Email: "no_clients_user@example.com", Password: user.Password}
|
||||
svc.Register(noClientsUser)
|
||||
clients := []manager.Client{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
offset int
|
||||
limit int
|
||||
res []manager.Client
|
||||
}{
|
||||
{"get a list of clients", user.Email, http.StatusOK, 1, 5, clients[1:6]},
|
||||
{"get a list of clients with invalid token", invalidEmail, http.StatusForbidden, 0, 1, nil},
|
||||
{"get a list of clients with invalid offset", user.Email, http.StatusBadRequest, -1, 5, nil},
|
||||
{"get a list of clients with invalid limit", user.Email, http.StatusBadRequest, 1, -5, nil},
|
||||
{"get a list of clients with zero limit", user.Email, http.StatusBadRequest, 1, 0, nil},
|
||||
{"get a list of clients with limit greater than max", user.Email, http.StatusBadRequest, 0, 110, nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/clients?offset=%d&limit=%d", ts.URL, tc.offset, tc.limit),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var data map[string][]manager.Client
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, data["clients"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data["clients"]))
|
||||
}
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/clients", ts.URL),
|
||||
token: user.Email,
|
||||
}
|
||||
defaults := "get a list of clients with no limit and offset params"
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", defaults, err))
|
||||
var data map[string][]manager.Client
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", defaults, http.StatusOK, res.StatusCode))
|
||||
assert.ElementsMatch(t, clients[0:10], data["clients"], fmt.Sprintf("%s: expected body %s got %s", defaults, clients[0:10], data["clients"]))
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"delete existing client", id, user.Email, http.StatusNoContent},
|
||||
{"delete non-existent client", wrongID, user.Email, http.StatusNoContent},
|
||||
{"delete client with invalid id", "1", user.Email, http.StatusNoContent},
|
||||
{"delete client with invalid token", id, invalidEmail, http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
data := toJSON(channel)
|
||||
svc.Register(user)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"create new channel", data, contentType, user.Email, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
|
||||
{"create new channel with invalid token", data, contentType, invalidEmail, http.StatusForbidden, ""},
|
||||
{"create new channel with invalid data format", "{", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"create new channel with empty JSON request", "{}", contentType, user.Email, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
|
||||
{"create new channel with empty request", "", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"create new channel with missing content type", data, "", user.Email, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/channels", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
updateData := toJSON(map[string]string{
|
||||
"name": "updated_channel",
|
||||
})
|
||||
svc.Register(user)
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing channel", updateData, id, contentType, user.Email, http.StatusOK},
|
||||
{"update non-existing channel", updateData, wrongID, contentType, user.Email, http.StatusNotFound},
|
||||
{"update channel with invalid token", updateData, id, contentType, invalidEmail, http.StatusForbidden},
|
||||
{"update channel with invalid id", updateData, "1", contentType, user.Email, http.StatusNotFound},
|
||||
{"update channel with invalid data format", "}", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update channel with empty JSON object", "{}", id, contentType, user.Email, http.StatusOK},
|
||||
{"update channel with empty request", "", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update channel with missing content type", updateData, id, "", user.Email, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
channel.ID = id
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing channel", id, user.Email, http.StatusOK, data},
|
||||
{"view non-existent channel", wrongID, user.Email, http.StatusNotFound, ""},
|
||||
{"view channel with invalid id", "1", user.Email, http.StatusNotFound, ""},
|
||||
{"view channel with invalid token", id, invalidEmail, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body := strings.Trim(string(data), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, body, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
channels := []manager.Channel{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
channel.ID = id
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
offset int
|
||||
limit int
|
||||
res []manager.Channel
|
||||
}{
|
||||
{"get a list of channels", user.Email, http.StatusOK, 1, 5, channels[1:6]},
|
||||
{"get a list of channels with invalid token", invalidEmail, http.StatusForbidden, 0, 1, nil},
|
||||
{"get a list of channels with invalid offset", user.Email, http.StatusBadRequest, -1, 5, nil},
|
||||
{"get a list of channels with invalid limit", user.Email, http.StatusBadRequest, 1, -5, nil},
|
||||
{"get a list of channels with zero limit", user.Email, http.StatusBadRequest, 1, 0, nil},
|
||||
{"get a list of channels with limit greater than max", user.Email, http.StatusBadRequest, 0, 110, nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels?offset=%d&limit=%d", ts.URL, tc.offset, tc.limit),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var body map[string][]manager.Channel
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, body["channels"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body["channels"]))
|
||||
}
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels", ts.URL),
|
||||
token: user.Email,
|
||||
}
|
||||
defaults := "get a list of channels with no limit and offset params"
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", defaults, err))
|
||||
var data map[string][]manager.Channel
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", defaults, http.StatusOK, res.StatusCode))
|
||||
assert.ElementsMatch(t, channels[0:10], data["channels"], fmt.Sprintf("%s: expected body %s got %s", defaults, channels[0:10], data["channels"]))
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
channel.ID = id
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"remove existing channel", channel.ID, user.Email, http.StatusNoContent},
|
||||
{"remove non-existent channel", channel.ID, user.Email, http.StatusNoContent},
|
||||
{"remove channel with invalid id", wrongID, user.Email, http.StatusNoContent},
|
||||
{"remove channel with invalid token", channel.ID, invalidEmail, http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
chanID, _ := svc.CreateChannel(user.Email, channel)
|
||||
|
||||
otherUser := manager.User{Email: "other_user@example.com", Password: "password"}
|
||||
svc.Register(otherUser)
|
||||
otherClientID, _ := svc.AddClient(otherUser.Email, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherUser.Email, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"connect existing client to existing channel", chanID, clientID, user.Email, http.StatusOK},
|
||||
{"connect existing client to non-existent channel", wrongID, clientID, user.Email, http.StatusNotFound},
|
||||
{"connect client with invalid id to channel", chanID, "1", user.Email, http.StatusNotFound},
|
||||
{"connect client to channel with invalid id", "1", clientID, user.Email, http.StatusNotFound},
|
||||
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidEmail, http.StatusForbidden},
|
||||
{"connect client from owner to channel of other user", otherChanID, clientID, user.Email, http.StatusNotFound},
|
||||
{"connect client from other user to owner's channel", chanID, otherClientID, user.Email, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnnect(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
chanID, _ := svc.CreateChannel(user.Email, channel)
|
||||
svc.Connect(user.Email, chanID, clientID)
|
||||
otherUser := manager.User{Email: "other_user@example.com", Password: "password"}
|
||||
svc.Register(otherUser)
|
||||
otherClientID, _ := svc.AddClient(otherUser.Email, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherUser.Email, channel)
|
||||
svc.Connect(otherUser.Email, otherChanID, otherClientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"disconnect connected client from channel", chanID, clientID, user.Email, http.StatusNoContent},
|
||||
{"disconnect non-connected client from channel", chanID, clientID, user.Email, http.StatusNotFound},
|
||||
{"disconnect non-existent client from channel", chanID, "1", user.Email, http.StatusNotFound},
|
||||
{"disconnect client from non-existent channel", "1", clientID, user.Email, http.StatusNotFound},
|
||||
{"disconnect client from channel with invalid token", chanID, clientID, invalidEmail, http.StatusForbidden},
|
||||
{"disconnect owner's client from someone elses channel", otherChanID, clientID, user.Email, http.StatusNotFound},
|
||||
{"disconnect other's client from owner's channel", chanID, otherClientID, user.Email, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
status int
|
||||
clientID string
|
||||
}{
|
||||
{"get client id using existing client key", clientID, http.StatusOK, clientID},
|
||||
{"get client id using non-existent client key", "", http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/access-grant", ts.URL),
|
||||
token: tc.key,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
clientID := res.Header.Get("X-client-id")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.clientID, clientID, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.clientID, clientID))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
notConnectedClientID, _ := svc.AddClient(user.Email, client)
|
||||
chanID, _ := svc.CreateChannel(user.Email, channel)
|
||||
svc.Connect(user.Email, chanID, clientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientKey string
|
||||
status int
|
||||
clientID string
|
||||
}{
|
||||
{"check access to existing channel given connected client", chanID, clientID, http.StatusOK, clientID},
|
||||
{"check access to existing channel given not connected client", chanID, notConnectedClientID, http.StatusForbidden, ""},
|
||||
{"check access to existing channel given non-existent client", chanID, "invalid_token", http.StatusForbidden, ""},
|
||||
{"check access to non-existent channel given existing client", "invalid_token", clientID, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels/%s/access-grant", ts.URL, tc.chanID),
|
||||
token: tc.clientKey,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
clientID := res.Header.Get("X-client-id")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.clientID, clientID, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.clientID, clientID))
|
||||
}
|
||||
}
|
||||
+107
-119
@@ -1,9 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
@@ -21,12 +22,13 @@ func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
|
||||
|
||||
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "register",
|
||||
"email", user.Email,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method register for user %s took %s to complete", user.Email, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Register(user)
|
||||
@@ -34,12 +36,12 @@ func (lm *loggingMiddleware) Register(user manager.User) (err error) {
|
||||
|
||||
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "login",
|
||||
"email", user.Email,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method login for user %s took %s to complete", user.Email, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Login(user)
|
||||
@@ -47,13 +49,12 @@ func (lm *loggingMiddleware) Login(user manager.User) (token string, err error)
|
||||
|
||||
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "add_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method add_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AddClient(key, client)
|
||||
@@ -61,13 +62,12 @@ func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id st
|
||||
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "update_client",
|
||||
"key", key,
|
||||
"id", client.ID,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method update_client for key %s and client %s took %s to complete", key, client.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateClient(key, client)
|
||||
@@ -75,40 +75,38 @@ func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (er
|
||||
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "view_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method view_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListClients(key string) (clients []manager.Client, err error) {
|
||||
func (lm *loggingMiddleware) ListClients(key string, offset, limit int) (clients []manager.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "list_clients",
|
||||
"key", key,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method list_clients for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListClients(key)
|
||||
return lm.svc.ListClients(key, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "remove_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method remove_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveClient(key, id)
|
||||
@@ -116,13 +114,12 @@ func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
|
||||
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "create_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method create_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateChannel(key, channel)
|
||||
@@ -130,13 +127,12 @@ func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel)
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "update_channel",
|
||||
"key", key,
|
||||
"id", channel.ID,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method update_channel for key %s and channel %s took %s to complete", key, channel.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannel(key, channel)
|
||||
@@ -144,83 +140,77 @@ func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel)
|
||||
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "view_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method view_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChannels(key string) (channels []manager.Channel, err error) {
|
||||
func (lm *loggingMiddleware) ListChannels(key string, offset, limit int) (channels []manager.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "list_channels",
|
||||
"key", key,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method list_channels for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListChannels(key)
|
||||
return lm.svc.ListChannels(key, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannel(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "remove_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method remove_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Connect(key, chanId, clientId string) (err error) {
|
||||
func (lm *loggingMiddleware) Connect(key, chanID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "connect",
|
||||
"key", key,
|
||||
"channel", chanId,
|
||||
"client", clientId,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method connect for key %s, channel %s, client %s took %s to complete", key, chanID, clientID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Connect(key, chanId, clientId)
|
||||
return lm.svc.Connect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Disconnect(key, chanId, clientId string) (err error) {
|
||||
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),
|
||||
)
|
||||
message := fmt.Sprintf("Method disconnect for key %s, channel %s, client %s took %s to complete", key, chanID, clientID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Disconnect(key, chanId, clientId)
|
||||
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,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method identity for client %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Identity(key)
|
||||
@@ -228,14 +218,12 @@ func (lm *loggingMiddleware) Identity(key string) (id string, err error) {
|
||||
|
||||
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "can_access",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"publisher", pub,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
message := fmt.Sprintf("Method can_access for key %s, channel %s and publisher %s took %s to complete", key, id, pub, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CanAccess(key, id)
|
||||
|
||||
@@ -70,13 +70,13 @@ func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client,
|
||||
return ms.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListClients(key string) ([]manager.Client, error) {
|
||||
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]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.svc.ListClients(key)
|
||||
return ms.svc.ListClients(key, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
|
||||
@@ -115,13 +115,13 @@ func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel
|
||||
return ms.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChannels(key string) ([]manager.Channel, error) {
|
||||
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]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.svc.ListChannels(key)
|
||||
return ms.svc.ListChannels(key, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
@@ -110,16 +112,16 @@ func (req viewResourceReq) validate() error {
|
||||
|
||||
type listResourcesReq struct {
|
||||
key string
|
||||
size int
|
||||
offset int
|
||||
limit int
|
||||
}
|
||||
|
||||
func (req listResourcesReq) validate() error {
|
||||
func (req *listResourcesReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.size > 0 && req.offset >= 0 {
|
||||
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -137,7 +139,7 @@ func (req connectionReq) validate() error {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.chanId) && !govalidator.IsUUID(req.clientId) {
|
||||
if !govalidator.IsUUID(req.chanId) || !govalidator.IsUUID(req.clientId) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
const wrong string = "?"
|
||||
|
||||
var (
|
||||
client manager.Client = manager.Client{Type: "app"}
|
||||
channel manager.Channel = manager.Channel{}
|
||||
client = manager.Client{Type: "app"}
|
||||
channel = manager.Channel{}
|
||||
)
|
||||
|
||||
func TestUserReqValidation(t *testing.T) {
|
||||
@@ -179,22 +179,23 @@ func TestListResourcesReqValidation(t *testing.T) {
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
size int
|
||||
offset int
|
||||
limit int
|
||||
err error
|
||||
}{
|
||||
"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},
|
||||
"negative offset": {key, -value, value, manager.ErrMalformedEntity},
|
||||
"zero limit": {key, value, 0, manager.ErrMalformedEntity},
|
||||
"negative limit": {key, value, -value, manager.ErrMalformedEntity},
|
||||
"too big limit": {key, value, 20 * value, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := listResourcesReq{
|
||||
key: tc.key,
|
||||
size: tc.size,
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
|
||||
@@ -108,7 +108,6 @@ func (res viewClientRes) empty() bool {
|
||||
|
||||
type listClientsRes struct {
|
||||
Clients []manager.Client `json:"clients"`
|
||||
count int
|
||||
}
|
||||
|
||||
func (res listClientsRes) code() int {
|
||||
@@ -116,9 +115,7 @@ func (res listClientsRes) code() int {
|
||||
}
|
||||
|
||||
func (res listClientsRes) headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-Count": fmt.Sprintf("%d", res.count),
|
||||
}
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listClientsRes) empty() bool {
|
||||
@@ -170,7 +167,6 @@ func (res viewChannelRes) empty() bool {
|
||||
|
||||
type listChannelsRes struct {
|
||||
Channels []manager.Channel `json:"channels"`
|
||||
count int
|
||||
}
|
||||
|
||||
func (res listChannelsRes) code() int {
|
||||
@@ -178,9 +174,7 @@ func (res listChannelsRes) code() int {
|
||||
}
|
||||
|
||||
func (res listChannelsRes) headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-Count": fmt.Sprintf("%d", res.count),
|
||||
}
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listChannelsRes) empty() bool {
|
||||
|
||||
@@ -3,7 +3,11 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
@@ -12,6 +16,9 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var errUnsupportedContentType = errors.New("unsupported content type")
|
||||
var errInvalidQueryParams = errors.New("invalid query params")
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc manager.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
@@ -147,6 +154,10 @@ func decodeIdentity(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
}
|
||||
|
||||
func decodeCredentials(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var user manager.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
return nil, err
|
||||
@@ -156,6 +167,10 @@ func decodeCredentials(_ context.Context, r *http.Request) (interface{}, error)
|
||||
}
|
||||
|
||||
func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client manager.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
@@ -170,6 +185,10 @@ func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, erro
|
||||
}
|
||||
|
||||
func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client manager.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
@@ -185,6 +204,10 @@ func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error)
|
||||
}
|
||||
|
||||
func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel manager.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
@@ -199,6 +222,10 @@ func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, err
|
||||
}
|
||||
|
||||
func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel manager.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
@@ -223,10 +250,51 @@ func decodeView(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
}
|
||||
|
||||
func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := 0
|
||||
limit := 10
|
||||
|
||||
n := len(q)
|
||||
if n == 0 {
|
||||
req := listResourcesReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
if n > 2 {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
off, lmt := q["offset"], q["limit"]
|
||||
|
||||
if len(off) > 1 || len(lmt) > 1 {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(off) == 1 {
|
||||
offset, err = strconv.Atoi(off[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(lmt) == 1 {
|
||||
limit, err = strconv.Atoi(lmt[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
req := listResourcesReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
size: 10,
|
||||
offset: 0,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -272,12 +340,20 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case manager.ErrConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case io.ErrUnexpectedEOF:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case io.EOF:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
if _, ok := err.(*json.SyntaxError); ok {
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ type ChannelRepository interface {
|
||||
// by the specified user.
|
||||
One(string, string) (Channel, error)
|
||||
|
||||
// All retrieves the channels owned by the specified user.
|
||||
All(string) []Channel
|
||||
// All retrieves the subset of channels owned by the specified user.
|
||||
All(string, int, int) []Channel
|
||||
|
||||
// Remove removes the channel having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
|
||||
+2
-2
@@ -44,8 +44,8 @@ type ClientRepository interface {
|
||||
// by the specified user.
|
||||
One(string, string) (Client, error)
|
||||
|
||||
// All retrieves the clients owned by the specified user.
|
||||
All(string) []Client
|
||||
// All retrieves the subset of clients owned by the specified user.
|
||||
All(string, int, int) []Client
|
||||
|
||||
// Remove removes the client having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ type jwtIdentityProvider struct {
|
||||
|
||||
// New instantiates a JWT identity provider.
|
||||
func New(secret string) manager.IdentityProvider {
|
||||
return &jwtIdentityProvider{}
|
||||
return &jwtIdentityProvider{secret}
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) TemporaryKey(id string) (string, error) {
|
||||
|
||||
+4
-4
@@ -89,7 +89,7 @@ func (ms *managerService) ViewClient(key, id string) (Client, error) {
|
||||
return ms.clients.One(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) ListClients(key string) ([]Client, error) {
|
||||
func (ms *managerService) ListClients(key string, offset, limit int) ([]Client, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -99,7 +99,7 @@ func (ms *managerService) ListClients(key string) ([]Client, error) {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.All(sub), nil
|
||||
return ms.clients.All(sub, offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *managerService) RemoveClient(key, id string) error {
|
||||
@@ -156,7 +156,7 @@ func (ms *managerService) ViewChannel(key, id string) (Channel, error) {
|
||||
return ms.channels.One(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) ListChannels(key string) ([]Channel, error) {
|
||||
func (ms *managerService) ListChannels(key string, offset, limit int) ([]Channel, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -166,7 +166,7 @@ func (ms *managerService) ListChannels(key string) ([]Channel, error) {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.All(sub), nil
|
||||
return ms.channels.All(sub, offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *managerService) RemoveChannel(key, id string) error {
|
||||
|
||||
+83
-37
@@ -13,14 +13,14 @@ const wrong string = "wrong-value"
|
||||
|
||||
var (
|
||||
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}}
|
||||
client manager.Client = manager.Client{Type: "app", Name: "test"}
|
||||
channel manager.Channel = manager.Channel{Name: "test", Clients: []manager.Client{}}
|
||||
)
|
||||
|
||||
func newService() manager.Service {
|
||||
users := mocks.NewUserRepository()
|
||||
clients := mocks.NewClientRepository()
|
||||
channels := mocks.NewChannelRepository()
|
||||
channels := mocks.NewChannelRepository(clients)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
@@ -30,17 +30,18 @@ func newService() manager.Service {
|
||||
func TestRegister(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
cases := map[string]struct {
|
||||
cases := []struct {
|
||||
desc string
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"register new user": {user, nil},
|
||||
"register existing user": {user, manager.ErrConflict},
|
||||
{"register new user", user, nil},
|
||||
{"register existing user", user, manager.ErrConflict},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
for _, 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))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +89,8 @@ func TestUpdateClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
@@ -110,7 +112,8 @@ func TestViewClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
@@ -133,16 +136,31 @@ func TestListClients(t *testing.T) {
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.AddClient(key, client)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list clients": {key, nil},
|
||||
"list clients with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
|
||||
"list clients": {key, 0, 5, 5, nil},
|
||||
"list clients 5-10": {key, 5, 10, 5, nil},
|
||||
"list last client": {key, 9, 10, 1, nil},
|
||||
"list empty response": {key, 11, 10, 0, nil},
|
||||
"list offset < 0": {key, -1, 10, 0, nil},
|
||||
"list limit < 0": {key, 1, -10, 0, nil},
|
||||
"list limit = 0": {key, 1, 0, 0, nil},
|
||||
"list clients with wrong credentials": {wrong, 0, 0, 0, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ListClients(tc.key)
|
||||
cl, err := svc.ListClients(tc.key, tc.offset, tc.limit)
|
||||
size := len(cl)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -151,7 +169,8 @@ func TestRemoveClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
@@ -194,7 +213,8 @@ func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
@@ -216,7 +236,8 @@ func TestViewChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
@@ -239,16 +260,30 @@ func TestListChannels(t *testing.T) {
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.CreateChannel(key, channel)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list channels": {key, nil},
|
||||
"list channels with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
|
||||
"list first 5 channels": {key, 0, 5, 5, nil},
|
||||
"list channels 5-10 channels": {key, 5, 10, 5, nil},
|
||||
"list last channel": {key, 6, 10, 4, nil},
|
||||
"list offset < 0": {key, -1, 10, 0, nil},
|
||||
"list limit < 0": {key, 1, -10, 0, nil},
|
||||
"list limit = 0": {key, 1, 0, 0, nil},
|
||||
"list channels with wrong credentials": {wrong, 0, 0, 0, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ListChannels(tc.key)
|
||||
ch, err := svc.ListChannels(tc.key, tc.offset, tc.limit)
|
||||
size := len(ch)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -257,7 +292,8 @@ func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
@@ -282,7 +318,9 @@ func TestConnect(t *testing.T) {
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
@@ -290,9 +328,9 @@ func TestConnect(t *testing.T) {
|
||||
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},
|
||||
"connect client": {key, channel.ID, client.ID, nil},
|
||||
"connect client with wrong credentials": {wrong, channel.ID, client.ID, manager.ErrUnauthorizedAccess},
|
||||
"connect client to non-existing channel": {key, wrong, client.ID, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@@ -307,26 +345,29 @@ func TestDisconnect(t *testing.T) {
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
svc.Connect(key, chanId, clientId)
|
||||
|
||||
cases := map[string]struct {
|
||||
cases := []struct {
|
||||
desc string
|
||||
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},
|
||||
{"disconnect connected client", key, channel.ID, client.ID, nil},
|
||||
{"disconnect disconnected client", key, channel.ID, client.ID, manager.ErrNotFound},
|
||||
{"disconnect client with wrong credentials", wrong, channel.ID, client.ID, manager.ErrUnauthorizedAccess},
|
||||
{"disconnect client from non-existing channel", key, wrong, client.ID, manager.ErrNotFound},
|
||||
{"disconnect non-existing client", key, channel.ID, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
for _, 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))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -355,8 +396,13 @@ func TestCanAccess(t *testing.T) {
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
svc.AddClient(key, client)
|
||||
svc.CreateChannel(key, channel)
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
client.Key = clientId
|
||||
|
||||
channel.Clients = []manager.Client{client}
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
|
||||
+20
-10
@@ -2,7 +2,6 @@ package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -11,16 +10,20 @@ import (
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
|
||||
const chanId = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type channelRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
channels map[string]manager.Channel
|
||||
clients manager.ClientRepository
|
||||
}
|
||||
|
||||
// NewChannelRepository creates in-memory channel repository.
|
||||
func NewChannelRepository() manager.ChannelRepository {
|
||||
func NewChannelRepository(clients manager.ClientRepository) manager.ChannelRepository {
|
||||
return &channelRepositoryMock{
|
||||
channels: make(map[string]manager.Channel),
|
||||
clients: clients,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +32,7 @@ func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error)
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
channel.ID = strconv.Itoa(crm.counter)
|
||||
channel.ID = fmt.Sprintf("%s%012d", chanId, crm.counter)
|
||||
|
||||
crm.channels[key(channel.Owner, channel.ID)] = channel
|
||||
|
||||
@@ -58,15 +61,21 @@ func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error)
|
||||
return manager.Channel{}, manager.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) All(owner string) []manager.Channel {
|
||||
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []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)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return channels
|
||||
}
|
||||
|
||||
first := fmt.Sprintf("%s%012d", chanId, offset)
|
||||
last := fmt.Sprintf("%s%012d", chanId, offset+limit)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
if strings.HasPrefix(k, prefix) && v.ID > first && v.ID <= last {
|
||||
channels = append(channels, v)
|
||||
}
|
||||
}
|
||||
@@ -85,10 +94,11 @@ func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error
|
||||
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})
|
||||
client, err := crm.clients.One(owner, clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channel.Clients = append(channel.Clients, client)
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -11,6 +10,8 @@ import (
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
|
||||
const cliId = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type clientRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
@@ -29,7 +30,7 @@ func (crm *clientRepositoryMock) Id() string {
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
return strconv.Itoa(crm.counter)
|
||||
return fmt.Sprintf("%s%012d", cliId, crm.counter)
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Save(client manager.Client) error {
|
||||
@@ -64,15 +65,21 @@ func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
|
||||
return manager.Client{}, manager.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) All(owner string) []manager.Client {
|
||||
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []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)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return clients
|
||||
}
|
||||
|
||||
first := fmt.Sprintf("%s%012d", cliId, offset)
|
||||
last := fmt.Sprintf("%s%012d", cliId, offset+limit)
|
||||
|
||||
for k, v := range crm.clients {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
if strings.HasPrefix(k, prefix) && v.ID > first && v.ID <= last {
|
||||
clients = append(clients, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // required by GORM
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
@@ -13,6 +13,8 @@ type channelRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewChannelRepository instantiates a PostgreSQL implementation of channel
|
||||
// repository.
|
||||
func NewChannelRepository(db *gorm.DB) manager.ChannelRepository {
|
||||
return &channelRepository{db}
|
||||
}
|
||||
@@ -54,11 +56,10 @@ func (cr channelRepository) One(owner, id string) (manager.Channel, error) {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) All(owner string) []manager.Channel {
|
||||
func (cr channelRepository) All(owner string, offset, limit int) []manager.Channel {
|
||||
var channels []manager.Channel
|
||||
|
||||
cr.db.Find(&channels, "owner = ?", owner)
|
||||
|
||||
cr.db.Offset(offset).Limit(limit).Find(&channels, "owner = ?", owner)
|
||||
return channels
|
||||
}
|
||||
|
||||
|
||||
@@ -105,16 +105,18 @@ func TestMultiChannelRetrieval(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
len int
|
||||
owner string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
}{
|
||||
"existing owner": {email, n},
|
||||
"non-existing owner": {wrong, 0},
|
||||
"existing owner": {email, 0, n, n},
|
||||
"non-existing owner": {wrong, 1, 6, 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))
|
||||
size := len(chanRepo.All(tc.owner, tc.offset, tc.limit))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,22 +194,23 @@ func TestChannelDisconnect(t *testing.T) {
|
||||
|
||||
chanRepo.Connect(email, chanId, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
cases := []struct {
|
||||
desc string
|
||||
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},
|
||||
{"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 {
|
||||
for _, 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))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // required by GORM
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
@@ -24,11 +24,7 @@ func (cr *clientRepository) Id() string {
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Save(client manager.Client) error {
|
||||
if err := cr.db.Create(&client).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return cr.db.Create(&client).Error
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Update(client manager.Client) error {
|
||||
@@ -58,10 +54,10 @@ func (cr *clientRepository) One(owner, id string) (manager.Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (cr *clientRepository) All(owner string) []manager.Client {
|
||||
func (cr *clientRepository) All(owner string, offset, limit int) []manager.Client {
|
||||
var clients []manager.Client
|
||||
|
||||
cr.db.Find(&clients, "owner = ?", owner)
|
||||
cr.db.Offset(offset).Limit(limit).Find(&clients, "owner = ?", owner)
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
@@ -122,16 +122,18 @@ func TestMultiClientRetrieval(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
len int
|
||||
owner string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
}{
|
||||
"existing owner": {email, n},
|
||||
"non-existing owner": {wrong, 0},
|
||||
"existing owner": {email, 0, n, n},
|
||||
"non-existing owner": {wrong, 1, 6, 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))
|
||||
n := len(clientRepo.All(tc.owner, tc.offset, tc.limit))
|
||||
assert.Equal(t, tc.size, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, n))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // required by GORM
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // required by GORM
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
@@ -12,19 +12,20 @@ import (
|
||||
func TestUserSave(t *testing.T) {
|
||||
email := "user-save@example.com"
|
||||
|
||||
cases := map[string]struct {
|
||||
cases := []struct {
|
||||
desc string
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"new user": {manager.User{email, "pass"}, nil},
|
||||
"duplicate user": {manager.User{email, "pass"}, manager.ErrConflict},
|
||||
{"new user", manager.User{email, "pass"}, nil},
|
||||
{"duplicate user", manager.User{email, "pass"}, manager.ErrConflict},
|
||||
}
|
||||
|
||||
repo := postgres.NewUserRepository(db)
|
||||
|
||||
for desc, tc := range cases {
|
||||
for _, 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))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -42,9 +42,9 @@ type Service interface {
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewClient(string, string) (Client, error)
|
||||
|
||||
// ListClients retrieves data about all clients that belongs to the user
|
||||
// identified by the provided key.
|
||||
ListClients(string) ([]Client, error)
|
||||
// ListClients retrieves data about subset of clients that belongs to the
|
||||
// user identified by the provided key.
|
||||
ListClients(string, int, int) ([]Client, error)
|
||||
|
||||
// RemoveClient removes the client identified with the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
@@ -61,9 +61,9 @@ type Service interface {
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewChannel(string, string) (Channel, error)
|
||||
|
||||
// ListChannels retrieves data about all clients that belongs to the user
|
||||
// identified by the provided key.
|
||||
ListChannels(string) ([]Channel, error)
|
||||
// ListChannels retrieves data about subset of channels that belongs to the
|
||||
// user identified by the provided key.
|
||||
ListChannels(string, int, int) ([]Channel, error)
|
||||
|
||||
// RemoveChannel removes the client identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
|
||||
+23
-5
@@ -30,6 +30,8 @@ paths:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using an existing email address.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/tokens:
|
||||
@@ -53,7 +55,12 @@ paths:
|
||||
$ref: "#/definitions/Token"
|
||||
400:
|
||||
description: |
|
||||
Failed due to malformed JSON or using an invalid credentials.
|
||||
Failed due to malformed JSON.
|
||||
403:
|
||||
description: |
|
||||
Failed due to using invalid credentials.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/clients:
|
||||
@@ -83,6 +90,8 @@ paths:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
@@ -96,7 +105,7 @@ paths:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Size"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
responses:
|
||||
200:
|
||||
@@ -160,6 +169,8 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Client does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
@@ -206,6 +217,8 @@ paths:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
@@ -219,7 +232,7 @@ paths:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Size"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
responses:
|
||||
200:
|
||||
@@ -283,6 +296,8 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Channel does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
@@ -407,12 +422,14 @@ parameters:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Size:
|
||||
name: size
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
@@ -420,6 +437,7 @@ parameters:
|
||||
in: query
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
|
||||
responses:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
syntax = "proto3";
|
||||
package mainflux;
|
||||
|
||||
// RawMessage represents a message emitted by the Mainflux adapters layer.
|
||||
message RawMessage {
|
||||
string Channel = 1;
|
||||
string Publisher = 2;
|
||||
string Protocol = 3;
|
||||
string ContentType = 4;
|
||||
bytes Payload = 5;
|
||||
}
|
||||
|
||||
// Message represents a resolved (normalized) raw message.
|
||||
message Message {
|
||||
string Channel = 1;
|
||||
string Publisher = 2;
|
||||
string Protocol = 3;
|
||||
string Name = 4;
|
||||
string Unit = 5;
|
||||
double Value = 6;
|
||||
string StringValue = 7;
|
||||
bool BoolValue = 8;
|
||||
string DataValue = 9;
|
||||
double ValueSum = 10;
|
||||
double Time = 11;
|
||||
double UpdateTime = 12;
|
||||
string Link = 13;
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
package mainflux
|
||||
|
||||
// Message represents a resolved (normalized) raw message.
|
||||
type Message struct {
|
||||
Channel string
|
||||
Publisher string
|
||||
Protocol string
|
||||
Name string `json:"n,omitempty"`
|
||||
Unit string `json:"u,omitempty"`
|
||||
Value float64 `json:"v,omitempty"`
|
||||
StringValue string `json:"vs,omitempty"`
|
||||
BoolValue bool `json:"vb,omitempty"`
|
||||
DataValue string `json:"vd,omitempty"`
|
||||
ValueSum float64 `json:"s,omitempty"`
|
||||
Time float64 `json:"t,omitempty"`
|
||||
UpdateTime float64 `json:"ut,omitempty"`
|
||||
Link string `json:"l,omitempty"`
|
||||
}
|
||||
|
||||
// RawMessage represents a message emitted by the mainflux adapters layer.
|
||||
type RawMessage struct {
|
||||
Channel string `json:"channel"`
|
||||
Publisher string `json:"publisher"`
|
||||
Protocol string `json:"protocol"`
|
||||
ContentType string `json:"content_type"`
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
|
||||
// MessagePublisher specifies a message publishing API.
|
||||
type MessagePublisher interface {
|
||||
// Publishes message to the stream. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Publish(RawMessage) error
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2016 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# 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 NON-INFRINGEMENT. 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.
|
||||
copyright: Copyright (c) 2015-2018 Mainflux
|
||||
repo_url: https://github.com/mainflux/mainflux
|
||||
site_description: Mainflux IoT System
|
||||
site_name: Mainflux
|
||||
theme: readthedocs
|
||||
|
||||
extra:
|
||||
logo: docs/img/logo.png
|
||||
author:
|
||||
github: mainflux/mainflux
|
||||
twitter: mainflux
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- toc:
|
||||
permalink: '#'
|
||||
|
||||
pages:
|
||||
- Overview:
|
||||
- About: index.md
|
||||
- Contributing: CONTRIBUTING.md
|
||||
- License: LICENSE.txt
|
||||
- Architecture: architecture.md
|
||||
- Getting started: getting-started.md
|
||||
@@ -0,0 +1,30 @@
|
||||
package normalizer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
ef eventFlow
|
||||
}
|
||||
|
||||
func newMetricsMiddleware(ef eventFlow, counter metrics.Counter, latency metrics.Histogram) *metricsMiddleware {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
ef: ef,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) handleMessage(msg *nats.Msg) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "handleMessage").Add(1)
|
||||
mm.latency.With("method", "handleMessage").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
mm.ef.handleMsg(msg)
|
||||
}
|
||||
+21
-21
@@ -1,18 +1,19 @@
|
||||
package normalizer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/cisco/senml"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
queue string = "normalizers"
|
||||
subject string = "src.*"
|
||||
subject string = "channel.*"
|
||||
output string = "normalized"
|
||||
)
|
||||
|
||||
@@ -22,43 +23,42 @@ type eventFlow struct {
|
||||
}
|
||||
|
||||
// Subscribe instantiates and starts a new NATS message flow.
|
||||
func Subscribe(nc *nats.Conn, logger log.Logger) {
|
||||
func Subscribe(nc *nats.Conn, logger log.Logger, counter metrics.Counter, latency metrics.Histogram) {
|
||||
flow := eventFlow{nc, logger}
|
||||
flow.start()
|
||||
mm := newMetricsMiddleware(flow, counter, latency)
|
||||
flow.nc.QueueSubscribe(subject, queue, mm.handleMessage)
|
||||
}
|
||||
|
||||
func (ef eventFlow) start() {
|
||||
ef.nc.QueueSubscribe(subject, queue, func(m *nats.Msg) {
|
||||
msg := mainflux.RawMessage{}
|
||||
func (ef eventFlow) handleMsg(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 := proto.Unmarshal(m.Data, &msg); err != nil {
|
||||
ef.logger.Warn(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
|
||||
}
|
||||
})
|
||||
if err := ef.publish(msg); err != nil {
|
||||
ef.logger.Warn(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))
|
||||
ef.logger.Warn(fmt.Sprintf("Normalization failed: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range normalized {
|
||||
data, err := json.Marshal(v)
|
||||
data, err := proto.Marshal(&v)
|
||||
if err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Marshalling failed: %s", err))
|
||||
ef.logger.Warn(fmt.Sprintf("Marshalling failed: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ef.nc.Publish(output, data); err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
|
||||
ef.logger.Warn(fmt.Sprintf("Publishing failed: %s", err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package mainflux
|
||||
|
||||
// MessagePublisher specifies a message publishing API.
|
||||
type MessagePublisher interface {
|
||||
// Publishes message to the stream. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Publish(RawMessage) error
|
||||
}
|
||||
+137
-137
@@ -1,137 +1,137 @@
|
||||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type fileFullEaInformation struct {
|
||||
NextEntryOffset uint32
|
||||
Flags uint8
|
||||
NameLength uint8
|
||||
ValueLength uint16
|
||||
}
|
||||
|
||||
var (
|
||||
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
|
||||
|
||||
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
|
||||
errEaNameTooLarge = errors.New("extended attribute name too large")
|
||||
errEaValueTooLarge = errors.New("extended attribute value too large")
|
||||
)
|
||||
|
||||
// ExtendedAttribute represents a single Windows EA.
|
||||
type ExtendedAttribute struct {
|
||||
Name string
|
||||
Value []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
|
||||
var info fileFullEaInformation
|
||||
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
err = errInvalidEaBuffer
|
||||
return
|
||||
}
|
||||
|
||||
nameOffset := fileFullEaInformationSize
|
||||
nameLen := int(info.NameLength)
|
||||
valueOffset := nameOffset + int(info.NameLength) + 1
|
||||
valueLen := int(info.ValueLength)
|
||||
nextOffset := int(info.NextEntryOffset)
|
||||
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
|
||||
err = errInvalidEaBuffer
|
||||
return
|
||||
}
|
||||
|
||||
ea.Name = string(b[nameOffset : nameOffset+nameLen])
|
||||
ea.Value = b[valueOffset : valueOffset+valueLen]
|
||||
ea.Flags = info.Flags
|
||||
if info.NextEntryOffset != 0 {
|
||||
nb = b[info.NextEntryOffset:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
|
||||
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
|
||||
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
|
||||
for len(b) != 0 {
|
||||
ea, nb, err := parseEa(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eas = append(eas, ea)
|
||||
b = nb
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
|
||||
if int(uint8(len(ea.Name))) != len(ea.Name) {
|
||||
return errEaNameTooLarge
|
||||
}
|
||||
if int(uint16(len(ea.Value))) != len(ea.Value) {
|
||||
return errEaValueTooLarge
|
||||
}
|
||||
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
|
||||
withPadding := (entrySize + 3) &^ 3
|
||||
nextOffset := uint32(0)
|
||||
if !last {
|
||||
nextOffset = withPadding
|
||||
}
|
||||
info := fileFullEaInformation{
|
||||
NextEntryOffset: nextOffset,
|
||||
Flags: ea.Flags,
|
||||
NameLength: uint8(len(ea.Name)),
|
||||
ValueLength: uint16(len(ea.Value)),
|
||||
}
|
||||
|
||||
err := binary.Write(buf, binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte(ea.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = buf.WriteByte(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write(ea.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
|
||||
// buffer for use with BackupWrite, ZwSetEaFile, etc.
|
||||
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for i := range eas {
|
||||
last := false
|
||||
if i == len(eas)-1 {
|
||||
last = true
|
||||
}
|
||||
|
||||
err := writeEa(&buf, &eas[i], last)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type fileFullEaInformation struct {
|
||||
NextEntryOffset uint32
|
||||
Flags uint8
|
||||
NameLength uint8
|
||||
ValueLength uint16
|
||||
}
|
||||
|
||||
var (
|
||||
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
|
||||
|
||||
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
|
||||
errEaNameTooLarge = errors.New("extended attribute name too large")
|
||||
errEaValueTooLarge = errors.New("extended attribute value too large")
|
||||
)
|
||||
|
||||
// ExtendedAttribute represents a single Windows EA.
|
||||
type ExtendedAttribute struct {
|
||||
Name string
|
||||
Value []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
|
||||
var info fileFullEaInformation
|
||||
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
err = errInvalidEaBuffer
|
||||
return
|
||||
}
|
||||
|
||||
nameOffset := fileFullEaInformationSize
|
||||
nameLen := int(info.NameLength)
|
||||
valueOffset := nameOffset + int(info.NameLength) + 1
|
||||
valueLen := int(info.ValueLength)
|
||||
nextOffset := int(info.NextEntryOffset)
|
||||
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
|
||||
err = errInvalidEaBuffer
|
||||
return
|
||||
}
|
||||
|
||||
ea.Name = string(b[nameOffset : nameOffset+nameLen])
|
||||
ea.Value = b[valueOffset : valueOffset+valueLen]
|
||||
ea.Flags = info.Flags
|
||||
if info.NextEntryOffset != 0 {
|
||||
nb = b[info.NextEntryOffset:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
|
||||
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
|
||||
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
|
||||
for len(b) != 0 {
|
||||
ea, nb, err := parseEa(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eas = append(eas, ea)
|
||||
b = nb
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
|
||||
if int(uint8(len(ea.Name))) != len(ea.Name) {
|
||||
return errEaNameTooLarge
|
||||
}
|
||||
if int(uint16(len(ea.Value))) != len(ea.Value) {
|
||||
return errEaValueTooLarge
|
||||
}
|
||||
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
|
||||
withPadding := (entrySize + 3) &^ 3
|
||||
nextOffset := uint32(0)
|
||||
if !last {
|
||||
nextOffset = withPadding
|
||||
}
|
||||
info := fileFullEaInformation{
|
||||
NextEntryOffset: nextOffset,
|
||||
Flags: ea.Flags,
|
||||
NameLength: uint8(len(ea.Name)),
|
||||
ValueLength: uint16(len(ea.Value)),
|
||||
}
|
||||
|
||||
err := binary.Write(buf, binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte(ea.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = buf.WriteByte(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write(ea.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
|
||||
// buffer for use with BackupWrite, ZwSetEaFile, etc.
|
||||
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for i := range eas {
|
||||
last := false
|
||||
if i == len(eas)-1 {
|
||||
last = true
|
||||
}
|
||||
|
||||
err := writeEa(&buf, &eas[i], last)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#### Support
|
||||
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
|
||||
|
||||
#### What to contribute
|
||||
If you don't know what to do, there are some features and functions that need to be done
|
||||
|
||||
- [ ] Refactor code
|
||||
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
|
||||
- [ ] Create actual list of contributors and projects that currently using this package
|
||||
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
|
||||
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
|
||||
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
|
||||
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
|
||||
- [ ] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
|
||||
- [ ] Implement fuzzing testing
|
||||
- [ ] Implement some struct/map/array utilities
|
||||
- [ ] Implement map/array validation
|
||||
- [ ] Implement benchmarking
|
||||
- [ ] Implement batch of examples
|
||||
- [ ] Look at forks for new features and fixes
|
||||
|
||||
#### Advice
|
||||
Feel free to create what you want, but keep in mind when you implement new features:
|
||||
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
|
||||
- Public functions must be documented and described in source file and added to README.md to the list of available functions
|
||||
- There are must be unit-tests for any new functions and improvements
|
||||
|
||||
## Financial contributions
|
||||
|
||||
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/govalidator).
|
||||
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you to all the people who have already contributed to govalidator!
|
||||
<a href="graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
|
||||
|
||||
|
||||
### Backers
|
||||
|
||||
Thank you to all our backers! [[Become a backer](https://opencollective.com/govalidator#backer)]
|
||||
|
||||
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
|
||||
|
||||
|
||||
### Sponsors
|
||||
|
||||
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/govalidator#sponsor))
|
||||
|
||||
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
|
||||
+119
-52
@@ -1,7 +1,7 @@
|
||||
govalidator
|
||||
===========
|
||||
[](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://godoc.org/github.com/asaskevich/govalidator) [](https://coveralls.io/r/asaskevich/govalidator?branch=master) [](https://app.wercker.com/project/bykey/1ec990b09ea86c910d5f08b0e02c6043)
|
||||
[](https://travis-ci.org/asaskevich/govalidator) [](https://goreportcard.com/report/github.com/asaskevich/govalidator) [](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator)
|
||||
[](https://travis-ci.org/asaskevich/govalidator) [](https://goreportcard.com/report/github.com/asaskevich/govalidator) [](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator) [](#backers) [](#sponsors)
|
||||
|
||||
A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).
|
||||
|
||||
@@ -156,6 +156,7 @@ func IsPort(str string) bool
|
||||
func IsPositive(value float64) bool
|
||||
func IsPrintableASCII(str string) bool
|
||||
func IsRFC3339(str string) bool
|
||||
func IsRFC3339WithoutZone(str string) bool
|
||||
func IsRGBcolor(str string) bool
|
||||
func IsRequestURI(rawurl string) bool
|
||||
func IsRequestURL(rawurl string) bool
|
||||
@@ -269,56 +270,57 @@ For completely custom validators (interface-based), see below.
|
||||
|
||||
Here is a list of available validators for struct fields (validator - used function):
|
||||
```go
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"rfc3339WithoutZone": IsRFC3339WithoutZone,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
```
|
||||
Validators with parameters
|
||||
|
||||
@@ -404,12 +406,50 @@ govalidator.CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator
|
||||
}))
|
||||
```
|
||||
|
||||
###### Custom error messages
|
||||
Custom error messages are supported via annotations by adding the `~` separator - here's an example of how to use it:
|
||||
```go
|
||||
type Ticket struct {
|
||||
Id int64 `json:"id"`
|
||||
FirstName string `json:"firstname" valid:"required~First name is blank"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Notes
|
||||
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
|
||||
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
|
||||
|
||||
#### Support
|
||||
If you do have a contribution for the package feel free to put up a Pull Request or open Issue.
|
||||
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
|
||||
|
||||
#### What to contribute
|
||||
If you don't know what to do, there are some features and functions that need to be done
|
||||
|
||||
- [ ] Refactor code
|
||||
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
|
||||
- [ ] Create actual list of contributors and projects that currently using this package
|
||||
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
|
||||
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
|
||||
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
|
||||
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
|
||||
- [ ] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
|
||||
- [ ] Implement fuzzing testing
|
||||
- [ ] Implement some struct/map/array utilities
|
||||
- [ ] Implement map/array validation
|
||||
- [ ] Implement benchmarking
|
||||
- [ ] Implement batch of examples
|
||||
- [ ] Look at forks for new features and fixes
|
||||
|
||||
#### Advice
|
||||
Feel free to create what you want, but keep in mind when you implement new features:
|
||||
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
|
||||
- Public functions must be documented and described in source file and added to README.md to the list of available functions
|
||||
- There are must be unit-tests for any new functions and improvements
|
||||
|
||||
## Credits
|
||||
### Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
|
||||
#### Special thanks to [contributors](https://github.com/asaskevich/govalidator/graphs/contributors)
|
||||
* [Daniel Lohse](https://github.com/annismckenzie)
|
||||
@@ -421,3 +461,30 @@ If you do have a contribution for the package feel free to put up a Pull Request
|
||||
* [Nathan Davies](https://github.com/nathj07)
|
||||
* [Matt Sanford](https://github.com/mzsanford)
|
||||
* [Simon ccl1115](https://github.com/ccl1115)
|
||||
|
||||
<a href="graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
|
||||
|
||||
|
||||
### Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/govalidator#backer)]
|
||||
|
||||
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
|
||||
|
||||
|
||||
### Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/govalidator#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
|
||||
|
||||
|
||||
|
||||
+24
-5
@@ -3,6 +3,7 @@ package govalidator
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
@@ -30,13 +31,31 @@ func ToFloat(str string) (float64, error) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ToInt convert the input string to an integer, or 0 if the input is not an integer.
|
||||
func ToInt(str string) (int64, error) {
|
||||
res, err := strconv.ParseInt(str, 0, 64)
|
||||
if err != nil {
|
||||
// ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer.
|
||||
func ToInt(value interface{}) (res int64, err error) {
|
||||
val := reflect.ValueOf(value)
|
||||
|
||||
switch value.(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
res = val.Int()
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
res = int64(val.Uint())
|
||||
case string:
|
||||
if IsInt(val.String()) {
|
||||
res, err = strconv.ParseInt(val.String(), 0, 64)
|
||||
if err != nil {
|
||||
res = 0
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("math: square root of negative number %g", value)
|
||||
res = 0
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("math: square root of negative number %g", value)
|
||||
res = 0
|
||||
}
|
||||
return res, err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ToBoolean convert the input string to a boolean.
|
||||
|
||||
+8
-3
@@ -1,5 +1,7 @@
|
||||
package govalidator
|
||||
|
||||
import "strings"
|
||||
|
||||
// Errors is an array of multiple errors and conforms to the error interface.
|
||||
type Errors []error
|
||||
|
||||
@@ -9,11 +11,11 @@ func (es Errors) Errors() []error {
|
||||
}
|
||||
|
||||
func (es Errors) Error() string {
|
||||
var err string
|
||||
var errs []string
|
||||
for _, e := range es {
|
||||
err += e.Error() + ";"
|
||||
errs = append(errs, e.Error())
|
||||
}
|
||||
return err
|
||||
return strings.Join(errs, ";")
|
||||
}
|
||||
|
||||
// Error encapsulates a name, an error and whether there's a custom error message or not.
|
||||
@@ -21,6 +23,9 @@ type Error struct {
|
||||
Name string
|
||||
Err error
|
||||
CustomErrorMessageExists bool
|
||||
|
||||
// Validator indicates the name of the validator that failed
|
||||
Validator string
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
|
||||
+42
-2
@@ -1,6 +1,9 @@
|
||||
package govalidator
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Abs returns absolute value of number
|
||||
func Abs(value float64) float64 {
|
||||
@@ -39,13 +42,50 @@ func IsNonPositive(value float64) bool {
|
||||
}
|
||||
|
||||
// InRange returns true if value lies between left and right border
|
||||
func InRange(value, left, right float64) bool {
|
||||
func InRangeInt(value, left, right interface{}) bool {
|
||||
value64, _ := ToInt(value)
|
||||
left64, _ := ToInt(left)
|
||||
right64, _ := ToInt(right)
|
||||
if left64 > right64 {
|
||||
left64, right64 = right64, left64
|
||||
}
|
||||
return value64 >= left64 && value64 <= right64
|
||||
}
|
||||
|
||||
// InRange returns true if value lies between left and right border
|
||||
func InRangeFloat32(value, left, right float32) bool {
|
||||
if left > right {
|
||||
left, right = right, left
|
||||
}
|
||||
return value >= left && value <= right
|
||||
}
|
||||
|
||||
// InRange returns true if value lies between left and right border
|
||||
func InRangeFloat64(value, left, right float64) bool {
|
||||
if left > right {
|
||||
left, right = right, left
|
||||
}
|
||||
return value >= left && value <= right
|
||||
}
|
||||
|
||||
// InRange returns true if value lies between left and right border, generic type to handle int, float32 or float64, all types must the same type
|
||||
func InRange(value interface{}, left interface{}, right interface{}) bool {
|
||||
|
||||
reflectValue := reflect.TypeOf(value).Kind()
|
||||
reflectLeft := reflect.TypeOf(left).Kind()
|
||||
reflectRight := reflect.TypeOf(right).Kind()
|
||||
|
||||
if reflectValue == reflect.Int && reflectLeft == reflect.Int && reflectRight == reflect.Int {
|
||||
return InRangeInt(value.(int), left.(int), right.(int))
|
||||
} else if reflectValue == reflect.Float32 && reflectLeft == reflect.Float32 && reflectRight == reflect.Float32 {
|
||||
return InRangeFloat32(value.(float32), left.(float32), right.(float32))
|
||||
} else if reflectValue == reflect.Float64 && reflectLeft == reflect.Float64 && reflectRight == reflect.Float64 {
|
||||
return InRangeFloat64(value.(float64), left.(float64), right.(float64))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsWhole returns true if value is whole number
|
||||
func IsWhole(value float64) bool {
|
||||
return math.Remainder(value, 1) == 0
|
||||
|
||||
+9
-3
@@ -4,7 +4,7 @@ import "regexp"
|
||||
|
||||
// Basic regular expressions for validating strings
|
||||
const (
|
||||
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||
//Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$"
|
||||
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||
ISBN13 string = "^(?:[0-9]{13})$"
|
||||
@@ -33,7 +33,6 @@ const (
|
||||
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
|
||||
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
|
||||
URLUsername string = `(\S+(:\S*)?@)`
|
||||
Hostname string = ``
|
||||
URLPath string = `((\/|\?|#)[^\s]*)`
|
||||
URLPort string = `(:(\d{1,5}))`
|
||||
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))`
|
||||
@@ -44,6 +43,8 @@ const (
|
||||
UnixPath string = `^(/[^/\x00]*)+/?$`
|
||||
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
|
||||
tagName string = "valid"
|
||||
hasLowerCase string = ".*[[:lower:]]"
|
||||
hasUpperCase string = ".*[[:upper:]]"
|
||||
)
|
||||
|
||||
// Used by IsFilePath func
|
||||
@@ -57,7 +58,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
rxEmail = regexp.MustCompile(Email)
|
||||
userRegexp = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$")
|
||||
hostRegexp = regexp.MustCompile("^[^\\s]+\\.[^\\s]+$")
|
||||
userDotRegexp = regexp.MustCompile("(^[.]{1})|([.]{1}$)|([.]{2,})")
|
||||
//rxEmail = regexp.MustCompile(Email)
|
||||
rxCreditCard = regexp.MustCompile(CreditCard)
|
||||
rxISBN10 = regexp.MustCompile(ISBN10)
|
||||
rxISBN13 = regexp.MustCompile(ISBN13)
|
||||
@@ -88,4 +92,6 @@ var (
|
||||
rxWinPath = regexp.MustCompile(WinPath)
|
||||
rxUnixPath = regexp.MustCompile(UnixPath)
|
||||
rxSemver = regexp.MustCompile(Semver)
|
||||
rxHasLowerCase = regexp.MustCompile(hasLowerCase)
|
||||
rxHasUpperCase = regexp.MustCompile(hasUpperCase)
|
||||
)
|
||||
|
||||
+54
-51
@@ -34,6 +34,7 @@ var ParamTagMap = map[string]ParamValidator{
|
||||
"stringlength": StringLength,
|
||||
"matches": StringMatches,
|
||||
"in": isInRaw,
|
||||
"rsapub": IsRsaPub,
|
||||
}
|
||||
|
||||
// ParamTagRegexMap maps param tags to their respective regexes.
|
||||
@@ -44,6 +45,7 @@ var ParamTagRegexMap = map[string]*regexp.Regexp{
|
||||
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
|
||||
"in": regexp.MustCompile(`^in\((.*)\)`),
|
||||
"matches": regexp.MustCompile(`^matches\((.+)\)$`),
|
||||
"rsapub": regexp.MustCompile("^rsapub\\((\\d+)\\)$"),
|
||||
}
|
||||
|
||||
type customTypeTagMap struct {
|
||||
@@ -72,57 +74,58 @@ var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeV
|
||||
|
||||
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
|
||||
var TagMap = map[string]Validator{
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
"ISO4217": IsISO4217,
|
||||
"email": IsEmail,
|
||||
"url": IsURL,
|
||||
"dialstring": IsDialString,
|
||||
"requrl": IsRequestURL,
|
||||
"requri": IsRequestURI,
|
||||
"alpha": IsAlpha,
|
||||
"utfletter": IsUTFLetter,
|
||||
"alphanum": IsAlphanumeric,
|
||||
"utfletternum": IsUTFLetterNumeric,
|
||||
"numeric": IsNumeric,
|
||||
"utfnumeric": IsUTFNumeric,
|
||||
"utfdigit": IsUTFDigit,
|
||||
"hexadecimal": IsHexadecimal,
|
||||
"hexcolor": IsHexcolor,
|
||||
"rgbcolor": IsRGBcolor,
|
||||
"lowercase": IsLowerCase,
|
||||
"uppercase": IsUpperCase,
|
||||
"int": IsInt,
|
||||
"float": IsFloat,
|
||||
"null": IsNull,
|
||||
"uuid": IsUUID,
|
||||
"uuidv3": IsUUIDv3,
|
||||
"uuidv4": IsUUIDv4,
|
||||
"uuidv5": IsUUIDv5,
|
||||
"creditcard": IsCreditCard,
|
||||
"isbn10": IsISBN10,
|
||||
"isbn13": IsISBN13,
|
||||
"json": IsJSON,
|
||||
"multibyte": IsMultibyte,
|
||||
"ascii": IsASCII,
|
||||
"printableascii": IsPrintableASCII,
|
||||
"fullwidth": IsFullWidth,
|
||||
"halfwidth": IsHalfWidth,
|
||||
"variablewidth": IsVariableWidth,
|
||||
"base64": IsBase64,
|
||||
"datauri": IsDataURI,
|
||||
"ip": IsIP,
|
||||
"port": IsPort,
|
||||
"ipv4": IsIPv4,
|
||||
"ipv6": IsIPv6,
|
||||
"dns": IsDNSName,
|
||||
"host": IsHost,
|
||||
"mac": IsMAC,
|
||||
"latitude": IsLatitude,
|
||||
"longitude": IsLongitude,
|
||||
"ssn": IsSSN,
|
||||
"semver": IsSemver,
|
||||
"rfc3339": IsRFC3339,
|
||||
"rfc3339WithoutZone": IsRFC3339WithoutZone,
|
||||
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||
"ISO4217": IsISO4217,
|
||||
}
|
||||
|
||||
// ISO3166Entry stores country codes
|
||||
|
||||
+3
-1
@@ -108,7 +108,9 @@ func CamelCaseToUnderscore(str string) string {
|
||||
var output []rune
|
||||
var segment []rune
|
||||
for _, r := range str {
|
||||
if !unicode.IsLower(r) {
|
||||
|
||||
// not treat number as separate segment
|
||||
if !unicode.IsLower(r) && string(r) != "_" && !unicode.IsNumber(r) {
|
||||
output = addSegment(output, segment)
|
||||
segment = nil
|
||||
}
|
||||
|
||||
+196
-27
@@ -2,8 +2,14 @@
|
||||
package govalidator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
@@ -20,10 +26,12 @@ var (
|
||||
fieldsRequiredByDefault bool
|
||||
notNumberRegexp = regexp.MustCompile("[^0-9]+")
|
||||
whiteSpacesAndMinus = regexp.MustCompile("[\\s-]+")
|
||||
paramsRegexp = regexp.MustCompile("\\(.*\\)$")
|
||||
)
|
||||
|
||||
const maxURLRuneCount = 2083
|
||||
const minURLRuneCount = 3
|
||||
const RF3339WithoutZone = "2006-01-02T15:04:05"
|
||||
|
||||
// SetFieldsRequiredByDefault causes validation to fail when struct fields
|
||||
// do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`).
|
||||
@@ -44,9 +52,33 @@ func SetFieldsRequiredByDefault(value bool) {
|
||||
}
|
||||
|
||||
// IsEmail check if the string is an email.
|
||||
func IsEmail(str string) bool {
|
||||
// TODO uppercase letters are not supported
|
||||
return rxEmail.MatchString(str)
|
||||
func IsEmail(email string) bool {
|
||||
if len(email) < 6 || len(email) > 254 {
|
||||
return false
|
||||
}
|
||||
at := strings.LastIndex(email, "@")
|
||||
if at <= 0 || at > len(email)-3 {
|
||||
return false
|
||||
}
|
||||
user := email[:at]
|
||||
host := email[at+1:]
|
||||
if len(user) > 64 {
|
||||
return false
|
||||
}
|
||||
if userDotRegexp.MatchString(user) || !userRegexp.MatchString(user) || !hostRegexp.MatchString(host) {
|
||||
return false
|
||||
}
|
||||
switch host {
|
||||
case "localhost", "example.com":
|
||||
return true
|
||||
}
|
||||
if _, err := net.LookupMX(host); err != nil {
|
||||
if _, err := net.LookupIP(host); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsURL check if the string is an URL.
|
||||
@@ -54,7 +86,13 @@ func IsURL(str string) bool {
|
||||
if str == "" || utf8.RuneCountInString(str) >= maxURLRuneCount || len(str) <= minURLRuneCount || strings.HasPrefix(str, ".") {
|
||||
return false
|
||||
}
|
||||
u, err := url.Parse(str)
|
||||
strTemp := str
|
||||
if strings.Index(str, ":") >= 0 && strings.Index(str, "://") == -1 {
|
||||
// support no indicated urlscheme but with colon for port number
|
||||
// http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString
|
||||
strTemp = "http://" + str
|
||||
}
|
||||
u, err := url.Parse(strTemp)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -65,7 +103,6 @@ func IsURL(str string) bool {
|
||||
return false
|
||||
}
|
||||
return rxURL.MatchString(str)
|
||||
|
||||
}
|
||||
|
||||
// IsRequestURL check if the string rawurl, assuming
|
||||
@@ -218,6 +255,22 @@ func IsUpperCase(str string) bool {
|
||||
return str == strings.ToUpper(str)
|
||||
}
|
||||
|
||||
// HasLowerCase check if the string contains at least 1 lowercase. Empty string is valid.
|
||||
func HasLowerCase(str string) bool {
|
||||
if IsNull(str) {
|
||||
return true
|
||||
}
|
||||
return rxHasLowerCase.MatchString(str)
|
||||
}
|
||||
|
||||
// HasUpperCase check if the string contians as least 1 uppercase. Empty string is valid.
|
||||
func HasUpperCase(str string) bool {
|
||||
if IsNull(str) {
|
||||
return true
|
||||
}
|
||||
return rxHasUpperCase.MatchString(str)
|
||||
}
|
||||
|
||||
// IsInt check if the string is an integer. Empty string is valid.
|
||||
func IsInt(str string) bool {
|
||||
if IsNull(str) {
|
||||
@@ -486,6 +539,33 @@ func IsDNSName(str string) bool {
|
||||
return !IsIP(str) && rxDNSName.MatchString(str)
|
||||
}
|
||||
|
||||
// IsHash checks if a string is a hash of type algorithm.
|
||||
// Algorithm is one of ['md4', 'md5', 'sha1', 'sha256', 'sha384', 'sha512', 'ripemd128', 'ripemd160', 'tiger128', 'tiger160', 'tiger192', 'crc32', 'crc32b']
|
||||
func IsHash(str string, algorithm string) bool {
|
||||
len := "0"
|
||||
algo := strings.ToLower(algorithm)
|
||||
|
||||
if algo == "crc32" || algo == "crc32b" {
|
||||
len = "8"
|
||||
} else if algo == "md5" || algo == "md4" || algo == "ripemd128" || algo == "tiger128" {
|
||||
len = "32"
|
||||
} else if algo == "sha1" || algo == "ripemd160" || algo == "tiger160" {
|
||||
len = "40"
|
||||
} else if algo == "tiger192" {
|
||||
len = "48"
|
||||
} else if algo == "sha256" {
|
||||
len = "64"
|
||||
} else if algo == "sha384" {
|
||||
len = "96"
|
||||
} else if algo == "sha512" {
|
||||
len = "128"
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
return Matches(str, "^[a-f0-9]{"+len+"}$")
|
||||
}
|
||||
|
||||
// IsDialString validates the given string for usage with the various Dial() functions
|
||||
func IsDialString(str string) bool {
|
||||
|
||||
@@ -560,6 +640,40 @@ func IsLongitude(str string) bool {
|
||||
return rxLongitude.MatchString(str)
|
||||
}
|
||||
|
||||
// IsRsaPublicKey check if a string is valid public key with provided length
|
||||
func IsRsaPublicKey(str string, keylen int) bool {
|
||||
bb := bytes.NewBufferString(str)
|
||||
pemBytes, err := ioutil.ReadAll(bb)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block != nil && block.Type != "PUBLIC KEY" {
|
||||
return false
|
||||
}
|
||||
var der []byte
|
||||
|
||||
if block != nil {
|
||||
der = block.Bytes
|
||||
} else {
|
||||
der, err = base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKIXPublicKey(der)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
pubkey, ok := key.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
bitlen := len(pubkey.N.Bytes()) * 8
|
||||
return bitlen == int(keylen)
|
||||
}
|
||||
|
||||
func toJSONName(tag string) string {
|
||||
if tag == "" {
|
||||
return ""
|
||||
@@ -568,7 +682,16 @@ func toJSONName(tag string) string {
|
||||
// JSON name always comes first. If there's no options then split[0] is
|
||||
// JSON name, if JSON name is not set, then split[0] is an empty string.
|
||||
split := strings.SplitN(tag, ",", 2)
|
||||
return split[0]
|
||||
|
||||
name := split[0]
|
||||
|
||||
// However it is possible that the field is skipped when
|
||||
// (de-)serializing from/to JSON, in which case assume that there is no
|
||||
// tag name to use
|
||||
if name == "-" {
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// ValidateStruct use tags for fields.
|
||||
@@ -595,7 +718,9 @@ func ValidateStruct(s interface{}) (bool, error) {
|
||||
continue // Private field
|
||||
}
|
||||
structResult := true
|
||||
if valueField.Kind() == reflect.Struct && typeField.Tag.Get(tagName) != "-" {
|
||||
if (valueField.Kind() == reflect.Struct ||
|
||||
(valueField.Kind() == reflect.Ptr && valueField.Elem().Kind() == reflect.Struct)) &&
|
||||
typeField.Tag.Get(tagName) != "-" {
|
||||
var err error
|
||||
structResult, err = ValidateStruct(valueField.Interface())
|
||||
if err != nil {
|
||||
@@ -613,6 +738,14 @@ func ValidateStruct(s interface{}) (bool, error) {
|
||||
jsonError.Name = jsonTag
|
||||
err2 = jsonError
|
||||
case Errors:
|
||||
for i2, err3 := range jsonError {
|
||||
switch customErr := err3.(type) {
|
||||
case Error:
|
||||
customErr.Name = jsonTag
|
||||
jsonError[i2] = customErr
|
||||
}
|
||||
}
|
||||
|
||||
err2 = jsonError
|
||||
}
|
||||
}
|
||||
@@ -630,8 +763,11 @@ func ValidateStruct(s interface{}) (bool, error) {
|
||||
// parseTagIntoMap parses a struct tag `valid:required~Some error message,length(2|3)` into map[string]string{"required": "Some error message", "length(2|3)": ""}
|
||||
func parseTagIntoMap(tag string) tagOptionsMap {
|
||||
optionsMap := make(tagOptionsMap)
|
||||
options := strings.SplitN(tag, ",", -1)
|
||||
options := strings.Split(tag, ",")
|
||||
|
||||
for _, option := range options {
|
||||
option = strings.TrimSpace(option)
|
||||
|
||||
validationOptions := strings.Split(option, "~")
|
||||
if !isValidTag(validationOptions[0]) {
|
||||
continue
|
||||
@@ -688,6 +824,11 @@ func IsRFC3339(str string) bool {
|
||||
return IsTime(str, time.RFC3339)
|
||||
}
|
||||
|
||||
// IsRFC3339WithoutZone check if string is valid timestamp value according to RFC3339 which excludes the timezone.
|
||||
func IsRFC3339WithoutZone(str string) bool {
|
||||
return IsTime(str, RF3339WithoutZone)
|
||||
}
|
||||
|
||||
// IsISO4217 check if string is valid ISO currency code
|
||||
func IsISO4217(str string) bool {
|
||||
for _, currency := range ISO4217List {
|
||||
@@ -716,6 +857,17 @@ func RuneLength(str string, params ...string) bool {
|
||||
return StringLength(str, params...)
|
||||
}
|
||||
|
||||
// IsRsaPub check whether string is valid RSA key
|
||||
// Alias for IsRsaPublicKey
|
||||
func IsRsaPub(str string, params ...string) bool {
|
||||
if len(params) == 1 {
|
||||
len, _ := ToInt(params[0])
|
||||
return IsRsaPublicKey(str, int(len))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// StringMatches checks if a string matches a given pattern.
|
||||
func StringMatches(s string, params ...string) bool {
|
||||
if len(params) == 1 {
|
||||
@@ -776,11 +928,11 @@ func IsIn(str string, params ...string) bool {
|
||||
func checkRequired(v reflect.Value, t reflect.StructField, options tagOptionsMap) (bool, error) {
|
||||
if requiredOption, isRequired := options["required"]; isRequired {
|
||||
if len(requiredOption) > 0 {
|
||||
return false, Error{t.Name, fmt.Errorf(requiredOption), true}
|
||||
return false, Error{t.Name, fmt.Errorf(requiredOption), true, "required"}
|
||||
}
|
||||
return false, Error{t.Name, fmt.Errorf("non zero value required"), false}
|
||||
return false, Error{t.Name, fmt.Errorf("non zero value required"), false, "required"}
|
||||
} else if _, isOptional := options["optional"]; fieldsRequiredByDefault && !isOptional {
|
||||
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false}
|
||||
return false, Error{t.Name, fmt.Errorf("Missing required field"), false, "required"}
|
||||
}
|
||||
// not required and empty is valid
|
||||
return true, nil
|
||||
@@ -799,7 +951,7 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
|
||||
if !fieldsRequiredByDefault {
|
||||
return true, nil
|
||||
}
|
||||
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false}
|
||||
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false, "required"}
|
||||
case "-":
|
||||
return true, nil
|
||||
}
|
||||
@@ -822,10 +974,10 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
|
||||
|
||||
if result := validatefunc(v.Interface(), o.Interface()); !result {
|
||||
if len(customErrorMessage) > 0 {
|
||||
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf(customErrorMessage), CustomErrorMessageExists: true})
|
||||
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf(customErrorMessage), CustomErrorMessageExists: true, Validator: stripParams(validatorName)})
|
||||
continue
|
||||
}
|
||||
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), validatorName), CustomErrorMessageExists: false})
|
||||
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), validatorName), CustomErrorMessageExists: false, Validator: stripParams(validatorName)})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -844,7 +996,7 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
|
||||
for validator := range options {
|
||||
isValid = false
|
||||
resultErr = Error{t.Name, fmt.Errorf(
|
||||
"The following validator is invalid or can't be applied to the field: %q", validator), false}
|
||||
"The following validator is invalid or can't be applied to the field: %q", validator), false, stripParams(validator)}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -884,20 +1036,24 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
|
||||
delete(options, validatorSpec)
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
case reflect.String,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
|
||||
field := fmt.Sprint(v) // make value into string, then validate with regex
|
||||
if result := validatefunc(field, ps[1:]...); (!result && !negate) || (result && negate) {
|
||||
if customMsgExists {
|
||||
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists}
|
||||
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists, stripParams(validatorSpec)}
|
||||
}
|
||||
if negate {
|
||||
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists}
|
||||
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
|
||||
}
|
||||
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists}
|
||||
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
|
||||
}
|
||||
default:
|
||||
// type not yet supported, fail
|
||||
return false, Error{t.Name, fmt.Errorf("Validator %s doesn't support kind %s", validator, v.Kind()), false}
|
||||
return false, Error{t.Name, fmt.Errorf("Validator %s doesn't support kind %s", validator, v.Kind()), false, stripParams(validatorSpec)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,17 +1065,17 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
|
||||
field := fmt.Sprint(v) // make value into string, then validate with regex
|
||||
if result := validatefunc(field); !result && !negate || result && negate {
|
||||
if customMsgExists {
|
||||
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists}
|
||||
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists, stripParams(validatorSpec)}
|
||||
}
|
||||
if negate {
|
||||
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists}
|
||||
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
|
||||
}
|
||||
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists}
|
||||
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
|
||||
}
|
||||
default:
|
||||
//Not Yet Supported Types (Fail here!)
|
||||
err := fmt.Errorf("Validator %s doesn't support kind %s for value %v", validator, v.Kind(), v)
|
||||
return false, Error{t.Name, err, false}
|
||||
return false, Error{t.Name, err, false, stripParams(validatorSpec)}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -933,9 +1089,18 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
|
||||
sort.Sort(sv)
|
||||
result := true
|
||||
for _, k := range sv {
|
||||
resultItem, err := ValidateStruct(v.MapIndex(k).Interface())
|
||||
if err != nil {
|
||||
return false, err
|
||||
var resultItem bool
|
||||
var err error
|
||||
if v.MapIndex(k).Kind() != reflect.Struct {
|
||||
resultItem, err = typeCheck(v.MapIndex(k), t, o, options)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
resultItem, err = ValidateStruct(v.MapIndex(k).Interface())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
result = result && resultItem
|
||||
}
|
||||
@@ -978,6 +1143,10 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
|
||||
}
|
||||
}
|
||||
|
||||
func stripParams(validatorString string) string {
|
||||
return paramsRegexp.ReplaceAllString(validatorString, "")
|
||||
}
|
||||
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.String, reflect.Array:
|
||||
|
||||
+21
-6
@@ -1,11 +1,15 @@
|
||||
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html)
|
||||
# jwt-go
|
||||
|
||||
[](https://travis-ci.org/dgrijalva/jwt-go)
|
||||
[](https://godoc.org/github.com/dgrijalva/jwt-go)
|
||||
|
||||
**BREAKING CHANGES:*** Version 3.0.0 is here. It includes _a lot_ of changes including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
|
||||
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html)
|
||||
|
||||
**NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided.
|
||||
**NEW VERSION COMING:** There have been a lot of improvements suggested since the version 3.0.0 released in 2016. I'm working now on cutting two different releases: 3.2.0 will contain any non-breaking changes or enhancements. 4.0.0 will follow shortly which will include breaking changes. See the 4.0.0 milestone to get an idea of what's coming. If you have other ideas, or would like to participate in 4.0.0, now's the time. If you depend on this library and don't want to be interrupted, I recommend you use your dependency mangement tool to pin to version 3.
|
||||
|
||||
**SECURITY NOTICE:** Some older versions of Go have a security issue in the cryotp/elliptic. Recommendation is to upgrade to at least 1.8.3. See issue #216 for more detail.
|
||||
|
||||
**SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided.
|
||||
|
||||
## What the heck is a JWT?
|
||||
|
||||
@@ -37,7 +41,7 @@ Here's an example of an extension that integrates with the Google App Engine sig
|
||||
|
||||
## Compliance
|
||||
|
||||
This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences:
|
||||
This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences:
|
||||
|
||||
* In order to protect against accidental use of [Unsecured JWTs](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#UnsecuredJWT), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key.
|
||||
|
||||
@@ -47,7 +51,10 @@ This library is considered production ready. Feedback and feature requests are
|
||||
|
||||
This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases).
|
||||
|
||||
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning.
|
||||
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v3`. It will do the right thing WRT semantic versioning.
|
||||
|
||||
**BREAKING CHANGES:***
|
||||
* Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
|
||||
|
||||
## Usage Tips
|
||||
|
||||
@@ -68,6 +75,14 @@ Symmetric signing methods, such as HSA, use only a single secret. This is probab
|
||||
|
||||
Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification.
|
||||
|
||||
### Signing Methods and Key Types
|
||||
|
||||
Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones:
|
||||
|
||||
* The [HMAC signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
|
||||
* The [RSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
|
||||
* The [ECDSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
|
||||
|
||||
### JWT and OAuth
|
||||
|
||||
It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication.
|
||||
@@ -77,7 +92,7 @@ Without going too far down the rabbit hole, here's a description of the interact
|
||||
* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth.
|
||||
* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token.
|
||||
* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL.
|
||||
|
||||
|
||||
## More
|
||||
|
||||
Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go).
|
||||
|
||||
+7
@@ -1,5 +1,12 @@
|
||||
## `jwt-go` Version History
|
||||
|
||||
#### 3.2.0
|
||||
|
||||
* Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation
|
||||
* HMAC signing method returns `ErrInvalidKeyType` instead of `ErrInvalidKey` where appropriate
|
||||
* Added options to `request.ParseFromRequest`, which allows for an arbitrary list of modifiers to parsing behavior. Initial set include `WithClaims` and `WithParser`. Existing usage of this function will continue to work as before.
|
||||
* Deprecated `ParseFromRequestWithClaims` to simplify API in the future.
|
||||
|
||||
#### 3.1.0
|
||||
|
||||
* Improvements to `jwt` command line tool
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ var (
|
||||
)
|
||||
|
||||
// Implements the ECDSA family of signing methods signing methods
|
||||
// Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification
|
||||
type SigningMethodECDSA struct {
|
||||
Name string
|
||||
Hash crypto.Hash
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// Implements the HMAC-SHA family of signing methods signing methods
|
||||
// Expects key type of []byte for both signing and validation
|
||||
type SigningMethodHMAC struct {
|
||||
Name string
|
||||
Hash crypto.Hash
|
||||
@@ -90,5 +91,5 @@ func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string,
|
||||
return EncodeSegment(hasher.Sum(nil)), nil
|
||||
}
|
||||
|
||||
return "", ErrInvalidKey
|
||||
return "", ErrInvalidKeyType
|
||||
}
|
||||
|
||||
+65
-48
@@ -21,55 +21,9 @@ func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||
}
|
||||
|
||||
func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
|
||||
}
|
||||
|
||||
var err error
|
||||
token := &Token{Raw: tokenString}
|
||||
|
||||
// parse Header
|
||||
var headerBytes []byte
|
||||
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
|
||||
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
|
||||
return token, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
|
||||
}
|
||||
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
|
||||
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
|
||||
// parse Claims
|
||||
var claimBytes []byte
|
||||
token.Claims = claims
|
||||
|
||||
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
|
||||
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
|
||||
if p.UseJSONNumber {
|
||||
dec.UseNumber()
|
||||
}
|
||||
// JSON Decode. Special case for map type to avoid weird pointer behavior
|
||||
if c, ok := token.Claims.(MapClaims); ok {
|
||||
err = dec.Decode(&c)
|
||||
} else {
|
||||
err = dec.Decode(&claims)
|
||||
}
|
||||
// Handle decode error
|
||||
token, parts, err := p.ParseUnverified(tokenString, claims)
|
||||
if err != nil {
|
||||
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
|
||||
// Lookup signature method
|
||||
if method, ok := token.Header["alg"].(string); ok {
|
||||
if token.Method = GetSigningMethod(method); token.Method == nil {
|
||||
return token, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
|
||||
}
|
||||
} else {
|
||||
return token, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
|
||||
return token, err
|
||||
}
|
||||
|
||||
// Verify signing method is in the required set
|
||||
@@ -96,6 +50,9 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||
}
|
||||
if key, err = keyFunc(token); err != nil {
|
||||
// keyFunc returned an error
|
||||
if ve, ok := err.(*ValidationError); ok {
|
||||
return token, ve
|
||||
}
|
||||
return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
|
||||
}
|
||||
|
||||
@@ -129,3 +86,63 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||
|
||||
return token, vErr
|
||||
}
|
||||
|
||||
// WARNING: Don't use this method unless you know what you're doing
|
||||
//
|
||||
// This method parses the token but doesn't validate the signature. It's only
|
||||
// ever useful in cases where you know the signature is valid (because it has
|
||||
// been checked previously in the stack) and you want to extract values from
|
||||
// it.
|
||||
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
|
||||
parts = strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
|
||||
}
|
||||
|
||||
token = &Token{Raw: tokenString}
|
||||
|
||||
// parse Header
|
||||
var headerBytes []byte
|
||||
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
|
||||
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
|
||||
return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
|
||||
}
|
||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
|
||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
|
||||
// parse Claims
|
||||
var claimBytes []byte
|
||||
token.Claims = claims
|
||||
|
||||
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
|
||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
|
||||
if p.UseJSONNumber {
|
||||
dec.UseNumber()
|
||||
}
|
||||
// JSON Decode. Special case for map type to avoid weird pointer behavior
|
||||
if c, ok := token.Claims.(MapClaims); ok {
|
||||
err = dec.Decode(&c)
|
||||
} else {
|
||||
err = dec.Decode(&claims)
|
||||
}
|
||||
// Handle decode error
|
||||
if err != nil {
|
||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
||||
}
|
||||
|
||||
// Lookup signature method
|
||||
if method, ok := token.Header["alg"].(string); ok {
|
||||
if token.Method = GetSigningMethod(method); token.Method == nil {
|
||||
return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
|
||||
}
|
||||
} else {
|
||||
return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
|
||||
}
|
||||
|
||||
return token, parts, nil
|
||||
}
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// Implements the RSA family of signing methods signing methods
|
||||
// Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation
|
||||
type SigningMethodRSA struct {
|
||||
Name string
|
||||
Hash crypto.Hash
|
||||
@@ -44,7 +45,7 @@ func (m *SigningMethodRSA) Alg() string {
|
||||
}
|
||||
|
||||
// Implements the Verify method from SigningMethod
|
||||
// For this signing method, must be an rsa.PublicKey structure.
|
||||
// For this signing method, must be an *rsa.PublicKey structure.
|
||||
func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error {
|
||||
var err error
|
||||
|
||||
@@ -73,7 +74,7 @@ func (m *SigningMethodRSA) Verify(signingString, signature string, key interface
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod
|
||||
// For this signing method, must be an rsa.PrivateKey structure.
|
||||
// For this signing method, must be an *rsa.PrivateKey structure.
|
||||
func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) {
|
||||
var rsaKey *rsa.PrivateKey
|
||||
var ok bool
|
||||
|
||||
+32
@@ -39,6 +39,38 @@ func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
|
||||
return pkey, nil
|
||||
}
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 private key protected with password
|
||||
func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) {
|
||||
var err error
|
||||
|
||||
// Parse PEM block
|
||||
var block *pem.Block
|
||||
if block, _ = pem.Decode(key); block == nil {
|
||||
return nil, ErrKeyMustBePEMEncoded
|
||||
}
|
||||
|
||||
var parsedKey interface{}
|
||||
|
||||
var blockDecrypted []byte
|
||||
if blockDecrypted, err = x509.DecryptPEMBlock(block, []byte(password)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil {
|
||||
if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var pkey *rsa.PrivateKey
|
||||
var ok bool
|
||||
if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok {
|
||||
return nil, ErrNotRSAPrivateKey
|
||||
}
|
||||
|
||||
return pkey, nil
|
||||
}
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 public key
|
||||
func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
|
||||
var err error
|
||||
|
||||
+3
-1
@@ -72,7 +72,9 @@ func RecursiveUnmount(target string) error {
|
||||
}
|
||||
|
||||
// Make the deepest mount be first
|
||||
sort.Sort(sort.Reverse(byMountpoint(mounts)))
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return len(mounts[i].Mountpoint) > len(mounts[j].Mountpoint)
|
||||
})
|
||||
|
||||
for i, m := range mounts {
|
||||
if !strings.HasPrefix(m.Mountpoint, target) {
|
||||
|
||||
-14
@@ -38,17 +38,3 @@ type Info struct {
|
||||
// VfsOpts represents per super block options.
|
||||
VfsOpts string
|
||||
}
|
||||
|
||||
type byMountpoint []*Info
|
||||
|
||||
func (by byMountpoint) Len() int {
|
||||
return len(by)
|
||||
}
|
||||
|
||||
func (by byMountpoint) Less(i, j int) bool {
|
||||
return by[i].Mountpoint < by[j].Mountpoint
|
||||
}
|
||||
|
||||
func (by byMountpoint) Swap(i, j int) {
|
||||
by[i], by[j] = by[j], by[i]
|
||||
}
|
||||
|
||||
+5
@@ -1,6 +1,7 @@
|
||||
package system // import "github.com/docker/docker/pkg/system"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -53,6 +54,10 @@ func GetOSVersion() OSVersion {
|
||||
return osv
|
||||
}
|
||||
|
||||
func (osv OSVersion) ToString() string {
|
||||
return fmt.Sprintf("%d.%d.%d", osv.MajorVersion, osv.MinorVersion, osv.Build)
|
||||
}
|
||||
|
||||
// IsWindowsClient returns true if the SKU is client
|
||||
// @engine maintainers - this function should not be removed or modified as it
|
||||
// is used to enforce licensing restrictions on Windows.
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: 1.4
|
||||
- go: 1.5
|
||||
- go: 1.6
|
||||
- go: 1.7
|
||||
- go: 1.8
|
||||
- go: tip
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d .)
|
||||
- go vet $(go list ./... | grep -v /vendor/)
|
||||
- go test -v -race ./...
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# This is the official list of Gorilla WebSocket authors for copyright
|
||||
# purposes.
|
||||
#
|
||||
# Please keep the list sorted.
|
||||
|
||||
Gary Burd <gary@beagledreams.com>
|
||||
Joachim Bauch <mail@joachim-bauch.de>
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Gorilla WebSocket
|
||||
|
||||
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
|
||||
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
|
||||
|
||||
[](https://travis-ci.org/gorilla/websocket)
|
||||
[](https://godoc.org/github.com/gorilla/websocket)
|
||||
|
||||
### Documentation
|
||||
|
||||
* [API Reference](http://godoc.org/github.com/gorilla/websocket)
|
||||
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
|
||||
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
|
||||
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
|
||||
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
|
||||
|
||||
### Status
|
||||
|
||||
The Gorilla WebSocket package provides a complete and tested implementation of
|
||||
the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
|
||||
package API is stable.
|
||||
|
||||
### Installation
|
||||
|
||||
go get github.com/gorilla/websocket
|
||||
|
||||
### Protocol Compliance
|
||||
|
||||
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
|
||||
Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn
|
||||
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
|
||||
|
||||
### Gorilla WebSocket compared with other packages
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th><a href="http://godoc.org/github.com/gorilla/websocket">github.com/gorilla</a></th>
|
||||
<th><a href="http://godoc.org/golang.org/x/net/websocket">golang.org/x/net</a></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
|
||||
<tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
|
||||
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
|
||||
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
|
||||
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>
|
||||
<tr><td>Get the <a href="https://tools.ietf.org/html/rfc6455#section-5.6">type</a> of a received data message</td><td>Yes</td><td>Yes, see note 2</td></tr>
|
||||
<tr><td colspan="3">Other Features</tr></td>
|
||||
<tr><td><a href="https://tools.ietf.org/html/rfc7692">Compression Extensions</a></td><td>Experimental</td><td>No</td></tr>
|
||||
<tr><td>Read message using io.Reader</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextReader">Yes</a></td><td>No, see note 3</td></tr>
|
||||
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
|
||||
</table>
|
||||
|
||||
Notes:
|
||||
|
||||
1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
|
||||
2. The application can get the type of a received data message by implementing
|
||||
a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal)
|
||||
function.
|
||||
3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
|
||||
Read returns when the input buffer is full or a frame boundary is
|
||||
encountered. Each call to Write sends a single frame message. The Gorilla
|
||||
io.Reader and io.WriteCloser operate on a single WebSocket message.
|
||||
|
||||
+392
@@ -0,0 +1,392 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrBadHandshake is returned when the server response to opening handshake is
|
||||
// invalid.
|
||||
var ErrBadHandshake = errors.New("websocket: bad handshake")
|
||||
|
||||
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
|
||||
|
||||
// NewClient creates a new client connection using the given net connection.
|
||||
// The URL u specifies the host and request URI. Use requestHeader to specify
|
||||
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
|
||||
// (Cookie). Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etc.
|
||||
//
|
||||
// Deprecated: Use Dialer instead.
|
||||
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
|
||||
d := Dialer{
|
||||
ReadBufferSize: readBufSize,
|
||||
WriteBufferSize: writeBufSize,
|
||||
NetDial: func(net, addr string) (net.Conn, error) {
|
||||
return netConn, nil
|
||||
},
|
||||
}
|
||||
return d.Dial(u.String(), requestHeader)
|
||||
}
|
||||
|
||||
// A Dialer contains options for connecting to WebSocket server.
|
||||
type Dialer struct {
|
||||
// NetDial specifies the dial function for creating TCP connections. If
|
||||
// NetDial is nil, net.Dial is used.
|
||||
NetDial func(network, addr string) (net.Conn, error)
|
||||
|
||||
// Proxy specifies a function to return a proxy for a given
|
||||
// Request. If the function returns a non-nil error, the
|
||||
// request is aborted with the provided error.
|
||||
// If Proxy is nil or returns a nil *URL, no proxy is used.
|
||||
Proxy func(*http.Request) (*url.URL, error)
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
|
||||
// If nil, the default configuration is used.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
|
||||
// size is zero, then a useful default size is used. The I/O buffer sizes
|
||||
// do not limit the size of the messages that can be sent or received.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// Subprotocols specifies the client's requested subprotocols.
|
||||
Subprotocols []string
|
||||
|
||||
// EnableCompression specifies if the client should attempt to negotiate
|
||||
// per message compression (RFC 7692). Setting this value to true does not
|
||||
// guarantee that compression will be supported. Currently only "no context
|
||||
// takeover" modes are supported.
|
||||
EnableCompression bool
|
||||
|
||||
// Jar specifies the cookie jar.
|
||||
// If Jar is nil, cookies are not sent in requests and ignored
|
||||
// in responses.
|
||||
Jar http.CookieJar
|
||||
}
|
||||
|
||||
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||
|
||||
// parseURL parses the URL.
|
||||
//
|
||||
// This function is a replacement for the standard library url.Parse function.
|
||||
// In Go 1.4 and earlier, url.Parse loses information from the path.
|
||||
func parseURL(s string) (*url.URL, error) {
|
||||
// From the RFC:
|
||||
//
|
||||
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
|
||||
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
|
||||
var u url.URL
|
||||
switch {
|
||||
case strings.HasPrefix(s, "ws://"):
|
||||
u.Scheme = "ws"
|
||||
s = s[len("ws://"):]
|
||||
case strings.HasPrefix(s, "wss://"):
|
||||
u.Scheme = "wss"
|
||||
s = s[len("wss://"):]
|
||||
default:
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "?"); i >= 0 {
|
||||
u.RawQuery = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "/"); i >= 0 {
|
||||
u.Opaque = s[i:]
|
||||
s = s[:i]
|
||||
} else {
|
||||
u.Opaque = "/"
|
||||
}
|
||||
|
||||
u.Host = s
|
||||
|
||||
if strings.Contains(u.Host, "@") {
|
||||
// Don't bother parsing user information because user information is
|
||||
// not allowed in websocket URIs.
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
hostPort = u.Host
|
||||
hostNoPort = u.Host
|
||||
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
|
||||
hostNoPort = hostNoPort[:i]
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "wss":
|
||||
hostPort += ":443"
|
||||
case "https":
|
||||
hostPort += ":443"
|
||||
default:
|
||||
hostPort += ":80"
|
||||
}
|
||||
}
|
||||
return hostPort, hostNoPort
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default zero values.
|
||||
var DefaultDialer = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
|
||||
// Dial creates a new client connection. Use requestHeader to specify the
|
||||
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
|
||||
// Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etcetera. The response body may not contain the entire response and does not
|
||||
// need to be closed by the application.
|
||||
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
|
||||
if d == nil {
|
||||
d = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
}
|
||||
|
||||
challengeKey, err := generateChallengeKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
u, err := parseURL(urlStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ws":
|
||||
u.Scheme = "http"
|
||||
case "wss":
|
||||
u.Scheme = "https"
|
||||
default:
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
// User name and password are not allowed in websocket URIs.
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
// Set the cookies present in the cookie jar of the dialer
|
||||
if d.Jar != nil {
|
||||
for _, cookie := range d.Jar.Cookies(u) {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the request headers using the capitalization for names and values in
|
||||
// RFC examples. Although the capitalization shouldn't matter, there are
|
||||
// servers that depend on it. The Header.Set method is not used because the
|
||||
// method canonicalizes the header names.
|
||||
req.Header["Upgrade"] = []string{"websocket"}
|
||||
req.Header["Connection"] = []string{"Upgrade"}
|
||||
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
|
||||
req.Header["Sec-WebSocket-Version"] = []string{"13"}
|
||||
if len(d.Subprotocols) > 0 {
|
||||
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
|
||||
}
|
||||
for k, vs := range requestHeader {
|
||||
switch {
|
||||
case k == "Host":
|
||||
if len(vs) > 0 {
|
||||
req.Host = vs[0]
|
||||
}
|
||||
case k == "Upgrade" ||
|
||||
k == "Connection" ||
|
||||
k == "Sec-Websocket-Key" ||
|
||||
k == "Sec-Websocket-Version" ||
|
||||
k == "Sec-Websocket-Extensions" ||
|
||||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
|
||||
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
|
||||
default:
|
||||
req.Header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
if d.EnableCompression {
|
||||
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
|
||||
}
|
||||
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
|
||||
var proxyURL *url.URL
|
||||
// Check wether the proxy method has been configured
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err = d.Proxy(req)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var targetHostPort string
|
||||
if proxyURL != nil {
|
||||
targetHostPort, _ = hostPortNoPort(proxyURL)
|
||||
} else {
|
||||
targetHostPort = hostPort
|
||||
}
|
||||
|
||||
var deadline time.Time
|
||||
if d.HandshakeTimeout != 0 {
|
||||
deadline = time.Now().Add(d.HandshakeTimeout)
|
||||
}
|
||||
|
||||
netDial := d.NetDial
|
||||
if netDial == nil {
|
||||
netDialer := &net.Dialer{Deadline: deadline}
|
||||
netDial = netDialer.Dial
|
||||
}
|
||||
|
||||
netConn, err := netDial("tcp", targetHostPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if netConn != nil {
|
||||
netConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := netConn.SetDeadline(deadline); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if proxyURL != nil {
|
||||
connectHeader := make(http.Header)
|
||||
if user := proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
connectReq := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{Opaque: hostPort},
|
||||
Host: hostPort,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
connectReq.Write(netConn)
|
||||
|
||||
// Read response.
|
||||
// Okay to use and discard buffered reader here, because
|
||||
// TLS server will not speak until spoken to.
|
||||
br := bufio.NewReader(netConn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, nil, errors.New(f[1])
|
||||
}
|
||||
}
|
||||
|
||||
if u.Scheme == "https" {
|
||||
cfg := cloneTLSConfig(d.TLSClientConfig)
|
||||
if cfg.ServerName == "" {
|
||||
cfg.ServerName = hostNoPort
|
||||
}
|
||||
tlsConn := tls.Client(netConn, cfg)
|
||||
netConn = tlsConn
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
|
||||
|
||||
if err := req.Write(netConn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(conn.br, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if d.Jar != nil {
|
||||
if rc := resp.Cookies(); len(rc) > 0 {
|
||||
d.Jar.SetCookies(u, rc)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode != 101 ||
|
||||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
|
||||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
|
||||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
|
||||
// Before closing the network connection on return from this
|
||||
// function, slurp up some of the response to aid application
|
||||
// debugging.
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := io.ReadFull(resp.Body, buf)
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
|
||||
return nil, resp, ErrBadHandshake
|
||||
}
|
||||
|
||||
for _, ext := range parseExtensions(resp.Header) {
|
||||
if ext[""] != "permessage-deflate" {
|
||||
continue
|
||||
}
|
||||
_, snct := ext["server_no_context_takeover"]
|
||||
_, cnct := ext["client_no_context_takeover"]
|
||||
if !snct || !cnct {
|
||||
return nil, resp, errInvalidCompression
|
||||
}
|
||||
conn.newCompressionWriter = compressNoContextTakeover
|
||||
conn.newDecompressionReader = decompressNoContextTakeover
|
||||
break
|
||||
}
|
||||
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
|
||||
|
||||
netConn.SetDeadline(time.Time{})
|
||||
netConn = nil // to avoid close in defer.
|
||||
return conn, resp, nil
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return cfg.Clone()
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// cloneTLSConfig clones all public fields except the fields
|
||||
// SessionTicketsDisabled and SessionTicketKey. This avoids copying the
|
||||
// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
|
||||
// config in active use.
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return &tls.Config{
|
||||
Rand: cfg.Rand,
|
||||
Time: cfg.Time,
|
||||
Certificates: cfg.Certificates,
|
||||
NameToCertificate: cfg.NameToCertificate,
|
||||
GetCertificate: cfg.GetCertificate,
|
||||
RootCAs: cfg.RootCAs,
|
||||
NextProtos: cfg.NextProtos,
|
||||
ServerName: cfg.ServerName,
|
||||
ClientAuth: cfg.ClientAuth,
|
||||
ClientCAs: cfg.ClientCAs,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||
ClientSessionCache: cfg.ClientSessionCache,
|
||||
MinVersion: cfg.MinVersion,
|
||||
MaxVersion: cfg.MaxVersion,
|
||||
CurvePreferences: cfg.CurvePreferences,
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
|
||||
maxCompressionLevel = flate.BestCompression
|
||||
defaultCompressionLevel = 1
|
||||
)
|
||||
|
||||
var (
|
||||
flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
|
||||
flateReaderPool = sync.Pool{New: func() interface{} {
|
||||
return flate.NewReader(nil)
|
||||
}}
|
||||
)
|
||||
|
||||
func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
|
||||
const tail =
|
||||
// Add four bytes as specified in RFC
|
||||
"\x00\x00\xff\xff" +
|
||||
// Add final block to squelch unexpected EOF error from flate reader.
|
||||
"\x01\x00\x00\xff\xff"
|
||||
|
||||
fr, _ := flateReaderPool.Get().(io.ReadCloser)
|
||||
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
|
||||
return &flateReadWrapper{fr}
|
||||
}
|
||||
|
||||
func isValidCompressionLevel(level int) bool {
|
||||
return minCompressionLevel <= level && level <= maxCompressionLevel
|
||||
}
|
||||
|
||||
func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
|
||||
p := &flateWriterPools[level-minCompressionLevel]
|
||||
tw := &truncWriter{w: w}
|
||||
fw, _ := p.Get().(*flate.Writer)
|
||||
if fw == nil {
|
||||
fw, _ = flate.NewWriter(tw, level)
|
||||
} else {
|
||||
fw.Reset(tw)
|
||||
}
|
||||
return &flateWriteWrapper{fw: fw, tw: tw, p: p}
|
||||
}
|
||||
|
||||
// truncWriter is an io.Writer that writes all but the last four bytes of the
|
||||
// stream to another io.Writer.
|
||||
type truncWriter struct {
|
||||
w io.WriteCloser
|
||||
n int
|
||||
p [4]byte
|
||||
}
|
||||
|
||||
func (w *truncWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
|
||||
// fill buffer first for simplicity.
|
||||
if w.n < len(w.p) {
|
||||
n = copy(w.p[w.n:], p)
|
||||
p = p[n:]
|
||||
w.n += n
|
||||
if len(p) == 0 {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
m := len(p)
|
||||
if m > len(w.p) {
|
||||
m = len(w.p)
|
||||
}
|
||||
|
||||
if nn, err := w.w.Write(w.p[:m]); err != nil {
|
||||
return n + nn, err
|
||||
}
|
||||
|
||||
copy(w.p[:], w.p[m:])
|
||||
copy(w.p[len(w.p)-m:], p[len(p)-m:])
|
||||
nn, err := w.w.Write(p[:len(p)-m])
|
||||
return n + nn, err
|
||||
}
|
||||
|
||||
type flateWriteWrapper struct {
|
||||
fw *flate.Writer
|
||||
tw *truncWriter
|
||||
p *sync.Pool
|
||||
}
|
||||
|
||||
func (w *flateWriteWrapper) Write(p []byte) (int, error) {
|
||||
if w.fw == nil {
|
||||
return 0, errWriteClosed
|
||||
}
|
||||
return w.fw.Write(p)
|
||||
}
|
||||
|
||||
func (w *flateWriteWrapper) Close() error {
|
||||
if w.fw == nil {
|
||||
return errWriteClosed
|
||||
}
|
||||
err1 := w.fw.Flush()
|
||||
w.p.Put(w.fw)
|
||||
w.fw = nil
|
||||
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
|
||||
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
|
||||
}
|
||||
err2 := w.tw.w.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
type flateReadWrapper struct {
|
||||
fr io.ReadCloser
|
||||
}
|
||||
|
||||
func (r *flateReadWrapper) Read(p []byte) (int, error) {
|
||||
if r.fr == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
n, err := r.fr.Read(p)
|
||||
if err == io.EOF {
|
||||
// Preemptively place the reader back in the pool. This helps with
|
||||
// scenarios where the application does not call NextReader() soon after
|
||||
// this final read.
|
||||
r.Close()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *flateReadWrapper) Close() error {
|
||||
if r.fr == nil {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
err := r.fr.Close()
|
||||
flateReaderPool.Put(r.fr)
|
||||
r.fr = nil
|
||||
return err
|
||||
}
|
||||
+1149
File diff suppressed because it is too large
Load Diff
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.5
|
||||
|
||||
package websocket
|
||||
|
||||
import "io"
|
||||
|
||||
func (c *Conn) read(n int) ([]byte, error) {
|
||||
p, err := c.br.Peek(n)
|
||||
if err == io.EOF {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
c.br.Discard(len(p))
|
||||
return p, err
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.5
|
||||
|
||||
package websocket
|
||||
|
||||
import "io"
|
||||
|
||||
func (c *Conn) read(n int) ([]byte, error) {
|
||||
p, err := c.br.Peek(n)
|
||||
if err == io.EOF {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
if len(p) > 0 {
|
||||
// advance over the bytes just read
|
||||
io.ReadFull(c.br, p)
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package websocket implements the WebSocket protocol defined in RFC 6455.
|
||||
//
|
||||
// Overview
|
||||
//
|
||||
// The Conn type represents a WebSocket connection. A server application uses
|
||||
// the Upgrade function from an Upgrader object with a HTTP request handler
|
||||
// to get a pointer to a Conn:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// ReadBufferSize: 1024,
|
||||
// WriteBufferSize: 1024,
|
||||
// }
|
||||
//
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// conn, err := upgrader.Upgrade(w, r, nil)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// ... Use conn to send and receive messages.
|
||||
// }
|
||||
//
|
||||
// Call the connection's WriteMessage and ReadMessage methods to send and
|
||||
// receive messages as a slice of bytes. This snippet of code shows how to echo
|
||||
// messages using these methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, p, err := conn.ReadMessage()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// if err = conn.WriteMessage(messageType, p); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// In above snippet of code, p is a []byte and messageType is an int with value
|
||||
// websocket.BinaryMessage or websocket.TextMessage.
|
||||
//
|
||||
// An application can also send and receive messages using the io.WriteCloser
|
||||
// and io.Reader interfaces. To send a message, call the connection NextWriter
|
||||
// method to get an io.WriteCloser, write the message to the writer and close
|
||||
// the writer when done. To receive a message, call the connection NextReader
|
||||
// method to get an io.Reader and read until io.EOF is returned. This snippet
|
||||
// shows how to echo messages using the NextWriter and NextReader methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, r, err := conn.NextReader()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// w, err := conn.NextWriter(messageType)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if _, err := io.Copy(w, r); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := w.Close(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Data Messages
|
||||
//
|
||||
// The WebSocket protocol distinguishes between text and binary data messages.
|
||||
// Text messages are interpreted as UTF-8 encoded text. The interpretation of
|
||||
// binary messages is left to the application.
|
||||
//
|
||||
// This package uses the TextMessage and BinaryMessage integer constants to
|
||||
// identify the two data message types. The ReadMessage and NextReader methods
|
||||
// return the type of the received message. The messageType argument to the
|
||||
// WriteMessage and NextWriter methods specifies the type of a sent message.
|
||||
//
|
||||
// It is the application's responsibility to ensure that text messages are
|
||||
// valid UTF-8 encoded text.
|
||||
//
|
||||
// Control Messages
|
||||
//
|
||||
// The WebSocket protocol defines three types of control messages: close, ping
|
||||
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
|
||||
// methods to send a control message to the peer.
|
||||
//
|
||||
// Connections handle received close messages by sending a close message to the
|
||||
// peer and returning a *CloseError from the the NextReader, ReadMessage or the
|
||||
// message Read method.
|
||||
//
|
||||
// Connections handle received ping and pong messages by invoking callback
|
||||
// functions set with SetPingHandler and SetPongHandler methods. The callback
|
||||
// functions are called from the NextReader, ReadMessage and the message Read
|
||||
// methods.
|
||||
//
|
||||
// The default ping handler sends a pong to the peer. The application's reading
|
||||
// goroutine can block for a short time while the handler writes the pong data
|
||||
// to the connection.
|
||||
//
|
||||
// The application must read the connection to process ping, pong and close
|
||||
// messages sent from the peer. If the application is not otherwise interested
|
||||
// in messages from the peer, then the application should start a goroutine to
|
||||
// read and discard messages from the peer. A simple example is:
|
||||
//
|
||||
// func readLoop(c *websocket.Conn) {
|
||||
// for {
|
||||
// if _, _, err := c.NextReader(); err != nil {
|
||||
// c.Close()
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Concurrency
|
||||
//
|
||||
// Connections support one concurrent reader and one concurrent writer.
|
||||
//
|
||||
// Applications are responsible for ensuring that no more than one goroutine
|
||||
// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
|
||||
// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and
|
||||
// that no more than one goroutine calls the read methods (NextReader,
|
||||
// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler)
|
||||
// concurrently.
|
||||
//
|
||||
// The Close and WriteControl methods can be called concurrently with all other
|
||||
// methods.
|
||||
//
|
||||
// Origin Considerations
|
||||
//
|
||||
// Web browsers allow Javascript applications to open a WebSocket connection to
|
||||
// any host. It's up to the server to enforce an origin policy using the Origin
|
||||
// request header sent by the browser.
|
||||
//
|
||||
// The Upgrader calls the function specified in the CheckOrigin field to check
|
||||
// the origin. If the CheckOrigin function returns false, then the Upgrade
|
||||
// method fails the WebSocket handshake with HTTP status 403.
|
||||
//
|
||||
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
|
||||
// the handshake if the Origin request header is present and not equal to the
|
||||
// Host request header.
|
||||
//
|
||||
// An application can allow connections from any origin by specifying a
|
||||
// function that always returns true:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// CheckOrigin: func(r *http.Request) bool { return true },
|
||||
// }
|
||||
//
|
||||
// The deprecated Upgrade function does not enforce an origin policy. It's the
|
||||
// application's responsibility to check the Origin header before calling
|
||||
// Upgrade.
|
||||
//
|
||||
// Compression EXPERIMENTAL
|
||||
//
|
||||
// Per message compression extensions (RFC 7692) are experimentally supported
|
||||
// by this package in a limited capacity. Setting the EnableCompression option
|
||||
// to true in Dialer or Upgrader will attempt to negotiate per message deflate
|
||||
// support.
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// EnableCompression: true,
|
||||
// }
|
||||
//
|
||||
// If compression was successfully negotiated with the connection's peer, any
|
||||
// message received in compressed form will be automatically decompressed.
|
||||
// All Read methods will return uncompressed bytes.
|
||||
//
|
||||
// Per message compression of messages written to a connection can be enabled
|
||||
// or disabled by calling the corresponding Conn method:
|
||||
//
|
||||
// conn.EnableWriteCompression(false)
|
||||
//
|
||||
// Currently this package does not support compression with "context takeover".
|
||||
// This means that messages must be compressed and decompressed in isolation,
|
||||
// without retaining sliding window or dictionary state across messages. For
|
||||
// more details refer to RFC 7692.
|
||||
//
|
||||
// Use of compression is experimental and may result in decreased performance.
|
||||
package websocket
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriteJSON is deprecated, use c.WriteJSON instead.
|
||||
func WriteJSON(c *Conn, v interface{}) error {
|
||||
return c.WriteJSON(v)
|
||||
}
|
||||
|
||||
// WriteJSON writes the JSON encoding of v to the connection.
|
||||
//
|
||||
// See the documentation for encoding/json Marshal for details about the
|
||||
// conversion of Go values to JSON.
|
||||
func (c *Conn) WriteJSON(v interface{}) error {
|
||||
w, err := c.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err1 := json.NewEncoder(w).Encode(v)
|
||||
err2 := w.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// ReadJSON is deprecated, use c.ReadJSON instead.
|
||||
func ReadJSON(c *Conn, v interface{}) error {
|
||||
return c.ReadJSON(v)
|
||||
}
|
||||
|
||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
||||
// it in the value pointed to by v.
|
||||
//
|
||||
// See the documentation for the encoding/json Unmarshal function for details
|
||||
// about the conversion of JSON to a Go value.
|
||||
func (c *Conn) ReadJSON(v interface{}) error {
|
||||
_, r, err := c.NextReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.NewDecoder(r).Decode(v)
|
||||
if err == io.EOF {
|
||||
// One value is expected in the message.
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in the
|
||||
// LICENSE file.
|
||||
|
||||
// +build !appengine
|
||||
|
||||
package websocket
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const wordSize = int(unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
||||
|
||||
// Mask one byte at a time for small buffers.
|
||||
if len(b) < 2*wordSize {
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
return pos & 3
|
||||
}
|
||||
|
||||
// Mask one byte at a time to word boundary.
|
||||
if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
|
||||
n = wordSize - n
|
||||
for i := range b[:n] {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
|
||||
// Create aligned word size key.
|
||||
var k [wordSize]byte
|
||||
for i := range k {
|
||||
k[i] = key[(pos+i)&3]
|
||||
}
|
||||
kw := *(*uintptr)(unsafe.Pointer(&k))
|
||||
|
||||
// Mask one word at a time.
|
||||
n := (len(b) / wordSize) * wordSize
|
||||
for i := 0; i < n; i += wordSize {
|
||||
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
|
||||
}
|
||||
|
||||
// Mask one byte at a time for remaining bytes.
|
||||
b = b[n:]
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
|
||||
return pos & 3
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user