Compare commits

..

25 Commits

Author SHA1 Message Date
Dejan Mijić e858e86ed9 NOISSUE - Fix paging (#232)
* Fix docs errors

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

* Fix edge case for limit check

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

* Explain limit value

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-18 23:06:11 +02:00
Dušan Borovčanin 84679ed42a MF-200 - Enable pagination of result sets (#227)
* Add pagination to clients and channels endpoints

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Refactor code

Change method signature and rename Bulk methods back to All.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Rename transport_test.go to endpoint_test.go

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix manager tests to support pagination

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Add default offset and limit support

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Update docs

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Update tests to support pagination

- Move maxLimitSize checking to request validation.
- Add tests to support pagination.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix handling query params for pagination

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix empty result set

Return empty results if invalid offset and limit is passed to channel and client repository.
Update tests accordingly.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Update manager API docs

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix response to invalid limit query param

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Remove offset and limmit checks in repository methods

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-04-18 22:36:24 +02:00
Aleksandar Novaković b0610f034c Fix http publish example in docs (#231)
Fix http publish example in docs. Fix docker-compose example in docs
and README.md.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-18 16:49:10 +02:00
Dejan Mijić cb9c0ee32a Specify targets for releasing docker containers (#230)
New targets will allow Makefile to be used as primary option for
releasing docker containers, both as 'latest' and versioned.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-18 14:13:38 +02:00
Aleksandar Novaković 3a5f4395e4 MF-171 - Extract websocket adapter as separate service (#188)
* Add websocket adapter

Add websocket adapter with basic logging and metrics middleware.
Add publish and subscribe to NATS subjects using websocket.
Add websocket handshake authorization over mainflux manager.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add test for websocket publish

Add test for websocket adapter's publish method. Add dependecy
injected logger to adapter. Remove unnecessary manager client
dependency from adapter.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add websocket library in dependencies

Add gorilla/websocket dependency in dep toml and lock file.
Add dependency in vendor dir.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add direct websocket connection

Add direct websocket connection. While messages are still beeing
published over NATS, they are not received over src.ws subject
in WebSocket adapter. Instead messages are sent directly over
websocket connection.

Add swagger file for WebSocket handshake endpoint. Update ReadMe
to reference new swagger file.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactore websocket adapter code

Extract listen part from handshake. Update WebSocket adapter code.
Fix subscribe to NATS subject, so that it is independent from other
adapters. Remove message base64 encoding from response..

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Remove connection management from websocket adapter

Align notion of channel with NATS topic. Remove connection
management from adapter logic. Add log messages to adapter's
transport layer.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update NATS subjects and add subscriber interface

Update NATS subject name to channel.<channel_id>. Add and implement
subscriber interface. Implement subscriber interface in adapter.
Update readme to use new make <service_name> command. Refactor code.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add basic tests for broadcast and subscribe

Improve mock implementation of NATS pubsub. Add multiple basic test
cases for broadcast and subscribe methods. Add logging for subscribe.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add listen method test

Add listen method test and refactor existing tests. Refactor listen
method in adapter.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Move broadcast method to message broker interface

Refactor broadcast method to receive send message callback and message
that needs to be sent. Update tests accordingly.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update pubsub API

Remove listen method from public API. Move listen call to subscribe
implementation. Update domain pubsub API in project root.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add connection error handlers to adapter API

Update publish and subscribe API to receive connection error handler.
Update tests accordingly. Handle NATS connection error.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update logs

Replace go-kit logger with custom mainflux logger. Update log messages
where needed..

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactor web socket adapter

Remove MessagePubSub interface. Remove unnecessary callbacks. Add
channels to web socket adapter implementation.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add API layer tests

Update existing tests and add API layer tests.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update docs with web socket related data

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactor web socket adapter

Update subscription struct, and refactor listen and broadcast methods.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add version and metrics endpoint to ws adapter

Update version to 0.2.0.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Fix race condition bug in ws adapter test

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Rename listen to broadcast and broadcast to listen

Switch names between listen and broadcast methods. Move channel
structure to service.go.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-18 13:09:01 +02:00
Aleksandar Novaković 36a00f3464 Update project documentation with example requests and responses (#220)
Update documentation with example requests and responses in accordance
with new API changes. Add detailed flow explanation. Update readme with
valid snippet.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-11 14:30:37 +02:00
Aleksandar Novaković 703d0543af Add HTTP adapter API test (#224)
Add HTTP adapter API tests. Fix Manager API tests.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-11 14:29:04 +02:00
Aleksandar Novaković fb3619645d Add channel Id validation (#223)
Add channel id validation and remove unnecessary error. Update
swagger doc accordingly.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-11 13:48:09 +02:00
Dejan Mijić 46a9cc086d Remove unhandled missing key error (#222)
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-11 11:44:15 +02:00
Dejan Mijić 367d3edbef Set JWT provider secret (#221)
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-10 17:30:13 +02:00
Dejan Mijić d5b913d432 MF-199 - Create documentation skeleton (#218)
* Copy basic docs contents to the main repo

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

* Include licensing and contributing info to docs

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

* Create documentation skeleton

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-08 23:03:12 +02:00
Aleksandar Novaković 2dace5564f MF-170 - Add manager API unit tests (#211)
* Fix connection request validation and EOF error handling

Fix validation of connection and client id in connection request.
Fix EOF error handling by returning HTTP status Bad Request.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add manager API tests and update swagger

Implement unit tests for every manager API endpoint. Update client
and connection mock implementations by switching to UUIDs. Update
swagger file with correct status codes.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add content type check and update documentation

Add content type check in implementation and update documentation
accordingly. Refactor tests and add empty content type test cases.
Add code coverage badge to readme.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-08 22:57:56 +02:00
Aleksandar Novaković 88b30626dd MF-174 - Add logger interface and go-kit logger implementation (#212)
* Add logger interface and go-kit logger implementation

Add wrapper logger interface and wrap go-kit logger in it. Add
tests for info, warn, error and log methods. Add log filtering by
level.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactor logger and replace go-kit logger with wrapper

Replace go-kit logger from services with logger wrapper. Refactor
code in logger wrapper. Remove unnecessary subpackage, methods and
log levels.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-04 10:15:23 +02:00
Drasko DRASKOVIC 301d855015 Add CA cert (#215)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-04-04 02:29:29 +02:00
Dušan Borovčanin 7a193829f4 MF-139 - Expose metrics from all services (#213)
* Expose metrics from normalizer service

Add service interface in order to expose method to be monitored.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Refactor normalizer

Add metrics to normalizer service without exporting its internals.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-04-03 20:25:49 +02:00
Dejan Mijić 523716b090 Rename NATS subjects for HTTP & normalizer (#207)
* Change NATS subject name

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

* Change normalizer subscription name

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-03-22 23:59:15 +01:00
Dejan Mijić 36ca9015ff Handle wrong type unmarshalling (#206)
* Update the contributing guide

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

* Handle wrong type unmarshalling

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-03-22 23:53:43 +01:00
Drasko DRASKOVIC a6dfc26959 Simplify Makefile (#205)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-03-22 13:10:11 +01:00
Dušan Borovčanin 924f6f120a Fix blinker tests (#193)
* Fix blinker tests

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Add protobuf generated files to .gitignore

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-03-20 09:14:54 +01:00
Drasko DRASKOVIC 2c1c94af9b Fix #195 (#196)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-03-20 00:09:44 +01:00
Dejan Mijić f5fbd6f22a Add protobuf (#192)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-03-19 10:14:43 +01:00
Aleksandar Novaković 55bdc029c3 Add service startup message (#190)
* Add service startup message

Add startup message to manager, http, coap and normalizer services.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Remove startup message from CoAP adapter

Remove startup message from CoAP adapter. Remove overhead startup
message variable.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-03-18 13:54:54 +01:00
Dejan Mijić e42d422410 Fix golint warnings (#189)
* Fix goling warnings

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

* Bump version to 0.1.2

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-03-16 14:06:12 +01:00
Nikola Marčetić bf5d1544b6 Fixed docker composition (#187)
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2018-03-16 13:52:51 +01:00
Max Peng 8876acbfec Fix docker compose failing start issue (#185)
* Fix docker compose failing start issue

* Delete unused code

* Fix nginx config issue
2018-03-16 09:59:13 +01:00
195 changed files with 8040 additions and 1196 deletions
+38 -48
View File
@@ -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.
+3
View File
@@ -1 +1,4 @@
build
*.pb.go
site/
Generated
+23 -17
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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.
+28 -28
View File
@@ -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
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
}
+88
View File
@@ -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
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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 /
+24 -7
View File
@@ -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
View File
@@ -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;
+21
View File
@@ -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-----
+1
View File
@@ -0,0 +1 @@
../.github/CONTRIBUTING.md
+1
View File
@@ -0,0 +1 @@
../LICENSE
+47
View File
@@ -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
![arch](img/architecture.jpg)
## 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).
+284
View File
@@ -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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+17
View File
@@ -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.
![banner](img/gopherBanner.jpg)
## 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
View File
@@ -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
View File
@@ -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:
+110
View File
@@ -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))
}
}
+16
View File
@@ -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
}
+5 -5
View File
@@ -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)
}
+2
View File
@@ -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:
+3
View File
@@ -0,0 +1,3 @@
// Package logger contains logger API definition, wrapper that
// can be used around any other logger.
package logger
+23
View File
@@ -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]
}
+42
View File
@@ -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)
}
+93
View File
@@ -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))
}
}
+4 -4
View File
@@ -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
}
}
+783
View File
@@ -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
View File
@@ -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)
+4 -4
View File
@@ -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 {
+6 -4
View File
@@ -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
}
+8 -7
View File
@@ -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()
+2 -8
View File
@@ -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 {
+82 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+12 -5
View File
@@ -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)
}
}
+5 -4
View File
@@ -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
}
+17 -14
View File
@@ -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))
}
}
+4 -8
View File
@@ -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
}
+8 -6
View File
@@ -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))
}
}
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -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"
)
+6 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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
+30
View File
@@ -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
View File
@@ -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
}
}
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,7 +1,7 @@
govalidator
===========
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GoDoc](https://godoc.org/github.com/asaskevich/govalidator?status.png)](https://godoc.org/github.com/asaskevich/govalidator) [![Coverage Status](https://img.shields.io/coveralls/asaskevich/govalidator.svg)](https://coveralls.io/r/asaskevich/govalidator?branch=master) [![wercker status](https://app.wercker.com/status/1ec990b09ea86c910d5f08b0e02c6043/s "wercker status")](https://app.wercker.com/project/bykey/1ec990b09ea86c910d5f08b0e02c6043)
[![Build Status](https://travis-ci.org/asaskevich/govalidator.svg?branch=master)](https://travis-ci.org/asaskevich/govalidator) [![Go Report Card](https://goreportcard.com/badge/github.com/asaskevich/govalidator)](https://goreportcard.com/report/github.com/asaskevich/govalidator) [![GoSearch](http://go-search.org/badge?id=github.com%2Fasaskevich%2Fgovalidator)](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator)
[![Build Status](https://travis-ci.org/asaskevich/govalidator.svg?branch=master)](https://travis-ci.org/asaskevich/govalidator) [![Go Report Card](https://goreportcard.com/badge/github.com/asaskevich/govalidator)](https://goreportcard.com/report/github.com/asaskevich/govalidator) [![GoSearch](http://go-search.org/badge?id=github.com%2Fasaskevich%2Fgovalidator)](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator) [![Backers on Open Collective](https://opencollective.com/govalidator/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/govalidator/sponsors/badge.svg)](#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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
[![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go)
[![GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go?status.svg)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket)
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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